diff --git a/code/components/citizen-server-impl/include/state/ServerGameState.h b/code/components/citizen-server-impl/include/state/ServerGameState.h index 0a1bcd2fdd..1a90e295fe 100644 --- a/code/components/citizen-server-impl/include/state/ServerGameState.h +++ b/code/components/citizen-server-impl/include/state/ServerGameState.h @@ -544,15 +544,102 @@ struct CVehicleAngVelocityNodeData float angVelX; float angVelY; float angVelZ; +}; + +#ifdef STATE_RDR3 + +struct CSyncedPedAttribute +{ + int32_t dword0[8]; + uint32_t m_defaultEnergy; + uint32_t m_coreEnergy; + uint32_t dword28; + uint32_t m_energyReservesEnergy; + uint32_t m_overPowerEnergy; + uint32_t m_goldCoreEnergy; + uint8_t byte38; + bool byte39; + bool m_hasEnergyReserves; + bool m_byte3B; + bool m_hasOverPowerEnergy; + bool m_hasGoldCoreEnergy; + bool byte3E; // "true" when something == "UPGRADE_HEALTH_TANK_1" + bool byte3F; // "true" when something == "UPGRADE_STAMINA_TANK_1" + + template + void Serialize(Serializer& s); + + inline virtual void Reset() + { + m_defaultEnergy = 0; + dword28 = 0; + byte38 = 0; + m_overPowerEnergy = 0; + memset(dword0, 0, sizeof(dword0)); + } +}; + +struct CSyncedPedAttributeExtra : CSyncedPedAttribute +{ + uint32_t m_goldCoreEnergyDuration; + uint32_t m_overPowerEnergyDuration; + uint32_t dword48; + bool m_hasOverPowerEnergyDuration; + bool m_hasGoldCoreEnergyDuration; + bool byte4E; + + template + void Serialize(Serializer& s); + + inline void Reset() override + { + CSyncedPedAttribute::Reset(); + + m_goldCoreEnergyDuration = 0; + dword48 = 0; + m_hasOverPowerEnergyDuration = 0; + byte4E = 0; + } +}; + +struct DataNode_1435995f0NodeData +{ + CSyncedPedAttribute m_maxHealthAttribute; + float m_float40; + bool m_byte44; + + // Added members, these doesn't exist on the client's actual data + uint32_t m_maxHealth; }; + +#endif struct CPedHealthNodeData -{ +{ +#ifdef STATE_FIVE + int maxHealth; int health; int armour; uint32_t causeOfDeath; - int sourceOfDamage; + int sourceOfDamage; + +#elif defined(STATE_RDR3) + + CSyncedPedAttributeExtra m_healthAttribute; + + bool byte4F; + uint32_t m_health; + uint32_t gap54; + uint32_t m_energyRecharge[6]; // value until we reach m_health; + bool m_hasMaxHealth; + bool byte71; + bool byte72; + bool byte73; + bool m_hasEnergyRecharges; + bool byte75; + +#endif }; struct CPedOrientationNodeData @@ -803,7 +890,11 @@ struct SyncTreeBase virtual CPlayerGameStateNodeData* GetPlayerGameState() = 0; - virtual CPedHealthNodeData* GetPedHealth() = 0; + virtual CPedHealthNodeData* GetPedHealth() = 0; + +#ifdef STATE_RDR3 + virtual DataNode_1435995f0NodeData* Get1435995f0() = 0; +#endif virtual CVehicleHealthNodeData* GetVehicleHealth() = 0; diff --git a/code/components/citizen-server-impl/include/state/SyncTrees_Header.h b/code/components/citizen-server-impl/include/state/SyncTrees_Header.h index 5976957ce8..842ef8fda2 100644 --- a/code/components/citizen-server-impl/include/state/SyncTrees_Header.h +++ b/code/components/citizen-server-impl/include/state/SyncTrees_Header.h @@ -506,6 +506,14 @@ struct ParseSerializer return true; } + template + bool SerializeCapped(int size, int maxValue, T& data) + { + data = std::min(data, static_cast(maxValue)); + + return Serialize(size, data); + } + bool SerializePosition(int size, float& dataX, float& dataY, float& dataZ) { dataX = state->buffer.ReadSignedFloat(size, 27648.0f); @@ -561,6 +569,14 @@ struct UnparseSerializer return true; } + template + bool SerializeCapped(int size, int maxValue, T& data) + { + data = std::min(data, static_cast(maxValue)); + + return Serialize(size, data); + } + bool SerializePosition(int size, float& dataX, float& dataY, float& dataZ) { state->buffer.WriteSignedFloat(size, 27648.0f, dataX); diff --git a/code/components/citizen-server-impl/include/state/SyncTrees_RDR3.h b/code/components/citizen-server-impl/include/state/SyncTrees_RDR3.h index e976dc5938..3a32c36d47 100644 --- a/code/components/citizen-server-impl/include/state/SyncTrees_RDR3.h +++ b/code/components/citizen-server-impl/include/state/SyncTrees_RDR3.h @@ -648,12 +648,255 @@ struct CPedComponentReservationDataNode { }; struct CPedScriptGameStateDataNode { }; struct CPedAttachDataNode { }; -struct CPedHealthDataNode +template +void CSyncedPedAttribute::Serialize(Serializer& s) +{ + bool hasCoreEnergy = m_coreEnergy != 0; + + bool hasByte38 = byte38 != 0; + + s.Serialize(hasCoreEnergy); + s.Serialize(byte39); + s.Serialize(m_byte3B); + s.Serialize(m_hasEnergyReserves); + s.Serialize(m_hasOverPowerEnergy); + s.Serialize(m_hasGoldCoreEnergy); + s.Serialize(hasByte38); + + s.Serialize(12, m_defaultEnergy); + + if (hasCoreEnergy) + { + s.Serialize(10, m_coreEnergy); + } + else + { + m_coreEnergy = 0; + } + + if (hasByte38) + { + s.SerializeCapped(4, 8, byte38); + + if (byte38) + { + for (int i = 0; i < byte38; ++i) + { + s.Serialize(10, dword0[i]); + } + } + } + else + { + byte38 = 0; + } + + if (m_hasEnergyReserves) + { + s.Serialize(11, m_energyReservesEnergy); + s.Serialize(byte3E); + s.Serialize(byte3F); + } + else + { + byte3E = 0; + m_energyReservesEnergy = 0; + } + + if (m_byte3B) + { + s.Serialize(10, dword28); + } + else + { + dword28 = 0; + } + + if (m_hasOverPowerEnergy) + { + s.Serialize(10, m_overPowerEnergy); + } + else + { + m_overPowerEnergy = 0; + } + + if (m_hasGoldCoreEnergy) + { + s.Serialize(10, m_goldCoreEnergy); + } + else + { + m_goldCoreEnergy = 0; + } +} + +template +void CSyncedPedAttributeExtra::Serialize(Serializer& s) +{ + CSyncedPedAttribute::Serialize(s); + + auto goldCoreEnergyDurationScaled = 0; + + if (m_byte3B) + { + s.Serialize(m_hasGoldCoreEnergyDuration); + + if (m_hasGoldCoreEnergyDuration) + { + goldCoreEnergyDurationScaled = m_goldCoreEnergyDuration / 1000; + + s.Serialize(12, goldCoreEnergyDurationScaled); + } + } + else + { + m_hasGoldCoreEnergyDuration = 0; + } + + m_goldCoreEnergyDuration = goldCoreEnergyDurationScaled * 1000; + + auto overPowerEnergyDurationScaled = 0; + + if (m_hasOverPowerEnergy) + { + s.Serialize(m_hasOverPowerEnergyDuration); + + if (m_hasOverPowerEnergyDuration) + { + overPowerEnergyDurationScaled = m_overPowerEnergyDuration / 1000; + + s.Serialize(12, overPowerEnergyDurationScaled); + } + } + else + { + m_hasOverPowerEnergyDuration = 0; + } + + m_overPowerEnergyDuration = overPowerEnergyDurationScaled * 1000; + + if (!m_hasGoldCoreEnergy) + { + byte4E = 0; + dword48 = 0; + + return; + } + + s.Serialize(byte4E); + + if (!byte4E) + { + dword48 = 0; + } + + auto dword48_scaled = dword48 / 1000; + + s.Serialize(12, dword48_scaled); + + dword48 = dword48_scaled * 1000; +} + +struct CPedHealthDataNode : GenericSerializeDataNode { CPedHealthNodeData data; - bool Parse(SyncParseState& state) + template + bool Serialize(Serializer& s) { + s.Serialize(data.m_hasMaxHealth); + s.Serialize(data.byte72); + s.Serialize(data.m_hasEnergyRecharges); + s.Serialize(data.byte75); + s.Serialize(data.byte73); + + if (!data.m_hasMaxHealth) + { + auto isDead = data.m_health <= 0; + + s.Serialize(isDead); + + if (!isDead) + { + s.Serialize(data.byte71); + + if (!data.byte71 && data.byte75) + { + data.m_healthAttribute.Serialize(s); + + /* + * This computation is part of the client's serialise code + * unlike the one done for maxHealth computation. + */ + data.m_health = 0; + data.m_health += data.m_healthAttribute.m_defaultEnergy; + data.m_health += data.m_healthAttribute.m_coreEnergy; + data.m_health += data.m_healthAttribute.dword28; + data.m_health += data.m_healthAttribute.m_energyReservesEnergy; + data.m_health += data.m_healthAttribute.m_overPowerEnergy; + data.m_health += data.m_healthAttribute.m_goldCoreEnergy; + + for (int i = 7; i >= 0; i--) + { + data.m_health += data.m_healthAttribute.dword0[i]; + } + } + else + { + data.m_healthAttribute.Reset(); + } + } + else + { + data.m_healthAttribute.Reset(); + + data.m_health = 0; + data.byte71 = 1; + } + } + else + { + if (data.byte75) + { + data.m_healthAttribute.Serialize(s); + } + else + { + data.m_healthAttribute.Reset(); + } + + data.byte71 = 0; + } + + if (!data.byte72) + { + s.Serialize(12, data.gap54); + } + + if (data.m_hasEnergyRecharges) + { + for (int i = 0; i < 6; i++) + { + auto hasRechargeAmount = data.m_energyRecharge[i] != 0; + + s.Serialize(hasRechargeAmount); + + if (hasRechargeAmount) + { + s.Serialize(11, data.m_energyRecharge[i]); + } + else + { + data.m_energyRecharge[i] = 0; + } + } + } + else + { + memset(data.m_energyRecharge, 0, sizeof(data.m_energyRecharge)); + } + return true; } }; @@ -1100,9 +1343,58 @@ struct DataNode_1435992d0 { }; struct DataNode_14359e920 { }; struct DataNode_14359e790 { }; struct DataNode_143599dc0 { }; -struct DataNode_1435995f0 { }; +struct DataNode_1435995f0 : GenericSerializeDataNode +{ + DataNode_1435995f0NodeData data; + + template + bool Serialize(Serializer& s) + { + bool hasFloat40 = (data.m_float40 - 1.0) > 0.001; + + s.Serialize(data.m_byte44); + s.Serialize(hasFloat40); + + if (data.m_byte44) + { + data.m_maxHealthAttribute.Serialize(s); + + /* + * Compute maxHealth ourselves so it's easier to access it + */ + data.m_maxHealth = 0; + data.m_maxHealth += data.m_maxHealthAttribute.m_defaultEnergy; + data.m_maxHealth += data.m_maxHealthAttribute.m_coreEnergy; + data.m_maxHealth += data.m_maxHealthAttribute.dword28; + data.m_maxHealth += data.m_maxHealthAttribute.m_energyReservesEnergy; + data.m_maxHealth += data.m_maxHealthAttribute.m_overPowerEnergy; + data.m_maxHealth += data.m_maxHealthAttribute.m_goldCoreEnergy; + + for (int i = 7; i >= 0; i--) + { + data.m_maxHealth += data.m_maxHealthAttribute.dword0[i]; + } + } + else + { + data.m_maxHealthAttribute.Reset(); + + data.m_maxHealth = 0; + } + + if (hasFloat40) + { + s.SerializeSigned(9, 5.0f, data.m_float40); + } + else + { + data.m_float40 = 1.0f; + } + + return true; + } +}; struct DataNode_143599780 { }; -struct DataNode_143599910 { }; struct DataNode_143599aa0 { }; struct DataNode_143599c30 { }; struct DataNode_143599f50 { }; @@ -1328,9 +1620,18 @@ struct SyncTree : public SyncTreeBaseImpl return nullptr; } + virtual DataNode_1435995f0NodeData* Get1435995f0() override + { + auto [hasNode, node] = this->template GetData(); + + return (hasNode) ? &node->data : nullptr; + } + virtual CPedHealthNodeData* GetPedHealth() override { - return nullptr; + auto [hasNode, node] = this->template GetData(); + + return (hasNode) ? &node->data : nullptr; } virtual CVehicleHealthNodeData* GetVehicleHealth() override @@ -1574,7 +1875,7 @@ using CAnimalSyncTree = SyncTree< NodeIds<127, 127, 0>, NodeWrapper, DataNode_1435995f0>, NodeWrapper, DataNode_143599780>, - NodeWrapper, DataNode_143599910> + NodeWrapper, CPedHealthDataNode> >, ParentNode< NodeIds<87, 87, 0>, @@ -1967,7 +2268,7 @@ using CPedSyncTree = SyncTree< NodeIds<127, 127, 0>, NodeWrapper, DataNode_1435995f0>, NodeWrapper, DataNode_143599780>, - NodeWrapper, DataNode_143599910> + NodeWrapper, CPedHealthDataNode> >, ParentNode< NodeIds<87, 87, 0>, @@ -2225,7 +2526,7 @@ using CPlayerSyncTree = SyncTree< NodeIds<127, 127, 0>, NodeWrapper, DataNode_1435995f0>, NodeWrapper, DataNode_143599780>, - NodeWrapper, DataNode_143599910> + NodeWrapper, CPedHealthDataNode> >, ParentNode< NodeIds<87, 87, 0>, @@ -2585,7 +2886,7 @@ using CHorseSyncTree = SyncTree< NodeIds<127, 127, 0>, NodeWrapper, DataNode_1435995f0>, NodeWrapper, DataNode_143599780>, - NodeWrapper, DataNode_143599910> + NodeWrapper, CPedHealthDataNode> >, ParentNode< NodeIds<87, 87, 0>, diff --git a/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp b/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp index 808b6c3869..f621a47a1c 100644 --- a/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp +++ b/code/components/citizen-server-impl/src/state/ServerGameState_Scripting.cpp @@ -118,6 +118,82 @@ static void Init() } }; + static auto GetPedMaxHealth = [](const fx::sync::SyncEntityPtr& entity) + { +#ifdef STATE_FIVE + auto nodeData = entity->syncTree->GetPedHealth(); + + if (nodeData) + { + return (uint32_t)nodeData->maxHealth; + } +#elif defined(STATE_RDR3) + auto nodeData = entity->syncTree->Get1435995f0(); + + if (nodeData) + { + return nodeData->m_maxHealth; + } +#endif + + return (uint32_t)0; + }; + + static auto GetEntityMaxHealth = [](const fx::sync::SyncEntityPtr& entity) + { + switch (GetEntityType(entity)) + { + case EntityType::Ped: + return GetPedMaxHealth(entity); + default: + return (uint32_t)0; + } + }; + + static auto GetEntityHealth = [](const fx::sync::SyncEntityPtr& entity) + { + switch (GetEntityType(entity)) + { + case EntityType::Ped: + { +#ifdef STATE_RDR3 + auto nodeData = entity->syncTree->GetPedHealth(); + + if (nodeData) + { + if (nodeData->m_hasMaxHealth) + { + return GetEntityMaxHealth(entity); + } + + return nodeData->m_health; + } +#elif defined(STATE_FIVE) + auto nodeData = entity->syncTree->GetPedHealth(); + + if (nodeData) + { + return (uint32_t)nodeData->health; + } +#endif + break; + } + case EntityType::Vehicle: + { +#ifdef STATE_FIVE + auto nodeData = entity->syncTree->GetVehicleHealth(); + + if (nodeData) + { + return (uint32_t)nodeData->health; + } +#endif + break; + } + } + + return (uint32_t)0; + }; fx::ScriptEngine::RegisterNativeHandler("DOES_ENTITY_EXIST", [](fx::ScriptContext& context) { @@ -797,24 +873,41 @@ static void Init() })); fx::ScriptEngine::RegisterNativeHandler("GET_PED_MAX_HEALTH", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) - { - auto pn = entity->syncTree->GetPedHealth(); - - return pn ? pn->maxHealth : 0; + { + if (GetEntityType(entity) != EntityType::Ped) + { + return (uint32_t)0; + } + + return GetPedMaxHealth(entity);; })); fx::ScriptEngine::RegisterNativeHandler("GET_PED_ARMOUR", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) - { - auto pn = entity->syncTree->GetPedHealth(); - - return pn ? pn->armour : 0; + { +#ifdef STATE_FIVE + auto pn = entity->syncTree->GetPedHealth(); + + if (pn) + { + return pn->armour; + } +#endif + + return 0; })); fx::ScriptEngine::RegisterNativeHandler("GET_PED_CAUSE_OF_DEATH", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) - { - auto pn = entity->syncTree->GetPedHealth(); - - return pn ? pn->causeOfDeath : 0; + { +#ifdef STATE_FIVE + auto pn = entity->syncTree->GetPedHealth(); + + if (pn) + { + return pn->causeOfDeath; + } +#endif + + return (uint32_t)0; })); fx::ScriptEngine::RegisterNativeHandler("GET_PED_DESIRED_HEADING", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) @@ -834,32 +927,13 @@ static void Init() })); fx::ScriptEngine::RegisterNativeHandler("GET_ENTITY_MAX_HEALTH", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) - { - if (GetEntityType(entity) == EntityType::Ped) - { - auto pn = entity->syncTree->GetPedHealth(); - return pn ? pn->maxHealth : 0; - } - - return 0; + { + return GetEntityMaxHealth(entity); })); fx::ScriptEngine::RegisterNativeHandler("GET_ENTITY_HEALTH", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) { - EntityType entType = GetEntityType(entity); - - if (entType == EntityType::Ped) - { - auto pn = entity->syncTree->GetPedHealth(); - return pn ? pn->health : 0; - } - else if (entType == EntityType::Vehicle) - { - auto pn = entity->syncTree->GetVehicleHealth(); - return pn ? pn->health : 0; - } - - return 0; + return GetEntityHealth(entity); })); fx::ScriptEngine::RegisterNativeHandler("GET_VEHICLE_NEON_COLOUR", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) @@ -1940,10 +2014,10 @@ static void Init() })); fx::ScriptEngine::RegisterNativeHandler("GET_PED_SOURCE_OF_DAMAGE", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) - { + { +#ifdef STATE_FIVE auto node = entity->syncTree->GetPedHealth(); - // get the current resource manager auto resourceManager = fx::ResourceManager::GetCurrent(); @@ -1962,14 +2036,17 @@ static void Init() return (uint32_t)0; // Return the entity - return gameState->MakeScriptHandle(returnEntity); + return gameState->MakeScriptHandle(returnEntity); +#endif + + return (uint32_t)0; })); fx::ScriptEngine::RegisterNativeHandler("GET_PED_SOURCE_OF_DEATH", makeEntityFunction([](fx::ScriptContext& context, const fx::sync::SyncEntityPtr& entity) - { + { +#ifdef STATE_FIVE auto node = entity->syncTree->GetPedHealth(); - // get the current resource manager auto resourceManager = fx::ResourceManager::GetCurrent(); @@ -1988,7 +2065,10 @@ static void Init() return (uint32_t)0; // Return the entity - return gameState->MakeScriptHandle(returnEntity); + return gameState->MakeScriptHandle(returnEntity); +#endif + + return (uint32_t)0; })); fx::ScriptEngine::RegisterNativeHandler("SET_PLAYER_CULLING_RADIUS", MakeClientFunction([](fx::ScriptContext& context, const fx::ClientSharedPtr& client) diff --git a/ext/native-decls/GetEntityHealth.md b/ext/native-decls/GetEntityHealth.md index 416b881652..ec361a6f20 100644 --- a/ext/native-decls/GetEntityHealth.md +++ b/ext/native-decls/GetEntityHealth.md @@ -8,12 +8,14 @@ apiset: server int GET_ENTITY_HEALTH(Entity entity); ``` -Only works for vehicle and peds - ## Parameters * **entity**: The entity to check the health of -## Return value -If the entity is a vehicle it will return 0-1000 -If the entity is a ped it will return 0-200 -If the entity is an object it will return 0 +## Supported Entity Types + +| Entity Type | FiveM | RedM | Returns (FiveM) | Returns (RedM) | +|-------------|-------|-------|-----------------|----------------| +| Vehicle | ✅ | ❌ | 0 - 1000 | 0 | +| Ped | ✅ | ✅ | 0 - 200 | 0 - 150 | +| Object | ✅ | ❌ | 0 | 0 | +| (Others) | ❌ | ❌ | 0 | 0 | \ No newline at end of file