1
0

Player data loading cleanup

* Kick player if data were corrupt to avoid making it worse
This commit is contained in:
Tiger Wang 2021-03-28 22:34:54 +01:00
parent 222d9957a1
commit 8a30a4a7b8
6 changed files with 106 additions and 210 deletions

View File

@ -140,15 +140,6 @@ void cClientHandle::Destroy(void)
void cClientHandle::GenerateOfflineUUID(void)
{
m_UUID = GenerateOfflineUUID(m_Username);
}
AString cClientHandle::FormatChatPrefix(
bool ShouldAppendChatPrefixes, const AString & a_ChatPrefixS,
const AString & m_Color1, const AString & m_Color2
@ -318,7 +309,19 @@ void cClientHandle::Authenticate(const AString & a_Name, const cUUID & a_UUID, c
void cClientHandle::FinishAuthenticate(const AString & a_Name, const cUUID & a_UUID, const Json::Value & a_Properties)
{
// Serverside spawned player (so data are loaded).
auto Player = std::make_unique<cPlayer>(shared_from_this());
std::unique_ptr<cPlayer> Player;
try
{
Player = std::make_unique<cPlayer>(shared_from_this());
}
catch (const std::exception & Oops)
{
LOGWARNING("Error reading player \"%s\": %s", GetUsername().c_str(), Oops.what());
Kick("Contact an operator.\n\nYour player's save files could not be parsed.\nTo avoid data loss you are prevented from joining.");
return;
}
m_Player = Player.get();
/*
@ -1461,9 +1464,7 @@ void cClientHandle::HandleChat(const AString & a_Message)
{
if ((a_Message.size()) > MAX_CHAT_MSG_LENGTH)
{
this->Kick(std::string("Please don't exceed the maximum message length of ")
+ std::to_string(MAX_CHAT_MSG_LENGTH)
);
Kick("Please don't exceed the maximum message length of " + std::to_string(MAX_CHAT_MSG_LENGTH));
return;
}
// We no longer need to postpone message processing, because the messages already arrive in the Tick thread

View File

@ -90,12 +90,6 @@ public: // tolua_export
and the results are passed to MCS running in offline mode. */
void SetProperties(const Json::Value & a_Properties) { m_Properties = a_Properties; }
/** Generates an UUID based on the username stored for this client, and stores it in the m_UUID member.
This is used for the offline (non-auth) mode, when there's no UUID source.
Each username generates a unique and constant UUID, so that when the player reconnects with the same name, their UUID is the same.
Internally calls the GenerateOfflineUUID static function. */
void GenerateOfflineUUID(void);
/** Generates an UUID based on the player name provided.
This is used for the offline (non-auth) mode, when there's no UUID source.
Each username generates a unique and constant UUID, so that when the player reconnects with the same name, their UUID is the same. */

View File

@ -37,23 +37,6 @@
namespace
{
/** Returns the old Offline UUID generated before becoming vanilla compliant. */
cUUID GetOldStyleOfflineUUID(const AString & a_PlayerName)
{
// Use lowercase username
auto BaseUUID = cUUID::GenerateVersion3(StrToLower(a_PlayerName)).ToRaw();
// Clobber a full nibble around the variant bits
BaseUUID[8] = (BaseUUID[8] & 0x0f) | 0x80;
cUUID Ret;
Ret.FromRaw(BaseUUID);
return Ret;
}
/** Returns the folder for the player data based on the UUID given.
This can be used both for online and offline UUIDs. */
AString GetUUIDFolderName(const cUUID & a_Uuid)
@ -115,7 +98,6 @@ cPlayer::cPlayer(const std::shared_ptr<cClientHandle> & a_Client) :
m_bIsInBed(false),
m_TicksUntilNextSave(PLAYER_INVENTORY_SAVE_INTERVAL),
m_bIsTeleporting(false),
m_UUID((a_Client != nullptr) ? a_Client->GetUUID() : cUUID{}),
m_SkinParts(0),
m_MainHand(mhRight)
{
@ -129,22 +111,7 @@ cPlayer::cPlayer(const std::shared_ptr<cClientHandle> & a_Client) :
m_Health = MAX_HEALTH;
cWorld * World = nullptr;
if (!LoadFromDisk(World))
{
m_Inventory.Clear();
SetPosX(World->GetSpawnX());
SetPosY(World->GetSpawnY());
SetPosZ(World->GetSpawnZ());
// This is a new player. Set the player spawn point to the spawn point of the default world
SetBedPos(Vector3i(static_cast<int>(World->GetSpawnX()), static_cast<int>(World->GetSpawnY()), static_cast<int>(World->GetSpawnZ())), World);
m_EnchantmentSeed = GetRandomProvider().RandInt<unsigned int>(); // Use a random number to seed the enchantment generator
FLOGD("Player \"{0}\" is connecting for the first time, spawning at default world spawn {1:.2f}",
GetName(), GetPosition()
);
}
LoadFromDisk(World);
m_LastGroundHeight = static_cast<float>(GetPosY());
m_Stance = GetPosY() + 1.62;
@ -1734,70 +1701,30 @@ void cPlayer::TossPickup(const cItem & a_Item)
bool cPlayer::LoadFromDisk(cWorldPtr & a_World)
void cPlayer::LoadFromDisk(cWorldPtr & a_World)
{
LoadRank();
const auto & UUID = GetUUID();
// Load from the UUID file:
if (LoadFromFile(GetUUIDFileName(m_UUID), a_World))
if (LoadFromFile(GetUUIDFileName(UUID), a_World))
{
return true;
return;
}
// Check for old offline UUID filename, if it exists migrate to new filename
cUUID OfflineUUID = cClientHandle::GenerateOfflineUUID(GetName());
auto OldFilename = GetUUIDFileName(GetOldStyleOfflineUUID(GetName()));
auto NewFilename = GetUUIDFileName(m_UUID);
// Only move if there isn't already a new file
if (!cFile::IsFile(NewFilename) && cFile::IsFile(OldFilename))
{
cFile::CreateFolderRecursive(GetUUIDFolderName(m_UUID)); // Ensure folder exists to move to
if (
cFile::Rename(OldFilename, NewFilename) &&
(m_UUID == OfflineUUID) &&
LoadFromFile(NewFilename, a_World)
)
{
return true;
}
}
// Player not found:
a_World = cRoot::Get()->GetDefaultWorld();
LOG("Player \"%s\" (%s) data not found, resetting to defaults", GetName().c_str(), UUID.ToShortString().c_str());
// Load from the offline UUID file, if allowed:
const char * OfflineUsage = " (unused)";
if (cRoot::Get()->GetServer()->ShouldLoadOfflinePlayerData())
{
OfflineUsage = "";
if (LoadFromFile(GetUUIDFileName(OfflineUUID), a_World))
{
return true;
}
}
const Vector3i WorldSpawn(static_cast<int>(a_World->GetSpawnX()), static_cast<int>(a_World->GetSpawnY()), static_cast<int>(a_World->GetSpawnZ()));
SetPosition(WorldSpawn);
SetBedPos(WorldSpawn, a_World);
// Load from the old-style name-based file, if allowed:
if (cRoot::Get()->GetServer()->ShouldLoadNamedPlayerData())
{
AString OldStyleFileName = Printf("players/%s.json", GetName().c_str());
if (LoadFromFile(OldStyleFileName, a_World))
{
// Save in new format and remove the old file
if (SaveToDisk())
{
cFile::Delete(OldStyleFileName);
}
return true;
}
}
m_Inventory.Clear();
m_EnchantmentSeed = GetRandomProvider().RandInt<unsigned int>(); // Use a random number to seed the enchantment generator
// None of the files loaded successfully
LOG("Player data file not found for %s (%s, offline %s%s), will be reset to defaults.",
GetName().c_str(), m_UUID.ToShortString().c_str(), OfflineUUID.ToShortString().c_str(), OfflineUsage
);
if (a_World == nullptr)
{
a_World = cRoot::Get()->GetDefaultWorld();
}
return false;
FLOGD("Player \"{0}\" is connecting for the first time, spawning at default world spawn {1:.2f}", GetName(), GetPosition());
}
@ -1806,35 +1733,31 @@ bool cPlayer::LoadFromDisk(cWorldPtr & a_World)
bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World)
{
// Load the data from the file:
cFile f;
if (!f.Open(a_FileName, cFile::fmRead))
{
// This is a new player whom we haven't seen yet, bail out, let them have the defaults
return false;
}
AString buffer;
if (f.ReadRestOfFile(buffer) != f.GetSize())
{
LOGWARNING("Cannot read player data from file \"%s\"", a_FileName.c_str());
return false;
}
f.Close();
Json::Value Root;
// Parse the JSON format:
Json::Value root;
AString ParseError;
if (!JsonUtils::ParseString(buffer, root, &ParseError))
try
{
FLOGWARNING(
"Cannot parse player data in file \"{0}\":\n {1}",
a_FileName, ParseError
);
return false;
// Load the data from the file and parse:
InputFileStream(a_FileName) >> Root;
}
catch (const Json::Exception & Oops)
{
// Parse failure:
throw std::runtime_error(Oops.what());
}
catch (const InputFileStream::failure &)
{
if (errno == ENOENT)
{
// This is a new player whom we haven't seen yet, bail out, let them have the defaults:
return false;
}
throw;
}
// Load the player data:
Json::Value & JSON_PlayerPosition = root["position"];
Json::Value & JSON_PlayerPosition = Root["position"];
if (JSON_PlayerPosition.size() == 3)
{
SetPosX(JSON_PlayerPosition[0].asDouble());
@ -1843,7 +1766,7 @@ bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World)
m_LastPosition = GetPosition();
}
Json::Value & JSON_PlayerRotation = root["rotation"];
Json::Value & JSON_PlayerRotation = Root["rotation"];
if (JSON_PlayerRotation.size() == 3)
{
SetYaw (static_cast<float>(JSON_PlayerRotation[0].asDouble()));
@ -1851,18 +1774,18 @@ bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World)
SetRoll (static_cast<float>(JSON_PlayerRotation[2].asDouble()));
}
m_Health = root.get("health", 0).asFloat();
m_AirLevel = root.get("air", MAX_AIR_LEVEL).asInt();
m_FoodLevel = root.get("food", MAX_FOOD_LEVEL).asInt();
m_FoodSaturationLevel = root.get("foodSaturation", MAX_FOOD_LEVEL).asDouble();
m_FoodTickTimer = root.get("foodTickTimer", 0).asInt();
m_FoodExhaustionLevel = root.get("foodExhaustion", 0).asDouble();
m_LifetimeTotalXp = root.get("xpTotal", 0).asInt();
m_CurrentXp = root.get("xpCurrent", 0).asInt();
m_IsFlying = root.get("isflying", 0).asBool();
m_EnchantmentSeed = root.get("enchantmentSeed", GetRandomProvider().RandInt<unsigned int>()).asUInt();
m_Health = Root.get("health", 0).asFloat();
m_AirLevel = Root.get("air", MAX_AIR_LEVEL).asInt();
m_FoodLevel = Root.get("food", MAX_FOOD_LEVEL).asInt();
m_FoodSaturationLevel = Root.get("foodSaturation", MAX_FOOD_LEVEL).asDouble();
m_FoodTickTimer = Root.get("foodTickTimer", 0).asInt();
m_FoodExhaustionLevel = Root.get("foodExhaustion", 0).asDouble();
m_LifetimeTotalXp = Root.get("xpTotal", 0).asInt();
m_CurrentXp = Root.get("xpCurrent", 0).asInt();
m_IsFlying = Root.get("isflying", 0).asBool();
m_EnchantmentSeed = Root.get("enchantmentSeed", GetRandomProvider().RandInt<unsigned int>()).asUInt();
Json::Value & JSON_KnownItems = root["knownItems"];
Json::Value & JSON_KnownItems = Root["knownItems"];
for (UInt32 i = 0; i < JSON_KnownItems.size(); i++)
{
cItem Item;
@ -1872,7 +1795,7 @@ bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World)
const auto & RecipeNameMap = cRoot::Get()->GetCraftingRecipes()->GetRecipeNameMap();
Json::Value & JSON_KnownRecipes = root["knownRecipes"];
Json::Value & JSON_KnownRecipes = Root["knownRecipes"];
for (UInt32 i = 0; i < JSON_KnownRecipes.size(); i++)
{
auto RecipeId = RecipeNameMap.find(JSON_KnownRecipes[i].asString());
@ -1882,21 +1805,21 @@ bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World)
}
}
m_GameMode = static_cast<eGameMode>(root.get("gamemode", eGameMode_NotSet).asInt());
m_GameMode = static_cast<eGameMode>(Root.get("gamemode", eGameMode_NotSet).asInt());
if (m_GameMode == eGameMode_Creative)
{
m_CanFly = true;
}
m_Inventory.LoadFromJson(root["inventory"]);
m_Inventory.LoadFromJson(Root["inventory"]);
int equippedSlotNum = root.get("equippedItemSlot", 0).asInt();
int equippedSlotNum = Root.get("equippedItemSlot", 0).asInt();
m_Inventory.SetEquippedSlotNum(equippedSlotNum);
cEnderChestEntity::LoadFromJson(root["enderchestinventory"], m_EnderChestContents);
cEnderChestEntity::LoadFromJson(Root["enderchestinventory"], m_EnderChestContents);
m_CurrentWorldName = root.get("world", "world").asString();
m_CurrentWorldName = Root.get("world", "world").asString();
a_World = cRoot::Get()->GetWorld(GetLoadedWorldName());
if (a_World == nullptr)
{
@ -1904,10 +1827,10 @@ bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World)
}
m_LastBedPos.x = root.get("SpawnX", a_World->GetSpawnX()).asInt();
m_LastBedPos.y = root.get("SpawnY", a_World->GetSpawnY()).asInt();
m_LastBedPos.z = root.get("SpawnZ", a_World->GetSpawnZ()).asInt();
m_SpawnWorldName = root.get("SpawnWorld", cRoot::Get()->GetDefaultWorld()->GetName()).asString();
m_LastBedPos.x = Root.get("SpawnX", a_World->GetSpawnX()).asInt();
m_LastBedPos.y = Root.get("SpawnY", a_World->GetSpawnY()).asInt();
m_LastBedPos.z = Root.get("SpawnZ", a_World->GetSpawnZ()).asInt();
m_SpawnWorldName = Root.get("SpawnWorld", cRoot::Get()->GetDefaultWorld()->GetName()).asString();
try
{
@ -1960,9 +1883,10 @@ void cPlayer::OpenHorseInventory()
bool cPlayer::SaveToDisk()
void cPlayer::SaveToDisk()
{
cFile::CreateFolderRecursive(GetUUIDFolderName(m_UUID));
const auto & UUID = GetUUID();
cFile::CreateFolderRecursive(GetUUIDFolderName(UUID));
// create the JSON data
Json::Value JSON_PlayerPosition;
@ -2023,22 +1947,22 @@ bool cPlayer::SaveToDisk()
root["gamemode"] = static_cast<int>(m_GameMode);
auto JsonData = JsonUtils::WriteStyledString(root);
AString SourceFile = GetUUIDFileName(m_UUID);
AString SourceFile = GetUUIDFileName(UUID);
cFile f;
if (!f.Open(SourceFile, cFile::fmWrite))
{
LOGWARNING("Error writing player \"%s\" to file \"%s\" - cannot open file. Player will lose their progress.",
LOGWARNING("Error writing player \"%s\" to file \"%s\": cannot open file. Player will lose their progress",
GetName().c_str(), SourceFile.c_str()
);
return false;
return;
}
if (f.Write(JsonData.c_str(), JsonData.size()) != static_cast<int>(JsonData.size()))
{
LOGWARNING("Error writing player \"%s\" to file \"%s\" - cannot save data. Player will lose their progress. ",
LOGWARNING("Error writing player \"%s\" to file \"%s\": cannot save data. Player will lose their progress",
GetName().c_str(), SourceFile.c_str()
);
return false;
return;
}
try
@ -2050,11 +1974,8 @@ bool cPlayer::SaveToDisk()
}
catch (...)
{
LOGWARNING("Could not save stats for player %s", GetName().c_str());
return false;
LOGWARNING("Error writing player \"%s\" statistics to file", GetName().c_str());
}
return true;
}
@ -2316,9 +2237,11 @@ void cPlayer::UpdateMovementStats(const Vector3d & a_DeltaPos, bool a_PreviousIs
void cPlayer::LoadRank(void)
{
// Load the values from cRankManager:
const auto & UUID = GetUUID();
cRankManager * RankMgr = cRoot::Get()->GetRankManager();
m_Rank = RankMgr->GetPlayerRankName(m_UUID);
// Load the values from cRankManager:
m_Rank = RankMgr->GetPlayerRankName(UUID);
if (m_Rank.empty())
{
m_Rank = RankMgr->GetDefaultRank();
@ -2326,10 +2249,10 @@ void cPlayer::LoadRank(void)
else
{
// Update the name:
RankMgr->UpdatePlayerName(m_UUID, GetName());
RankMgr->UpdatePlayerName(UUID, GetName());
}
m_Permissions = RankMgr->GetPlayerPermissions(m_UUID);
m_Restrictions = RankMgr->GetPlayerRestrictions(m_UUID);
m_Permissions = RankMgr->GetPlayerPermissions(UUID);
m_Restrictions = RankMgr->GetPlayerRestrictions(UUID);
RankMgr->GetRankVisuals(m_Rank, m_MsgPrefix, m_MsgSuffix, m_MsgNameColorCode);
// Break up the individual permissions on each dot, into m_SplitPermissions:
@ -2469,6 +2392,15 @@ bool cPlayer::DoesPlacingBlocksIntersectEntity(const sSetBlockVector & a_Blocks)
const cUUID & cPlayer::GetUUID(void) const
{
return m_ClientHandle->GetUUID();
}
bool cPlayer::PlaceBlocks(const sSetBlockVector & a_Blocks)
{
if (DoesPlacingBlocksIntersectEntity(a_Blocks))

View File

@ -379,21 +379,18 @@ public:
void SetVisible( bool a_bVisible); // tolua_export
bool IsVisible(void) const { return m_bVisible; } // tolua_export
/** Saves all player data, such as inventory, to JSON */
bool SaveToDisk(void);
/** Saves all player data, such as inventory, to JSON. */
void SaveToDisk(void);
typedef cWorld * cWorldPtr;
/** Loads the player data from the disk file
Sets a_World to the world where the player will spawn, based on the stored world name or the default world by calling LoadFromFile()
Returns true on success, false on failure
*/
bool LoadFromDisk(cWorldPtr & a_World);
/** Loads the player data from the disk file.
Sets a_World to the world where the player will spawn, based on the stored world name or the default world by calling LoadFromFile(). */
void LoadFromDisk(cWorldPtr & a_World);
/** Loads the player data from the specified file
Sets a_World to the world where the player will spawn, based on the stored world name or the default world
Returns true on success, false on failure
*/
/** Loads the player data from the specified file.
Sets a_World to the world where the player will spawn, based on the stored world name or the default world.
Returns true on success, false if the player wasn't found, and excepts with base std::runtime_error if the data couldn't be read or parsed. */
bool LoadFromFile(const AString & a_FileName, cWorldPtr & a_World);
const AString & GetLoadedWorldName() const { return m_CurrentWorldName; }
@ -499,7 +496,7 @@ public:
bool DoesPlacingBlocksIntersectEntity(const sSetBlockVector & a_Blocks);
/** Returns the UUID that has been read from the client, or nil if not available. */
const cUUID & GetUUID(void) const { return m_UUID; } // Exported in ManualBindings.cpp
const cUUID & GetUUID(void) const; // Exported in ManualBindings.cpp
// tolua_begin
@ -507,7 +504,6 @@ public:
virtual bool CanFly(void) const { return m_CanFly; }
/** (Re)loads the rank and permissions from the cRankManager.
Expects the m_UUID member to be valid.
Loads the m_Rank, m_Permissions, m_MsgPrefix, m_MsgSuffix and m_MsgNameColorCode members. */
void LoadRank(void);
@ -718,10 +714,6 @@ private:
*/
bool m_bIsTeleporting;
/** The UUID of the player, as read from the ClientHandle.
If no ClientHandle is given, the UUID is nil. */
cUUID m_UUID;
AString m_CustomName;
/** Displayed skin part bit mask */

View File

@ -117,9 +117,7 @@ cServer::cServer(void) :
m_MaxPlayers(0),
m_bIsHardcore(false),
m_TickThread(*this),
m_ShouldAuthenticate(false),
m_ShouldLoadOfflinePlayerData(false),
m_ShouldLoadNamedPlayerData(true)
m_ShouldAuthenticate(false)
{
// Initialize the LuaStateTracker singleton before the app goes multithreaded:
cLuaStateTracker::GetStats();
@ -206,8 +204,6 @@ bool cServer::InitServer(cSettingsRepositoryInterface & a_Settings, bool a_Shoul
m_ShouldAllowMultiWorldTabCompletion = a_Settings.GetValueSetB("Server", "AllowMultiWorldTabCompletion", true);
m_ShouldLimitPlayerBlockChanges = a_Settings.GetValueSetB("AntiCheat", "LimitPlayerBlockChanges", true);
m_ShouldLoadOfflinePlayerData = a_Settings.GetValueSetB("PlayerData", "LoadOfflinePlayerData", false);
m_ShouldLoadNamedPlayerData = a_Settings.GetValueSetB("PlayerData", "LoadNamedPlayerData", true);
const auto ClientViewDistance = a_Settings.GetValueSetI("Server", "DefaultViewDistance", cClientHandle::DEFAULT_VIEW_DISTANCE);
if (ClientViewDistance < cClientHandle::MIN_VIEW_DISTANCE)

View File

@ -141,15 +141,6 @@ public:
/** Returns true if limit for number of block changes per tick by a player has been turned on in server settings. */
bool ShouldLimitPlayerBlockChanges(void) const { return m_ShouldLimitPlayerBlockChanges; }
/** Returns true if offline UUIDs should be used to load data for players whose normal UUIDs cannot be found.
Loaded from the settings.ini [PlayerData].LoadOfflinePlayerData setting. */
bool ShouldLoadOfflinePlayerData(void) const { return m_ShouldLoadOfflinePlayerData; }
/** Returns true if old-style playernames should be used to load data for players whose regular datafiles cannot be found.
This allows a seamless transition from name-based to UUID-based player storage.
Loaded from the settings.ini [PlayerData].LoadNamedPlayerData setting. */
bool ShouldLoadNamedPlayerData(void) const { return m_ShouldLoadNamedPlayerData; }
/** Returns true if BungeeCord logins (that specify the player's UUID) are allowed.
Read from settings, admins should set this to true only when they chain to BungeeCord,
it makes the server vulnerable to identity theft through direct connections. */
@ -243,16 +234,6 @@ private:
/** True if limit for number of block changes per tick by a player should be enabled. */
bool m_ShouldLimitPlayerBlockChanges;
/** True if offline UUIDs should be used to load data for players whose normal UUIDs cannot be found.
This allows transitions from an offline (no-auth) server to an online one.
Loaded from the settings.ini [PlayerData].LoadOfflinePlayerData setting. */
bool m_ShouldLoadOfflinePlayerData;
/** True if old-style playernames should be used to load data for players whose regular datafiles cannot be found.
This allows a seamless transition from name-based to UUID-based player storage.
Loaded from the settings.ini [PlayerData].LoadNamedPlayerData setting. */
bool m_ShouldLoadNamedPlayerData;
/** True if BungeeCord handshake packets (with player UUID) should be accepted. */
bool m_ShouldAllowBungeeCord;