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); +}