From d7833bd4c6b7f0f5919e9c28f6a1c93a5ae37b29 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 02:34:10 -0500 Subject: [PATCH] fix(skills): EchoStartSkill carries the cast, not the caster's input Live sends used_mouse false, no caster latency, no clicked position and a zero originator rotation (present, all four components 0) in every echo: 78,841 of 78,841 in the live captures, player and server casts alike. DLU forwarded the casting client's values and wrote the caster's facing for server casts. The client copies the rotation into its local StartSkill (LWOSkillComponent::msgEchoStartSkill), which is why a real rotation made power-up pickups jitter; the per-call override that worked around that is no longer needed and is removed. Check: with two players, attack, use a thrown or ranged item and pick up power-ups; the other player sees the same animations and the caster no longer snaps or jitters. Co-Authored-By: Claude Opus 5.5 --- dGame/Entity.cpp | 2 +- dGame/dComponents/SkillComponent.cpp | 17 ++------ dGame/dComponents/SkillComponent.h | 4 +- dGame/dGameMessages/SkillMessages.cpp | 26 ++++++------ dGame/dGameMessages/SkillMessages.h | 12 ++++++ .../dGameMessagesTests/SkillMessagesTests.cpp | 41 +++++++++++++++++++ 6 files changed, 74 insertions(+), 28 deletions(-) diff --git a/dGame/Entity.cpp b/dGame/Entity.cpp index 4a6fc118b..111afd67c 100644 --- a/dGame/Entity.cpp +++ b/dGame/Entity.cpp @@ -1532,7 +1532,7 @@ void Entity::PickupItem(const LWOOBJID& objectID) const { const auto skills = skillsTable->Query([&info](CDObjectSkills entry) {return (entry.objectTemplate == info.lot); }); for (const auto& skill : skills) { const auto [skillComponent, missionComponent] = GetComponentsMut(); - if (skillComponent) skillComponent->CastSkill(skill.skillID, GetObjectID(), GetObjectID(), skill.castOnType, NiQuaternion(0, 0, 0, 0)); + if (skillComponent) skillComponent->CastSkill(skill.skillID, GetObjectID(), GetObjectID(), skill.castOnType); if (missionComponent != nullptr) { missionComponent->Progress(eMissionTaskType::POWERUP, skill.skillID); diff --git a/dGame/dComponents/SkillComponent.cpp b/dGame/dComponents/SkillComponent.cpp index 8bde4265d..b42d81fc2 100644 --- a/dGame/dComponents/SkillComponent.cpp +++ b/dGame/dComponents/SkillComponent.cpp @@ -304,7 +304,7 @@ void SkillComponent::RegisterCalculatedProjectile(const LWOOBJID projectileId, B this->m_managedProjectiles.push_back(entry); } -bool SkillComponent::CastSkill(const uint32_t skillId, LWOOBJID target, const LWOOBJID optionalOriginatorID, const int32_t castType, const NiQuaternion rotationOverride) { +bool SkillComponent::CastSkill(const uint32_t skillId, LWOOBJID target, const LWOOBJID optionalOriginatorID, const int32_t castType) { uint32_t behaviorId = -1; // try to find it via the cache const auto& pair = m_skillBehaviorCache.find(skillId); @@ -324,7 +324,7 @@ bool SkillComponent::CastSkill(const uint32_t skillId, LWOOBJID target, const LW return false; } - return CalculateBehavior(skillId, behaviorId, target, false, false, optionalOriginatorID, castType, rotationOverride).success; + return CalculateBehavior(skillId, behaviorId, target, false, false, optionalOriginatorID, castType).success; } @@ -335,8 +335,7 @@ SkillExecutionResult SkillComponent::CalculateBehavior( const bool ignoreTarget, const bool clientInitalized, const LWOOBJID originatorOverride, - const int32_t castType, - const NiQuaternion rotationOverride) { + const int32_t castType) { RakNet::BitStream bitStream{}; auto* behavior = Behavior::CreateBehavior(behaviorId); @@ -375,15 +374,7 @@ SkillExecutionResult SkillComponent::CalculateBehavior( start.optionalOriginatorID = context->originator; start.optionalTargetID = target; - auto* originator = Game::entityManager->GetEntity(context->originator); - - if (originator != nullptr) { - start.originatorRot = originator->GetRotation(); - } - - if (rotationOverride != QuatUtils::IDENTITY) { - start.originatorRot = rotationOverride; - } + start.originatorRot = GameMessages::EchoStartSkill::LIVE_ORIGINATOR_ROT; //start.optionalTargetID = target; start.sBitStream.assign(reinterpret_cast(bitStream.GetData()), bitStream.GetNumberOfBytesUsed()); diff --git a/dGame/dComponents/SkillComponent.h b/dGame/dComponents/SkillComponent.h index 27f836540..6be86b170 100644 --- a/dGame/dComponents/SkillComponent.h +++ b/dGame/dComponents/SkillComponent.h @@ -162,7 +162,7 @@ public: * @param optionalOriginatorID change the originator of the skill * @return if the case succeeded */ - bool CastSkill(const uint32_t skillId, LWOOBJID target = LWOOBJID_EMPTY, const LWOOBJID optionalOriginatorID = LWOOBJID_EMPTY, const int32_t castType = 0, const NiQuaternion rotationOverride = QuatUtils::IDENTITY); + bool CastSkill(const uint32_t skillId, LWOOBJID target = LWOOBJID_EMPTY, const LWOOBJID optionalOriginatorID = LWOOBJID_EMPTY, const int32_t castType = 0); /** * Initializes a server-side skill calculation. @@ -174,7 +174,7 @@ public: * @param originatorOverride an override for the originator of the skill calculation * @return the result of the skill calculation */ - SkillExecutionResult CalculateBehavior(uint32_t skillId, uint32_t behaviorId, LWOOBJID target, bool ignoreTarget = false, bool clientInitalized = false, LWOOBJID originatorOverride = LWOOBJID_EMPTY, const int32_t castType = 0, const NiQuaternion rotationOverride = QuatUtils::IDENTITY); + SkillExecutionResult CalculateBehavior(uint32_t skillId, uint32_t behaviorId, LWOOBJID target, bool ignoreTarget = false, bool clientInitalized = false, LWOOBJID originatorOverride = LWOOBJID_EMPTY, const int32_t castType = 0); /** * Register a server-side projectile. diff --git a/dGame/dGameMessages/SkillMessages.cpp b/dGame/dGameMessages/SkillMessages.cpp index 5d05623ec..dc0a72f5c 100644 --- a/dGame/dGameMessages/SkillMessages.cpp +++ b/dGame/dGameMessages/SkillMessages.cpp @@ -166,22 +166,24 @@ namespace GameMessages { if (success) { // Echo the cast to every other client - EchoStartSkill echoStartSkill; - echoStartSkill.target = entity->GetObjectID(); - echoStartSkill.bUsedMouse = bUsedMouse; - echoStartSkill.fCasterLatency = fCasterLatency; - echoStartSkill.iCastType = iCastType; - echoStartSkill.lastClickedPosit = lastClickedPosit; - echoStartSkill.optionalOriginatorID = optionalOriginatorID; - echoStartSkill.optionalTargetID = optionalTargetID; - echoStartSkill.originatorRot = originatorRot; - echoStartSkill.sBitStream = sBitStream; - echoStartSkill.skillID = skillID; - echoStartSkill.uiSkillHandle = uiSkillHandle; + auto echoStartSkill = MakeEcho(entity->GetObjectID()); echoStartSkill.BroadcastExcept(entity->GetSystemAddress()); } } + EchoStartSkill StartSkill::MakeEcho(const LWOOBJID caster) const { + EchoStartSkill echo; + echo.target = caster; + echo.iCastType = iCastType; + echo.optionalOriginatorID = optionalOriginatorID; + echo.optionalTargetID = optionalTargetID; + echo.originatorRot = EchoStartSkill::LIVE_ORIGINATOR_ROT; + echo.sBitStream = sBitStream; + echo.skillID = skillID; + echo.uiSkillHandle = uiSkillHandle; + return echo; + } + void EchoStartSkill::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(bUsedMouse); BitStreamUtils::WriteOptional(bitStream, fCasterLatency, 0.0f); diff --git a/dGame/dGameMessages/SkillMessages.h b/dGame/dGameMessages/SkillMessages.h index 5804615c0..704bd5d19 100644 --- a/dGame/dGameMessages/SkillMessages.h +++ b/dGame/dGameMessages/SkillMessages.h @@ -74,6 +74,8 @@ namespace GameMessages { int32_t skillID{}; }; + struct EchoStartSkill; + // Client -> server. The player cast a skill. struct StartSkill : public NetGameMsg { StartSkill() : NetGameMsg(MessageType::Game::START_SKILL) {} @@ -92,6 +94,11 @@ namespace GameMessages { std::string sBitStream{}; TSkillID skillID{ 0 }; uint32_t uiSkillHandle{ 0 }; // optional + + // The echo other clients get for this cast. Live keeps the cast (originator, target, cast type, behavior + // data, skill and handle) and drops the caster's input: used_mouse false, no latency, no clicked position + // and a zero rotation (every one of the 78,841 live EchoStartSkill). + EchoStartSkill MakeEcho(LWOOBJID caster) const; }; // Server -> clients. A skill cast echoed to the other clients so they play it too. @@ -100,6 +107,11 @@ namespace GameMessages { void Serialize(RakNet::BitStream& bitStream) const override; bool Deserialize(RakNet::BitStream& bitStream) override; + // What live writes for originatorRot in every echo, client casts and server casts alike: present, all four + // components 0. The client copies it into its local StartSkill (LWOSkillComponent::msgEchoStartSkill); a real + // rotation turns the caster, which is the jitter DLU once worked around for power-ups. + static constexpr NiQuaternion LIVE_ORIGINATOR_ROT{ 0.0f, 0.0f, 0.0f, 0.0f }; + bool bUsedMouse{ false }; float fCasterLatency{ 0.0f }; // optional int32_t iCastType{ 0 }; // optional diff --git a/tests/dGameTests/dGameMessagesTests/SkillMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/SkillMessagesTests.cpp index aade06003..7084300f3 100644 --- a/tests/dGameTests/dGameMessagesTests/SkillMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/SkillMessagesTests.cpp @@ -311,3 +311,44 @@ TEST_F(SkillMessagesTests, GoldenBytes) { request.i64TargetID = 0x11; EXPECT_PACKET_EQ(FromHex("44 40 00 00 00 00 00 00 00 00 00 00 00", 98), Payload(request)); } + +// Live echoes keep the cast and drop the caster's input (all 78,841 EchoStartSkill in the live captures). +TEST_F(SkillMessagesTests, EchoOfClientCastMatchesLive) { + GameMessages::StartSkill cast; + cast.bUsedMouse = true; + cast.consumableItemID = 0x0102030405060700LL; + cast.fCasterLatency = 0.25f; + cast.iCastType = 1; + cast.lastClickedPosit = NiPoint3(1.0f, 2.0f, 3.0f); + cast.optionalOriginatorID = 0x1000000000000001LL; + cast.optionalTargetID = 0x0102030405060708LL; + cast.originatorRot = NiQuaternion(0.5f, 0.5f, -0.5f, 0.5f); + cast.skillID = 381; + cast.uiSkillHandle = 1; + + const auto echo = cast.MakeEcho(0x1000000000000001LL); + EXPECT_EQ(echo.target, 0x1000000000000001LL); + EXPECT_FALSE(echo.bUsedMouse); + EXPECT_EQ(echo.fCasterLatency, 0.0f); + EXPECT_EQ(echo.lastClickedPosit, NiPoint3Constant::ZERO); + EXPECT_EQ(echo.originatorRot, GameMessages::EchoStartSkill::LIVE_ORIGINATOR_ROT); + EXPECT_EQ(echo.iCastType, 1); + EXPECT_EQ(echo.optionalOriginatorID, cast.optionalOriginatorID); + EXPECT_EQ(echo.optionalTargetID, cast.optionalTargetID); + EXPECT_EQ(echo.skillID, 381); + EXPECT_EQ(echo.uiSkillHandle, 1u); + + // A live echo of skill 381 (cast type 1, handle 1, no behavior data) with the caster and target IDs replaced. + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 01 00 00 00 00 00 00 10 76 00 20 20 00 00 00 10 00 00 00 00 00 01 08 40 38 30 28 20 18 10 0c " + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 f4 04 00 02 02 00 00 00", 535), StructPacket(echo)); +} + +// Server casts echo a zero rotation too, never the caster's facing. +TEST_F(SkillMessagesTests, LiveOriginatorRotationIsWrittenAsZero) { + GameMessages::EchoStartSkill echo; + echo.originatorRot = GameMessages::EchoStartSkill::LIVE_ORIGINATOR_ROT; + const auto copy = RoundTrip(echo); + EXPECT_EQ(copy.originatorRot, GameMessages::EchoStartSkill::LIVE_ORIGINATOR_ROT); + EXPECT_NE(copy.originatorRot, QuatUtils::IDENTITY); +}