fix(destroyable): resurrect only a dead player that isn't already resurrecting

RequestResurrect resurrected whoever sent it, so a living player could reset
their health and imagination, and repeated requests stacked respawns. The
client's destroyable resurrects only when smashed and not resurrecting
(LWODestroyableComponent::SendMessage 0x00d29e40); live, all 17 requests in
the captures came after the player's Die and before its Resurrect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-02 09:17:28 -05:00
parent cf1e8a54c6
commit 4b84677cba
4 changed files with 25 additions and 0 deletions

View File

@@ -1391,10 +1391,12 @@ void DestroyableComponent::Resurrect(Entity& entityRef) {
// Add a half second in case someone decides to cheat and move during the death animation
// and just make sure the client has time to be ready.
constexpr float respawnTime = 3.66700005531311f + 0.5f;
if (auto* const destroyable = entity->GetComponent<DestroyableComponent>()) destroyable->m_IsResurrecting = true;
entity->AddCallbackTimer(respawnTime, [=]() {
GameMessages::PlayerResurrectionFinished msg;
entity->NotifyPlayerResurrectionFinished(msg);
auto* destroyableComponent = entity->GetComponent<DestroyableComponent>();
if (destroyableComponent) destroyableComponent->m_IsResurrecting = false;
if (destroyableComponent != nullptr && entity->GetLOT() == 1) {
destroyableComponent->SetIsDead(false);

View File

@@ -471,6 +471,9 @@ public:
*/
static void Resurrect(Entity& entity);
// Whether a resurrection has started and not finished (the client's isResurrecting)
bool GetIsResurrecting() const { return m_IsResurrecting; }
/**
* Push or Pop a layer of status immunity to this entity
*/
@@ -634,6 +637,9 @@ private:
*/
bool m_IsDead;
// A resurrection has started (Resurrect) and its respawn animation hasn't finished
bool m_IsResurrecting = false;
/**
* Whether this entity is smashed. Unused, here for serialization
*/

View File

@@ -112,7 +112,11 @@ namespace GameMessages {
return true;
}
// LWODestroyableComponent::SendMessage (0x00d29e40): only an object that is dead and not already resurrecting is
// resurrected (live: all 17 requests in the captures came after the player's Die and before its Resurrect)
void RequestResurrect::Handle(Entity& entity, const SystemAddress& sysAddr) {
const auto* const destroyable = entity.GetComponent<DestroyableComponent>();
if (!destroyable || !destroyable->GetIsDead() || destroyable->GetIsResurrecting()) return;
DestroyableComponent::Resurrect(entity);
}

View File

@@ -718,3 +718,16 @@ TEST_F(DestroyableTest, DamageReductionsAddUp) {
destroyableComponent->RemoveDamageReduction(10);
EXPECT_EQ(destroyableComponent->GetDamageReduction(), 0);
}
// The client resurrects only a dead object that isn't already resurrecting; a RequestResurrect from a living player
// does nothing
TEST_F(DestroyableTest, RequestResurrectOnlyWhenDead) {
GameMessages::RequestResurrect request;
destroyableComponent->SetIsDead(false);
request.Handle(*baseEntity, UNASSIGNED_SYSTEM_ADDRESS);
EXPECT_FALSE(destroyableComponent->GetIsResurrecting());
destroyableComponent->SetIsDead(true);
request.Handle(*baseEntity, UNASSIGNED_SYSTEM_ADDRESS);
EXPECT_TRUE(destroyableComponent->GetIsResurrecting());
}