diff options
Diffstat (limited to 'src/Entities/Player.cpp')
-rw-r--r-- | src/Entities/Player.cpp | 458 |
1 files changed, 207 insertions, 251 deletions
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<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()); - SaveToDisk(); + // "Times ragequit": + m_Stats.AddValue(Statistic::LeaveGame); - m_ClientHandle = nullptr; + SaveToDisk(); 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,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<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(); |