From 1ac1248161d6844f50a258dbfe41eace4d4bbd68 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Fri, 2 Oct 2026 09:58:27 -0500 Subject: [PATCH] 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 --- dGame/dComponents/SkillComponent.cpp | 9 ++-- dGame/dComponents/SkillComponent.h | 3 +- dGame/dGameMessages/SkillMessages.cpp | 2 +- .../dComponentsTests/SkillComponentTests.cpp | 41 +++++++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index 667dd1987..8a6a83c55 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -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(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) { diff --git a/dGame/dComponents/SkillComponent.h b/dGame/dComponents/SkillComponent.h index bfda7708c..aedc8ede8 100644 --- a/dGame/dComponents/SkillComponent.h +++ b/dGame/dComponents/SkillComponent.h @@ -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. diff --git a/dGame/dGameMessages/SkillMessages.cpp b/dGame/dGameMessages/SkillMessages.cpp index 98f826d7e..0caddee81 100644 --- a/dGame/dGameMessages/SkillMessages.cpp +++ b/dGame/dGameMessages/SkillMessages.cpp @@ -156,7 +156,7 @@ namespace GameMessages { auto* const skillComponent = entity->GetComponent(); - 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(); diff --git a/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp b/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp index f7dab9185..4d9dfd5a7 100644 --- a/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp +++ b/tests/dGameTests/dComponentsTests/SkillComponentTests.cpp @@ -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()[394] = shortSkill; + CDSkillBehavior longSkill = shortSkill; + longSkill.skillID = 395; + longSkill.cooldown = 30.0f; + CDClientManager::GetEntriesMutable()[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(-1, &character, address); + ASSERT_TRUE(player.IsPlayer()); + auto* const skills = player.AddComponent(-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().clear(); +}