diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 0ca0546f78f..899a68cf0d8 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 @@ -38,6 +39,7 @@ #include #include #include "CServerInfo.h" +#include "CClientPed.h" SString StringZeroPadout(const SString& strInput, uint uiPadoutSize) { @@ -102,10 +104,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 = BODYPART_INVALID; + m_ucDamageWeapon = WEAPONTYPE_INVALID; m_ulDamageTime = 0; m_bDamageSent = true; + m_serverProcessedDeath = false; m_bShowNetstat = false; m_bShowFPS = false; m_bHudAreaNameDisabled = false; @@ -1456,10 +1459,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 = BODYPART_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_serverProcessedDeath) { + DoWastedCheck(m_DamagerID, m_ucDamageWeapon, m_ucDamageBodyPiece); + } } - DoWastedCheck(m_DamagerID, m_ucDamageWeapon, m_ucDamageBodyPiece); } // Game hacks, restore certain variables @@ -2002,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; } @@ -4502,7 +4513,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_serverProcessedDeath) { + DoWastedCheck(damagerID, weaponUsed, hitZone, animGroup, animID); + } } // Allow GTA to kill us if we've fell to our death @@ -4570,6 +4583,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 +5617,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 != BODYPART_INVALID) Arguments.PushNumber(ucBodyPiece); else Arguments.PushBoolean(false); @@ -7150,3 +7171,20 @@ 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 +// +////////////////////////////////////////////////////////////////// +std::uint8_t CClientGame::TryGetCurrentWeapon(CClientPlayer* player) +{ + if (!player) + return 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 84c10099171..74d64f7660f 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 = BODYPART_INVALID; + m_ulDamageTime = 0; + m_serverProcessedDeath = true; + } + + void ResetDeathProcessingFlag() noexcept { + m_serverProcessedDeath = false; + } + + 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_serverProcessedDeath = false; + return; + } + + m_DamagerID = INVALID_ELEMENT_ID; + m_ucDamageWeapon = TryGetCurrentWeapon(localPlayer); + m_ucDamageBodyPiece = BODYPART_TORSO; + m_ulDamageTime = CClientTime::GetTime(); + m_serverProcessedDeath = false; + } + + void SetExplosionDamageData() noexcept { + m_DamagerID = INVALID_ELEMENT_ID; + m_ucDamageWeapon = WEAPONTYPE_EXPLOSION; + m_ucDamageBodyPiece = BODYPART_TORSO; + m_ulDamageTime = CClientTime::GetTime(); + m_serverProcessedDeath = 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 with error handling + 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); @@ -770,6 +811,7 @@ class CClientGame unsigned char m_ucDamageBodyPiece; unsigned long m_ulDamageTime; bool m_bDamageSent; + bool m_serverProcessedDeath{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 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); + } } }