From 8b10a873657fe103877a6ad013b38c1c124e154d Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:39:11 +0300 Subject: [PATCH 1/3] Implement death processing improvements --- Client/mods/deathmatch/logic/CClientGame.cpp | 55 ++++++++++++++++--- Client/mods/deathmatch/logic/CClientGame.h | 42 ++++++++++++++ .../mods/deathmatch/logic/CPacketHandler.cpp | 11 ++++ .../logic/CStaticFunctionDefinitions.cpp | 6 ++ .../deathmatch/logic/rpc/CElementRPCs.cpp | 7 +++ .../multiplayer_sa/multiplayer_shotsync.cpp | 4 +- 6 files changed, 116 insertions(+), 9 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 33841956baa..062efa5073d 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -102,10 +103,11 @@ CClientGame::CClientGame(bool bLocalPlay) : m_ServerInfo(new CServerInfo()) m_TargetedPlayerID = INVALID_ELEMENT_ID; m_pDamageEntity = NULL; m_DamagerID = INVALID_ELEMENT_ID; - m_ucDamageBodyPiece = 0xFF; - m_ucDamageWeapon = 0xFF; + m_ucDamageBodyPiece = WEAPONTYPE_INVALID; + m_ucDamageWeapon = WEAPONTYPE_INVALID; m_ulDamageTime = 0; m_bDamageSent = true; + m_bServerProcessedDeath = false; m_bShowNetstat = false; m_bShowFPS = false; m_bHudAreaNameDisabled = false; @@ -1456,10 +1458,18 @@ void CClientGame::DoPulses() if (CClientTime::GetTime() - m_ulDamageTime > 2000) { m_DamagerID = INVALID_ELEMENT_ID; - m_ucDamageWeapon = 0xFF; - m_ucDamageBodyPiece = 0xFF; + m_ucDamageWeapon = WEAPONTYPE_INVALID; + m_ucDamageBodyPiece = WEAPONTYPE_INVALID; + } + // Check if we need to trigger death event + if (!m_pLocalPlayer->IsDeadOnNetwork() && m_pLocalPlayer->GetHealth() == 0.0f) + { + // Only call DoWastedCheck if server hasn't already processed this death + // This prevents duplicate events when server processes death via unified context system + if (!m_bServerProcessedDeath) { + DoWastedCheck(m_DamagerID, m_ucDamageWeapon, m_ucDamageBodyPiece); + } } - DoWastedCheck(m_DamagerID, m_ucDamageWeapon, m_ucDamageBodyPiece); } // Game hacks, restore certain variables @@ -4502,7 +4512,9 @@ bool CClientGame::ApplyPedDamageFromGame(eWeaponType weaponUsed, float fDamage, GetDeathAnim(pDamagedPed, pEvent, animGroup, animID); // Check if we're dead - DoWastedCheck(damagerID, weaponUsed, hitZone, animGroup, animID); + if (!m_bServerProcessedDeath) { + DoWastedCheck(damagerID, weaponUsed, hitZone, animGroup, animID); + } } // Allow GTA to kill us if we've fell to our death @@ -4570,6 +4582,14 @@ void CClientGame::DeathHandler(CPed* pKilledPedSA, unsigned char ucDeathReason, if (!pKilledPed) return; + // For local player in vehicle explosions, set damage data for consistent client events + if (IS_PLAYER(pKilledPed) && pKilledPed->IsLocalPlayer() && ucDeathReason == WEAPONTYPE_EXPLOSION) + { + // Set explosion damage data so DoWastedCheck uses correct parameters + SetExplosionDamageData(); + return; // Local player death is handled by DoWastedCheck + } + // Not required for remote players. Local player is handled in DoPulses->DoWastedCheck if (IS_PLAYER(pKilledPed)) return; @@ -5596,11 +5616,11 @@ void CClientGame::DoWastedCheck(ElementID damagerID, unsigned char ucWeapon, uns Arguments.PushElement(pKiller); else Arguments.PushBoolean(false); - if (ucWeapon != 0xFF) + if (ucWeapon != WEAPONTYPE_INVALID) Arguments.PushNumber(ucWeapon); else Arguments.PushBoolean(false); - if (ucBodyPiece != 0xFF) + if (ucBodyPiece != WEAPONTYPE_INVALID) Arguments.PushNumber(ucBodyPiece); else Arguments.PushBoolean(false); @@ -7144,3 +7164,22 @@ void CClientGame::AudioZoneRadioSwitchHandler(DWORD dwStationID) g_pGame->GetAudioEngine()->StartRadio(dwStationID); } } + +////////////////////////////////////////////////////////////////// +// +// CClientGame::TryGetCurrentWeapon +// +// Helper method to get current weapon type with error handling +// Returns actual weapon type or WEAPONTYPE_UNARMED as fallback +// +////////////////////////////////////////////////////////////////// +unsigned char CClientGame::TryGetCurrentWeapon(const CClientPlayer* pPlayer) const noexcept +{ + if (!pPlayer) + return WEAPONTYPE_UNARMED; + + // Remove const to call non-const method + auto* player = const_cast(pPlayer); + const auto weaponType = player->GetCurrentWeaponType(); + return (weaponType != WEAPONTYPE_INVALID) ? static_cast(weaponType) : WEAPONTYPE_UNARMED; +} diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index 84c10099171..3c790505891 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -405,6 +405,44 @@ class CClientGame AnimationId animId = 15); void SendPedWastedPacket(CClientPed* Ped, ElementID damagerID = INVALID_ELEMENT_ID, unsigned char ucWeapon = 0xFF, unsigned char ucBodyPiece = 0xFF, AssocGroupId animGroup = 0, AnimationId animID = 15); + + void ClearDamageData() noexcept { + m_DamagerID = INVALID_ELEMENT_ID; + m_ucDamageWeapon = WEAPONTYPE_INVALID; + m_ucDamageBodyPiece = PED_PIECE_UNKNOWN; + m_ulDamageTime = 0; + m_bServerProcessedDeath = true; + } + + void ResetDeathProcessingFlag() noexcept { + m_bServerProcessedDeath = false; + } + + void SetScriptedDeathData() noexcept { + const auto* pLocalPlayer = GetLocalPlayer(); + if (!pLocalPlayer) { + m_DamagerID = INVALID_ELEMENT_ID; + m_ucDamageWeapon = WEAPONTYPE_INVALID; + m_ucDamageBodyPiece = PED_PIECE_UNKNOWN; + m_ulDamageTime = CClientTime::GetTime(); + m_bServerProcessedDeath = false; + return; + } + + m_DamagerID = INVALID_ELEMENT_ID; + m_ucDamageWeapon = TryGetCurrentWeapon(pLocalPlayer); + m_ucDamageBodyPiece = BODYPART_TORSO; + m_ulDamageTime = CClientTime::GetTime(); + m_bServerProcessedDeath = false; + } + + void SetExplosionDamageData() noexcept { + m_DamagerID = INVALID_ELEMENT_ID; + m_ucDamageWeapon = WEAPONTYPE_EXPLOSION; + m_ucDamageBodyPiece = BODYPART_TORSO; + m_ulDamageTime = CClientTime::GetTime(); + m_bServerProcessedDeath = false; + } CClientGUIElement* GetClickedGUIElement() { return m_pClickedGUIElement; } void SetClickedGUIElement(CClientGUIElement* pElement) { m_pClickedGUIElement = NULL; } @@ -616,6 +654,9 @@ class CClientGame AnimationId DrivebyAnimationHandler(AnimationId animGroup, AssocGroupId animId); void AudioZoneRadioSwitchHandler(DWORD dwStationID); + // Helper method to get current weapon type + unsigned char TryGetCurrentWeapon(const CClientPlayer* pPlayer) const noexcept; + static bool StaticProcessMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); bool ProcessMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); @@ -770,6 +811,7 @@ class CClientGame unsigned char m_ucDamageBodyPiece; unsigned long m_ulDamageTime; bool m_bDamageSent; + bool m_bServerProcessedDeath = false; eWeaponSlot m_lastWeaponSlot; SFixedArray m_wasWeaponAmmoInClip; diff --git a/Client/mods/deathmatch/logic/CPacketHandler.cpp b/Client/mods/deathmatch/logic/CPacketHandler.cpp index 7dace41f4fa..b43b1cb17b0 100644 --- a/Client/mods/deathmatch/logic/CPacketHandler.cpp +++ b/Client/mods/deathmatch/logic/CPacketHandler.cpp @@ -1137,6 +1137,11 @@ void CPacketHandler::Packet_PlayerSpawn(NetBitStreamInterface& bitStream) // He's no longer dead pPlayer->SetDeadOnNetwork(false); + + // Reset death processing flag for new life + if (pPlayer->IsLocalPlayer()) { + g_pClientGame->ResetDeathProcessingFlag(); + } // Reset weapons pPlayer->RemoveAllWeapons(); @@ -1216,6 +1221,12 @@ void CPacketHandler::Packet_PlayerWasted(NetBitStreamInterface& bitStream) } // Update our sync-time context pPed->SetSyncTimeContext(ucTimeContext); + + // Clear stale damage data if this is the local player + // This prevents DoWastedCheck from firing with stale data when server processes death + if (pPed->IsLocalPlayer()) { + g_pClientGame->ClearDamageData(); + } // To at least here needs to be done on the local player to avoid desync // Caz: Issue 8148 - Desync when calling spawnPlayer from an event handler remotely triggered from within onClientPlayerWasted diff --git a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp index a9447a58a55..7faddeef7ca 100644 --- a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp +++ b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp @@ -1499,6 +1499,12 @@ bool CStaticFunctionDefinitions::SetElementHealth(CClientEntity& Entity, float f // Grab the model CClientPed& Ped = static_cast(Entity); + // If setting health to 0 for local player, clear stale damage data + // and set proper scripted death parameters for DoWastedCheck + if (fHealth == 0.0f && Ped.IsLocalPlayer() && Ped.GetHealth() > 0.0f) { + g_pClientGame->SetScriptedDeathData(); + } + // Set the new health Ped.SetHealth(Clamp(0.0f, fHealth, Ped.GetMaxHealth())); return true; diff --git a/Client/mods/deathmatch/logic/rpc/CElementRPCs.cpp b/Client/mods/deathmatch/logic/rpc/CElementRPCs.cpp index 50c4a5053a4..40738952991 100644 --- a/Client/mods/deathmatch/logic/rpc/CElementRPCs.cpp +++ b/Client/mods/deathmatch/logic/rpc/CElementRPCs.cpp @@ -466,7 +466,14 @@ void CElementRPCs::SetElementHealth(CClientEntity* pSource, NetBitStreamInterfac if (pPed->IsHealthLocked()) pPed->LockHealth(fHealth); else + { pPed->SetHealth(fHealth); + // If server sets health to 0 for local player, mark as server-processed death + // to prevent DoWastedCheck from firing with stale local damage data + if (fHealth == 0.0f && pPed->IsLocalPlayer()) { + g_pClientGame->ClearDamageData(); + } + } break; } diff --git a/Client/multiplayer_sa/multiplayer_shotsync.cpp b/Client/multiplayer_sa/multiplayer_shotsync.cpp index 915dfcff403..a7c585e6795 100644 --- a/Client/multiplayer_sa/multiplayer_shotsync.cpp +++ b/Client/multiplayer_sa/multiplayer_shotsync.cpp @@ -1691,7 +1691,9 @@ void CEventVehicleExplosion_NotifyDeathmatch() CPed* pPed = pPedClientEntity ? pPedClientEntity->pEntity : nullptr; if (pPed) - m_pDeathHandler(pPed, 63, 3); + { + m_pDeathHandler(pPed, WEAPONTYPE_EXPLOSION, 3); + } } } From 5722b1453ce58a29d399c2c2b95eba8aa15ef2fc Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:59:43 +0300 Subject: [PATCH 2/3] Refactor damage handling to use BODYPART_INVALID constant for clarity and consistency --- Client/mods/deathmatch/logic/CClientGame.cpp | 9 +++++---- Client/mods/deathmatch/logic/CClientGame.h | 8 ++++---- Client/mods/deathmatch/logic/CClientPed.h | 1 + 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 062efa5073d..1de446f32eb 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -39,6 +39,7 @@ #include #include #include "CServerInfo.h" +#include "CClientPed.h" SString StringZeroPadout(const SString& strInput, uint uiPadoutSize) { @@ -103,7 +104,7 @@ CClientGame::CClientGame(bool bLocalPlay) : m_ServerInfo(new CServerInfo()) m_TargetedPlayerID = INVALID_ELEMENT_ID; m_pDamageEntity = NULL; m_DamagerID = INVALID_ELEMENT_ID; - m_ucDamageBodyPiece = WEAPONTYPE_INVALID; + m_ucDamageBodyPiece = BODYPART_INVALID; m_ucDamageWeapon = WEAPONTYPE_INVALID; m_ulDamageTime = 0; m_bDamageSent = true; @@ -1459,7 +1460,7 @@ void CClientGame::DoPulses() { m_DamagerID = INVALID_ELEMENT_ID; m_ucDamageWeapon = WEAPONTYPE_INVALID; - m_ucDamageBodyPiece = WEAPONTYPE_INVALID; + m_ucDamageBodyPiece = BODYPART_INVALID; } // Check if we need to trigger death event if (!m_pLocalPlayer->IsDeadOnNetwork() && m_pLocalPlayer->GetHealth() == 0.0f) @@ -2012,7 +2013,7 @@ void CClientGame::UpdateFireKey() { if (pTargetPed->IsLocalEntity()) { - CStaticFunctionDefinitions::KillPed(*pTargetPed, m_pLocalPlayer, 4 /*WEAPONTYPE_KNIFE*/, 9 /*BODYPART_HEAD*/, true); + CStaticFunctionDefinitions::KillPed(*pTargetPed, m_pLocalPlayer, WEAPONTYPE_KNIFE, BODYPART_HEAD, true); return; } @@ -5620,7 +5621,7 @@ void CClientGame::DoWastedCheck(ElementID damagerID, unsigned char ucWeapon, uns Arguments.PushNumber(ucWeapon); else Arguments.PushBoolean(false); - if (ucBodyPiece != WEAPONTYPE_INVALID) + if (ucBodyPiece != BODYPART_INVALID) Arguments.PushNumber(ucBodyPiece); else Arguments.PushBoolean(false); diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index 3c790505891..58c7502c6c0 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -409,7 +409,7 @@ class CClientGame void ClearDamageData() noexcept { m_DamagerID = INVALID_ELEMENT_ID; m_ucDamageWeapon = WEAPONTYPE_INVALID; - m_ucDamageBodyPiece = PED_PIECE_UNKNOWN; + m_ucDamageBodyPiece = BODYPART_INVALID; m_ulDamageTime = 0; m_bServerProcessedDeath = true; } @@ -423,7 +423,7 @@ class CClientGame if (!pLocalPlayer) { m_DamagerID = INVALID_ELEMENT_ID; m_ucDamageWeapon = WEAPONTYPE_INVALID; - m_ucDamageBodyPiece = PED_PIECE_UNKNOWN; + m_ucDamageBodyPiece = BODYPART_INVALID; m_ulDamageTime = CClientTime::GetTime(); m_bServerProcessedDeath = false; return; @@ -654,7 +654,7 @@ class CClientGame AnimationId DrivebyAnimationHandler(AnimationId animGroup, AssocGroupId animId); void AudioZoneRadioSwitchHandler(DWORD dwStationID); - // Helper method to get current weapon type + // Helper method to get current weapon type with error handling unsigned char TryGetCurrentWeapon(const CClientPlayer* pPlayer) const noexcept; static bool StaticProcessMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); @@ -811,7 +811,7 @@ class CClientGame unsigned char m_ucDamageBodyPiece; unsigned long m_ulDamageTime; bool m_bDamageSent; - bool m_bServerProcessedDeath = false; + bool m_bServerProcessedDeath = false; // Flag to track server-processed deaths eWeaponSlot m_lastWeaponSlot; SFixedArray m_wasWeaponAmmoInClip; diff --git a/Client/mods/deathmatch/logic/CClientPed.h b/Client/mods/deathmatch/logic/CClientPed.h index 489b1a0a501..97ceb99824e 100644 --- a/Client/mods/deathmatch/logic/CClientPed.h +++ b/Client/mods/deathmatch/logic/CClientPed.h @@ -61,6 +61,7 @@ enum eBodyPart BODYPART_LEFT_LEG = 7, BODYPART_RIGHT_LEG = 8, BODYPART_HEAD = 9, + BODYPART_INVALID = 255, }; enum eMovementState From 6d025969774487f18c5e9433ab4cda507356ad61 Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:40:39 +0300 Subject: [PATCH 3/3] Refactor --- Client/mods/deathmatch/logic/CClientGame.cpp | 16 +++++++------- Client/mods/deathmatch/logic/CClientGame.h | 22 ++++++++++---------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index aca75cf21cc..899a68cf0d8 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -108,7 +108,7 @@ CClientGame::CClientGame(bool bLocalPlay) : m_ServerInfo(new CServerInfo()) m_ucDamageWeapon = WEAPONTYPE_INVALID; m_ulDamageTime = 0; m_bDamageSent = true; - m_bServerProcessedDeath = false; + m_serverProcessedDeath = false; m_bShowNetstat = false; m_bShowFPS = false; m_bHudAreaNameDisabled = false; @@ -1467,7 +1467,7 @@ void CClientGame::DoPulses() { // Only call DoWastedCheck if server hasn't already processed this death // This prevents duplicate events when server processes death via unified context system - if (!m_bServerProcessedDeath) { + if (!m_serverProcessedDeath) { DoWastedCheck(m_DamagerID, m_ucDamageWeapon, m_ucDamageBodyPiece); } } @@ -4513,7 +4513,7 @@ bool CClientGame::ApplyPedDamageFromGame(eWeaponType weaponUsed, float fDamage, GetDeathAnim(pDamagedPed, pEvent, animGroup, animID); // Check if we're dead - if (!m_bServerProcessedDeath) { + if (!m_serverProcessedDeath) { DoWastedCheck(damagerID, weaponUsed, hitZone, animGroup, animID); } } @@ -7180,13 +7180,11 @@ void CClientGame::AudioZoneRadioSwitchHandler(DWORD dwStationID) // Returns actual weapon type or WEAPONTYPE_UNARMED as fallback // ////////////////////////////////////////////////////////////////// -unsigned char CClientGame::TryGetCurrentWeapon(const CClientPlayer* pPlayer) const noexcept +std::uint8_t CClientGame::TryGetCurrentWeapon(CClientPlayer* player) { - if (!pPlayer) + if (!player) return WEAPONTYPE_UNARMED; - // Remove const to call non-const method - auto* player = const_cast(pPlayer); - const auto weaponType = player->GetCurrentWeaponType(); - return (weaponType != WEAPONTYPE_INVALID) ? static_cast(weaponType) : WEAPONTYPE_UNARMED; + eWeaponType weaponType = player->GetCurrentWeaponType(); + return (weaponType != WEAPONTYPE_INVALID) ? static_cast(weaponType) : WEAPONTYPE_UNARMED; } diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index 58c7502c6c0..74d64f7660f 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -411,29 +411,29 @@ class CClientGame m_ucDamageWeapon = WEAPONTYPE_INVALID; m_ucDamageBodyPiece = BODYPART_INVALID; m_ulDamageTime = 0; - m_bServerProcessedDeath = true; + m_serverProcessedDeath = true; } void ResetDeathProcessingFlag() noexcept { - m_bServerProcessedDeath = false; + m_serverProcessedDeath = false; } - void SetScriptedDeathData() noexcept { - const auto* pLocalPlayer = GetLocalPlayer(); - if (!pLocalPlayer) { + void SetScriptedDeathData() { + auto* localPlayer = GetLocalPlayer(); + if (!localPlayer) { m_DamagerID = INVALID_ELEMENT_ID; m_ucDamageWeapon = WEAPONTYPE_INVALID; m_ucDamageBodyPiece = BODYPART_INVALID; m_ulDamageTime = CClientTime::GetTime(); - m_bServerProcessedDeath = false; + m_serverProcessedDeath = false; return; } m_DamagerID = INVALID_ELEMENT_ID; - m_ucDamageWeapon = TryGetCurrentWeapon(pLocalPlayer); + m_ucDamageWeapon = TryGetCurrentWeapon(localPlayer); m_ucDamageBodyPiece = BODYPART_TORSO; m_ulDamageTime = CClientTime::GetTime(); - m_bServerProcessedDeath = false; + m_serverProcessedDeath = false; } void SetExplosionDamageData() noexcept { @@ -441,7 +441,7 @@ class CClientGame m_ucDamageWeapon = WEAPONTYPE_EXPLOSION; m_ucDamageBodyPiece = BODYPART_TORSO; m_ulDamageTime = CClientTime::GetTime(); - m_bServerProcessedDeath = false; + m_serverProcessedDeath = false; } CClientGUIElement* GetClickedGUIElement() { return m_pClickedGUIElement; } @@ -655,7 +655,7 @@ class CClientGame void AudioZoneRadioSwitchHandler(DWORD dwStationID); // Helper method to get current weapon type with error handling - unsigned char TryGetCurrentWeapon(const CClientPlayer* pPlayer) const noexcept; + std::uint8_t TryGetCurrentWeapon(CClientPlayer* player); static bool StaticProcessMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); bool ProcessMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); @@ -811,7 +811,7 @@ class CClientGame unsigned char m_ucDamageBodyPiece; unsigned long m_ulDamageTime; bool m_bDamageSent; - bool m_bServerProcessedDeath = false; // Flag to track server-processed deaths + bool m_serverProcessedDeath{false}; // Flag to track server-processed deaths eWeaponSlot m_lastWeaponSlot; SFixedArray m_wasWeaponAmmoInClip;