From 65acc19978ffbacb91eb67d73d4ef786a80a4b51 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 02:37:25 -0500 Subject: [PATCH] fix(combat): Die never claims a client death Live sent client_death=false on every Die, players included (19,428 in the live captures, 452 of them player deaths); DLU sent true for players. The client only uses the flag to match a death it already predicted locally (LWODestroyableComponent::msgDie): with it set, a Die that arrives while a post-death respawn is pending is dropped. Check: die to an enemy, to falling and to a quickbuild trap; the death animation, coin loss and respawn prompt all still appear, and other players see you die. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/DestroyableComponent.cpp | 4 +- .../DestroyableComponentTests.cpp | 61 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/dGame/dComponents/DestroyableComponent.cpp b/dGame/dComponents/DestroyableComponent.cpp index ea8f50d5a..61eefb833 100644 --- a/dGame/dComponents/DestroyableComponent.cpp +++ b/dGame/dComponents/DestroyableComponent.cpp @@ -746,7 +746,9 @@ void DestroyableComponent::Smash(const LWOOBJID source, const eKillType killType GameMessages::Die die; die.target = m_Parent->GetObjectID(); - die.bClientDeath = isPlayer; + // Live never set client_death, players included (19,428 Die in the live captures). The client only uses it to + // match a death it already predicted locally (LWODestroyableComponent::msgDie). + die.bClientDeath = false; die.bSpawnLoot = true; die.deathType = deathType; die.killType = killType; diff --git a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp index 14d96dd80..3f3137777 100644 --- a/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/DestroyableComponentTests.cpp @@ -6,6 +6,32 @@ #include "Entity.h" #include "eReplicaComponentType.h" #include "eStateChangeType.h" +#include "Character.h" +#include "CharacterComponent.h" +#include "CombatMessages.h" +#include "PacketTestUtils.h" +#include "dCommonVars.h" + +#include + +namespace { + // The first game message with this ID among captured packets, read into T. + template + std::optional FindGameMessage(const std::vector& packets, const MessageType::Game id) { + for (const auto& packet : packets) { + RakNet::BitStream bitStream(const_cast(packet.bytes.data()), packet.bytes.size(), false); + bitStream.IgnoreBytes(8); // CLIENT GAME_MSG header + LWOOBJID target{}; + MessageType::Game messageID{}; + if (!bitStream.Read(target) || !bitStream.Read(messageID) || messageID != id) continue; + T msg; + if (!msg.Deserialize(bitStream)) continue; + msg.target = target; + return msg; + } + return std::nullopt; + } +} class DestroyableTest : public GameDependenciesTest { protected: @@ -555,3 +581,38 @@ TEST_F(DestroyableTest, DestroyableComponentDamageCooldownTest) { EXPECT_FLOAT_EQ(destroyableComponent->GetDamageCooldownTimer(), 0.0f); ASSERT_FALSE(destroyableComponent->IsCooldownImmune()); } + +// Live Die never sets client_death (19,428 live Die, 452 of them players). +TEST_F(DestroyableTest, DieDoesNotClaimAClientDeath) { + destroyableComponent->SetMaxArmor(0.0f); + const auto sent = PacketTestUtils::Capture([&] { destroyableComponent->Damage(UINT32_MAX, LWOOBJID_EMPTY); }); + const auto die = FindGameMessage(sent, MessageType::Game::DIE); + ASSERT_TRUE(die.has_value()); + EXPECT_EQ(die->target, baseEntity->GetObjectID()); + EXPECT_FALSE(die->bClientDeath); + EXPECT_TRUE(die->bSpawnLoot); +} + +TEST_F(DestroyableTest, PlayerDieDoesNotClaimAClientDeath) { + auto playerInfo = info; + playerInfo.lot = 1; + Entity player(0x1000000000000001LL, playerInfo); + Character character(1, nullptr); + player.SetCharacter(&character); + character.SetEntity(&player); + SystemAddress address; + address.binaryAddress = 0x0100007f; + address.port = 2003; + player.AddComponent(-1, &character, address); + auto* destroyable = player.AddComponent(-1); + destroyable->SetMaxHealth(4.0f); + destroyable->SetHealth(4); + ASSERT_TRUE(player.IsPlayer()); + + const auto sent = PacketTestUtils::Capture([&] { destroyable->Smash(LWOOBJID_EMPTY); }); + const auto die = FindGameMessage(sent, MessageType::Game::DIE); + ASSERT_TRUE(die.has_value()); + EXPECT_EQ(die->target, player.GetObjectID()); + EXPECT_FALSE(die->bClientDeath); + player.SetCharacter(nullptr); +}