From 24932b128295cfb39e2fc5b13fd564e67e568abd Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Fri, 2 Oct 2026 09:58:24 -0500 Subject: [PATCH] fix(skill): a dead caster's sync ends the behavior without running it, as the client LWOSkillComponent::MsgSyncSkill (0x00d454b0) checks whether the caster is dead before running a synced behavior; if so it only removes the behavior handle from the skill's pending ones. The server ran the sync anyway, so a player's delayed hit still landed after they died. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/SkillComponent.cpp | 15 ++++++++++++++ .../dComponentsTests/SkillComponentTests.cpp | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index c600e93da..667dd1987 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -22,6 +22,7 @@ #include "BaseCombatAIComponent.h" #include "ScriptComponent.h" #include "BuffComponent.h" +#include "DestroyableComponent.h" #include "SkillMessages.h" #include "CDClientManager.h" #include "CDSkillBehaviorTable.h" @@ -117,6 +118,20 @@ void SkillComponent::SyncPlayerSkill(const uint32_t skillUid, const uint32_t syn return; } + // A dead caster's sync only ends that behavior: LWOSkillComponent::MsgSyncSkill (0x00d454b0) drops it unrun + const auto* const destroyable = m_Parent->GetComponent(); + if (destroyable && destroyable->GetIsDead()) { + for (auto it = index.first; it != index.second; ++it) { + auto& entries = it->second->syncEntries; + const auto entry = std::find_if(entries.begin(), entries.end(), [syncId](const auto& sync) { return sync.handle == syncId; }); + if (entry == entries.end()) continue; + entries.erase(entry); + return; + } + LOG("Failed to find sync id (%i) for skill with uid (%i)!", syncId, skillUid); + return; + } + bool foundSyncId = false; for (auto it = index.first; it != index.second && !foundSyncId; ++it) { const auto& context = it->second; diff --git a/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp b/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp index d7473d158..f7dab9185 100644 --- a/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp @@ -3,6 +3,7 @@ #include "Behavior.h" #include "BehaviorBranchContext.h" #include "BehaviorContext.h" +#include "DestroyableComponent.h" #include "Entity.h" #include "SkillComponent.h" @@ -60,3 +61,22 @@ TEST_F(SkillComponentTests, StartWithAPendingSkillHandleIsDropped) { EXPECT_TRUE(skills->CastPlayerSkill(BEHAVIOR_ID, 5, empty, LWOOBJID_EMPTY)); } +// LWOSkillComponent::MsgSyncSkill (0x00d454b0): a sync for a dead caster ends that behavior without running it. +TEST_F(SkillComponentTests, DeadCastersSyncIsDroppedUnrun) { + Entity caster(31, info); + auto* const destroyable = caster.AddComponent(-1); + auto* const skills = caster.AddComponent(-1); + RakNet::BitStream empty; + + ASSERT_TRUE(skills->CastPlayerSkill(BEHAVIOR_ID, 1, empty, LWOOBJID_EMPTY)); + destroyable->SetIsDead(true); + skills->SyncPlayerSkill(1, WaitsForSync::SYNC_HANDLE, empty); + EXPECT_EQ(behavior->syncs, 0); + EXPECT_FALSE(skills->IsSkillPending(1)); + + destroyable->SetIsDead(false); + ASSERT_TRUE(skills->CastPlayerSkill(BEHAVIOR_ID, 2, empty, LWOOBJID_EMPTY)); + skills->SyncPlayerSkill(2, WaitsForSync::SYNC_HANDLE, empty); + EXPECT_EQ(behavior->syncs, 1); +} +