1
0

Clarify cClientHandle, cPlayer ownership semantics

+ A cPlayer, once created, has a strong pointer to the cClientHandle. The player ticks the clienthandle. If he finds the handle destroyed, he destroys himself in turn. Nothing else can kill the player.
* The client handle has a pointer to the player. Once a player is created, the client handle never outlasts the player, nor does it manage the player's lifetime. The pointer is always safe to use after FinishAuthenticate, which is also the point where cProtocol is put into the Game state that allows player manipulation.
+ Entities are once again never lost by constructing a chunk when they try to move into one that doesn't exist.
* Fixed a forgotten Super invocation in cPlayer::OnRemoveFromWorld.
* Fix SaveToDisk usage in destructor by only saving things cPlayer owns, instead of accessing cWorld.
This commit is contained in:
Tiger Wang
2021-01-06 00:35:42 +00:00
parent 9328afe65c
commit 054a89dd9e
21 changed files with 505 additions and 1126 deletions

View File

@@ -86,7 +86,7 @@ const int cPlayer::EATING_TICKS = 30;
cPlayer::cPlayer(const cClientHandlePtr & a_Client, const AString & a_PlayerName) :
cPlayer::cPlayer(const cClientHandlePtr & a_Client) :
Super(etPlayer, 0.6, 1.8),
m_bVisible(true),
m_FoodLevel(MAX_FOOD_LEVEL),
@@ -96,10 +96,8 @@ cPlayer::cPlayer(const cClientHandlePtr & a_Client, const AString & a_PlayerName
m_Stance(0.0),
m_Inventory(*this),
m_EnderChestContents(9, 3),
m_CurrentWindow(nullptr),
m_InventoryWindow(nullptr),
m_DefaultWorldPath(cRoot::Get()->GetDefaultWorld()->GetDataPath()),
m_GameMode(eGameMode_NotSet),
m_IP(""),
m_ClientHandle(a_Client),
m_IsFrozen(false),
m_NormalMaxSpeed(1.0),
@@ -113,7 +111,6 @@ cPlayer::cPlayer(const cClientHandlePtr & a_Client, const AString & a_PlayerName
m_EatingFinishTick(-1),
m_LifetimeTotalXp(0),
m_CurrentXp(0),
m_bDirtyExperience(false),
m_IsChargingBow(false),
m_BowCharge(0),
m_FloaterID(cEntity::INVALID_ID),
@@ -122,11 +119,10 @@ cPlayer::cPlayer(const cClientHandlePtr & a_Client, const AString & a_PlayerName
m_TicksUntilNextSave(PLAYER_INVENTORY_SAVE_INTERVAL),
m_bIsTeleporting(false),
m_UUID((a_Client != nullptr) ? a_Client->GetUUID() : cUUID{}),
m_CustomName(""),
m_SkinParts(0),
m_MainHand(mhRight)
{
ASSERT(a_PlayerName.length() <= 16); // Otherwise this player could crash many clients...
ASSERT(GetName().length() <= 16); // Otherwise this player could crash many clients...
m_InventoryWindow = new cInventoryWindow(*this);
m_CurrentWindow = m_InventoryWindow;
@@ -136,7 +132,6 @@ cPlayer::cPlayer(const cClientHandlePtr & a_Client, const AString & a_PlayerName
m_Health = MAX_HEALTH;
m_LastPlayerListTime = std::chrono::steady_clock::now();
m_PlayerName = a_PlayerName;
cWorld * World = nullptr;
if (!LoadFromDisk(World))
@@ -149,19 +144,16 @@ cPlayer::cPlayer(const cClientHandlePtr & a_Client, const AString & a_PlayerName
// 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);
SetWorld(World); // Use default 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}",
a_PlayerName, GetPosition()
GetName(), GetPosition()
);
}
m_LastGroundHeight = static_cast<float>(GetPosY());
m_Stance = GetPosY() + 1.62;
if (m_GameMode == gmNotSet)
{
if (World->IsGameModeCreative())
@@ -187,34 +179,6 @@ cPlayer::cPlayer(const cClientHandlePtr & a_Client, const AString & a_PlayerName
bool cPlayer::Initialize(OwnedEntity a_Self, cWorld & a_World)
{
UNUSED(a_World);
ASSERT(GetWorld() != nullptr);
ASSERT(GetParentChunk() == nullptr);
GetWorld()->AddPlayer(std::unique_ptr<cPlayer>(static_cast<cPlayer *>(a_Self.release())));
cPluginManager::Get()->CallHookSpawnedEntity(*GetWorld(), *this);
if (m_KnownRecipes.empty())
{
m_ClientHandle->SendInitRecipes(0);
}
else
{
for (const auto KnownRecipe : m_KnownRecipes)
{
m_ClientHandle->SendInitRecipes(KnownRecipe);
}
}
return true;
}
void cPlayer::AddKnownItem(const cItem & a_Item)
{
if (a_Item.m_ItemType < 0)
@@ -258,20 +222,14 @@ void cPlayer::AddKnownRecipe(UInt32 a_RecipeId)
cPlayer::~cPlayer(void)
{
if (!cRoot::Get()->GetPluginManager()->CallHookPlayerDestroyed(*this))
{
cRoot::Get()->BroadcastChatLeave(Printf("%s has left the game", GetName().c_str()));
LOGINFO("Player %s has left the game", GetName().c_str());
}
LOGD("Deleting cPlayer \"%s\" at %p, ID %d", GetName().c_str(), static_cast<void *>(this), GetUniqueID());
// "Times ragequit":
m_Stats.AddValue(Statistic::LeaveGame);
SaveToDisk();
m_ClientHandle = nullptr;
delete m_InventoryWindow;
m_InventoryWindow = nullptr;
LOGD("Player %p deleted", static_cast<void *>(this));
}
@@ -280,9 +238,105 @@ cPlayer::~cPlayer(void)
void cPlayer::OnAddToWorld(cWorld & a_World)
{
Super::OnAddToWorld(a_World);
// Update world name tracking:
m_CurrentWorldName = m_World->GetName();
// Fix to stop the player falling through the world, until we get serversided collision detection:
FreezeInternal(GetPosition(), false);
// Set capabilities based on new world:
SetCapabilities();
// Send contents of the inventory window:
m_ClientHandle->SendWholeInventory(*m_CurrentWindow);
// Send health (the respawn packet, which understandably resets health, is also used for world travel...):
m_ClientHandle->SendHealth();
// Send experience, similar story with the respawn packet:
m_ClientHandle->SendExperience();
// Send hotbar active slot (also reset by respawn):
m_ClientHandle->SendHeldItemChange(m_Inventory.GetEquippedSlotNum());
// Update player team:
UpdateTeam();
// Send scoreboard data:
m_World->GetScoreBoard().SendTo(*m_ClientHandle);
// Update the view distance:
m_ClientHandle->SetViewDistance(m_ClientHandle->GetRequestedViewDistance());
// Send current weather of target world:
m_ClientHandle->SendWeather(a_World.GetWeather());
// Send time:
m_ClientHandle->SendTimeUpdate(a_World.GetWorldAge(), a_World.GetTimeOfDay(), a_World.IsDaylightCycleEnabled());
// Finally, deliver the notification hook:
cRoot::Get()->GetPluginManager()->CallHookPlayerSpawned(*this);
}
void cPlayer::OnRemoveFromWorld(cWorld & a_World)
{
Super::OnRemoveFromWorld(a_World);
// Remove any references to this player pointer by windows in the old world:
CloseWindow(false);
// Remove the client handle from the world:
m_World->RemoveClientFromChunks(m_ClientHandle.get());
if (m_ClientHandle->IsDestroyed()) // Note: checking IsWorldChangeScheduled not appropriate here since we can disconnecting while having a scheduled warp
{
// Disconnecting, do the necessary cleanup.
// This isn't in the destructor to avoid crashing accessing destroyed objects during shutdown.
if (!cRoot::Get()->GetPluginManager()->CallHookPlayerDestroyed(*this))
{
cRoot::Get()->BroadcastChatLeave(Printf("%s has left the game", GetName().c_str()));
LOGINFO("Player %s has left the game", GetName().c_str());
}
// Remove ourself from everyone's lists:
cRoot::Get()->BroadcastPlayerListsRemovePlayer(*this);
// Atomically decrement player count (in world thread):
cRoot::Get()->GetServer()->PlayerDestroyed();
// We're just disconnecting. The remaining code deals with going through portals, so bail:
return;
}
const auto DestinationDimension = m_WorldChangeInfo.m_NewWorld->GetDimension();
// Award relevant achievements:
if (DestinationDimension == dimEnd)
{
AwardAchievement(Statistic::AchTheEnd);
}
else if (DestinationDimension == dimNether)
{
AwardAchievement(Statistic::AchPortal);
}
// Clear sent chunk lists from the clienthandle:
m_ClientHandle->RemoveFromWorld();
// The clienthandle caches the coords of the chunk we're standing at. Invalidate this.
m_ClientHandle->InvalidateCachedSentChunk();
// Clientside warp start:
m_ClientHandle->SendRespawn(DestinationDimension, false);
}
@@ -313,30 +367,23 @@ void cPlayer::SpawnOn(cClientHandle & a_Client)
void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
{
if (m_ClientHandle != nullptr)
{
if (m_ClientHandle->IsDestroyed())
{
// This should not happen, because destroying a client will remove it from the world, but just in case
ASSERT(!"Player ticked whilst in the process of destruction!");
m_ClientHandle = nullptr;
return;
}
m_ClientHandle->Tick(a_Dt.count());
if (!m_ClientHandle->IsPlaying())
{
// We're not yet in the game, ignore everything
return;
}
}
else
if (m_ClientHandle->IsDestroyed())
{
ASSERT(!"Player ticked whilst in the process of destruction!");
Destroy();
return;
}
if (!m_ClientHandle->IsPlaying())
{
// We're not yet in the game, ignore everything:
return;
}
m_Stats.AddValue(Statistic::PlayOneMinute);
m_Stats.AddValue(Statistic::TimeSinceDeath);
if (IsCrouched())
{
m_Stats.AddValue(Statistic::SneakTime);
@@ -356,15 +403,26 @@ void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
Detach();
}
// Handle a frozen player
if (!a_Chunk.IsValid())
{
// Players are ticked even if the parent chunk is invalid.
// We've processed as much as we can, bail:
return;
}
ASSERT((GetParentChunk() != nullptr) && (GetParentChunk()->IsValid()));
ASSERT(a_Chunk.IsValid());
// Handle a frozen player:
TickFreezeCode();
if (m_IsFrozen)
if (
m_IsFrozen || // Don't do Tick updates if frozen
IsWorldChangeScheduled() // If we're about to change worlds (e.g. respawn), abort processing all world interactions (GH #3939)
)
{
return;
}
ASSERT((GetParentChunk() != nullptr) && (GetParentChunk()->IsValid()));
ASSERT(a_Chunk.IsValid());
Super::Tick(a_Dt, a_Chunk);
@@ -374,12 +432,6 @@ void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
m_BowCharge += 1;
}
// Handle updating experience
if (m_bDirtyExperience)
{
SendExperience();
}
BroadcastMovementUpdate(m_ClientHandle.get());
if (m_Health > 0) // make sure player is alive
@@ -504,6 +556,15 @@ int cPlayer::CalcLevelFromXp(int a_XpTotal)
const std::set<UInt32> & cPlayer::GetKnownRecipes() const
{
return m_KnownRecipes;
}
int cPlayer::XpForLevel(int a_Level)
{
// level 0 to 15
@@ -558,8 +619,8 @@ bool cPlayer::SetCurrentExperience(int a_CurrentXp)
m_CurrentXp = a_CurrentXp;
// Set experience to be updated
m_bDirtyExperience = true;
// Update experience:
m_ClientHandle->SendExperience();
return true;
}
@@ -591,8 +652,8 @@ int cPlayer::DeltaExperience(int a_Xp_delta)
LOGD("Player \"%s\" gained / lost %d experience, total is now: %d", GetName().c_str(), a_Xp_delta, m_CurrentXp);
// Set experience to be updated
m_bDirtyExperience = true;
// Set experience to be updated:
m_ClientHandle->SendExperience();
return m_CurrentXp;
}
@@ -661,7 +722,7 @@ void cPlayer::SetTouchGround(bool a_bTouchGround)
void cPlayer::Heal(int a_Health)
{
Super::Heal(a_Health);
SendHealth();
m_ClientHandle->SendHealth();
}
@@ -679,7 +740,7 @@ void cPlayer::SetFoodLevel(int a_FoodLevel)
}
m_FoodLevel = FoodLevel;
SendHealth();
m_ClientHandle->SendHealth();
}
@@ -807,43 +868,6 @@ void cPlayer::AbortEating(void)
void cPlayer::SendHealth(void)
{
if (m_ClientHandle != nullptr)
{
m_ClientHandle->SendHealth();
}
}
void cPlayer::SendHotbarActiveSlot(void)
{
if (m_ClientHandle != nullptr)
{
m_ClientHandle->SendHeldItemChange(m_Inventory.GetEquippedSlotNum());
}
}
void cPlayer::SendExperience(void)
{
if (m_ClientHandle != nullptr)
{
m_ClientHandle->SendExperience();
m_bDirtyExperience = false;
}
}
void cPlayer::ClearInventoryPaintSlots(void)
{
// Clear the list of slots that are being inventory-painted. Used by cWindow only
@@ -1007,7 +1031,7 @@ void cPlayer::SetCustomName(const AString & a_CustomName)
}
m_World->BroadcastPlayerListAddPlayer(*this);
m_World->BroadcastSpawnEntity(*this, GetClientHandle());
m_World->BroadcastSpawnEntity(*this, m_ClientHandle.get());
}
@@ -1017,7 +1041,7 @@ void cPlayer::SetCustomName(const AString & a_CustomName)
void cPlayer::SetBedPos(const Vector3i & a_Pos)
{
m_LastBedPos = a_Pos;
m_SpawnWorld = m_World;
m_SpawnWorldName = m_World->GetName();
}
@@ -1028,7 +1052,7 @@ void cPlayer::SetBedPos(const Vector3i & a_Pos, cWorld * a_World)
{
m_LastBedPos = a_Pos;
ASSERT(a_World != nullptr);
m_SpawnWorld = a_World;
m_SpawnWorldName = a_World->GetName();
}
@@ -1037,7 +1061,12 @@ void cPlayer::SetBedPos(const Vector3i & a_Pos, cWorld * a_World)
cWorld * cPlayer::GetBedWorld()
{
return m_SpawnWorld;
if (const auto World = cRoot::Get()->GetWorld(m_SpawnWorldName); World != nullptr)
{
return World;
}
return cRoot::Get()->GetDefaultWorld();
}
@@ -1106,7 +1135,7 @@ bool cPlayer::DoTakeDamage(TakeDamageInfo & a_TDI)
{
// Any kind of damage adds food exhaustion
AddFoodExhaustion(0.3f);
SendHealth();
m_ClientHandle->SendHealth();
// Tell the wolves
if (a_TDI.Attacker != nullptr)
@@ -1296,17 +1325,16 @@ void cPlayer::Respawn(void)
m_LifetimeTotalXp = 0;
// ToDo: send score to client? How?
m_ClientHandle->SendRespawn(m_SpawnWorld->GetDimension(), true);
// Extinguish the fire:
StopBurning();
if (GetWorld() != m_SpawnWorld)
if (const auto BedWorld = GetBedWorld(); m_World != BedWorld)
{
MoveToWorld(*m_SpawnWorld, GetLastBedPos(), false, false);
MoveToWorld(*BedWorld, GetLastBedPos(), false, false);
}
else
{
m_ClientHandle->SendRespawn(m_World->GetDimension(), true);
TeleportToCoords(GetLastBedPos().x, GetLastBedPos().y, GetLastBedPos().z);
}
@@ -1380,6 +1408,15 @@ bool cPlayer::CanMobsTarget(void) const
AString cPlayer::GetIP(void) const
{
return m_ClientHandle->GetIPString();
}
void cPlayer::SetTeam(cTeam * a_Team)
{
if (m_Team == a_Team)
@@ -1600,6 +1637,15 @@ void cPlayer::SendAboveActionBarMessage(const cCompositeChat & a_Message)
const AString & cPlayer::GetName(void) const
{
return m_ClientHandle->GetUsername();
}
void cPlayer::SetGameMode(eGameMode a_GameMode)
{
if ((a_GameMode < gmMin) || (a_GameMode >= gmMax))
@@ -1671,15 +1717,6 @@ void cPlayer::SetCapabilities()
void cPlayer::SetIP(const AString & a_IP)
{
m_IP = a_IP;
}
void cPlayer::AwardAchievement(const Statistic a_Ach)
{
// Check if the prerequisites are met:
@@ -1753,7 +1790,7 @@ void cPlayer::Unfreeze()
GetClientHandle()->SendPlayerMaxSpeed();
m_IsFrozen = false;
BroadcastMovementUpdate(GetClientHandle());
BroadcastMovementUpdate(m_ClientHandle.get());
GetClientHandle()->SendPlayerPosition();
}
@@ -1819,6 +1856,29 @@ Vector3d cPlayer::GetThrowSpeed(double a_SpeedCoeff) const
eGameMode cPlayer::GetEffectiveGameMode(void) const
{
// Since entities' m_World aren't set until Initialize, but cClientHandle sends the player's gamemode early
// the below block deals with m_World being nullptr when called.
auto World = m_World;
if (World == nullptr)
{
World = cRoot::Get()->GetDefaultWorld();
}
else if (IsWorldChangeScheduled())
{
World = m_WorldChangeInfo.m_NewWorld;
}
return (m_GameMode == gmNotSet) ? World->GetGameMode() : m_GameMode;
}
void cPlayer::ForceSetSpeed(const Vector3d & a_Speed)
{
SetSpeed(a_Speed);
@@ -2099,79 +2159,6 @@ void cPlayer::TossPickup(const cItem & a_Item)
void cPlayer::DoMoveToWorld(const cEntity::sWorldChangeInfo & a_WorldChangeInfo)
{
ASSERT(a_WorldChangeInfo.m_NewWorld != nullptr);
// Reset portal cooldown
if (a_WorldChangeInfo.m_SetPortalCooldown)
{
m_PortalCooldownData.m_TicksDelayed = 0;
m_PortalCooldownData.m_ShouldPreventTeleportation = true;
}
if (m_World == a_WorldChangeInfo.m_NewWorld)
{
// Moving to same world, don't need to remove from world
SetPosition(a_WorldChangeInfo.m_NewPosition);
return;
}
LOGD("Warping player \"%s\" from world \"%s\" to \"%s\". Source chunk: (%d, %d) ",
GetName(), GetWorld()->GetName(), a_WorldChangeInfo.m_NewWorld->GetName(),
GetChunkX(), GetChunkZ()
);
// Stop all mobs from targeting this player
StopEveryoneFromTargetingMe();
// If player is attached to entity, detach, to prevent client side effects
Detach();
// Prevent further ticking in this world
SetIsTicking(false);
// Remove from the old world
auto & OldWorld = *GetWorld();
auto Self = OldWorld.RemovePlayer(*this);
ResetPosition(a_WorldChangeInfo.m_NewPosition);
FreezeInternal(a_WorldChangeInfo.m_NewPosition, false);
SetWorld(a_WorldChangeInfo.m_NewWorld); // Chunks may be streamed before cWorld::AddPlayer() sets the world to the new value
// Set capabilities based on new world
SetCapabilities();
cClientHandle * ch = GetClientHandle();
if (ch != nullptr)
{
// The clienthandle caches the coords of the chunk we're standing at. Invalidate this.
ch->InvalidateCachedSentChunk();
// Send the respawn packet:
if (a_WorldChangeInfo.m_SendRespawn)
{
ch->SendRespawn(a_WorldChangeInfo.m_NewWorld->GetDimension());
}
// Update the view distance.
ch->SetViewDistance(ch->GetRequestedViewDistance());
// Send current weather of target world to player
if (a_WorldChangeInfo.m_NewWorld->GetDimension() == dimOverworld)
{
ch->SendWeather(a_WorldChangeInfo.m_NewWorld->GetWeather());
}
}
// New world will take over and announce client at its next tick
a_WorldChangeInfo.m_NewWorld->AddPlayer(std::move(Self), &OldWorld);
}
bool cPlayer::LoadFromDisk(cWorldPtr & a_World)
{
LoadRank();
@@ -2334,7 +2321,7 @@ bool cPlayer::LoadFromFile(const AString & a_FileName, cWorldPtr & a_World)
cEnderChestEntity::LoadFromJson(root["enderchestinventory"], m_EnderChestContents);
m_LoadedWorldName = root.get("world", "world").asString();
m_CurrentWorldName = root.get("world", "world").asString();
a_World = cRoot::Get()->GetWorld(GetLoadedWorldName());
if (a_World == nullptr)
{
@@ -2345,18 +2332,13 @@ 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();
AString SpawnWorldName = root.get("SpawnWorld", cRoot::Get()->GetDefaultWorld()->GetName()).asString();
m_SpawnWorld = cRoot::Get()->GetWorld(SpawnWorldName);
if (m_SpawnWorld == nullptr)
{
m_SpawnWorld = cRoot::Get()->GetDefaultWorld();
}
m_SpawnWorldName = root.get("SpawnWorld", cRoot::Get()->GetDefaultWorld()->GetName()).asString();
try
{
// Load the player stats.
// We use the default world name (like bukkit) because stats are shared between dimensions / worlds.
StatSerializer::Load(m_Stats, cRoot::Get()->GetDefaultWorld()->GetDataPath(), GetUUID().ToLongString());
StatSerializer::Load(m_Stats, m_DefaultWorldPath, GetUUID().ToLongString());
}
catch (...)
{
@@ -2460,27 +2442,10 @@ bool cPlayer::SaveToDisk()
root["SpawnX"] = GetLastBedPos().x;
root["SpawnY"] = GetLastBedPos().y;
root["SpawnZ"] = GetLastBedPos().z;
root["SpawnWorld"] = m_SpawnWorld->GetName();
root["SpawnWorld"] = m_SpawnWorldName;
root["enchantmentSeed"] = m_EnchantmentSeed;
if (m_World != nullptr)
{
root["world"] = m_World->GetName();
if (m_GameMode == m_World->GetGameMode())
{
root["gamemode"] = static_cast<int>(eGameMode_NotSet);
}
else
{
root["gamemode"] = static_cast<int>(m_GameMode);
}
}
else
{
// This happens if the player is saved to new format after loading from the old format
root["world"] = m_LoadedWorldName;
root["gamemode"] = static_cast<int>(eGameMode_NotSet);
}
root["world"] = m_CurrentWorldName;
root["gamemode"] = static_cast<int>(m_GameMode);
auto JsonData = JsonUtils::WriteStyledString(root);
AString SourceFile = GetUUIDFileName(m_UUID);
@@ -2505,7 +2470,8 @@ bool cPlayer::SaveToDisk()
{
// Save the player stats.
// We use the default world name (like bukkit) because stats are shared between dimensions / worlds.
StatSerializer::Save(m_Stats, cRoot::Get()->GetDefaultWorld()->GetDataPath(), GetUUID().ToLongString());
// TODO: save together with player.dat, not in some other place.
StatSerializer::Save(m_Stats, m_DefaultWorldPath, GetUUID().ToLongString());
}
catch (...)
{
@@ -2787,7 +2753,7 @@ void cPlayer::LoadRank(void)
else
{
// Update the name:
RankMgr->UpdatePlayerName(m_UUID, m_PlayerName);
RankMgr->UpdatePlayerName(m_UUID, GetName());
}
m_Permissions = RankMgr->GetPlayerPermissions(m_UUID);
m_Restrictions = RankMgr->GetPlayerRestrictions(m_UUID);
@@ -3062,16 +3028,6 @@ void cPlayer::Detach()
void cPlayer::RemoveClientHandle(void)
{
ASSERT(m_ClientHandle != nullptr);
m_ClientHandle.reset();
}
AString cPlayer::GetUUIDFileName(const cUUID & a_UUID)
{
AString UUID = a_UUID.ToLongString();