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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 02:37:25 -05:00
parent d7833bd4c6
commit 65acc19978
2 changed files with 64 additions and 1 deletions

View File

@@ -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 <optional>
namespace {
// The first game message with this ID among captured packets, read into T.
template<typename T>
std::optional<T> FindGameMessage(const std::vector<CapturedPacket>& packets, const MessageType::Game id) {
for (const auto& packet : packets) {
RakNet::BitStream bitStream(const_cast<unsigned char*>(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<GameMessages::Die>(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<CharacterComponent>(-1, &character, address);
auto* destroyable = player.AddComponent<DestroyableComponent>(-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<GameMessages::Die>(sent, MessageType::Game::DIE);
ASSERT_TRUE(die.has_value());
EXPECT_EQ(die->target, player.GetObjectID());
EXPECT_FALSE(die->bClientDeath);
player.SetCharacter(nullptr);
}