From 054a89dd9e5d6819adede9d7ba781b69f98ff2f4 Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Wed, 6 Jan 2021 00:35:42 +0000 Subject: 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. --- src/Entities/Player.cpp | 458 ++++++++++++++++++++++-------------------------- 1 file changed, 207 insertions(+), 251 deletions(-) (limited to 'src/Entities/Player.cpp') diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp index 391a5ce71..d20795643 100644 --- a/src/Entities/Player.cpp +++ b/src/Entities/Player.cpp @@ -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(World->GetSpawnX()), static_cast(World->GetSpawnY()), static_cast(World->GetSpawnZ())), World); - SetWorld(World); // Use default world - m_EnchantmentSeed = GetRandomProvider().RandInt(); // 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(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(static_cast(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(this), GetUniqueID()); - SaveToDisk(); + // "Times ragequit": + m_Stats.AddValue(Statistic::LeaveGame); - m_ClientHandle = nullptr; + SaveToDisk(); delete m_InventoryWindow; - m_InventoryWindow = nullptr; LOGD("Player %p deleted", static_cast(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,16 +403,27 @@ void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) Detach(); } - // Handle a frozen player - TickFreezeCode(); - if (m_IsFrozen) + 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((GetParentChunk() != nullptr) && (GetParentChunk()->IsValid())); ASSERT(a_Chunk.IsValid()); + // Handle a frozen player: + TickFreezeCode(); + + 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; + } + Super::Tick(a_Dt, a_Chunk); // Handle charging the bow: @@ -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 & 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(eGameMode_NotSet); - } - else - { - root["gamemode"] = static_cast(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(eGameMode_NotSet); - } + root["world"] = m_CurrentWorldName; + root["gamemode"] = static_cast(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(); -- cgit v1.2.3