From be2bf999c280d6892b38272d3f783b3462f0b745 Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Sat, 6 Feb 2021 18:37:03 +0000 Subject: Fix #5118 --- src/Entities/Entity.cpp | 47 +- src/Entities/Entity.h | 11 - src/Entities/Minecart.cpp | 37 +- src/Entities/Minecart.h | 3 - src/Entities/Player.cpp | 1961 +++++++++++++++++++++++---------------------- src/Entities/Player.h | 67 +- 6 files changed, 1011 insertions(+), 1115 deletions(-) (limited to 'src/Entities') diff --git a/src/Entities/Entity.cpp b/src/Entities/Entity.cpp index a185b8f69..d6bb057f4 100644 --- a/src/Entities/Entity.cpp +++ b/src/Entities/Entity.cpp @@ -1715,17 +1715,6 @@ void cEntity::SetIsTicking(bool a_IsTicking) -void cEntity::DoSetSpeed(double a_SpeedX, double a_SpeedY, double a_SpeedZ) -{ - m_Speed.Set(a_SpeedX, a_SpeedY, a_SpeedZ); - - WrapSpeed(); -} - - - - - void cEntity::HandleAir(void) { // Ref.: https://minecraft.gamepedia.com/Chunk_format @@ -2095,7 +2084,8 @@ void cEntity::SetRoll(double a_Roll) void cEntity::SetSpeed(double a_SpeedX, double a_SpeedY, double a_SpeedZ) { - DoSetSpeed(a_SpeedX, a_SpeedY, a_SpeedZ); + m_Speed.Set(a_SpeedX, a_SpeedY, a_SpeedZ); + WrapSpeed(); } @@ -2140,7 +2130,7 @@ void cEntity::SetWidth(double a_Width) void cEntity::AddSpeed(double a_AddSpeedX, double a_AddSpeedY, double a_AddSpeedZ) { - DoSetSpeed(m_Speed.x + a_AddSpeedX, m_Speed.y + a_AddSpeedY, m_Speed.z + a_AddSpeedZ); + SetSpeed(m_Speed.x + a_AddSpeedX, m_Speed.y + a_AddSpeedY, m_Speed.z + a_AddSpeedZ); } @@ -2280,34 +2270,3 @@ void cEntity::BroadcastLeashedMobs() } } } - - - - - -float cEntity::GetExplosionExposureRate(Vector3d a_ExplosionPosition, float a_ExlosionPower) -{ - double EntitySize = m_Width * m_Width * m_Height; - if (EntitySize <= 0) - { - // Handle entity with invalid size - return 0; - } - - auto EntityBox = GetBoundingBox(); - cBoundingBox ExplosionBox(a_ExplosionPosition, a_ExlosionPower * 2.0); - cBoundingBox IntersectionBox(EntityBox); - - bool Overlap = EntityBox.Intersect(ExplosionBox, IntersectionBox); - if (Overlap) - { - Vector3d Diff = IntersectionBox.GetMax() - IntersectionBox.GetMin(); - double OverlapSize = Diff.x * Diff.y * Diff.z; - - return static_cast(OverlapSize / EntitySize); - } - else - { - return 0; - } -} diff --git a/src/Entities/Entity.h b/src/Entities/Entity.h index 143993dad..9fe7f16f5 100644 --- a/src/Entities/Entity.h +++ b/src/Entities/Entity.h @@ -591,12 +591,6 @@ public: /** Returs whether the entity has any mob leashed to it. */ bool HasAnyMobLeashed() const { return m_LeashedMobs.size() > 0; } - /** a lightweight calculation approach to get explosion exposure rate - @param a_ExplosionPosition explosion position - @param a_ExlosionPower explosion power - @return exposure rate */ - virtual float GetExplosionExposureRate(Vector3d a_ExplosionPosition, float a_ExlosionPower); - protected: @@ -705,11 +699,6 @@ protected: /** The number of ticks this entity has been alive for */ long int m_TicksAlive; - - /** Does the actual speed-setting. The default implementation just sets the member variable value; - overrides can provide further processing, such as forcing players to move at the given speed. */ - virtual void DoSetSpeed(double a_SpeedX, double a_SpeedY, double a_SpeedZ); - /** Handles the moving of this entity between worlds. Should handle degenerate cases such as moving to the same world. */ void DoMoveToWorld(const sWorldChangeInfo & a_WorldChangeInfo); diff --git a/src/Entities/Minecart.cpp b/src/Entities/Minecart.cpp index 3ddcea6ee..96eebe2fd 100644 --- a/src/Entities/Minecart.cpp +++ b/src/Entities/Minecart.cpp @@ -216,6 +216,9 @@ void cMinecart::HandlePhysics(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) m_DetectorRailPosition = Vector3i(POSX_TOINT, POSY_TOINT, POSZ_TOINT); } + // Enforce speed limit: + m_Speed.Clamp(MAX_SPEED_NEGATIVE, MAX_SPEED); + // Broadcast positioning changes to client BroadcastMovementUpdate(); } @@ -1268,40 +1271,6 @@ void cMinecart::ApplyAcceleration(Vector3d a_ForwardDirection, double a_Accelera -void cMinecart::DoSetSpeed(double a_SpeedX, double a_SpeedY, double a_SpeedZ) -{ - if (a_SpeedX > MAX_SPEED) - { - a_SpeedX = MAX_SPEED; - } - else if (a_SpeedX < MAX_SPEED_NEGATIVE) - { - a_SpeedX = MAX_SPEED_NEGATIVE; - } - if (a_SpeedY > MAX_SPEED) - { - a_SpeedY = MAX_SPEED; - } - else if (a_SpeedY < MAX_SPEED_NEGATIVE) - { - a_SpeedY = MAX_SPEED_NEGATIVE; - } - if (a_SpeedZ > MAX_SPEED) - { - a_SpeedZ = MAX_SPEED; - } - else if (a_SpeedZ < MAX_SPEED_NEGATIVE) - { - a_SpeedZ = MAX_SPEED_NEGATIVE; - } - - Super::DoSetSpeed(a_SpeedX, a_SpeedY, a_SpeedZ); -} - - - - - //////////////////////////////////////////////////////////////////////////////// // cRideableMinecart: diff --git a/src/Entities/Minecart.h b/src/Entities/Minecart.h index 73011b7b5..a98014799 100644 --- a/src/Entities/Minecart.h +++ b/src/Entities/Minecart.h @@ -56,9 +56,6 @@ protected: /** Applies an acceleration to the minecart parallel to a_ForwardDirection but without allowing backward speed. */ void ApplyAcceleration(Vector3d a_ForwardDirection, double a_Acceleration); - // Overwrite to enforce speed limit - virtual void DoSetSpeed(double a_SpeedX, double a_SpeedY, double a_SpeedZ) override; - cMinecart(ePayload a_Payload, Vector3d a_Pos); /** Handles physics on normal rails diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp index d20795643..e3994e88c 100644 --- a/src/Entities/Player.cpp +++ b/src/Entities/Player.cpp @@ -179,354 +179,285 @@ cPlayer::cPlayer(const cClientHandlePtr & a_Client) : -void cPlayer::AddKnownItem(const cItem & a_Item) +cPlayer::~cPlayer(void) { - if (a_Item.m_ItemType < 0) - { - return; - } + LOGD("Deleting cPlayer \"%s\" at %p, ID %d", GetName().c_str(), static_cast(this), GetUniqueID()); - auto Response = m_KnownItems.insert(a_Item.CopyOne()); - if (!Response.second) + // "Times ragequit": + m_Stats.AddValue(Statistic::LeaveGame); + + SaveToDisk(); + + delete m_InventoryWindow; + + LOGD("Player %p deleted", static_cast(this)); +} + + + + + +int cPlayer::CalcLevelFromXp(int a_XpTotal) +{ + // level 0 to 15 + if (a_XpTotal <= XP_TO_LEVEL15) { - // The item was already known, bail out: - return; + return a_XpTotal / XP_PER_LEVEL_TO15; } - // Process the recipes that got unlocked by this newly-known item: - auto Recipes = cRoot::Get()->GetCraftingRecipes()->FindNewRecipesForItem(a_Item, m_KnownItems); - for (const auto & RecipeId : Recipes) + // level 30+ + if (a_XpTotal > XP_TO_LEVEL30) { - AddKnownRecipe(RecipeId); + return static_cast((151.5 + sqrt( 22952.25 - (14 * (2220 - a_XpTotal)))) / 7); } + + // level 16 to 30 + return static_cast((29.5 + sqrt( 870.25 - (6 * ( 360 - a_XpTotal)))) / 3); } -void cPlayer::AddKnownRecipe(UInt32 a_RecipeId) +const std::set & cPlayer::GetKnownRecipes() const { - auto Response = m_KnownRecipes.insert(a_RecipeId); - if (!Response.second) + return m_KnownRecipes; +} + + + + + +int cPlayer::XpForLevel(int a_Level) +{ + // level 0 to 15 + if (a_Level <= 15) { - // The recipe was already known, bail out: - return; + return a_Level * XP_PER_LEVEL_TO15; } - m_ClientHandle->SendUnlockRecipe(a_RecipeId); + + // level 30+ + if (a_Level >= 31) + { + return static_cast((3.5 * a_Level * a_Level) - (151.5 * a_Level) + 2220); + } + + // level 16 to 30 + return static_cast((1.5 * a_Level * a_Level) - (29.5 * a_Level) + 360); } -cPlayer::~cPlayer(void) +int cPlayer::GetXpLevel() const { - LOGD("Deleting cPlayer \"%s\" at %p, ID %d", GetName().c_str(), static_cast(this), GetUniqueID()); + return CalcLevelFromXp(m_CurrentXp); +} - // "Times ragequit": - m_Stats.AddValue(Statistic::LeaveGame); - SaveToDisk(); - delete m_InventoryWindow; - LOGD("Player %p deleted", static_cast(this)); + +float cPlayer::GetXpPercentage() const +{ + int currentLevel = CalcLevelFromXp(m_CurrentXp); + int currentLevel_XpBase = XpForLevel(currentLevel); + + return static_cast(m_CurrentXp - currentLevel_XpBase) / + static_cast(XpForLevel(1 + currentLevel) - currentLevel_XpBase); } -void cPlayer::OnAddToWorld(cWorld & a_World) +bool cPlayer::SetCurrentExperience(int a_CurrentXp) { - Super::OnAddToWorld(a_World); + if (!(a_CurrentXp >= 0) || (a_CurrentXp > (std::numeric_limits::max() - m_LifetimeTotalXp))) + { + LOGWARNING("Tried to update experiece with an invalid Xp value: %d", a_CurrentXp); + return false; // oops, they gave us a dodgey number + } - // Update world name tracking: - m_CurrentWorldName = m_World->GetName(); + m_CurrentXp = a_CurrentXp; - // Fix to stop the player falling through the world, until we get serversided collision detection: - FreezeInternal(GetPosition(), false); + // Update experience: + m_ClientHandle->SendExperience(); - // Set capabilities based on new world: - SetCapabilities(); + return true; +} - // 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(); +int cPlayer::DeltaExperience(int a_Xp_delta) +{ + if (a_Xp_delta > (std::numeric_limits().max() - m_CurrentXp)) + { + // Value was bad, abort and report + LOGWARNING("Attempt was made to increment Xp by %d, which overflowed the int datatype. Ignoring.", a_Xp_delta); + return -1; // Should we instead just return the current Xp? + } - // Send scoreboard data: - m_World->GetScoreBoard().SendTo(*m_ClientHandle); + m_CurrentXp += a_Xp_delta; - // Update the view distance: - m_ClientHandle->SetViewDistance(m_ClientHandle->GetRequestedViewDistance()); + // Make sure they didn't subtract too much + m_CurrentXp = std::max(m_CurrentXp, 0); - // 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()); + // Update total for score calculation + if (a_Xp_delta > 0) + { + m_LifetimeTotalXp += a_Xp_delta; + } - // Finally, deliver the notification hook: - cRoot::Get()->GetPluginManager()->CallHookPlayerSpawned(*this); + 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_ClientHandle->SendExperience(); + + return m_CurrentXp; } -void cPlayer::OnRemoveFromWorld(cWorld & a_World) +void cPlayer::StartChargingBow(void) { - Super::OnRemoveFromWorld(a_World); - - // Remove any references to this player pointer by windows in the old world: - CloseWindow(false); + LOGD("Player \"%s\" started charging their bow", GetName().c_str()); + m_IsChargingBow = true; + m_BowCharge = 0; + m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get()); +} - // 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(); +int cPlayer::FinishChargingBow(void) +{ + LOGD("Player \"%s\" finished charging their bow at a charge of %d", GetName().c_str(), m_BowCharge); + int res = m_BowCharge; + m_IsChargingBow = false; + m_BowCharge = 0; + m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get()); - // We're just disconnecting. The remaining code deals with going through portals, so bail: - return; - } + return res; +} - 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); +void cPlayer::CancelChargingBow(void) +{ + LOGD("Player \"%s\" cancelled charging their bow at a charge of %d", GetName().c_str(), m_BowCharge); + m_IsChargingBow = false; + m_BowCharge = 0; + m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get()); } -void cPlayer::SpawnOn(cClientHandle & a_Client) +void cPlayer::SetTouchGround(bool a_bTouchGround) { - if (!m_bVisible || (m_ClientHandle.get() == (&a_Client))) + if (IsGameModeSpectator()) // You can fly through the ground in Spectator { return; } - LOGD("Spawing %s on %s", GetName().c_str(), a_Client.GetUsername().c_str()); + UNUSED(a_bTouchGround); + /* Unfortunately whatever the reason, there are still desyncs in on-ground status between the client and server. For example: + 1. Walking off a ledge (whatever height) + 2. Initial login + Thus, it is too risky to compare their value against ours and kick them for hacking */ +} - a_Client.SendPlayerSpawn(*this); - a_Client.SendEntityHeadLook(*this); - a_Client.SendEntityEquipment(*this, 0, m_Inventory.GetEquippedItem()); - a_Client.SendEntityEquipment(*this, 1, m_Inventory.GetEquippedBoots()); - a_Client.SendEntityEquipment(*this, 2, m_Inventory.GetEquippedLeggings()); - a_Client.SendEntityEquipment(*this, 3, m_Inventory.GetEquippedChestplate()); - a_Client.SendEntityEquipment(*this, 4, m_Inventory.GetEquippedHelmet()); + + + + +void cPlayer::Heal(int a_Health) +{ + Super::Heal(a_Health); + m_ClientHandle->SendHealth(); } -void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) +void cPlayer::SetFoodLevel(int a_FoodLevel) { - m_ClientHandle->Tick(a_Dt.count()); + int FoodLevel = Clamp(a_FoodLevel, 0, MAX_FOOD_LEVEL); - if (m_ClientHandle->IsDestroyed()) + if (cRoot::Get()->GetPluginManager()->CallHookPlayerFoodLevelChange(*this, FoodLevel)) { - Destroy(); + m_FoodSaturationLevel = 5.0; return; } - if (!m_ClientHandle->IsPlaying()) - { - // We're not yet in the game, ignore everything: - return; - } + m_FoodLevel = FoodLevel; + m_ClientHandle->SendHealth(); +} - m_Stats.AddValue(Statistic::PlayOneMinute); - m_Stats.AddValue(Statistic::TimeSinceDeath); - if (IsCrouched()) - { - m_Stats.AddValue(Statistic::SneakTime); - } - // Handle the player detach, when the player is in spectator mode - if ( - (IsGameModeSpectator()) && - (m_AttachedTo != nullptr) && - ( - (m_AttachedTo->IsDestroyed()) || // Watching entity destruction - (m_AttachedTo->GetHealth() <= 0) || // Watching entity dead - (IsCrouched()) // Or the player wants to be detached - ) - ) - { - Detach(); - } - 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()); +void cPlayer::SetFoodSaturationLevel(double a_FoodSaturationLevel) +{ + m_FoodSaturationLevel = Clamp(a_FoodSaturationLevel, 0.0, static_cast(m_FoodLevel)); +} - // 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: - if (m_IsChargingBow) - { - m_BowCharge += 1; - } - BroadcastMovementUpdate(m_ClientHandle.get()); +void cPlayer::SetFoodTickTimer(int a_FoodTickTimer) +{ + m_FoodTickTimer = a_FoodTickTimer; +} - if (m_Health > 0) // make sure player is alive - { - m_World->CollectPickupsByPlayer(*this); - if ((m_EatingFinishTick >= 0) && (m_EatingFinishTick <= m_World->GetWorldAge())) - { - FinishEating(); - } - HandleFood(); - } - if (m_IsFishing) - { - HandleFloater(); - } - // Update items (e.g. Maps) - m_Inventory.UpdateItems(); +void cPlayer::SetFoodExhaustionLevel(double a_FoodExhaustionLevel) +{ + m_FoodExhaustionLevel = Clamp(a_FoodExhaustionLevel, 0.0, 40.0); +} - // Send Player List (Once per m_LastPlayerListTime/1000 ms) - if (m_LastPlayerListTime + PLAYER_LIST_TIME_MS <= std::chrono::steady_clock::now()) - { - m_World->BroadcastPlayerListUpdatePing(*this); - m_LastPlayerListTime = std::chrono::steady_clock::now(); - } - if (m_TicksUntilNextSave == 0) - { - SaveToDisk(); - m_TicksUntilNextSave = PLAYER_INVENTORY_SAVE_INTERVAL; - } - else + + + +bool cPlayer::Feed(int a_Food, double a_Saturation) +{ + if (IsSatiated()) { - m_TicksUntilNextSave--; + return false; } + + SetFoodSaturationLevel(m_FoodSaturationLevel + a_Saturation); + SetFoodLevel(m_FoodLevel + a_Food); + return true; } -void cPlayer::TickFreezeCode() +void cPlayer::AddFoodExhaustion(double a_Exhaustion) { - if (m_IsFrozen) - { - if ((!m_IsManuallyFrozen) && (GetClientHandle()->IsPlayerChunkSent())) - { - // If the player was automatically frozen, unfreeze if the chunk the player is inside is loaded and sent - Unfreeze(); - - // Pull the player out of any solids that might have loaded on them. - PREPARE_REL_AND_CHUNK(GetPosition(), *(GetParentChunk())); - if (RelSuccess) - { - int NewY = Rel.y; - if (NewY < 0) - { - NewY = 0; - } - while (NewY < cChunkDef::Height - 2) - { - // If we find a position with enough space for the player - if ( - !cBlockInfo::IsSolid(Chunk->GetBlock(Rel.x, NewY, Rel.z)) && - !cBlockInfo::IsSolid(Chunk->GetBlock(Rel.x, NewY + 1, Rel.z)) - ) - { - // If the found position is not the same as the original - if (NewY != Rel.y) - { - SetPosition(GetPosition().x, NewY, GetPosition().z); - GetClientHandle()->SendPlayerPosition(); - } - break; - } - ++NewY; - } - } - } - else if (GetWorld()->GetWorldAge() % 100 == 0) - { - // Despite the client side freeze, the player may be able to move a little by - // Jumping or canceling flight. Re-freeze every now and then - FreezeInternal(GetPosition(), m_IsManuallyFrozen); - } - } - else + if (!(IsGameModeCreative() || IsGameModeSpectator())) { - if (!GetClientHandle()->IsPlayerChunkSent() || (!GetParentChunk()->IsValid())) - { - FreezeInternal(GetPosition(), false); - } + m_FoodExhaustionLevel = std::min(m_FoodExhaustionLevel + a_Exhaustion, 40.0); } } @@ -534,557 +465,575 @@ void cPlayer::TickFreezeCode() -int cPlayer::CalcLevelFromXp(int a_XpTotal) +void cPlayer::TossItems(const cItems & a_Items) { - // level 0 to 15 - if (a_XpTotal <= XP_TO_LEVEL15) + if (IsGameModeSpectator()) // Players can't toss items in spectator { - return a_XpTotal / XP_PER_LEVEL_TO15; + return; } - // level 30+ - if (a_XpTotal > XP_TO_LEVEL30) - { - return static_cast((151.5 + sqrt( 22952.25 - (14 * (2220 - a_XpTotal)))) / 7); - } + m_Stats.AddValue(Statistic::Drop, static_cast(a_Items.Size())); - // level 16 to 30 - return static_cast((29.5 + sqrt( 870.25 - (6 * ( 360 - a_XpTotal)))) / 3); + const auto Speed = (GetLookVector() + Vector3d(0, 0.2, 0)) * 6; // A dash of height and a dollop of speed + const auto Position = GetEyePosition() - Vector3d(0, 0.2, 0); // Correct for eye-height weirdness + m_World->SpawnItemPickups(a_Items, Position, Speed, true); // 'true' because created by player } -const std::set & cPlayer::GetKnownRecipes() const +void cPlayer::StartEating(void) { - return m_KnownRecipes; + // Set the timer: + m_EatingFinishTick = m_World->GetWorldAge() + EATING_TICKS; + + // Send the packets: + m_World->BroadcastEntityAnimation(*this, 3); + m_World->BroadcastEntityMetadata(*this); } -int cPlayer::XpForLevel(int a_Level) +void cPlayer::FinishEating(void) { - // level 0 to 15 - if (a_Level <= 15) - { - return a_Level * XP_PER_LEVEL_TO15; - } + // Reset the timer: + m_EatingFinishTick = -1; - // level 30+ - if (a_Level >= 31) + // Send the packets: + m_ClientHandle->SendEntityStatus(*this, esPlayerEatingAccepted); + m_World->BroadcastEntityMetadata(*this); + + // consume the item: + cItem Item(GetEquippedItem()); + Item.m_ItemCount = 1; + cItemHandler * ItemHandler = cItemHandler::GetItemHandler(Item.m_ItemType); + if (!ItemHandler->EatItem(this, &Item)) { - return static_cast((3.5 * a_Level * a_Level) - (151.5 * a_Level) + 2220); + return; } - - // level 16 to 30 - return static_cast((1.5 * a_Level * a_Level) - (29.5 * a_Level) + 360); + ItemHandler->OnFoodEaten(m_World, this, &Item); } -int cPlayer::GetXpLevel() const +void cPlayer::AbortEating(void) { - return CalcLevelFromXp(m_CurrentXp); + m_EatingFinishTick = -1; + m_World->BroadcastEntityMetadata(*this); } -float cPlayer::GetXpPercentage() const +void cPlayer::ClearInventoryPaintSlots(void) { - int currentLevel = CalcLevelFromXp(m_CurrentXp); - int currentLevel_XpBase = XpForLevel(currentLevel); - - return static_cast(m_CurrentXp - currentLevel_XpBase) / - static_cast(XpForLevel(1 + currentLevel) - currentLevel_XpBase); + // Clear the list of slots that are being inventory-painted. Used by cWindow only + m_InventoryPaintSlots.clear(); } -bool cPlayer::SetCurrentExperience(int a_CurrentXp) +void cPlayer::AddInventoryPaintSlot(int a_SlotNum) { - if (!(a_CurrentXp >= 0) || (a_CurrentXp > (std::numeric_limits::max() - m_LifetimeTotalXp))) - { - LOGWARNING("Tried to update experiece with an invalid Xp value: %d", a_CurrentXp); - return false; // oops, they gave us a dodgey number - } + // Add a slot to the list for inventory painting. Used by cWindow only + m_InventoryPaintSlots.push_back(a_SlotNum); +} - m_CurrentXp = a_CurrentXp; - // Update experience: - m_ClientHandle->SendExperience(); - return true; + + +const cSlotNums & cPlayer::GetInventoryPaintSlots(void) const +{ + // Return the list of slots currently stored for inventory painting. Used by cWindow only + return m_InventoryPaintSlots; } -int cPlayer::DeltaExperience(int a_Xp_delta) +double cPlayer::GetMaxSpeed(void) const { - if (a_Xp_delta > (std::numeric_limits().max() - m_CurrentXp)) + if (m_IsFlying) { - // Value was bad, abort and report - LOGWARNING("Attempt was made to increment Xp by %d, which overflowed the int datatype. Ignoring.", a_Xp_delta); - return -1; // Should we instead just return the current Xp? + return m_FlyingMaxSpeed; } - - m_CurrentXp += a_Xp_delta; - - // Make sure they didn't subtract too much - m_CurrentXp = std::max(m_CurrentXp, 0); - - - // Update total for score calculation - if (a_Xp_delta > 0) + else if (m_IsSprinting) { - m_LifetimeTotalXp += a_Xp_delta; + return m_SprintingMaxSpeed; + } + else + { + return m_NormalMaxSpeed; } +} - 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_ClientHandle->SendExperience(); - return m_CurrentXp; -} - - - -void cPlayer::StartChargingBow(void) +void cPlayer::SetNormalMaxSpeed(double a_Speed) { - LOGD("Player \"%s\" started charging their bow", GetName().c_str()); - m_IsChargingBow = true; - m_BowCharge = 0; - m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get()); + m_NormalMaxSpeed = a_Speed; + if (!m_IsSprinting && !m_IsFlying && !m_IsFrozen) + { + // If we are frozen, we do not send this yet. We send when unfreeze() is called + m_ClientHandle->SendPlayerMaxSpeed(); + } } -int cPlayer::FinishChargingBow(void) +void cPlayer::SetSprintingMaxSpeed(double a_Speed) { - LOGD("Player \"%s\" finished charging their bow at a charge of %d", GetName().c_str(), m_BowCharge); - int res = m_BowCharge; - m_IsChargingBow = false; - m_BowCharge = 0; - m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get()); - - return res; + m_SprintingMaxSpeed = a_Speed; + if (m_IsSprinting && !m_IsFlying && !m_IsFrozen) + { + // If we are frozen, we do not send this yet. We send when unfreeze() is called + m_ClientHandle->SendPlayerMaxSpeed(); + } } -void cPlayer::CancelChargingBow(void) +void cPlayer::SetFlyingMaxSpeed(double a_Speed) { - LOGD("Player \"%s\" cancelled charging their bow at a charge of %d", GetName().c_str(), m_BowCharge); - m_IsChargingBow = false; - m_BowCharge = 0; - m_World->BroadcastEntityMetadata(*this, m_ClientHandle.get()); + m_FlyingMaxSpeed = a_Speed; + + // Update the flying speed, always: + if (!m_IsFrozen) + { + // If we are frozen, we do not send this yet. We send when unfreeze() is called + m_ClientHandle->SendPlayerAbilities(); + } } -void cPlayer::SetTouchGround(bool a_bTouchGround) +void cPlayer::SetCrouch(bool a_IsCrouched) { - if (IsGameModeSpectator()) // You can fly through the ground in Spectator + // Set the crouch status, broadcast to all visible players + if (a_IsCrouched == m_IsCrouched) { + // No change return; } - UNUSED(a_bTouchGround); - /* Unfortunately whatever the reason, there are still desyncs in on-ground status between the client and server. For example: - 1. Walking off a ledge (whatever height) - 2. Initial login - Thus, it is too risky to compare their value against ours and kick them for hacking */ -} - - - - + if (a_IsCrouched) + { + cRoot::Get()->GetPluginManager()->CallHookPlayerCrouched(*this); + } -void cPlayer::Heal(int a_Health) -{ - Super::Heal(a_Health); - m_ClientHandle->SendHealth(); + m_IsCrouched = a_IsCrouched; + m_World->BroadcastEntityMetadata(*this); } -void cPlayer::SetFoodLevel(int a_FoodLevel) +void cPlayer::SetSprint(bool a_IsSprinting) { - int FoodLevel = Clamp(a_FoodLevel, 0, MAX_FOOD_LEVEL); - - if (cRoot::Get()->GetPluginManager()->CallHookPlayerFoodLevelChange(*this, FoodLevel)) + if (a_IsSprinting == m_IsSprinting) { - m_FoodSaturationLevel = 5.0; + // No change return; } - m_FoodLevel = FoodLevel; - m_ClientHandle->SendHealth(); + m_IsSprinting = a_IsSprinting; + m_ClientHandle->SendPlayerMaxSpeed(); } -void cPlayer::SetFoodSaturationLevel(double a_FoodSaturationLevel) +void cPlayer::SetCanFly(bool a_CanFly) { - m_FoodSaturationLevel = Clamp(a_FoodSaturationLevel, 0.0, static_cast(m_FoodLevel)); -} - - - - + if (a_CanFly == m_CanFly) + { + return; + } -void cPlayer::SetFoodTickTimer(int a_FoodTickTimer) -{ - m_FoodTickTimer = a_FoodTickTimer; + m_CanFly = a_CanFly; + m_ClientHandle->SendPlayerAbilities(); } -void cPlayer::SetFoodExhaustionLevel(double a_FoodExhaustionLevel) +void cPlayer::SetCustomName(const AString & a_CustomName) { - m_FoodExhaustionLevel = Clamp(a_FoodExhaustionLevel, 0.0, 40.0); -} - - - + if (m_CustomName == a_CustomName) + { + return; + } + m_World->BroadcastPlayerListRemovePlayer(*this); -bool cPlayer::Feed(int a_Food, double a_Saturation) -{ - if (IsSatiated()) + m_CustomName = a_CustomName; + if (m_CustomName.length() > 16) { - return false; + m_CustomName = m_CustomName.substr(0, 16); } - SetFoodSaturationLevel(m_FoodSaturationLevel + a_Saturation); - SetFoodLevel(m_FoodLevel + a_Food); - return true; + m_World->BroadcastPlayerListAddPlayer(*this); + m_World->BroadcastSpawnEntity(*this, m_ClientHandle.get()); } -void cPlayer::AddFoodExhaustion(double a_Exhaustion) +void cPlayer::SetBedPos(const Vector3i & a_Pos) { - if (!(IsGameModeCreative() || IsGameModeSpectator())) - { - m_FoodExhaustionLevel = std::min(m_FoodExhaustionLevel + a_Exhaustion, 40.0); - } + m_LastBedPos = a_Pos; + m_SpawnWorldName = m_World->GetName(); } -void cPlayer::TossItems(const cItems & a_Items) +void cPlayer::SetBedPos(const Vector3i & a_Pos, cWorld * a_World) { - if (IsGameModeSpectator()) // Players can't toss items in spectator - { - return; - } - - m_Stats.AddValue(Statistic::Drop, static_cast(a_Items.Size())); - - const auto Speed = (GetLookVector() + Vector3d(0, 0.2, 0)) * 6; // A dash of height and a dollop of speed - const auto Position = GetEyePosition() - Vector3d(0, 0.2, 0); // Correct for eye-height weirdness - m_World->SpawnItemPickups(a_Items, Position, Speed, true); // 'true' because created by player + m_LastBedPos = a_Pos; + ASSERT(a_World != nullptr); + m_SpawnWorldName = a_World->GetName(); } -void cPlayer::StartEating(void) +cWorld * cPlayer::GetBedWorld() { - // Set the timer: - m_EatingFinishTick = m_World->GetWorldAge() + EATING_TICKS; + if (const auto World = cRoot::Get()->GetWorld(m_SpawnWorldName); World != nullptr) + { + return World; + } - // Send the packets: - m_World->BroadcastEntityAnimation(*this, 3); - m_World->BroadcastEntityMetadata(*this); + return cRoot::Get()->GetDefaultWorld(); } -void cPlayer::FinishEating(void) +void cPlayer::SetFlying(bool a_IsFlying) { - // Reset the timer: - m_EatingFinishTick = -1; - - // Send the packets: - m_ClientHandle->SendEntityStatus(*this, esPlayerEatingAccepted); - m_World->BroadcastEntityMetadata(*this); - - // consume the item: - cItem Item(GetEquippedItem()); - Item.m_ItemCount = 1; - cItemHandler * ItemHandler = cItemHandler::GetItemHandler(Item.m_ItemType); - if (!ItemHandler->EatItem(this, &Item)) + if (a_IsFlying == m_IsFlying) { return; } - ItemHandler->OnFoodEaten(m_World, this, &Item); -} - - - - -void cPlayer::AbortEating(void) -{ - m_EatingFinishTick = -1; - m_World->BroadcastEntityMetadata(*this); + m_IsFlying = a_IsFlying; + if (!m_IsFrozen) + { + // If we are frozen, we do not send this yet. We send when unfreeze() is called + m_ClientHandle->SendPlayerAbilities(); + } } -void cPlayer::ClearInventoryPaintSlots(void) +void cPlayer::NotifyNearbyWolves(cPawn * a_Opponent, bool a_IsPlayerInvolved) { - // Clear the list of slots that are being inventory-painted. Used by cWindow only - m_InventoryPaintSlots.clear(); -} - - - - + ASSERT(a_Opponent != nullptr); -void cPlayer::AddInventoryPaintSlot(int a_SlotNum) -{ - // Add a slot to the list for inventory painting. Used by cWindow only - m_InventoryPaintSlots.push_back(a_SlotNum); + m_World->ForEachEntityInBox(cBoundingBox(GetPosition(), 16), [&] (cEntity & a_Entity) + { + if (a_Entity.IsMob()) + { + auto & Mob = static_cast(a_Entity); + if (Mob.GetMobType() == mtWolf) + { + auto & Wolf = static_cast(Mob); + Wolf.ReceiveNearbyFightInfo(GetUUID(), a_Opponent, a_IsPlayerInvolved); + } + } + return false; + } + ); } -const cSlotNums & cPlayer::GetInventoryPaintSlots(void) const +void cPlayer::KilledBy(TakeDamageInfo & a_TDI) { - // Return the list of slots currently stored for inventory painting. Used by cWindow only - return m_InventoryPaintSlots; -} + Super::KilledBy(a_TDI); + if (m_Health > 0) + { + return; // not dead yet =] + } + m_bVisible = false; // So new clients don't see the player + // Detach player from object / entity. If the player dies, the server still says + // that the player is attached to the entity / object + Detach(); + // Puke out all the items + cItems Pickups; + m_Inventory.CopyToItems(Pickups); + m_Inventory.Clear(); -double cPlayer::GetMaxSpeed(void) const -{ - if (m_IsFlying) - { - return m_FlyingMaxSpeed; - } - else if (m_IsSprinting) - { - return m_SprintingMaxSpeed; - } - else + if (GetName() == "Notch") { - return m_NormalMaxSpeed; + Pickups.Add(cItem(E_ITEM_RED_APPLE)); } -} - - + m_Stats.AddValue(Statistic::Drop, static_cast(Pickups.Size())); + m_World->SpawnItemPickups(Pickups, GetPosX(), GetPosY(), GetPosZ(), 10); + SaveToDisk(); // Save it, yeah the world is a tough place ! + cPluginManager * PluginManager = cRoot::Get()->GetPluginManager(); - -void cPlayer::SetNormalMaxSpeed(double a_Speed) -{ - m_NormalMaxSpeed = a_Speed; - if (!m_IsSprinting && !m_IsFlying && !m_IsFrozen) + if ((a_TDI.Attacker == nullptr) && m_World->ShouldBroadcastDeathMessages()) { - // If we are frozen, we do not send this yet. We send when unfreeze() is called - m_ClientHandle->SendPlayerMaxSpeed(); + const AString DamageText = [&] + { + switch (a_TDI.DamageType) + { + case dtRangedAttack: return "was shot"; + case dtLightning: return "was plasmified by lightining"; + case dtFalling: return GetRandomProvider().RandBool() ? "fell to death" : "hit the ground too hard"; + case dtDrowning: return "drowned"; + case dtSuffocating: return GetRandomProvider().RandBool() ? "git merge'd into a block" : "fused with a block"; + case dtStarving: return "forgot the importance of food"; + case dtCactusContact: return "was impaled on a cactus"; + case dtMagmaContact: return "discovered the floor was lava"; + case dtLavaContact: return "was melted by lava"; + case dtPoisoning: return "died from septicaemia"; + case dtWithering: return "is a husk of their former selves"; + case dtOnFire: return "forgot to stop, drop, and roll"; + case dtFireContact: return "burnt themselves to death"; + case dtInVoid: return "somehow fell out of the world"; + case dtPotionOfHarming: return "was magicked to death"; + case dtEnderPearl: return "misused an ender pearl"; + case dtAdmin: return "was administrator'd"; + case dtExplosion: return "blew up"; + case dtAttack: return "was attacked by thin air"; + case dtEnvironment: return "played too much dress up"; // This is not vanilla - added a own pun + } + UNREACHABLE("Unsupported damage type"); + }(); + AString DeathMessage = Printf("%s %s", GetName().c_str(), DamageText.c_str()); + PluginManager->CallHookKilled(*this, a_TDI, DeathMessage); + if (DeathMessage != AString("")) + { + GetWorld()->BroadcastChatDeath(DeathMessage); + } + } + else if (a_TDI.Attacker == nullptr) // && !m_World->ShouldBroadcastDeathMessages() by fallthrough + { + // no-op + } + else if (a_TDI.Attacker->IsPlayer()) + { + cPlayer * Killer = static_cast(a_TDI.Attacker); + AString DeathMessage = Printf("%s was killed by %s", GetName().c_str(), Killer->GetName().c_str()); + PluginManager->CallHookKilled(*this, a_TDI, DeathMessage); + if (DeathMessage != AString("")) + { + GetWorld()->BroadcastChatDeath(DeathMessage); + } + } + else + { + AString KillerClass = a_TDI.Attacker->GetClass(); + KillerClass.erase(KillerClass.begin()); // Erase the 'c' of the class (e.g. "cWitch" -> "Witch") + AString DeathMessage = Printf("%s was killed by a %s", GetName().c_str(), KillerClass.c_str()); + PluginManager->CallHookKilled(*this, a_TDI, DeathMessage); + if (DeathMessage != AString("")) + { + GetWorld()->BroadcastChatDeath(DeathMessage); + } } + + m_Stats.AddValue(Statistic::Deaths); + m_Stats.SetValue(Statistic::TimeSinceDeath, 0); + + m_World->GetScoreBoard().AddPlayerScore(GetName(), cObjective::otDeathCount, 1); } -void cPlayer::SetSprintingMaxSpeed(double a_Speed) +void cPlayer::Killed(cEntity * a_Victim) { - m_SprintingMaxSpeed = a_Speed; - if (m_IsSprinting && !m_IsFlying && !m_IsFrozen) + cScoreboard & ScoreBoard = m_World->GetScoreBoard(); + + if (a_Victim->IsPlayer()) { - // If we are frozen, we do not send this yet. We send when unfreeze() is called - m_ClientHandle->SendPlayerMaxSpeed(); + m_Stats.AddValue(Statistic::PlayerKills); + + ScoreBoard.AddPlayerScore(GetName(), cObjective::otPlayerKillCount, 1); + } + else if (a_Victim->IsMob()) + { + if (static_cast(a_Victim)->GetMobFamily() == cMonster::mfHostile) + { + AwardAchievement(Statistic::AchKillEnemy); + } + + m_Stats.AddValue(Statistic::MobKills); } + + ScoreBoard.AddPlayerScore(GetName(), cObjective::otTotalKillCount, 1); } -void cPlayer::SetFlyingMaxSpeed(double a_Speed) +void cPlayer::Respawn(void) { - m_FlyingMaxSpeed = a_Speed; - - // Update the flying speed, always: - if (!m_IsFrozen) - { - // If we are frozen, we do not send this yet. We send when unfreeze() is called - m_ClientHandle->SendPlayerAbilities(); - } -} + ASSERT(m_World != nullptr); + m_Health = GetMaxHealth(); + SetInvulnerableTicks(20); + // Reset food level: + m_FoodLevel = MAX_FOOD_LEVEL; + m_FoodSaturationLevel = 5.0; + m_FoodExhaustionLevel = 0.0; + // Reset Experience + m_CurrentXp = 0; + m_LifetimeTotalXp = 0; + // ToDo: send score to client? How? + // Extinguish the fire: + StopBurning(); -void cPlayer::SetCrouch(bool a_IsCrouched) -{ - // Set the crouch status, broadcast to all visible players - if (a_IsCrouched == m_IsCrouched) + if (const auto BedWorld = GetBedWorld(); m_World != BedWorld) { - // No change - return; + MoveToWorld(*BedWorld, GetLastBedPos(), false, false); } - - if (a_IsCrouched) + else { - cRoot::Get()->GetPluginManager()->CallHookPlayerCrouched(*this); + m_ClientHandle->SendRespawn(m_World->GetDimension(), true); + TeleportToCoords(GetLastBedPos().x, GetLastBedPos().y, GetLastBedPos().z); } - m_IsCrouched = a_IsCrouched; - m_World->BroadcastEntityMetadata(*this); + SetVisible(true); } -void cPlayer::SetSprint(bool a_IsSprinting) +double cPlayer::GetEyeHeight(void) const { - if (a_IsSprinting == m_IsSprinting) - { - // No change - return; - } - - m_IsSprinting = a_IsSprinting; - m_ClientHandle->SendPlayerMaxSpeed(); + return m_Stance; } -void cPlayer::SetCanFly(bool a_CanFly) +Vector3d cPlayer::GetEyePosition(void) const { - if (a_CanFly == m_CanFly) - { - return; - } - - m_CanFly = a_CanFly; - m_ClientHandle->SendPlayerAbilities(); + return Vector3d( GetPosX(), m_Stance, GetPosZ()); } -void cPlayer::SetCustomName(const AString & a_CustomName) +bool cPlayer::IsGameModeCreative(void) const { - if (m_CustomName == a_CustomName) - { - return; - } + return (GetEffectiveGameMode() == gmCreative); +} - m_World->BroadcastPlayerListRemovePlayer(*this); - m_CustomName = a_CustomName; - if (m_CustomName.length() > 16) - { - m_CustomName = m_CustomName.substr(0, 16); - } - m_World->BroadcastPlayerListAddPlayer(*this); - m_World->BroadcastSpawnEntity(*this, m_ClientHandle.get()); + + +bool cPlayer::IsGameModeSurvival(void) const +{ + return (GetEffectiveGameMode() == gmSurvival); } -void cPlayer::SetBedPos(const Vector3i & a_Pos) +bool cPlayer::IsGameModeAdventure(void) const { - m_LastBedPos = a_Pos; - m_SpawnWorldName = m_World->GetName(); + return (GetEffectiveGameMode() == gmAdventure); } -void cPlayer::SetBedPos(const Vector3i & a_Pos, cWorld * a_World) +bool cPlayer::IsGameModeSpectator(void) const { - m_LastBedPos = a_Pos; - ASSERT(a_World != nullptr); - m_SpawnWorldName = a_World->GetName(); + return (GetEffectiveGameMode() == gmSpectator); } -cWorld * cPlayer::GetBedWorld() +bool cPlayer::CanMobsTarget(void) const { - if (const auto World = cRoot::Get()->GetWorld(m_SpawnWorldName); World != nullptr) - { - return World; - } + return (IsGameModeSurvival() || IsGameModeAdventure()) && (m_Health > 0); +} - return cRoot::Get()->GetDefaultWorld(); + + + + +AString cPlayer::GetIP(void) const +{ + return m_ClientHandle->GetIPString(); } -void cPlayer::SetFlying(bool a_IsFlying) +void cPlayer::SetTeam(cTeam * a_Team) { - if (a_IsFlying == m_IsFlying) + if (m_Team == a_Team) { return; } - m_IsFlying = a_IsFlying; - if (!m_IsFrozen) + if (m_Team) { - // If we are frozen, we do not send this yet. We send when unfreeze() is called - m_ClientHandle->SendPlayerAbilities(); + m_Team->RemovePlayer(GetName()); + } + + m_Team = a_Team; + + if (m_Team) + { + m_Team->AddPlayer(GetName()); } } @@ -1092,572 +1041,223 @@ void cPlayer::SetFlying(bool a_IsFlying) -void cPlayer::ApplyArmorDamage(int a_DamageBlocked) +cTeam * cPlayer::UpdateTeam(void) { - short ArmorDamage = static_cast(std::max(a_DamageBlocked / 4, 1)); - - for (int i = 0; i < 4; i++) + if (m_World == nullptr) { - UseItem(cInventory::invArmorOffset + i, ArmorDamage); + SetTeam(nullptr); + } + else + { + cScoreboard & Scoreboard = m_World->GetScoreBoard(); + + SetTeam(Scoreboard.QueryPlayerTeam(GetName())); } + + return m_Team; } -bool cPlayer::DoTakeDamage(TakeDamageInfo & a_TDI) +void cPlayer::OpenWindow(cWindow & a_Window) { - if ((a_TDI.DamageType != dtInVoid) && (a_TDI.DamageType != dtPlugin)) + if (cRoot::Get()->GetPluginManager()->CallHookPlayerOpeningWindow(*this, a_Window)) { - if (IsGameModeCreative() || IsGameModeSpectator()) - { - // No damage / health in creative or spectator mode if not void or plugin damage - return false; - } + return; } - if ((a_TDI.Attacker != nullptr) && (a_TDI.Attacker->IsPlayer())) + if (&a_Window != m_CurrentWindow) { - cPlayer * Attacker = static_cast(a_TDI.Attacker); + CloseWindow(false); + } - if ((m_Team != nullptr) && (m_Team == Attacker->m_Team)) - { - if (!m_Team->AllowsFriendlyFire()) - { - // Friendly fire is disabled - return false; - } - } - } - - if (Super::DoTakeDamage(a_TDI)) - { - // Any kind of damage adds food exhaustion - AddFoodExhaustion(0.3f); - m_ClientHandle->SendHealth(); - - // Tell the wolves - if (a_TDI.Attacker != nullptr) - { - if (a_TDI.Attacker->IsPawn()) - { - NotifyNearbyWolves(static_cast(a_TDI.Attacker), true); - } - } - m_Stats.AddValue(Statistic::DamageTaken, FloorC(a_TDI.FinalDamage * 10 + 0.5)); - return true; - } - return false; + a_Window.OpenedByPlayer(*this); + m_CurrentWindow = &a_Window; + a_Window.SendWholeWindow(*GetClientHandle()); } -void cPlayer::NotifyNearbyWolves(cPawn * a_Opponent, bool a_IsPlayerInvolved) +void cPlayer::CloseWindow(bool a_CanRefuse) { - ASSERT(a_Opponent != nullptr); + if (m_CurrentWindow == nullptr) + { + m_CurrentWindow = m_InventoryWindow; + return; + } - m_World->ForEachEntityInBox(cBoundingBox(GetPosition(), 16), [&] (cEntity & a_Entity) - { - if (a_Entity.IsMob()) - { - auto & Mob = static_cast(a_Entity); - if (Mob.GetMobType() == mtWolf) - { - auto & Wolf = static_cast(Mob); - Wolf.ReceiveNearbyFightInfo(GetUUID(), a_Opponent, a_IsPlayerInvolved); - } - } - return false; - } - ); + if (m_CurrentWindow->ClosedByPlayer(*this, a_CanRefuse) || !a_CanRefuse) + { + // Close accepted, go back to inventory window (the default): + m_CurrentWindow = m_InventoryWindow; + } + else + { + // Re-open the window + m_CurrentWindow->OpenedByPlayer(*this); + m_CurrentWindow->SendWholeWindow(*GetClientHandle()); + } } -void cPlayer::KilledBy(TakeDamageInfo & a_TDI) +void cPlayer::CloseWindowIfID(char a_WindowID, bool a_CanRefuse) { - Super::KilledBy(a_TDI); - - if (m_Health > 0) + if ((m_CurrentWindow == nullptr) || (m_CurrentWindow->GetWindowID() != a_WindowID)) { - return; // not dead yet =] + return; } + CloseWindow(); +} - m_bVisible = false; // So new clients don't see the player - // Detach player from object / entity. If the player dies, the server still says - // that the player is attached to the entity / object - Detach(); - // Puke out all the items - cItems Pickups; - m_Inventory.CopyToItems(Pickups); - m_Inventory.Clear(); - if (GetName() == "Notch") - { - Pickups.Add(cItem(E_ITEM_RED_APPLE)); - } - m_Stats.AddValue(Statistic::Drop, static_cast(Pickups.Size())); - m_World->SpawnItemPickups(Pickups, GetPosX(), GetPosY(), GetPosZ(), 10); - SaveToDisk(); // Save it, yeah the world is a tough place ! - cPluginManager * PluginManager = cRoot::Get()->GetPluginManager(); +void cPlayer::SendMessage(const AString & a_Message) +{ + m_ClientHandle->SendChat(a_Message, mtCustom); +} - if ((a_TDI.Attacker == nullptr) && m_World->ShouldBroadcastDeathMessages()) - { - const AString DamageText = [&] - { - switch (a_TDI.DamageType) - { - case dtRangedAttack: return "was shot"; - case dtLightning: return "was plasmified by lightining"; - case dtFalling: return GetRandomProvider().RandBool() ? "fell to death" : "hit the ground too hard"; - case dtDrowning: return "drowned"; - case dtSuffocating: return GetRandomProvider().RandBool() ? "git merge'd into a block" : "fused with a block"; - case dtStarving: return "forgot the importance of food"; - case dtCactusContact: return "was impaled on a cactus"; - case dtMagmaContact: return "discovered the floor was lava"; - case dtLavaContact: return "was melted by lava"; - case dtPoisoning: return "died from septicaemia"; - case dtWithering: return "is a husk of their former selves"; - case dtOnFire: return "forgot to stop, drop, and roll"; - case dtFireContact: return "burnt themselves to death"; - case dtInVoid: return "somehow fell out of the world"; - case dtPotionOfHarming: return "was magicked to death"; - case dtEnderPearl: return "misused an ender pearl"; - case dtAdmin: return "was administrator'd"; - case dtExplosion: return "blew up"; - case dtAttack: return "was attacked by thin air"; - case dtEnvironment: return "played too much dress up"; // This is not vanilla - added a own pun - } - UNREACHABLE("Unsupported damage type"); - }(); - AString DeathMessage = Printf("%s %s", GetName().c_str(), DamageText.c_str()); - PluginManager->CallHookKilled(*this, a_TDI, DeathMessage); - if (DeathMessage != AString("")) - { - GetWorld()->BroadcastChatDeath(DeathMessage); - } - } - else if (a_TDI.Attacker == nullptr) // && !m_World->ShouldBroadcastDeathMessages() by fallthrough - { - // no-op - } - else if (a_TDI.Attacker->IsPlayer()) - { - cPlayer * Killer = static_cast(a_TDI.Attacker); - AString DeathMessage = Printf("%s was killed by %s", GetName().c_str(), Killer->GetName().c_str()); - PluginManager->CallHookKilled(*this, a_TDI, DeathMessage); - if (DeathMessage != AString("")) - { - GetWorld()->BroadcastChatDeath(DeathMessage); - } - } - else - { - AString KillerClass = a_TDI.Attacker->GetClass(); - KillerClass.erase(KillerClass.begin()); // Erase the 'c' of the class (e.g. "cWitch" -> "Witch") - AString DeathMessage = Printf("%s was killed by a %s", GetName().c_str(), KillerClass.c_str()); - PluginManager->CallHookKilled(*this, a_TDI, DeathMessage); - if (DeathMessage != AString("")) - { - GetWorld()->BroadcastChatDeath(DeathMessage); - } - } - m_Stats.AddValue(Statistic::Deaths); - m_Stats.SetValue(Statistic::TimeSinceDeath, 0); - m_World->GetScoreBoard().AddPlayerScore(GetName(), cObjective::otDeathCount, 1); + + +void cPlayer::SendMessageInfo(const AString & a_Message) +{ + m_ClientHandle->SendChat(a_Message, mtInformation); } -void cPlayer::Killed(cEntity * a_Victim) +void cPlayer::SendMessageFailure(const AString & a_Message) { - cScoreboard & ScoreBoard = m_World->GetScoreBoard(); + m_ClientHandle->SendChat(a_Message, mtFailure); +} - if (a_Victim->IsPlayer()) - { - m_Stats.AddValue(Statistic::PlayerKills); - ScoreBoard.AddPlayerScore(GetName(), cObjective::otPlayerKillCount, 1); - } - else if (a_Victim->IsMob()) - { - if (static_cast(a_Victim)->GetMobFamily() == cMonster::mfHostile) - { - AwardAchievement(Statistic::AchKillEnemy); - } - m_Stats.AddValue(Statistic::MobKills); - } - ScoreBoard.AddPlayerScore(GetName(), cObjective::otTotalKillCount, 1); + +void cPlayer::SendMessageSuccess(const AString & a_Message) +{ + m_ClientHandle->SendChat(a_Message, mtSuccess); } -void cPlayer::Respawn(void) +void cPlayer::SendMessageWarning(const AString & a_Message) { - ASSERT(m_World != nullptr); - - m_Health = GetMaxHealth(); - SetInvulnerableTicks(20); + m_ClientHandle->SendChat(a_Message, mtWarning); +} - // Reset food level: - m_FoodLevel = MAX_FOOD_LEVEL; - m_FoodSaturationLevel = 5.0; - m_FoodExhaustionLevel = 0.0; - // Reset Experience - m_CurrentXp = 0; - m_LifetimeTotalXp = 0; - // ToDo: send score to client? How? - // Extinguish the fire: - StopBurning(); - if (const auto BedWorld = GetBedWorld(); m_World != BedWorld) - { - MoveToWorld(*BedWorld, GetLastBedPos(), false, false); - } - else - { - m_ClientHandle->SendRespawn(m_World->GetDimension(), true); - TeleportToCoords(GetLastBedPos().x, GetLastBedPos().y, GetLastBedPos().z); - } - SetVisible(true); +void cPlayer::SendMessageFatal(const AString & a_Message) +{ + m_ClientHandle->SendChat(a_Message, mtFailure); } -double cPlayer::GetEyeHeight(void) const +void cPlayer::SendMessagePrivateMsg(const AString & a_Message, const AString & a_Sender) { - return m_Stance; + m_ClientHandle->SendChat(a_Message, mtPrivateMessage, a_Sender); } -Vector3d cPlayer::GetEyePosition(void) const +void cPlayer::SendMessage(const cCompositeChat & a_Message) { - return Vector3d( GetPosX(), m_Stance, GetPosZ()); + m_ClientHandle->SendChat(a_Message); } -bool cPlayer::IsGameModeCreative(void) const +void cPlayer::SendMessageRaw(const AString & a_MessageRaw, eChatType a_Type) { - return (GetEffectiveGameMode() == gmCreative); + m_ClientHandle->SendChatRaw(a_MessageRaw, a_Type); } -bool cPlayer::IsGameModeSurvival(void) const +void cPlayer::SendSystemMessage(const AString & a_Message) { - return (GetEffectiveGameMode() == gmSurvival); + m_ClientHandle->SendChatSystem(a_Message, mtCustom); } -bool cPlayer::IsGameModeAdventure(void) const +void cPlayer::SendAboveActionBarMessage(const AString & a_Message) { - return (GetEffectiveGameMode() == gmAdventure); + m_ClientHandle->SendChatAboveActionBar(a_Message, mtCustom); } -bool cPlayer::IsGameModeSpectator(void) const +void cPlayer::SendSystemMessage(const cCompositeChat & a_Message) { - return (GetEffectiveGameMode() == gmSpectator); + m_ClientHandle->SendChatSystem(a_Message); } -bool cPlayer::CanMobsTarget(void) const +void cPlayer::SendAboveActionBarMessage(const cCompositeChat & a_Message) { - return (IsGameModeSurvival() || IsGameModeAdventure()) && (m_Health > 0); + m_ClientHandle->SendChatAboveActionBar(a_Message); } -AString cPlayer::GetIP(void) const +const AString & cPlayer::GetName(void) const { - return m_ClientHandle->GetIPString(); + return m_ClientHandle->GetUsername(); } -void cPlayer::SetTeam(cTeam * a_Team) +void cPlayer::SetGameMode(eGameMode a_GameMode) { - if (m_Team == a_Team) + if ((a_GameMode < gmMin) || (a_GameMode >= gmMax)) { + LOGWARNING("%s: Setting invalid gamemode: %d", GetName().c_str(), a_GameMode); return; } - if (m_Team) + if (m_GameMode == a_GameMode) { - m_Team->RemovePlayer(GetName()); - } - - m_Team = a_Team; - - if (m_Team) - { - m_Team->AddPlayer(GetName()); - } -} - - - - - -cTeam * cPlayer::UpdateTeam(void) -{ - if (m_World == nullptr) - { - SetTeam(nullptr); - } - else - { - cScoreboard & Scoreboard = m_World->GetScoreBoard(); - - SetTeam(Scoreboard.QueryPlayerTeam(GetName())); - } - - return m_Team; -} - - - - - -void cPlayer::OpenWindow(cWindow & a_Window) -{ - if (cRoot::Get()->GetPluginManager()->CallHookPlayerOpeningWindow(*this, a_Window)) - { - return; - } - - if (&a_Window != m_CurrentWindow) - { - CloseWindow(false); - } - - a_Window.OpenedByPlayer(*this); - m_CurrentWindow = &a_Window; - a_Window.SendWholeWindow(*GetClientHandle()); -} - - - - - -void cPlayer::CloseWindow(bool a_CanRefuse) -{ - if (m_CurrentWindow == nullptr) - { - m_CurrentWindow = m_InventoryWindow; - return; - } - - if (m_CurrentWindow->ClosedByPlayer(*this, a_CanRefuse) || !a_CanRefuse) - { - // Close accepted, go back to inventory window (the default): - m_CurrentWindow = m_InventoryWindow; - } - else - { - // Re-open the window - m_CurrentWindow->OpenedByPlayer(*this); - m_CurrentWindow->SendWholeWindow(*GetClientHandle()); - } -} - - - - - -void cPlayer::CloseWindowIfID(char a_WindowID, bool a_CanRefuse) -{ - if ((m_CurrentWindow == nullptr) || (m_CurrentWindow->GetWindowID() != a_WindowID)) - { - return; - } - CloseWindow(); -} - - - - - -void cPlayer::SendMessage(const AString & a_Message) -{ - m_ClientHandle->SendChat(a_Message, mtCustom); -} - - - - - -void cPlayer::SendMessageInfo(const AString & a_Message) -{ - m_ClientHandle->SendChat(a_Message, mtInformation); -} - - - - - -void cPlayer::SendMessageFailure(const AString & a_Message) -{ - m_ClientHandle->SendChat(a_Message, mtFailure); -} - - - - - -void cPlayer::SendMessageSuccess(const AString & a_Message) -{ - m_ClientHandle->SendChat(a_Message, mtSuccess); -} - - - - - -void cPlayer::SendMessageWarning(const AString & a_Message) -{ - m_ClientHandle->SendChat(a_Message, mtWarning); -} - - - - - -void cPlayer::SendMessageFatal(const AString & a_Message) -{ - m_ClientHandle->SendChat(a_Message, mtFailure); -} - - - - - -void cPlayer::SendMessagePrivateMsg(const AString & a_Message, const AString & a_Sender) -{ - m_ClientHandle->SendChat(a_Message, mtPrivateMessage, a_Sender); -} - - - - - -void cPlayer::SendMessage(const cCompositeChat & a_Message) -{ - m_ClientHandle->SendChat(a_Message); -} - - - - - -void cPlayer::SendMessageRaw(const AString & a_MessageRaw, eChatType a_Type) -{ - m_ClientHandle->SendChatRaw(a_MessageRaw, a_Type); -} - - - - - -void cPlayer::SendSystemMessage(const AString & a_Message) -{ - m_ClientHandle->SendChatSystem(a_Message, mtCustom); -} - - - - - -void cPlayer::SendAboveActionBarMessage(const AString & a_Message) -{ - m_ClientHandle->SendChatAboveActionBar(a_Message, mtCustom); -} - - - - - -void cPlayer::SendSystemMessage(const cCompositeChat & a_Message) -{ - m_ClientHandle->SendChatSystem(a_Message); -} - - - - - -void cPlayer::SendAboveActionBarMessage(const cCompositeChat & a_Message) -{ - m_ClientHandle->SendChatAboveActionBar(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)) - { - LOGWARNING("%s: Setting invalid gamemode: %d", GetName().c_str(), a_GameMode); - return; - } - - if (m_GameMode == a_GameMode) - { - // Gamemode already set - return; + // Gamemode already set + return; } // Detach, if the player is switching from or to the spectator mode @@ -1888,22 +1488,6 @@ void cPlayer::ForceSetSpeed(const Vector3d & a_Speed) -void cPlayer::DoSetSpeed(double a_SpeedX, double a_SpeedY, double a_SpeedZ) -{ - if (m_IsFrozen) - { - // Do not set speed to a frozen client - return; - } - Super::DoSetSpeed(a_SpeedX, a_SpeedY, a_SpeedZ); - // Send the speed to the client so he actualy moves - m_ClientHandle->SendEntityVelocity(*this); -} - - - - - void cPlayer::SetVisible(bool a_bVisible) { // Need to Check if the player or other players are in gamemode spectator, but will break compatibility @@ -3211,15 +2795,436 @@ bool cPlayer::CanInstantlyMine(BLOCKTYPE a_Block) -float cPlayer::GetExplosionExposureRate(Vector3d a_ExplosionPosition, float a_ExlosionPower) +void cPlayer::AddKnownItem(const cItem & a_Item) { - if ( - IsGameModeSpectator() || - (IsGameModeCreative() && !IsOnGround()) - ) + if (a_Item.m_ItemType < 0) + { + return; + } + + auto Response = m_KnownItems.insert(a_Item.CopyOne()); + if (!Response.second) { - return 0; // No impact from explosion + // The item was already known, bail out: + return; } - return Super::GetExplosionExposureRate(a_ExplosionPosition, a_ExlosionPower) / 30.0f; + // Process the recipes that got unlocked by this newly-known item: + auto Recipes = cRoot::Get()->GetCraftingRecipes()->FindNewRecipesForItem(a_Item, m_KnownItems); + for (const auto & RecipeId : Recipes) + { + AddKnownRecipe(RecipeId); + } +} + + + + + +void cPlayer::AddKnownRecipe(UInt32 a_RecipeId) +{ + auto Response = m_KnownRecipes.insert(a_RecipeId); + if (!Response.second) + { + // The recipe was already known, bail out: + return; + } + m_ClientHandle->SendUnlockRecipe(a_RecipeId); +} + + + + + +void cPlayer::TickFreezeCode() +{ + if (m_IsFrozen) + { + if ((!m_IsManuallyFrozen) && (GetClientHandle()->IsPlayerChunkSent())) + { + // If the player was automatically frozen, unfreeze if the chunk the player is inside is loaded and sent + Unfreeze(); + + // Pull the player out of any solids that might have loaded on them. + PREPARE_REL_AND_CHUNK(GetPosition(), *(GetParentChunk())); + if (RelSuccess) + { + int NewY = Rel.y; + if (NewY < 0) + { + NewY = 0; + } + while (NewY < cChunkDef::Height - 2) + { + // If we find a position with enough space for the player + if ( + !cBlockInfo::IsSolid(Chunk->GetBlock(Rel.x, NewY, Rel.z)) && + !cBlockInfo::IsSolid(Chunk->GetBlock(Rel.x, NewY + 1, Rel.z)) + ) + { + // If the found position is not the same as the original + if (NewY != Rel.y) + { + SetPosition(GetPosition().x, NewY, GetPosition().z); + GetClientHandle()->SendPlayerPosition(); + } + break; + } + ++NewY; + } + } + } + else if (GetWorld()->GetWorldAge() % 100 == 0) + { + // Despite the client side freeze, the player may be able to move a little by + // Jumping or canceling flight. Re-freeze every now and then + FreezeInternal(GetPosition(), m_IsManuallyFrozen); + } + } + else + { + if (!GetClientHandle()->IsPlayerChunkSent() || (!GetParentChunk()->IsValid())) + { + FreezeInternal(GetPosition(), false); + } + } +} + + + + + +void cPlayer::ApplyArmorDamage(int a_DamageBlocked) +{ + short ArmorDamage = static_cast(std::max(a_DamageBlocked / 4, 1)); + + for (int i = 0; i < 4; i++) + { + UseItem(cInventory::invArmorOffset + i, ArmorDamage); + } +} + + + + + +void cPlayer::BroadcastMovementUpdate(const cClientHandle * a_Exclude) +{ + if (!m_IsFrozen && m_Speed.SqrLength() > 0.001) + { + // If the player is not frozen, has a non-zero speed, + // send the speed to the client so he is forced to move so: + m_ClientHandle->SendEntityVelocity(*this); + } + + // Since we do no physics processing for players, speed will otherwise never decrease: + m_Speed.Set(0, 0, 0); + + Super::BroadcastMovementUpdate(a_Exclude); +} + + + + + +bool cPlayer::DoTakeDamage(TakeDamageInfo & a_TDI) +{ + // Filters out damage for creative mode / friendly fire. + + if ((a_TDI.DamageType != dtInVoid) && (a_TDI.DamageType != dtPlugin)) + { + if (IsGameModeCreative() || IsGameModeSpectator()) + { + // No damage / health in creative or spectator mode if not void or plugin damage + return false; + } + } + + if ((a_TDI.Attacker != nullptr) && (a_TDI.Attacker->IsPlayer())) + { + cPlayer * Attacker = static_cast(a_TDI.Attacker); + + if ((m_Team != nullptr) && (m_Team == Attacker->m_Team)) + { + if (!m_Team->AllowsFriendlyFire()) + { + // Friendly fire is disabled + return false; + } + } + } + + if (Super::DoTakeDamage(a_TDI)) + { + // Any kind of damage adds food exhaustion + AddFoodExhaustion(0.3f); + m_ClientHandle->SendHealth(); + + // Tell the wolves + if (a_TDI.Attacker != nullptr) + { + if (a_TDI.Attacker->IsPawn()) + { + NotifyNearbyWolves(static_cast(a_TDI.Attacker), true); + } + } + m_Stats.AddValue(Statistic::DamageTaken, FloorC(a_TDI.FinalDamage * 10 + 0.5)); + return true; + } + return false; +} + + + + + +float cPlayer::GetEnchantmentBlastKnockbackReduction() +{ + if ( + IsGameModeSpectator() || + (IsGameModeCreative() && !IsOnGround()) + ) + { + return 1; // No impact from explosion + } + + return Super::GetEnchantmentBlastKnockbackReduction(); +} + + + + + +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); +} + + + + + +void cPlayer::SpawnOn(cClientHandle & a_Client) +{ + if (!m_bVisible || (m_ClientHandle.get() == (&a_Client))) + { + return; + } + + LOGD("Spawing %s on %s", GetName().c_str(), a_Client.GetUsername().c_str()); + + a_Client.SendPlayerSpawn(*this); + a_Client.SendEntityHeadLook(*this); + a_Client.SendEntityEquipment(*this, 0, m_Inventory.GetEquippedItem()); + a_Client.SendEntityEquipment(*this, 1, m_Inventory.GetEquippedBoots()); + a_Client.SendEntityEquipment(*this, 2, m_Inventory.GetEquippedLeggings()); + a_Client.SendEntityEquipment(*this, 3, m_Inventory.GetEquippedChestplate()); + a_Client.SendEntityEquipment(*this, 4, m_Inventory.GetEquippedHelmet()); +} + + + + + +void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) +{ + m_ClientHandle->Tick(a_Dt.count()); + + if (m_ClientHandle->IsDestroyed()) + { + 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); + } + + // Handle the player detach, when the player is in spectator mode + if ( + (IsGameModeSpectator()) && + (m_AttachedTo != nullptr) && + ( + (m_AttachedTo->IsDestroyed()) || // Watching entity destruction + (m_AttachedTo->GetHealth() <= 0) || // Watching entity dead + (IsCrouched()) // Or the player wants to be detached + ) + ) + { + Detach(); + } + + 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 || // 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: + if (m_IsChargingBow) + { + m_BowCharge += 1; + } + + BroadcastMovementUpdate(m_ClientHandle.get()); + + if (m_Health > 0) // make sure player is alive + { + m_World->CollectPickupsByPlayer(*this); + + if ((m_EatingFinishTick >= 0) && (m_EatingFinishTick <= m_World->GetWorldAge())) + { + FinishEating(); + } + + HandleFood(); + } + + if (m_IsFishing) + { + HandleFloater(); + } + + // Update items (e.g. Maps) + m_Inventory.UpdateItems(); + + // Send Player List (Once per m_LastPlayerListTime/1000 ms) + if (m_LastPlayerListTime + PLAYER_LIST_TIME_MS <= std::chrono::steady_clock::now()) + { + m_World->BroadcastPlayerListUpdatePing(*this); + m_LastPlayerListTime = std::chrono::steady_clock::now(); + } + + if (m_TicksUntilNextSave == 0) + { + SaveToDisk(); + m_TicksUntilNextSave = PLAYER_INVENTORY_SAVE_INTERVAL; + } + else + { + m_TicksUntilNextSave--; + } } diff --git a/src/Entities/Player.h b/src/Entities/Player.h index ba3c345ed..f7d54340e 100644 --- a/src/Entities/Player.h +++ b/src/Entities/Player.h @@ -47,42 +47,9 @@ public: CLASS_PROTODEF(cPlayer) - cPlayer(const cClientHandlePtr & a_Client); - virtual ~cPlayer() override; - virtual void OnAddToWorld(cWorld & a_World) override; - virtual void OnRemoveFromWorld(cWorld & a_World) override; - - virtual void SpawnOn(cClientHandle & a_Client) override; - - virtual void Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) override; - - void TickFreezeCode(); - - virtual void HandlePhysics(std::chrono::milliseconds a_Dt, cChunk &) override { UNUSED(a_Dt); } - - /** Returns the currently equipped weapon; empty item if none */ - virtual cItem GetEquippedWeapon(void) const override { return m_Inventory.GetEquippedItem(); } - - /** Returns the currently equipped helmet; empty item if none */ - virtual cItem GetEquippedHelmet(void) const override { return m_Inventory.GetEquippedHelmet(); } - - /** Returns the currently equipped chestplate; empty item if none */ - virtual cItem GetEquippedChestplate(void) const override { return m_Inventory.GetEquippedChestplate(); } - - /** Returns the currently equipped leggings; empty item if none */ - virtual cItem GetEquippedLeggings(void) const override { return m_Inventory.GetEquippedLeggings(); } - - /** Returns the currently equipped boots; empty item if none */ - virtual cItem GetEquippedBoots(void) const override { return m_Inventory.GetEquippedBoots(); } - - /** Returns the currently offhand equipped item; empty item if none */ - virtual cItem GetOffHandEquipedItem(void) const override { return m_Inventory.GetShieldSlot(); } - - virtual void ApplyArmorDamage(int DamageBlocked) override; - // tolua_begin /** Sets the experience total @@ -598,14 +565,19 @@ public: Source: https://minecraft.gamepedia.com/Breaking#Instant_breaking */ bool CanInstantlyMine(BLOCKTYPE a_Block); - /** get player explosion exposure rate */ - virtual float GetExplosionExposureRate(Vector3d a_ExplosionPosition, float a_ExlosionPower) override; - /** Adds an Item to the list of known items. If the item is already known, does nothing. */ void AddKnownItem(const cItem & a_Item); -protected: + // cEntity overrides: + virtual cItem GetEquippedWeapon(void) const override { return m_Inventory.GetEquippedItem(); } + virtual cItem GetEquippedHelmet(void) const override { return m_Inventory.GetEquippedHelmet(); } + virtual cItem GetEquippedChestplate(void) const override { return m_Inventory.GetEquippedChestplate(); } + virtual cItem GetEquippedLeggings(void) const override { return m_Inventory.GetEquippedLeggings(); } + virtual cItem GetEquippedBoots(void) const override { return m_Inventory.GetEquippedBoots(); } + virtual cItem GetOffHandEquipedItem(void) const override { return m_Inventory.GetShieldSlot(); } + +private: typedef std::vector > AStringVectorVector; @@ -766,12 +738,6 @@ protected: /** List of known items as Ids */ std::set m_KnownItems; - /** Sets the speed and sends it to the client, so that they are forced to move so. */ - virtual void DoSetSpeed(double a_SpeedX, double a_SpeedY, double a_SpeedZ) override; - - /** Filters out damage for creative mode / friendly fire */ - virtual bool DoTakeDamage(TakeDamageInfo & TDI) override; - /** Called in each tick to handle food-related processing */ void HandleFood(void); @@ -782,8 +748,6 @@ protected: This can be used both for online and offline UUIDs. */ AString GetUUIDFileName(const cUUID & a_UUID); -private: - /** Pins the player to a_Location until Unfreeze() is called. If ManuallyFrozen is false, the player will unfreeze when the chunk is loaded. */ void FreezeInternal(const Vector3d & a_Location, bool a_ManuallyFrozen); @@ -807,4 +771,17 @@ private: If the recipe is already known, does nothing. */ void AddKnownRecipe(UInt32 RecipeId); + void TickFreezeCode(); + + // cEntity overrides: + virtual void ApplyArmorDamage(int DamageBlocked) override; + virtual void BroadcastMovementUpdate(const cClientHandle * a_Exclude = nullptr) override; + virtual bool DoTakeDamage(TakeDamageInfo & TDI) override; + virtual float GetEnchantmentBlastKnockbackReduction() override; + virtual void HandlePhysics(std::chrono::milliseconds a_Dt, cChunk &) override { UNUSED(a_Dt); } + virtual void OnAddToWorld(cWorld & a_World) override; + virtual void OnRemoveFromWorld(cWorld & a_World) override; + virtual void SpawnOn(cClientHandle & a_Client) override; + virtual void Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) override; + } ; // tolua_export -- cgit v1.2.3