Files
DarkflameServer/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp
Aaron Kimbrell 24932b1282 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>
2026-10-02 10:15:53 -05:00

83 lines
2.9 KiB
C++

#include "GameDependencies.h"
#include "Behavior.h"
#include "BehaviorBranchContext.h"
#include "BehaviorContext.h"
#include "DestroyableComponent.h"
#include "Entity.h"
#include "SkillComponent.h"
#include <gtest/gtest.h>
namespace {
// A behavior that waits for one client sync (handle 7) and counts the syncs it gets
class WaitsForSync final : public Behavior {
public:
static constexpr uint32_t SYNC_HANDLE = 7;
int syncs = 0;
explicit WaitsForSync(const uint32_t behaviorId) : Behavior(behaviorId) {}
void Handle(BehaviorContext* context, RakNet::BitStream& bitStream, BehaviorBranchContext branch) override {
context->RegisterSyncBehavior(SYNC_HANDLE, this, branch, 10.0f);
}
void Sync(BehaviorContext* context, RakNet::BitStream& bitStream, BehaviorBranchContext branch) override { ++syncs; }
};
}
class SkillComponentTests : public GameDependenciesTest {
protected:
static constexpr uint32_t BEHAVIOR_ID = 990001;
WaitsForSync* behavior = nullptr;
void SetUp() override {
SetUpDependencies();
behavior = new WaitsForSync(BEHAVIOR_ID);
}
void TearDown() override {
Behavior::Cache.erase(BEHAVIOR_ID);
delete behavior;
TearDownDependencies();
}
};
// LWOSkillComponent::msgStartSkill (0x00d45cb0): a start whose skill handle still waits on syncs is logged and dropped;
// once its behaviors are synced the handle can start a skill again.
TEST_F(SkillComponentTests, StartWithAPendingSkillHandleIsDropped) {
Entity caster(30, info);
auto* const skills = caster.AddComponent<SkillComponent>(-1);
RakNet::BitStream empty;
EXPECT_TRUE(skills->CastPlayerSkill(BEHAVIOR_ID, 5, empty, LWOOBJID_EMPTY));
EXPECT_TRUE(skills->IsSkillPending(5));
EXPECT_FALSE(skills->CastPlayerSkill(BEHAVIOR_ID, 5, empty, LWOOBJID_EMPTY));
EXPECT_TRUE(skills->CastPlayerSkill(BEHAVIOR_ID, 6, empty, LWOOBJID_EMPTY)); // another handle is free
skills->SyncPlayerSkill(5, WaitsForSync::SYNC_HANDLE, empty);
EXPECT_EQ(behavior->syncs, 1); // the dropped start registered no second sync
EXPECT_FALSE(skills->IsSkillPending(5));
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);
}