@@ -1788,7 +1788,25 @@ bool CStaticFunctionDefinitions::SetElementHealth(CElement* pElement, float fHea
17881788 if (pPed->IsDead () && fHealth > 0 .0f )
17891789 pPed->SetIsDead (false );
17901790 else if (fHealth <= 0 .0f && !pPed->IsDead ())
1791- KillPed (pElement, nullptr , 0xFF , 0xFF , false );
1791+ {
1792+ // Preserve #4482 (onPlayerWasted fires server-side from setElementHealth(p, 0)) without
1793+ // regressing instant-respawn flows (e.g. race respawntime=0). Two things matter:
1794+ // 1) Skip the WASTED broadcast to the dying player so the originator isn't forced into
1795+ // CClientPed::Kill() / TaskComplexDie - that traps the camera in GTA's death cam
1796+ // because the immediately-following PLAYER_SPAWN can't cleanly cancel the transition.
1797+ // 2) Send SET_ELEMENT_HEALTH=0 BEFORE KillPed and return early. KillPed fires
1798+ // onPlayerWasted, whose handlers commonly call spawnPlayer; if the trailing health
1799+ // RPC ran after that PLAYER_SPAWN, all clients would reset the freshly-spawned
1800+ // player back to 0 health.
1801+ CBitStream BitStream;
1802+ BitStream.pBitStream ->Write (fHealth );
1803+ BitStream.pBitStream ->Write (pElement->GenerateSyncTimeContext ());
1804+ m_pPlayerManager->BroadcastOnlyJoined (CElementRPCPacket (pElement, SET_ELEMENT_HEALTH, *BitStream.pBitStream ));
1805+
1806+ CPlayer* pSkipBroadcastPlayer = IS_PLAYER (pPed) ? static_cast <CPlayer*>(pPed) : nullptr ;
1807+ KillPed (pElement, nullptr , 0xFF , 0xFF , false , pSkipBroadcastPlayer);
1808+ return true ;
1809+ }
17921810
17931811 break ;
17941812 }
@@ -3808,9 +3826,13 @@ bool CStaticFunctionDefinitions::SetPedArmor(CElement* pElement, float armor)
38083826 return true ;
38093827}
38103828
3811- bool CStaticFunctionDefinitions::KillPed (CElement* pElement, CElement* pKiller, unsigned char ucKillerWeapon, unsigned char ucBodyPart, bool bStealth)
3829+ bool CStaticFunctionDefinitions::KillPed (CElement* pElement, CElement* pKiller, unsigned char ucKillerWeapon, unsigned char ucBodyPart, bool bStealth,
3830+ CPlayer* pSkipBroadcastPlayer)
38123831{
38133832 assert (pElement);
3833+ // Note: pSkipBroadcastPlayer is intentionally NOT propagated through RUN_CHILDREN. It only ever applies
3834+ // to a single, specific player in the SetElementHealth auto-kill path, not to recursive kills on element
3835+ // hierarchies (which historically broadcast the wasted packet to everyone).
38143836 RUN_CHILDREN (KillPed (*iter, pKiller, ucKillerWeapon, ucBodyPart))
38153837
38163838 if (IS_PED (pElement))
@@ -3868,9 +3890,11 @@ bool CStaticFunctionDefinitions::KillPed(CElement* pElement, CElement* pKiller,
38683890 // TODO: change to onPedWasted
38693891 if (IS_PLAYER (pPed))
38703892 {
3871- // Tell everyone to kill this player
3893+ // Tell everyone to kill this player. pSkipBroadcastPlayer (when set) is excluded so that
3894+ // server-initiated kills via SetElementHealth do not push the dying player into a forced
3895+ // client-side TaskComplexDie. See header comment on KillPed for the full rationale.
38723896 CPlayerWastedPacket WastedPacket (pPed, pKiller, ucKillerWeapon, ucBodyPart, bStealth);
3873- m_pPlayerManager->BroadcastOnlyJoined (WastedPacket);
3897+ m_pPlayerManager->BroadcastOnlyJoined (WastedPacket, pSkipBroadcastPlayer );
38743898 pPed->CallEvent (" onPlayerWasted" , Arguments);
38753899 }
38763900 else
0 commit comments