Miscellaneous fixes (#5320)
* Protocol: update Abilities flags + Add Spectator handling * BioGen: move <iostream> include * ClientHandle: rename Respawn packet dimension check flag * Make it clearer what it's doing. * ClientHandle: move ProcessProtocolIn calls to World * Player: remove some redundant initialisation * Player: UpdateCapabilities enables flight for spectators * Produce growth: improve comments * ClientHandle: run unload checks using delta time * Fix forgotten initialisation of time member
This commit is contained in:
parent
afe07fe090
commit
d49ce751ba
@ -2147,7 +2147,7 @@ function OnAllChunksAvailable()</pre> All return values from the callbacks are i
|
|||||||
Type = "boolean",
|
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 =
|
GrowTree =
|
||||||
{
|
{
|
||||||
|
@ -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_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_LastStreamedChunkZ(std::numeric_limits<decltype(m_LastStreamedChunkZ)>::max()),
|
||||||
m_TicksSinceLastPacket(0),
|
m_TicksSinceLastPacket(0),
|
||||||
|
m_TimeSinceLastUnloadCheck(0),
|
||||||
m_Ping(1000),
|
m_Ping(1000),
|
||||||
m_PingID(1),
|
m_PingID(1),
|
||||||
m_BlockDigAnimStage(-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()
|
void cClientHandle::ProcessProtocolOut()
|
||||||
{
|
{
|
||||||
decltype(m_OutgoingData) OutgoingData;
|
decltype(m_OutgoingData) OutgoingData;
|
||||||
@ -394,7 +425,7 @@ void cClientHandle::FinishAuthenticate()
|
|||||||
}
|
}
|
||||||
catch (const std::exception & Oops)
|
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.");
|
Kick("Contact an operator.\n\nYour player's save files could not be parsed.\nTo avoid data loss you are prevented from joining.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -632,8 +663,6 @@ void cClientHandle::UnloadOutOfRangeChunks(void)
|
|||||||
m_Player->GetWorld()->RemoveChunkClient(itr->m_ChunkX, itr->m_ChunkZ, this);
|
m_Player->GetWorld()->RemoveChunkClient(itr->m_ChunkX, itr->m_ChunkZ, this);
|
||||||
SendUnloadChunk(itr->m_ChunkX, itr->m_ChunkZ);
|
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:
|
// Only allow enchantment if the player has sufficient levels and lapis to enchant:
|
||||||
if ((m_Player->GetCurrentXp() >= XpRequired) && (LapisStack.m_ItemCount >= LapisRequired))
|
if ((m_Player->GetCurrentXp() >= XpRequired) && (LapisStack.m_ItemCount >= LapisRequired))
|
||||||
{
|
{
|
||||||
/** We need to reduce the player's level by the number of lapis required.
|
// 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. */
|
// However we need to keep the resulting percentage filled the same.
|
||||||
|
|
||||||
const auto TargetLevel = m_Player->GetXpLevel() - LapisRequired;
|
const auto TargetLevel = m_Player->GetXpLevel() - LapisRequired;
|
||||||
const auto CurrentFillPercent = m_Player->GetXpPercentage();
|
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);
|
cItem EnchantedItem = Window->m_SlotArea->SelectEnchantedOption(a_Enchantment);
|
||||||
|
|
||||||
// Set the item slot to our new enchanted item:
|
// 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;
|
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())
|
if (IsDestroyed())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -2156,16 +2176,24 @@ void cClientHandle::Tick(float a_Dt)
|
|||||||
StreamNextChunks();
|
StreamNextChunks();
|
||||||
|
|
||||||
// Unload all chunks that are out of the view distance (every 5 seconds):
|
// 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();
|
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:
|
// Handle block break animation:
|
||||||
if (m_BlockDigAnimStage > -1)
|
if (m_BlockDigAnimStage > -1)
|
||||||
{
|
{
|
||||||
int lastAnimVal = m_BlockDigAnimStage;
|
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)
|
if (m_BlockDigAnimStage > 9000)
|
||||||
{
|
{
|
||||||
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
|
// The client goes crazy if we send a respawn packet with the dimension of the current world
|
||||||
// So we send a temporary one first.
|
// So we send a temporary one first.
|
||||||
// This is not needed when the player dies, hence the a_ShouldIgnoreDimensionChecks flag.
|
// This is not needed when the player dies, hence the a_IsRespawningFromDeath flag.
|
||||||
// a_ShouldIgnoreDimensionChecks is true only at cPlayer::Respawn, which is called after
|
// a_IsRespawningFromDeath is true only at cPlayer::Respawn, which is called after the player dies.
|
||||||
// the player dies.
|
|
||||||
eDimension TemporaryDimension = (a_Dimension == dimOverworld) ? dimNether : dimOverworld;
|
// First send a temporary dimension to placate the client:
|
||||||
m_Protocol->SendRespawn(TemporaryDimension);
|
m_Protocol->SendRespawn((a_Dimension == dimOverworld) ? dimNether : dimOverworld);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Protocol->SendRespawn(a_Dimension);
|
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)
|
void cClientHandle::OnLinkCreated(cTCPLinkPtr a_Link)
|
||||||
{
|
{
|
||||||
m_Link = a_Link;
|
m_Link = a_Link;
|
||||||
|
@ -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);
|
||||||
void ProxyInit(const AString & a_IPString, const cUUID & a_UUID, const Json::Value & a_Properties);
|
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. */
|
/** Flushes all buffered outgoing data to the network. */
|
||||||
void ProcessProtocolOut();
|
void ProcessProtocolOut();
|
||||||
|
|
||||||
@ -133,7 +137,7 @@ public: // tolua_export
|
|||||||
inline bool IsLoggedIn(void) const { return (m_State >= csAuthenticating); }
|
inline bool IsLoggedIn(void) const { return (m_State >= csAuthenticating); }
|
||||||
|
|
||||||
/** Called while the client is being ticked from the world via its cPlayer object */
|
/** 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 */
|
/** Called while the client is being ticked from the cServer object */
|
||||||
void ServerTick(float a_Dt);
|
void ServerTick(float a_Dt);
|
||||||
@ -208,7 +212,7 @@ public: // tolua_export
|
|||||||
void SendRemoveEntityEffect (const cEntity & a_Entity, int a_EffectID);
|
void SendRemoveEntityEffect (const cEntity & a_Entity, int a_EffectID);
|
||||||
void SendResourcePack (const AString & a_ResourcePackUrl);
|
void SendResourcePack (const AString & a_ResourcePackUrl);
|
||||||
void SendResetTitle (void); // tolua_export
|
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 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 SendScoreboardObjective (const AString & a_Name, const AString & a_DisplayName, Byte a_Mode);
|
||||||
void SendSetSubTitle (const cCompositeChat & a_SubTitle); // tolua_export
|
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.
|
/** 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.
|
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.
|
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. */
|
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;
|
cPlayer * m_Player;
|
||||||
|
|
||||||
/** This is an optimization which saves you an iteration of m_SentChunks if you just want to know
|
/** 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_LastStreamedChunkX;
|
||||||
int m_LastStreamedChunkZ;
|
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()) */
|
/** Number of ticks since the last network packet was received (increased in Tick(), reset in OnReceivedData()) */
|
||||||
std::atomic<int> m_TicksSinceLastPacket;
|
std::atomic<int> m_TicksSinceLastPacket;
|
||||||
|
|
||||||
|
/** The time since UnloadOutOfRangeChunks was last called. */
|
||||||
|
std::chrono::milliseconds m_TimeSinceLastUnloadCheck;
|
||||||
|
|
||||||
/** Duration of the last completed client ping. */
|
/** Duration of the last completed client ping. */
|
||||||
std::chrono::steady_clock::duration m_Ping;
|
std::chrono::steady_clock::duration m_Ping;
|
||||||
|
|
||||||
@ -604,10 +609,6 @@ private:
|
|||||||
Only succeeds if a_NewState > m_State, otherwise returns false. */
|
Only succeeds if a_NewState > m_State, otherwise returns false. */
|
||||||
bool SetState(eState a_NewState);
|
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:
|
// cTCPLink::cCallbacks overrides:
|
||||||
virtual void OnLinkCreated(cTCPLinkPtr a_Link) override;
|
virtual void OnLinkCreated(cTCPLinkPtr a_Link) override;
|
||||||
virtual void OnReceivedData(const char * a_Data, size_t a_Length) override;
|
virtual void OnReceivedData(const char * a_Data, size_t a_Length) override;
|
||||||
|
@ -66,6 +66,9 @@ const int cPlayer::MAX_FOOD_LEVEL = 20;
|
|||||||
// 6000 ticks or 5 minutes
|
// 6000 ticks or 5 minutes
|
||||||
#define PLAYER_INVENTORY_SAVE_INTERVAL 6000
|
#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_TO_LEVEL15 255
|
||||||
#define XP_PER_LEVEL_TO15 17
|
#define XP_PER_LEVEL_TO15 17
|
||||||
#define XP_TO_LEVEL30 825
|
#define XP_TO_LEVEL30 825
|
||||||
@ -114,29 +117,19 @@ cPlayer::BodyStanceGliding::BodyStanceGliding(cPlayer & a_Player) :
|
|||||||
cPlayer::cPlayer(const std::shared_ptr<cClientHandle> & a_Client) :
|
cPlayer::cPlayer(const std::shared_ptr<cClientHandle> & a_Client) :
|
||||||
Super(etPlayer, 0.6f, 1.8f),
|
Super(etPlayer, 0.6f, 1.8f),
|
||||||
m_BodyStance(BodyStanceStanding(*this)),
|
m_BodyStance(BodyStanceStanding(*this)),
|
||||||
m_FoodLevel(MAX_FOOD_LEVEL),
|
|
||||||
m_FoodSaturationLevel(5.0),
|
|
||||||
m_FoodTickTimer(0),
|
|
||||||
m_FoodExhaustionLevel(0.0),
|
|
||||||
m_Inventory(*this),
|
m_Inventory(*this),
|
||||||
m_EnderChestContents(9, 3),
|
m_EnderChestContents(9, 3),
|
||||||
m_DefaultWorldPath(cRoot::Get()->GetDefaultWorld()->GetDataPath()),
|
m_DefaultWorldPath(cRoot::Get()->GetDefaultWorld()->GetDataPath()),
|
||||||
m_GameMode(eGameMode_NotSet),
|
|
||||||
m_ClientHandle(a_Client),
|
m_ClientHandle(a_Client),
|
||||||
m_NormalMaxSpeed(1.0),
|
m_NormalMaxSpeed(1.0),
|
||||||
m_SprintingMaxSpeed(1.3),
|
m_SprintingMaxSpeed(1.3),
|
||||||
m_FlyingMaxSpeed(1.0),
|
m_FlyingMaxSpeed(1.0),
|
||||||
m_IsChargingBow(false),
|
m_IsChargingBow(false),
|
||||||
m_IsFishing(false),
|
m_IsFishing(false),
|
||||||
m_IsFlightCapable(false),
|
|
||||||
m_IsFlying(false),
|
|
||||||
m_IsFrozen(false),
|
m_IsFrozen(false),
|
||||||
m_IsLeftHanded(false),
|
m_IsLeftHanded(false),
|
||||||
m_IsTeleporting(false),
|
m_IsTeleporting(false),
|
||||||
m_IsVisible(true),
|
|
||||||
m_EatingFinishTick(-1),
|
m_EatingFinishTick(-1),
|
||||||
m_LifetimeTotalXp(0),
|
|
||||||
m_CurrentXp(0),
|
|
||||||
m_BowCharge(0),
|
m_BowCharge(0),
|
||||||
m_FloaterID(cEntity::INVALID_ID),
|
m_FloaterID(cEntity::INVALID_ID),
|
||||||
m_Team(nullptr),
|
m_Team(nullptr),
|
||||||
@ -150,13 +143,9 @@ cPlayer::cPlayer(const std::shared_ptr<cClientHandle> & a_Client) :
|
|||||||
m_CurrentWindow = m_InventoryWindow;
|
m_CurrentWindow = m_InventoryWindow;
|
||||||
m_InventoryWindow->OpenedByPlayer(*this);
|
m_InventoryWindow->OpenedByPlayer(*this);
|
||||||
|
|
||||||
SetMaxHealth(MAX_HEALTH);
|
|
||||||
m_Health = MAX_HEALTH;
|
|
||||||
|
|
||||||
LoadFromDisk();
|
LoadFromDisk();
|
||||||
UpdateCapabilities();
|
SetMaxHealth(MAX_HEALTH);
|
||||||
|
UpdateCapabilities(); // Only required for plugins listening to HOOK_SPAWNING_ENTITY to not read uninitialised variables.
|
||||||
m_LastGroundHeight = static_cast<float>(GetPosY());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -382,7 +371,6 @@ void cPlayer::SetFoodLevel(int a_FoodLevel)
|
|||||||
|
|
||||||
if (cRoot::Get()->GetPluginManager()->CallHookPlayerFoodLevelChange(*this, FoodLevel))
|
if (cRoot::Get()->GetPluginManager()->CallHookPlayerFoodLevelChange(*this, FoodLevel))
|
||||||
{
|
{
|
||||||
m_FoodSaturationLevel = 5.0;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -943,8 +931,8 @@ void cPlayer::Respawn(void)
|
|||||||
|
|
||||||
// Reset food level:
|
// Reset food level:
|
||||||
m_FoodLevel = MAX_FOOD_LEVEL;
|
m_FoodLevel = MAX_FOOD_LEVEL;
|
||||||
m_FoodSaturationLevel = 5.0;
|
m_FoodSaturationLevel = RESPAWN_FOOD_SATURATION;
|
||||||
m_FoodExhaustionLevel = 0.0;
|
m_FoodExhaustionLevel = 0;
|
||||||
|
|
||||||
// Reset Experience
|
// Reset Experience
|
||||||
m_CurrentXp = 0;
|
m_CurrentXp = 0;
|
||||||
@ -1341,24 +1329,23 @@ void cPlayer::SetGameMode(eGameMode a_GameMode)
|
|||||||
|
|
||||||
void cPlayer::UpdateCapabilities()
|
void cPlayer::UpdateCapabilities()
|
||||||
{
|
{
|
||||||
// Fly ability:
|
if (IsGameModeCreative())
|
||||||
if (IsGameModeCreative() || IsGameModeSpectator())
|
|
||||||
{
|
{
|
||||||
m_IsFlightCapable = true;
|
m_IsFlightCapable = true;
|
||||||
|
m_IsVisible = true;
|
||||||
|
}
|
||||||
|
else if (IsGameModeSpectator())
|
||||||
|
{
|
||||||
|
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.
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_IsFlying = false;
|
|
||||||
m_IsFlightCapable = false;
|
m_IsFlightCapable = false;
|
||||||
}
|
m_IsFlying = false;
|
||||||
|
m_IsVisible = true;
|
||||||
// Visible:
|
|
||||||
m_IsVisible = !IsGameModeSpectator();
|
|
||||||
|
|
||||||
// Clear the current dragging item of spectators:
|
|
||||||
if (IsGameModeSpectator())
|
|
||||||
{
|
|
||||||
m_DraggingItem.Empty();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1801,79 +1788,54 @@ void cPlayer::LoadFromDisk()
|
|||||||
{
|
{
|
||||||
LoadRank();
|
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;
|
Json::Value Root;
|
||||||
|
const auto & UUID = GetUUID();
|
||||||
|
const auto & FileName = GetUUIDFileName(UUID);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Load the data from the file and parse:
|
// Load the data from the save file and parse:
|
||||||
InputFileStream(a_FileName) >> Root;
|
InputFileStream(FileName) >> Root;
|
||||||
}
|
|
||||||
catch (const Json::Exception & Oops)
|
// Load the player stats.
|
||||||
{
|
// We use the default world name (like bukkit) because stats are shared between dimensions / worlds.
|
||||||
// Parse failure:
|
StatisticsSerializer::Load(m_Stats, m_DefaultWorldPath, UUID.ToLongString());
|
||||||
throw std::runtime_error(Oops.what());
|
|
||||||
}
|
}
|
||||||
catch (const InputFileStream::failure &)
|
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:
|
// Save file exists but unreadable:
|
||||||
return false;
|
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:
|
m_CurrentWorldName = Root.get("world", cRoot::Get()->GetDefaultWorld()->GetName()).asString();
|
||||||
Json::Value & JSON_PlayerPosition = Root["position"];
|
m_World = cRoot::Get()->GetWorld(m_CurrentWorldName);
|
||||||
if (JSON_PlayerPosition.size() == 3)
|
|
||||||
|
if (const auto & PlayerPosition = Root["position"]; PlayerPosition.size() == 3)
|
||||||
{
|
{
|
||||||
SetPosX(JSON_PlayerPosition[0].asDouble());
|
SetPosition(PlayerPosition[0].asDouble(), PlayerPosition[1].asDouble(), PlayerPosition[2].asDouble());
|
||||||
SetPosY(JSON_PlayerPosition[1].asDouble());
|
|
||||||
SetPosZ(JSON_PlayerPosition[2].asDouble());
|
|
||||||
m_LastPosition = GetPosition();
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
Json::Value & JSON_PlayerRotation = Root["rotation"];
|
|
||||||
if (JSON_PlayerRotation.size() == 3)
|
|
||||||
{
|
{
|
||||||
SetYaw (static_cast<float>(JSON_PlayerRotation[0].asDouble()));
|
SetPosition(Vector3d(0.5, 0.5, 0.5) + Vector3i(m_World->GetSpawnX(), m_World->GetSpawnY(), m_World->GetSpawnZ()));
|
||||||
SetPitch (static_cast<float>(JSON_PlayerRotation[1].asDouble()));
|
|
||||||
SetRoll (static_cast<float>(JSON_PlayerRotation[2].asDouble()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Health = Root.get("health", 0).asFloat();
|
if (const auto & PlayerRotation = Root["rotation"]; PlayerRotation.size() == 3)
|
||||||
|
{
|
||||||
|
SetYaw (PlayerRotation[0].asDouble());
|
||||||
|
SetPitch(PlayerRotation[1].asDouble());
|
||||||
|
SetRoll (PlayerRotation[2].asDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Health = Root.get("health", MAX_HEALTH).asFloat();
|
||||||
m_AirLevel = Root.get("air", MAX_AIR_LEVEL).asInt();
|
m_AirLevel = Root.get("air", MAX_AIR_LEVEL).asInt();
|
||||||
m_FoodLevel = Root.get("food", MAX_FOOD_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_FoodTickTimer = Root.get("foodTickTimer", 0).asInt();
|
||||||
m_FoodExhaustionLevel = Root.get("foodExhaustion", 0).asDouble();
|
m_FoodExhaustionLevel = Root.get("foodExhaustion", 0).asDouble();
|
||||||
m_LifetimeTotalXp = Root.get("xpTotal", 0).asInt();
|
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());
|
m_GameMode = static_cast<eGameMode>(Root.get("gamemode", eGameMode_NotSet).asInt());
|
||||||
|
|
||||||
if (m_GameMode == eGameMode_Creative)
|
|
||||||
{
|
|
||||||
m_IsFlightCapable = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_Inventory.LoadFromJson(Root["inventory"]);
|
m_Inventory.LoadFromJson(Root["inventory"]);
|
||||||
|
m_Inventory.SetEquippedSlotNum(Root.get("equippedItemSlot", 0).asInt());
|
||||||
int equippedSlotNum = Root.get("equippedItemSlot", 0).asInt();
|
|
||||||
m_Inventory.SetEquippedSlotNum(equippedSlotNum);
|
|
||||||
|
|
||||||
cEnderChestEntity::LoadFromJson(Root["enderchestinventory"], m_EnderChestContents);
|
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.x = Root.get("SpawnX", m_World->GetSpawnX()).asInt();
|
||||||
m_RespawnPosition.y = Root.get("SpawnY", m_World->GetSpawnY()).asInt();
|
m_RespawnPosition.y = Root.get("SpawnY", m_World->GetSpawnY()).asInt();
|
||||||
m_RespawnPosition.z = Root.get("SpawnZ", m_World->GetSpawnZ()).asInt();
|
m_RespawnPosition.z = Root.get("SpawnZ", m_World->GetSpawnZ()).asInt();
|
||||||
m_IsRespawnPointForced = Root.get("SpawnForced", true).asBool();
|
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
|
FLOGD("Player \"{0}\" with save file \"{1}\" is spawning at {2:.2f} in world \"{3}\"",
|
||||||
{
|
GetName(), FileName, GetPosition(), m_World->GetName()
|
||||||
// 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()
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -3181,7 +3116,7 @@ void cPlayer::SpawnOn(cClientHandle & a_Client)
|
|||||||
|
|
||||||
void cPlayer::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk)
|
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())
|
if (m_ClientHandle->IsDestroyed())
|
||||||
{
|
{
|
||||||
|
@ -419,15 +419,10 @@ public:
|
|||||||
/** Saves all player data, such as inventory, to JSON. */
|
/** Saves all player data, such as inventory, to JSON. */
|
||||||
void SaveToDisk(void);
|
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(). */
|
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();
|
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; }
|
const AString & GetLoadedWorldName() const { return m_CurrentWorldName; }
|
||||||
|
|
||||||
/** Opens the inventory of any tame horse the player is riding.
|
/** Opens the inventory of any tame horse the player is riding.
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#include "Globals.h"
|
#include "Globals.h"
|
||||||
#include "BioGen.h"
|
#include "BioGen.h"
|
||||||
#include <iostream>
|
|
||||||
#include "IntGen.h"
|
#include "IntGen.h"
|
||||||
#include "ProtIntGen.h"
|
#include "ProtIntGen.h"
|
||||||
#include "../IniFile.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:
|
// Change to 1 to enable the perf test:
|
||||||
#if 0
|
#if 0
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
class cBioGenPerfTest
|
class cBioGenPerfTest
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -128,21 +128,18 @@ public:
|
|||||||
|
|
||||||
case E_BLOCK_BEETROOTS:
|
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)
|
if (a_World.GrowPlantAt(a_BlockPos, 1) <= 0)
|
||||||
{
|
{
|
||||||
// Fix GH #4805 (bonemeal should only advance growth, not spawn produce):
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0);
|
a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0);
|
||||||
|
if (GetRandomProvider().RandBool(0.25))
|
||||||
// 75% chance of 1-stage growth:
|
|
||||||
if (!GetRandomProvider().RandBool(0.75))
|
|
||||||
{
|
{
|
||||||
// Hit the 25%, rollback:
|
// 75% chance of 1-stage growth, but we hit the 25%, rollback:
|
||||||
a_World.GrowPlantAt(a_BlockPos, -1);
|
a_World.GrowPlantAt(a_BlockPos, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} // case beetroots
|
} // case beetroots
|
||||||
|
|
||||||
|
@ -241,7 +241,6 @@ namespace Explodinator
|
|||||||
Currently missing conduits from 1.13 */
|
Currently missing conduits from 1.13 */
|
||||||
static bool BlockAlwaysDrops(const BLOCKTYPE a_Block)
|
static bool BlockAlwaysDrops(const BLOCKTYPE a_Block)
|
||||||
{
|
{
|
||||||
// If it's a Shulker box
|
|
||||||
if (IsBlockShulkerBox(a_Block))
|
if (IsBlockShulkerBox(a_Block))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
@ -90,9 +90,6 @@ void cProtocol_1_14::SendLogin(const cPlayer & a_Player, const cWorld & a_World)
|
|||||||
// cPacketizer Pkt(*this, pktDifficulty);
|
// cPacketizer Pkt(*this, pktDifficulty);
|
||||||
// Pkt.WriteBEInt8(1);
|
// Pkt.WriteBEInt8(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send player abilities:
|
|
||||||
SendPlayerAbilities();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -934,13 +934,12 @@ void cProtocol_1_8_0::SendPlayerAbilities(void)
|
|||||||
{
|
{
|
||||||
ASSERT(m_State == 3); // In game mode?
|
ASSERT(m_State == 3); // In game mode?
|
||||||
|
|
||||||
cPacketizer Pkt(*this, pktPlayerAbilities);
|
|
||||||
Byte Flags = 0;
|
Byte Flags = 0;
|
||||||
cPlayer * Player = m_Client->GetPlayer();
|
const cPlayer * Player = m_Client->GetPlayer();
|
||||||
if (Player->IsGameModeCreative())
|
|
||||||
|
if (Player->IsGameModeCreative() || Player->IsGameModeSpectator())
|
||||||
{
|
{
|
||||||
Flags |= 0x01;
|
Flags |= 0x01; // Invulnerability.
|
||||||
Flags |= 0x08; // Godmode, used for creative
|
|
||||||
}
|
}
|
||||||
if (Player->IsFlying())
|
if (Player->IsFlying())
|
||||||
{
|
{
|
||||||
@ -950,6 +949,12 @@ void cProtocol_1_8_0::SendPlayerAbilities(void)
|
|||||||
{
|
{
|
||||||
Flags |= 0x04;
|
Flags |= 0x04;
|
||||||
}
|
}
|
||||||
|
if (Player->IsGameModeCreative())
|
||||||
|
{
|
||||||
|
Flags |= 0x08; // Godmode: creative instant break.
|
||||||
|
}
|
||||||
|
|
||||||
|
cPacketizer Pkt(*this, pktPlayerAbilities);
|
||||||
Pkt.WriteBEUInt8(Flags);
|
Pkt.WriteBEUInt8(Flags);
|
||||||
Pkt.WriteBEFloat(static_cast<float>(0.05 * Player->GetFlyingMaxSpeed()));
|
Pkt.WriteBEFloat(static_cast<float>(0.05 * Player->GetFlyingMaxSpeed()));
|
||||||
Pkt.WriteBEFloat(static_cast<float>(0.1 * Player->GetNormalMaxSpeed()));
|
Pkt.WriteBEFloat(static_cast<float>(0.1 * Player->GetNormalMaxSpeed()));
|
||||||
|
@ -1028,6 +1028,12 @@ void cWorld::Tick(std::chrono::milliseconds a_Dt, std::chrono::milliseconds a_La
|
|||||||
BroadcastPlayerListUpdatePing();
|
BroadcastPlayerListUpdatePing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process all clients' buffered actions:
|
||||||
|
for (const auto Player : m_Players)
|
||||||
|
{
|
||||||
|
Player->GetClientHandle()->ProcessProtocolIn();
|
||||||
|
}
|
||||||
|
|
||||||
TickQueuedChunkDataSets();
|
TickQueuedChunkDataSets();
|
||||||
TickQueuedBlocks();
|
TickQueuedBlocks();
|
||||||
m_ChunkMap.Tick(a_Dt);
|
m_ChunkMap.Tick(a_Dt);
|
||||||
@ -1234,7 +1240,7 @@ void cWorld::TickQueuedEntityAdditions(void)
|
|||||||
decltype(m_EntitiesToAdd) EntitiesToAdd;
|
decltype(m_EntitiesToAdd) EntitiesToAdd;
|
||||||
{
|
{
|
||||||
cCSLock Lock(m_CSEntitiesToAdd);
|
cCSLock Lock(m_CSEntitiesToAdd);
|
||||||
EntitiesToAdd = std::move(m_EntitiesToAdd);
|
std::swap(EntitiesToAdd, m_EntitiesToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensures m_Players manipulation happens under the chunkmap lock.
|
// Ensures m_Players manipulation happens under the chunkmap lock.
|
||||||
|
@ -55,26 +55,15 @@ static void LoadCustomStatFromJSON(StatisticsManager & Manager, const Json::Valu
|
|||||||
for (auto it = a_In.begin(); it != a_In.end(); ++it)
|
for (auto it = a_In.begin(); it != a_In.end(); ++it)
|
||||||
{
|
{
|
||||||
const auto & Key = it.key().asString();
|
const auto & Key = it.key().asString();
|
||||||
const auto StatInfo = NamespaceSerializer::SplitNamespacedID(Key);
|
const auto & [Namespace, Name] = NamespaceSerializer::SplitNamespacedID(Key);
|
||||||
if (StatInfo.first == NamespaceSerializer::Namespace::Unknown)
|
|
||||||
|
if (Namespace == NamespaceSerializer::Namespace::Unknown)
|
||||||
{
|
{
|
||||||
// Ignore non-Vanilla, non-Cuberite namespaces for now:
|
// Ignore non-Vanilla, non-Cuberite namespaces for now:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto & StatName = StatInfo.second;
|
Manager.Custom[NamespaceSerializer::ToCustomStatistic(Name)] = it->asUInt();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user