fix(skill): cooldowns start as the client starts them

LWOSkillComponent::msgStartSkill (0x00d45cb0) starts no cooldown for cast
types 1 and 2, and inserts the cooldown (std::map insert), so a cooldown
group already running keeps its time left. The server started one for
every cast type and extended a running group to the longer cooldown.
Live client casts used cast types 0, 3 and 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-02 09:58:27 -05:00
parent 24932b1282
commit 1ac1248161
4 changed files with 49 additions and 6 deletions

View File

@@ -41,7 +41,7 @@ bool SkillComponent::IsSkillPending(const uint32_t skillUid) const {
return std::any_of(begin, end, [](const auto& pair) { return pair.second && !pair.second->syncEntries.empty(); });
}
bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t skillUid, RakNet::BitStream& bitStream, const LWOOBJID target, uint32_t skillID) {
bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t skillUid, RakNet::BitStream& bitStream, const LWOOBJID target, uint32_t skillID, const int32_t castType) {
// A skill handle still waiting on syncs is taken: the client logs and drops a second start with it
if (IsSkillPending(skillUid)) {
LOG("StartSkill with existing skill handle %u for %llu", skillUid, m_Parent->GetObjectID());
@@ -64,7 +64,8 @@ bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t s
context->ExecuteUpdates();
if (!context->failed && m_Parent->IsPlayer()) StartCooldown(skillID);
// Cast types 1 (equip cast) and 2 start no cooldown (LWOSkillComponent::msgStartSkill 0x00d45cb0)
if (!context->failed && m_Parent->IsPlayer() && castType != 1 && castType != 2) StartCooldown(skillID);
return !context->failed;
}
@@ -74,8 +75,8 @@ void SkillComponent::StartCooldown(const uint32_t skillID) {
// The client keys cooldown groups 0 and up (a null group is -1); skills without a cooldown have none running
const auto group = static_cast<int32_t>(skill.cooldowngroup);
if (group < 0 || skill.cooldown <= 0.0f) return;
auto& left = m_CooldownGroups[group];
left = std::max(left, skill.cooldown);
// One already running keeps its time left (LWOSkillComponent::msgStartSkill 0x00d45cb0 inserts, never overwrites)
m_CooldownGroups.try_emplace(group, skill.cooldown);
}
void SkillComponent::LoadFromSaveData(const tinyxml2::XMLDocument& doc) {

View File

@@ -137,8 +137,9 @@ public:
* @param skillUid the unique ID of the skill given by the client
* @param bitStream the bitSteam given by the client to determine the behavior path
* @param target the explicit target of the skill
* @param castType the StartSkill cast type
*/
bool CastPlayerSkill(uint32_t behaviorId, uint32_t skillUid, RakNet::BitStream& bitStream, LWOOBJID target, uint32_t skillID = 0);
bool CastPlayerSkill(uint32_t behaviorId, uint32_t skillUid, RakNet::BitStream& bitStream, LWOOBJID target, uint32_t skillID = 0, int32_t castType = 0);
/**
* Continues a player skill. Should only be called when the server receives a sync message from the client.

View File

@@ -156,7 +156,7 @@ namespace GameMessages {
auto* const skillComponent = entity->GetComponent<SkillComponent>();
if (skillComponent) success = skillComponent->CastPlayerSkill(behaviorId, uiSkillHandle, bs, optionalTargetID, skillID);
if (skillComponent) success = skillComponent->CastPlayerSkill(behaviorId, uiSkillHandle, bs, optionalTargetID, skillID, iCastType);
if (success && entity->GetCharacter()) {
DestroyableComponent* destComp = entity->GetComponent<DestroyableComponent>();

View File

@@ -3,6 +3,10 @@
#include "Behavior.h"
#include "BehaviorBranchContext.h"
#include "BehaviorContext.h"
#include "CDClientManager.h"
#include "CDSkillBehaviorTable.h"
#include "Character.h"
#include "CharacterComponent.h"
#include "DestroyableComponent.h"
#include "Entity.h"
#include "SkillComponent.h"
@@ -80,3 +84,40 @@ TEST_F(SkillComponentTests, DeadCastersSyncIsDroppedUnrun) {
EXPECT_EQ(behavior->syncs, 1);
}
// LWOSkillComponent::msgStartSkill (0x00d45cb0): cast types 1 and 2 start no cooldown, and a cooldown group already
// running keeps its time left.
TEST_F(SkillComponentTests, CooldownsFollowTheClient) {
CDSkillBehavior shortSkill{};
shortSkill.skillID = 394;
shortSkill.cooldowngroup = 17;
shortSkill.cooldown = 5.0f;
CDClientManager::GetEntriesMutable<CDSkillBehaviorTable>()[394] = shortSkill;
CDSkillBehavior longSkill = shortSkill;
longSkill.skillID = 395;
longSkill.cooldown = 30.0f;
CDClientManager::GetEntriesMutable<CDSkillBehaviorTable>()[395] = longSkill;
auto playerInfo = info;
playerInfo.lot = 1;
Entity player(32, playerInfo);
Character character(1, nullptr);
SystemAddress address;
address.binaryAddress = 0x0100007f;
address.port = 2003;
player.AddComponent<CharacterComponent>(-1, &character, address);
ASSERT_TRUE(player.IsPlayer());
auto* const skills = player.AddComponent<SkillComponent>(-1);
RakNet::BitStream empty;
ASSERT_TRUE(skills->CastPlayerSkill(0, 1, empty, LWOOBJID_EMPTY, 394, 1));
ASSERT_TRUE(skills->CastPlayerSkill(0, 2, empty, LWOOBJID_EMPTY, 394, 2));
EXPECT_TRUE(skills->GetCooldownGroups().empty());
ASSERT_TRUE(skills->CastPlayerSkill(0, 3, empty, LWOOBJID_EMPTY, 394, 0));
skills->Run(1.0f);
ASSERT_TRUE(skills->CastPlayerSkill(0, 4, empty, LWOOBJID_EMPTY, 395, 4));
ASSERT_EQ(skills->GetCooldownGroups().size(), 1u);
EXPECT_FLOAT_EQ(skills->GetCooldownGroups().at(17), 4.0f);
CDClientManager::GetEntriesMutable<CDSkillBehaviorTable>().clear();
}