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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-02 09:58:24 -05:00
parent c9929f1c1e
commit 24932b1282
2 changed files with 35 additions and 0 deletions

View File

@@ -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<DestroyableComponent>();
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;

View File

@@ -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<DestroyableComponent>(-1);
auto* const skills = caster.AddComponent<SkillComponent>(-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);
}