summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Server/Plugins/APIDump/Classes/World.lua2
-rw-r--r--src/ClientHandle.cpp108
-rw-r--r--src/ClientHandle.h23
-rw-r--r--src/Entities/Player.cpp167
-rw-r--r--src/Entities/Player.h7
-rw-r--r--src/Generating/BioGen.cpp3
-rw-r--r--src/Items/ItemDye.h11
-rw-r--r--src/Physics/Explodinator.cpp1
-rw-r--r--src/Protocol/Protocol_1_14.cpp3
-rw-r--r--src/Protocol/Protocol_1_8.cpp15
-rw-r--r--src/World.cpp8
-rw-r--r--src/WorldStorage/StatisticsSerializer.cpp19
12 files changed, 145 insertions, 222 deletions
diff --git a/Server/Plugins/APIDump/Classes/World.lua b/Server/Plugins/APIDump/Classes/World.lua
index 149c70423..2a9e3d4e8 100644
--- a/Server/Plugins/APIDump/Classes/World.lua
+++ b/Server/Plugins/APIDump/Classes/World.lua
@@ -2147,7 +2147,7 @@ function OnAllChunksAvailable()</pre> All return values from the callbacks are i
Type = "boolean",
},
},
- Notes = "Grows the plant at the specified coords to its full. Returns true if the plant was grown, false if not.",
+ Notes = "Grows the plant at the specified coords to maturity. Returns true if the plant was grown, false if not.",
},
GrowTree =
{
diff --git a/src/ClientHandle.cpp b/src/ClientHandle.cpp
index 4745ff7cd..a42212002 100644
--- a/src/ClientHandle.cpp
+++ b/src/ClientHandle.cpp
@@ -76,6 +76,7 @@ cClientHandle::cClientHandle(const AString & a_IPString, int a_ViewDistance) :
m_LastStreamedChunkX(std::numeric_limits<decltype(m_LastStreamedChunkX)>::max()), // bogus chunk coords to force streaming upon login
m_LastStreamedChunkZ(std::numeric_limits<decltype(m_LastStreamedChunkZ)>::max()),
m_TicksSinceLastPacket(0),
+ m_TimeSinceLastUnloadCheck(0),
m_Ping(1000),
m_PingID(1),
m_BlockDigAnimStage(-1),
@@ -243,6 +244,36 @@ void cClientHandle::ProxyInit(const AString & a_IPString, const cUUID & a_UUID,
+void cClientHandle::ProcessProtocolIn(void)
+{
+ // Process received network data:
+ decltype(m_IncomingData) IncomingData;
+ {
+ cCSLock Lock(m_CSIncomingData);
+
+ // Bail out when nothing was received:
+ if (m_IncomingData.empty())
+ {
+ return;
+ }
+
+ std::swap(IncomingData, m_IncomingData);
+ }
+
+ try
+ {
+ m_Protocol.HandleIncomingData(*this, IncomingData);
+ }
+ catch (const std::exception & Oops)
+ {
+ Kick(Oops.what());
+ }
+}
+
+
+
+
+
void cClientHandle::ProcessProtocolOut()
{
decltype(m_OutgoingData) OutgoingData;
@@ -394,7 +425,7 @@ void cClientHandle::FinishAuthenticate()
}
catch (const std::exception & Oops)
{
- LOGWARNING("Error reading player \"%s\": %s", GetUsername().c_str(), Oops.what());
+ LOGWARNING("Player \"%s\" save or statistics file loading failed: %s", GetUsername().c_str(), Oops.what());
Kick("Contact an operator.\n\nYour player's save files could not be parsed.\nTo avoid data loss you are prevented from joining.");
return;
}
@@ -632,8 +663,6 @@ void cClientHandle::UnloadOutOfRangeChunks(void)
m_Player->GetWorld()->RemoveChunkClient(itr->m_ChunkX, itr->m_ChunkZ, this);
SendUnloadChunk(itr->m_ChunkX, itr->m_ChunkZ);
}
-
- m_LastUnloadCheck = m_Player->GetWorld()->GetWorldAge();
}
@@ -815,8 +844,8 @@ void cClientHandle::HandleEnchantItem(UInt8 a_WindowID, UInt8 a_Enchantment)
// Only allow enchantment if the player has sufficient levels and lapis to enchant:
if ((m_Player->GetCurrentXp() >= XpRequired) && (LapisStack.m_ItemCount >= LapisRequired))
{
- /** We need to reduce the player's level by the number of lapis required.
- However we need to keep the resulting percentage filled the same. */
+ // We need to reduce the player's level by the number of lapis required.
+ // However we need to keep the resulting percentage filled the same.
const auto TargetLevel = m_Player->GetXpLevel() - LapisRequired;
const auto CurrentFillPercent = m_Player->GetXpPercentage();
@@ -842,7 +871,7 @@ void cClientHandle::HandleEnchantItem(UInt8 a_WindowID, UInt8 a_Enchantment)
}
}
- // Retrieve the enchanted item corresponding to our chosen option (top, middle, bottom)
+ // The enchanted item corresponding to our chosen option (top, middle, bottom).
cItem EnchantedItem = Window->m_SlotArea->SelectEnchantedOption(a_Enchantment);
// Set the item slot to our new enchanted item:
@@ -2071,19 +2100,10 @@ bool cClientHandle::CheckBlockInteractionsRate(void)
-void cClientHandle::Tick(float a_Dt)
+void cClientHandle::Tick(std::chrono::milliseconds a_Dt)
{
using namespace std::chrono_literals;
- // anticheat fastbreak
- if (m_HasStartedDigging)
- {
- BLOCKTYPE Block = m_Player->GetWorld()->GetBlock({ m_LastDigBlockX, m_LastDigBlockY, m_LastDigBlockZ });
- m_BreakProgress += m_Player->GetMiningProgressPerTick(Block);
- }
-
- ProcessProtocolIn();
-
if (IsDestroyed())
{
return;
@@ -2156,16 +2176,24 @@ void cClientHandle::Tick(float a_Dt)
StreamNextChunks();
// Unload all chunks that are out of the view distance (every 5 seconds):
- if ((m_Player->GetWorld()->GetWorldAge() - m_LastUnloadCheck) > 5s)
+ if ((m_TimeSinceLastUnloadCheck += a_Dt) > 5s)
{
UnloadOutOfRangeChunks();
+ m_TimeSinceLastUnloadCheck = 0s;
+ }
+
+ // anticheat fastbreak
+ if (m_HasStartedDigging)
+ {
+ BLOCKTYPE Block = m_Player->GetWorld()->GetBlock({ m_LastDigBlockX, m_LastDigBlockY, m_LastDigBlockZ });
+ m_BreakProgress += m_Player->GetMiningProgressPerTick(Block);
}
// Handle block break animation:
if (m_BlockDigAnimStage > -1)
{
int lastAnimVal = m_BlockDigAnimStage;
- m_BlockDigAnimStage += static_cast<int>(m_BlockDigAnimSpeed * a_Dt);
+ m_BlockDigAnimStage += static_cast<int>(m_BlockDigAnimSpeed * a_Dt.count());
if (m_BlockDigAnimStage > 9000)
{
m_BlockDigAnimStage = 9000;
@@ -2903,17 +2931,17 @@ void cClientHandle::SendResetTitle()
-void cClientHandle::SendRespawn(eDimension a_Dimension, bool a_ShouldIgnoreDimensionChecks)
+void cClientHandle::SendRespawn(const eDimension a_Dimension, const bool a_IsRespawningFromDeath)
{
- if (!a_ShouldIgnoreDimensionChecks && (a_Dimension == m_Player->GetWorld()->GetDimension()))
+ if (!a_IsRespawningFromDeath && (a_Dimension == m_Player->GetWorld()->GetDimension()))
{
// The client goes crazy if we send a respawn packet with the dimension of the current world
// So we send a temporary one first.
- // This is not needed when the player dies, hence the a_ShouldIgnoreDimensionChecks flag.
- // a_ShouldIgnoreDimensionChecks is true only at cPlayer::Respawn, which is called after
- // the player dies.
- eDimension TemporaryDimension = (a_Dimension == dimOverworld) ? dimNether : dimOverworld;
- m_Protocol->SendRespawn(TemporaryDimension);
+ // This is not needed when the player dies, hence the a_IsRespawningFromDeath flag.
+ // a_IsRespawningFromDeath is true only at cPlayer::Respawn, which is called after the player dies.
+
+ // First send a temporary dimension to placate the client:
+ m_Protocol->SendRespawn((a_Dimension == dimOverworld) ? dimNether : dimOverworld);
}
m_Protocol->SendRespawn(a_Dimension);
@@ -3383,36 +3411,6 @@ bool cClientHandle::SetState(eState a_NewState)
-void cClientHandle::ProcessProtocolIn(void)
-{
- // Process received network data:
- decltype(m_IncomingData) IncomingData;
- {
- cCSLock Lock(m_CSIncomingData);
-
- // Bail out when nothing was received:
- if (m_IncomingData.empty())
- {
- return;
- }
-
- std::swap(IncomingData, m_IncomingData);
- }
-
- try
- {
- m_Protocol.HandleIncomingData(*this, IncomingData);
- }
- catch (const std::exception & Oops)
- {
- Kick(Oops.what());
- }
-}
-
-
-
-
-
void cClientHandle::OnLinkCreated(cTCPLinkPtr a_Link)
{
m_Link = a_Link;
diff --git a/src/ClientHandle.h b/src/ClientHandle.h
index f454e53bd..86a5248c5 100644
--- a/src/ClientHandle.h
+++ b/src/ClientHandle.h
@@ -105,6 +105,10 @@ public: // tolua_export
void ProxyInit(const AString & a_IPString, const cUUID & a_UUID);
void ProxyInit(const AString & a_IPString, const cUUID & a_UUID, const Json::Value & a_Properties);
+ /** Processes the data in the network input buffer.
+ Called by both cWorld::Tick() and ServerTick(). */
+ void ProcessProtocolIn(void);
+
/** Flushes all buffered outgoing data to the network. */
void ProcessProtocolOut();
@@ -133,7 +137,7 @@ public: // tolua_export
inline bool IsLoggedIn(void) const { return (m_State >= csAuthenticating); }
/** Called while the client is being ticked from the world via its cPlayer object */
- void Tick(float a_Dt);
+ void Tick(std::chrono::milliseconds a_Dt);
/** Called while the client is being ticked from the cServer object */
void ServerTick(float a_Dt);
@@ -208,7 +212,7 @@ public: // tolua_export
void SendRemoveEntityEffect (const cEntity & a_Entity, int a_EffectID);
void SendResourcePack (const AString & a_ResourcePackUrl);
void SendResetTitle (void); // tolua_export
- void SendRespawn (eDimension a_Dimension, bool a_ShouldIgnoreDimensionChecks);
+ void SendRespawn (eDimension a_Dimension, bool a_IsRespawningFromDeath);
void SendScoreUpdate (const AString & a_Objective, const AString & a_Player, cObjective::Score a_Score, Byte a_Mode);
void SendScoreboardObjective (const AString & a_Name, const AString & a_DisplayName, Byte a_Mode);
void SendSetSubTitle (const cCompositeChat & a_SubTitle); // tolua_export
@@ -464,8 +468,9 @@ private:
/** A pointer to a World-owned player object, created in FinishAuthenticate when authentication succeeds.
The player should only be accessed from the tick thread of the World that owns him.
- After the player object is handed off to the World, lifetime is managed automatically, guaranteed to outlast this client handle.
- The player self-destructs some time after the client handle enters the Destroyed state. */
+ After the player object is handed off to the World, its lifetime is managed automatically, and strongly owns this client handle.
+ The player self-destructs some time after the client handle enters the Destroyed state.
+ We are therefore guaranteed that while m_State < Destroyed, that is when when we need to access m_Player, m_Player is valid. */
cPlayer * m_Player;
/** This is an optimization which saves you an iteration of m_SentChunks if you just want to know
@@ -483,12 +488,12 @@ private:
int m_LastStreamedChunkX;
int m_LastStreamedChunkZ;
- /** The last time UnloadOutOfRangeChunks was called. */
- cTickTimeLong m_LastUnloadCheck;
-
/** Number of ticks since the last network packet was received (increased in Tick(), reset in OnReceivedData()) */
std::atomic<int> m_TicksSinceLastPacket;
+ /** The time since UnloadOutOfRangeChunks was last called. */
+ std::chrono::milliseconds m_TimeSinceLastUnloadCheck;
+
/** Duration of the last completed client ping. */
std::chrono::steady_clock::duration m_Ping;
@@ -604,10 +609,6 @@ private:
Only succeeds if a_NewState > m_State, otherwise returns false. */
bool SetState(eState a_NewState);
- /** Processes the data in the network input buffer.
- Called by both Tick() and ServerTick(). */
- void ProcessProtocolIn(void);
-
// cTCPLink::cCallbacks overrides:
virtual void OnLinkCreated(cTCPLinkPtr a_Link) override;
virtual void OnReceivedData(const char * a_Data, size_t a_Length) override;
diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp
index 24b9c19af..e1caef7f9 100644
--- a/src/Entities/Player.cpp
+++ b/src/Entities/Player.cpp
@@ -66,6 +66,9 @@ const int cPlayer::MAX_FOOD_LEVEL = 20;
// 6000 ticks or 5 minutes
#define PLAYER_INVENTORY_SAVE_INTERVAL 6000
+// Food saturation for newly-joined or just-respawned players.
+#define RESPAWN_FOOD_SATURATION 5
+
#define XP_TO_LEVEL15 255
#define XP_PER_LEVEL_TO15 17
#define XP_TO_LEVEL30 825
@@ -114,29 +117,19 @@ cPlayer::BodyStanceGliding::BodyStanceGliding(cPlayer & a_Player) :
cPlayer::cPlayer(const std::shared_ptr<cClientHandle> & a_Client) :
Super(etPlayer, 0.6f, 1.8f),
m_BodyStance(BodyStanceStanding(*this)),
- m_FoodLevel(MAX_FOOD_LEVEL),
- m_FoodSaturationLevel(5.0),
- m_FoodTickTimer(0),
- m_FoodExhaustionLevel(0.0),
m_Inventory(*this),
m_EnderChestContents(9, 3),
m_DefaultWorldPath(cRoot::Get()->GetDefaultWorld()->GetDataPath()),
- m_GameMode(eGameMode_NotSet),
m_ClientHandle(a_Client),
m_NormalMaxSpeed(1.0),
m_SprintingMaxSpeed(1.3),
m_FlyingMaxSpeed(1.0),
m_IsChargingBow(false),
m_IsFishing(false),
- m_IsFlightCapable(false),
- m_IsFlying(false),
m_IsFrozen(false),
m_IsLeftHanded(false),
m_IsTeleporting(false),
- m_IsVisible(true),
m_EatingFinishTick(-1),
- m_LifetimeTotalXp(0),
- m_CurrentXp(0),
m_BowCharge(0),
m_FloaterID(cEntity::INVALID_ID),
m_Team(nullptr),
@@ -150,13 +143,9 @@ cPlayer::cPlayer(const std::shared_ptr<cClientHandle> & a_Client) :
m_CurrentWindow = m_InventoryWindow;
m_InventoryWindow->OpenedByPlayer(*this);
- SetMaxHealth(MAX_HEALTH);
- m_Health = MAX_HEALTH;
-
LoadFromDisk();
- UpdateCapabilities();
-
- m_LastGroundHeight = static_cast<float>(GetPosY());
+ SetMaxHealth(MAX_HEALTH);
+ UpdateCapabilities(); // Only required for plugins listening to HOOK_SPAWNING_ENTITY to not read uninitialised variables.
}
@@ -382,7 +371,6 @@ void cPlayer::SetFoodLevel(int a_FoodLevel)
if (cRoot::Get()->GetPluginManager()->CallHookPlayerFoodLevelChange(*this, FoodLevel))
{
- m_FoodSaturationLevel = 5.0;
return;
}
@@ -943,8 +931,8 @@ void cPlayer::Respawn(void)
// Reset food level:
m_FoodLevel = MAX_FOOD_LEVEL;
- m_FoodSaturationLevel = 5.0;
- m_FoodExhaustionLevel = 0.0;
+ m_FoodSaturationLevel = RESPAWN_FOOD_SATURATION;
+ m_FoodExhaustionLevel = 0;
// Reset Experience
m_CurrentXp = 0;
@@ -1341,24 +1329,23 @@ void cPlayer::SetGameMode(eGameMode a_GameMode)
void cPlayer::UpdateCapabilities()
{
- // Fly ability:
- if (IsGameModeCreative() || IsGameModeSpectator())
+ if (IsGameModeCreative())
{
m_IsFlightCapable = true;
+ m_IsVisible = true;
}
- else
+ else if (IsGameModeSpectator())
{
- m_IsFlying = false;
- m_IsFlightCapable = false;
+ m_DraggingItem.Empty(); // Clear the current dragging item of spectators.
+ m_IsFlightCapable = true;
+ m_IsFlying = true; // Spectators are always in flight mode.
+ m_IsVisible = false; // Spectators are invisible.
}
-
- // Visible:
- m_IsVisible = !IsGameModeSpectator();
-
- // Clear the current dragging item of spectators:
- if (IsGameModeSpectator())
+ else
{
- m_DraggingItem.Empty();
+ m_IsFlightCapable = false;
+ m_IsFlying = false;
+ m_IsVisible = true;
}
}
@@ -1801,79 +1788,54 @@ void cPlayer::LoadFromDisk()
{
LoadRank();
- const auto & UUID = GetUUID();
-
- // Load from the UUID file:
- if (LoadFromFile(GetUUIDFileName(UUID)))
- {
- return;
- }
-
- // Player not found:
- m_World = cRoot::Get()->GetDefaultWorld();
- LOG("Player \"%s\" (%s) data not found, resetting to defaults", GetName().c_str(), UUID.ToShortString().c_str());
-
- const Vector3i WorldSpawn(static_cast<int>(m_World->GetSpawnX()), static_cast<int>(m_World->GetSpawnY()), static_cast<int>(m_World->GetSpawnZ()));
- SetPosition(WorldSpawn);
- SetRespawnPosition(WorldSpawn, *m_World);
-
- m_Inventory.Clear();
- m_EnchantmentSeed = GetRandomProvider().RandInt<unsigned int>(); // Use a random number to seed the enchantment generator
-
- FLOGD("Player \"{0}\" is connecting for the first time, spawning at default world spawn {1:.2f}", GetName(), GetPosition());
-}
-
-
-
-
-
-bool cPlayer::LoadFromFile(const AString & a_FileName)
-{
Json::Value Root;
+ const auto & UUID = GetUUID();
+ const auto & FileName = GetUUIDFileName(UUID);
try
{
- // Load the data from the file and parse:
- InputFileStream(a_FileName) >> Root;
- }
- catch (const Json::Exception & Oops)
- {
- // Parse failure:
- throw std::runtime_error(Oops.what());
+ // Load the data from the save file and parse:
+ InputFileStream(FileName) >> Root;
+
+ // Load the player stats.
+ // We use the default world name (like bukkit) because stats are shared between dimensions / worlds.
+ StatisticsSerializer::Load(m_Stats, m_DefaultWorldPath, UUID.ToLongString());
}
catch (const InputFileStream::failure &)
{
- if (errno == ENOENT)
+ if (errno != ENOENT)
{
- // This is a new player whom we haven't seen yet, bail out, let them have the defaults:
- return false;
+ // Save file exists but unreadable:
+ throw;
}
- throw;
+ // This is a new player whom we haven't seen yet with no save file, let them have the defaults:
+ LOG("Player \"%s\" (%s) save or statistics file not found, resetting to defaults", GetName().c_str(), UUID.ToShortString().c_str());
}
- // Load the player data:
- Json::Value & JSON_PlayerPosition = Root["position"];
- if (JSON_PlayerPosition.size() == 3)
+ m_CurrentWorldName = Root.get("world", cRoot::Get()->GetDefaultWorld()->GetName()).asString();
+ m_World = cRoot::Get()->GetWorld(m_CurrentWorldName);
+
+ if (const auto & PlayerPosition = Root["position"]; PlayerPosition.size() == 3)
+ {
+ SetPosition(PlayerPosition[0].asDouble(), PlayerPosition[1].asDouble(), PlayerPosition[2].asDouble());
+ }
+ else
{
- SetPosX(JSON_PlayerPosition[0].asDouble());
- SetPosY(JSON_PlayerPosition[1].asDouble());
- SetPosZ(JSON_PlayerPosition[2].asDouble());
- m_LastPosition = GetPosition();
+ SetPosition(Vector3d(0.5, 0.5, 0.5) + Vector3i(m_World->GetSpawnX(), m_World->GetSpawnY(), m_World->GetSpawnZ()));
}
- Json::Value & JSON_PlayerRotation = Root["rotation"];
- if (JSON_PlayerRotation.size() == 3)
+ if (const auto & PlayerRotation = Root["rotation"]; PlayerRotation.size() == 3)
{
- SetYaw (static_cast<float>(JSON_PlayerRotation[0].asDouble()));
- SetPitch (static_cast<float>(JSON_PlayerRotation[1].asDouble()));
- SetRoll (static_cast<float>(JSON_PlayerRotation[2].asDouble()));
+ SetYaw (PlayerRotation[0].asDouble());
+ SetPitch(PlayerRotation[1].asDouble());
+ SetRoll (PlayerRotation[2].asDouble());
}
- m_Health = Root.get("health", 0).asFloat();
+ m_Health = Root.get("health", MAX_HEALTH).asFloat();
m_AirLevel = Root.get("air", MAX_AIR_LEVEL).asInt();
m_FoodLevel = Root.get("food", MAX_FOOD_LEVEL).asInt();
- m_FoodSaturationLevel = Root.get("foodSaturation", MAX_FOOD_LEVEL).asDouble();
+ m_FoodSaturationLevel = Root.get("foodSaturation", RESPAWN_FOOD_SATURATION).asDouble();
m_FoodTickTimer = Root.get("foodTickTimer", 0).asInt();
m_FoodExhaustionLevel = Root.get("foodExhaustion", 0).asDouble();
m_LifetimeTotalXp = Root.get("xpTotal", 0).asInt();
@@ -1903,47 +1865,20 @@ bool cPlayer::LoadFromFile(const AString & a_FileName)
m_GameMode = static_cast<eGameMode>(Root.get("gamemode", eGameMode_NotSet).asInt());
- if (m_GameMode == eGameMode_Creative)
- {
- m_IsFlightCapable = true;
- }
-
m_Inventory.LoadFromJson(Root["inventory"]);
-
- int equippedSlotNum = Root.get("equippedItemSlot", 0).asInt();
- m_Inventory.SetEquippedSlotNum(equippedSlotNum);
+ m_Inventory.SetEquippedSlotNum(Root.get("equippedItemSlot", 0).asInt());
cEnderChestEntity::LoadFromJson(Root["enderchestinventory"], m_EnderChestContents);
- m_CurrentWorldName = Root.get("world", "world").asString();
- m_World = cRoot::Get()->GetWorld(m_CurrentWorldName);
- if (m_World == nullptr)
- {
- m_World = cRoot::Get()->GetDefaultWorld();
- }
-
m_RespawnPosition.x = Root.get("SpawnX", m_World->GetSpawnX()).asInt();
m_RespawnPosition.y = Root.get("SpawnY", m_World->GetSpawnY()).asInt();
m_RespawnPosition.z = Root.get("SpawnZ", m_World->GetSpawnZ()).asInt();
m_IsRespawnPointForced = Root.get("SpawnForced", true).asBool();
- m_SpawnWorldName = Root.get("SpawnWorld", cRoot::Get()->GetDefaultWorld()->GetName()).asString();
+ m_SpawnWorldName = Root.get("SpawnWorld", m_World->GetName()).asString();
- try
- {
- // Load the player stats.
- // We use the default world name (like bukkit) because stats are shared between dimensions / worlds.
- StatisticsSerializer::Load(m_Stats, m_DefaultWorldPath, GetUUID().ToLongString());
- }
- catch (...)
- {
- LOGWARNING("Failed loading player statistics");
- }
-
- FLOGD("Player {0} was read from file \"{1}\", spawning at {2:.2f} in world \"{3}\"",
- GetName(), a_FileName, GetPosition(), m_World->GetName()
+ FLOGD("Player \"{0}\" with save file \"{1}\" is spawning at {2:.2f} in world \"{3}\"",
+ GetName(), FileName, GetPosition(), m_World->GetName()
);
-
- return true;
}
@@ -3181,7 +3116,7 @@ void cPlayer::SpawnOn(cClientHandle & a_Client)
void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
{
- m_ClientHandle->Tick(a_Dt.count());
+ m_ClientHandle->Tick(a_Dt);
if (m_ClientHandle->IsDestroyed())
{
diff --git a/src/Entities/Player.h b/src/Entities/Player.h
index d1dfffa0b..87eaad2fe 100644
--- a/src/Entities/Player.h
+++ b/src/Entities/Player.h
@@ -419,15 +419,10 @@ public:
/** Saves all player data, such as inventory, to JSON. */
void SaveToDisk(void);
- /** Loads the player data from the disk file.
+ /** Loads the player data from the save file.
Sets m_World to the world where the player will spawn, based on the stored world name or the default world by calling LoadFromFile(). */
void LoadFromDisk();
- /** Loads the player data from the specified file.
- Sets m_World to the world where the player will spawn, based on the stored world name or the default world.
- Returns true on success, false if the player wasn't found, and excepts with base std::runtime_error if the data couldn't be read or parsed. */
- bool LoadFromFile(const AString & a_FileName);
-
const AString & GetLoadedWorldName() const { return m_CurrentWorldName; }
/** Opens the inventory of any tame horse the player is riding.
diff --git a/src/Generating/BioGen.cpp b/src/Generating/BioGen.cpp
index 74547800f..5e8f5dddc 100644
--- a/src/Generating/BioGen.cpp
+++ b/src/Generating/BioGen.cpp
@@ -5,7 +5,6 @@
#include "Globals.h"
#include "BioGen.h"
-#include <iostream>
#include "IntGen.h"
#include "ProtIntGen.h"
#include "../IniFile.h"
@@ -1198,6 +1197,8 @@ std::unique_ptr<cBiomeGen> cBiomeGen::CreateBiomeGen(cIniFile & a_IniFile, int a
// Change to 1 to enable the perf test:
#if 0
+#include <iostream>
+
class cBioGenPerfTest
{
public:
diff --git a/src/Items/ItemDye.h b/src/Items/ItemDye.h
index 683e8a73f..38cc2e4b2 100644
--- a/src/Items/ItemDye.h
+++ b/src/Items/ItemDye.h
@@ -128,21 +128,18 @@ public:
case E_BLOCK_BEETROOTS:
{
+ // Fix GH #4805.
+ // Bonemeal should only advance growth, not spawn produce, and should not be consumed if plant at maturity:
if (a_World.GrowPlantAt(a_BlockPos, 1) <= 0)
{
- // Fix GH #4805 (bonemeal should only advance growth, not spawn produce):
return false;
}
-
a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0);
-
- // 75% chance of 1-stage growth:
- if (!GetRandomProvider().RandBool(0.75))
+ if (GetRandomProvider().RandBool(0.25))
{
- // Hit the 25%, rollback:
+ // 75% chance of 1-stage growth, but we hit the 25%, rollback:
a_World.GrowPlantAt(a_BlockPos, -1);
}
-
return true;
} // case beetroots
diff --git a/src/Physics/Explodinator.cpp b/src/Physics/Explodinator.cpp
index b220508ce..72d050c59 100644
--- a/src/Physics/Explodinator.cpp
+++ b/src/Physics/Explodinator.cpp
@@ -241,7 +241,6 @@ namespace Explodinator
Currently missing conduits from 1.13 */
static bool BlockAlwaysDrops(const BLOCKTYPE a_Block)
{
- // If it's a Shulker box
if (IsBlockShulkerBox(a_Block))
{
return true;
diff --git a/src/Protocol/Protocol_1_14.cpp b/src/Protocol/Protocol_1_14.cpp
index 0d0bc49a0..f38227edd 100644
--- a/src/Protocol/Protocol_1_14.cpp
+++ b/src/Protocol/Protocol_1_14.cpp
@@ -90,9 +90,6 @@ void cProtocol_1_14::SendLogin(const cPlayer & a_Player, const cWorld & a_World)
// cPacketizer Pkt(*this, pktDifficulty);
// Pkt.WriteBEInt8(1);
}
-
- // Send player abilities:
- SendPlayerAbilities();
}
diff --git a/src/Protocol/Protocol_1_8.cpp b/src/Protocol/Protocol_1_8.cpp
index d29126e4e..e2e8ca499 100644
--- a/src/Protocol/Protocol_1_8.cpp
+++ b/src/Protocol/Protocol_1_8.cpp
@@ -934,13 +934,12 @@ void cProtocol_1_8_0::SendPlayerAbilities(void)
{
ASSERT(m_State == 3); // In game mode?
- cPacketizer Pkt(*this, pktPlayerAbilities);
Byte Flags = 0;
- cPlayer * Player = m_Client->GetPlayer();
- if (Player->IsGameModeCreative())
+ const cPlayer * Player = m_Client->GetPlayer();
+
+ if (Player->IsGameModeCreative() || Player->IsGameModeSpectator())
{
- Flags |= 0x01;
- Flags |= 0x08; // Godmode, used for creative
+ Flags |= 0x01; // Invulnerability.
}
if (Player->IsFlying())
{
@@ -950,6 +949,12 @@ void cProtocol_1_8_0::SendPlayerAbilities(void)
{
Flags |= 0x04;
}
+ if (Player->IsGameModeCreative())
+ {
+ Flags |= 0x08; // Godmode: creative instant break.
+ }
+
+ cPacketizer Pkt(*this, pktPlayerAbilities);
Pkt.WriteBEUInt8(Flags);
Pkt.WriteBEFloat(static_cast<float>(0.05 * Player->GetFlyingMaxSpeed()));
Pkt.WriteBEFloat(static_cast<float>(0.1 * Player->GetNormalMaxSpeed()));
diff --git a/src/World.cpp b/src/World.cpp
index 3b52bd9aa..94fc0ba00 100644
--- a/src/World.cpp
+++ b/src/World.cpp
@@ -1028,6 +1028,12 @@ void cWorld::Tick(std::chrono::milliseconds a_Dt, std::chrono::milliseconds a_La
BroadcastPlayerListUpdatePing();
}
+ // Process all clients' buffered actions:
+ for (const auto Player : m_Players)
+ {
+ Player->GetClientHandle()->ProcessProtocolIn();
+ }
+
TickQueuedChunkDataSets();
TickQueuedBlocks();
m_ChunkMap.Tick(a_Dt);
@@ -1234,7 +1240,7 @@ void cWorld::TickQueuedEntityAdditions(void)
decltype(m_EntitiesToAdd) EntitiesToAdd;
{
cCSLock Lock(m_CSEntitiesToAdd);
- EntitiesToAdd = std::move(m_EntitiesToAdd);
+ std::swap(EntitiesToAdd, m_EntitiesToAdd);
}
// Ensures m_Players manipulation happens under the chunkmap lock.
diff --git a/src/WorldStorage/StatisticsSerializer.cpp b/src/WorldStorage/StatisticsSerializer.cpp
index df7aa9895..5143e174d 100644
--- a/src/WorldStorage/StatisticsSerializer.cpp
+++ b/src/WorldStorage/StatisticsSerializer.cpp
@@ -55,26 +55,15 @@ static void LoadCustomStatFromJSON(StatisticsManager & Manager, const Json::Valu
for (auto it = a_In.begin(); it != a_In.end(); ++it)
{
const auto & Key = it.key().asString();
- const auto StatInfo = NamespaceSerializer::SplitNamespacedID(Key);
- if (StatInfo.first == NamespaceSerializer::Namespace::Unknown)
+ const auto & [Namespace, Name] = NamespaceSerializer::SplitNamespacedID(Key);
+
+ if (Namespace == NamespaceSerializer::Namespace::Unknown)
{
// Ignore non-Vanilla, non-Cuberite namespaces for now:
continue;
}
- const auto & StatName = StatInfo.second;
- try
- {
- Manager.Custom[NamespaceSerializer::ToCustomStatistic(StatName)] = it->asUInt();
- }
- catch (const std::out_of_range &)
- {
- FLOGWARNING("Invalid statistic type \"{}\"", StatName);
- }
- catch (const Json::LogicError &)
- {
- FLOGWARNING("Invalid statistic value for type \"{}\"", StatName);
- }
+ Manager.Custom[NamespaceSerializer::ToCustomStatistic(Name)] = it->asUInt();
}
}