From f4d3d794fd0af5f2f3fcb4a7c726e2d41026096a Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 13:07:28 -0600 Subject: [PATCH] fix: construct a player in jetpack mode with its jetpack LWOControllablePhysComponent::Deserialize (0x00c45770) reads the jetpack mode, effect, flying and bypass checks bits on construction, and LWOControllablePhysComponent::SendMessage (0x00d2f020) keeps the same from SetJetPackMode. DLU sent the message but never kept it, so a player who came into range of someone flying was told they had no jetpack. Live player constructions in jetpack mode carried effect 167, flying 0 and bypass checks 1, and a 0 after the pickup radius (the client reads that bit into the flying flag). DLU wrote the jetpack mode there. Co-Authored-By: Claude Opus 5.5 --- dGame/dBehaviors/JetPackBehavior.cpp | 3 ++ .../Activity/RacingControlComponent.cpp | 2 + .../Character/PossessorComponent.cpp | 1 + .../Physics/ControllablePhysicsComponent.cpp | 10 ++++- .../Physics/ControllablePhysicsComponent.h | 12 ++++-- .../GMGreaterThanZeroCommands.cpp | 3 ++ .../ReplicaConstructionTests.cpp | 37 +++++++++++++++++++ 7 files changed, 64 insertions(+), 4 deletions(-) diff --git a/dGame/dBehaviors/JetPackBehavior.cpp b/dGame/dBehaviors/JetPackBehavior.cpp index 61afc2385..4d2d9f27d 100644 --- a/dGame/dBehaviors/JetPackBehavior.cpp +++ b/dGame/dBehaviors/JetPackBehavior.cpp @@ -1,4 +1,5 @@ #include "JetPackBehavior.h" +#include "ControllablePhysicsComponent.h" #include "BehaviorBranchContext.h" #include "GameMessages.h" @@ -21,6 +22,7 @@ void JetPackBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bit_st jetPackMode.fVertVel = this->m_VerticalVelocity; jetPackMode.iWarningEffectID = this->m_WarningEffectID; jetPackMode.Send(UNASSIGNED_SYSTEM_ADDRESS); + if (auto* const physics = entity->GetComponent()) physics->SetJetPackMode(jetPackMode.bUse, jetPackMode.effectID, jetPackMode.bBypassChecks); if (entity->IsPlayer()) { auto* character = entity->GetCharacter(); @@ -39,6 +41,7 @@ void JetPackBehavior::UnCast(BehaviorContext* context, BehaviorBranchContext bra jetPackMode.target = entity->GetObjectID(); jetPackMode.bUse = false; jetPackMode.Send(UNASSIGNED_SYSTEM_ADDRESS); + if (auto* const physics = entity->GetComponent()) physics->SetJetPackMode(jetPackMode.bUse, jetPackMode.effectID, jetPackMode.bBypassChecks); if (entity->IsPlayer()) { auto* character = entity->GetCharacter(); diff --git a/dGame/dComponents/Activity/RacingControlComponent.cpp b/dGame/dComponents/Activity/RacingControlComponent.cpp index 63fbdb8d2..652cefeb5 100644 --- a/dGame/dComponents/Activity/RacingControlComponent.cpp +++ b/dGame/dComponents/Activity/RacingControlComponent.cpp @@ -3,6 +3,7 @@ */ #include "RacingControlComponent.h" +#include "ControllablePhysicsComponent.h" #include "CharacterComponent.h" #include "DestroyableComponent.h" @@ -276,6 +277,7 @@ void RacingControlComponent::LoadPlayerVehicle(Entity* player, jetPackMode.target = player->GetObjectID(); jetPackMode.bUse = false; jetPackMode.Send(UNASSIGNED_SYSTEM_ADDRESS); + if (auto* const physics = player->GetComponent()) physics->SetJetPackMode(jetPackMode.bUse, jetPackMode.effectID, jetPackMode.bBypassChecks); // Set the vehicle's state. GameMessages::NotifyVehicleOfRacingObject notifyVehicle; diff --git a/dGame/dComponents/Character/PossessorComponent.cpp b/dGame/dComponents/Character/PossessorComponent.cpp index 399c41f92..8e9a3c95a 100644 --- a/dGame/dComponents/Character/PossessorComponent.cpp +++ b/dGame/dComponents/Character/PossessorComponent.cpp @@ -70,6 +70,7 @@ void PossessorComponent::Mount(Entity* mount) { jetPackMode.target = m_Parent->GetObjectID(); jetPackMode.bUse = false; jetPackMode.Send(UNASSIGNED_SYSTEM_ADDRESS); + if (auto* const physics = m_Parent->GetComponent()) physics->SetJetPackMode(jetPackMode.bUse, jetPackMode.effectID, jetPackMode.bBypassChecks); if (mount->GetComponent()) { auto characterComponent = m_Parent->GetComponent(); if (characterComponent) characterComponent->SetIsRacing(true); diff --git a/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp b/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp index d195546c4..2e8efcea7 100644 --- a/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp +++ b/dGame/dComponents/Physics/ControllablePhysicsComponent.cpp @@ -125,7 +125,9 @@ void ControllablePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bo outBitStream.Write(m_DirtyEquippedItemInfo || bIsInitialUpdate); if (m_DirtyEquippedItemInfo || bIsInitialUpdate) { outBitStream.Write(m_PickupRadius); - outBitStream.Write(m_InJetpackMode); + // LWOControllablePhysComponent::Deserialize (0x00c45770) reads this into the same flag as the jetpack block's + // flying bit; live wrote 0, also for players in jetpack mode (10,069 player constructions) + outBitStream.Write(m_JetpackFlying); if (!bIsInitialUpdate) m_DirtyEquippedItemInfo = false; } @@ -181,6 +183,12 @@ void ControllablePhysicsComponent::Serialize(RakNet::BitStream& outBitStream, bo } } +void ControllablePhysicsComponent::SetJetPackMode(const bool use, const int32_t effectID, const bool bypassChecks) { + m_InJetpackMode = use; + m_JetpackEffectID = effectID; + m_JetpackBypassChecks = bypassChecks; +} + // A character's save, then where it comes into this world void ControllablePhysicsComponent::LoadFromSaveData(const tinyxml2::XMLDocument& doc) { ReadSaveData(doc); diff --git a/dGame/dComponents/Physics/ControllablePhysicsComponent.h b/dGame/dComponents/Physics/ControllablePhysicsComponent.h index d82564ca8..5d5e8e78d 100644 --- a/dGame/dComponents/Physics/ControllablePhysicsComponent.h +++ b/dGame/dComponents/Physics/ControllablePhysicsComponent.h @@ -150,6 +150,12 @@ public: */ const bool GetJetpackBypassChecks() const { return m_JetpackBypassChecks; } + /** + * Keeps what a SetJetPackMode sent to the clients said, for the construction of this object: use, effect and + * bypass checks, as LWOControllablePhysComponent::SendMessage (0x00d2f020) keeps them + */ + void SetJetPackMode(bool use, int32_t effectID, bool bypassChecks); + /** * Set the jetpack effect ID * @param effectID the effect to play while using the jetpack @@ -337,12 +343,12 @@ private: /** * Whether or not this entity is currently flying a jetpack */ - bool m_JetpackFlying; + bool m_JetpackFlying{ false }; /** - * Bypass jetpack checks, currently unused + * Bypass jetpack checks, from the last SetJetPackMode */ - bool m_JetpackBypassChecks; + bool m_JetpackBypassChecks{ false }; /** * The effect that plays while using the jetpack diff --git a/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp index bb35b3563..d5a6c8448 100644 --- a/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp +++ b/dGame/dUtilities/SlashCommands/GMGreaterThanZeroCommands.cpp @@ -1,4 +1,5 @@ #include "GMGreaterThanZeroCommands.h" +#include "ControllablePhysicsComponent.h" #include "ChatServerLink.h" #include "DashboardNotify.h" #include "SlashCommandHandler.h" @@ -206,6 +207,7 @@ namespace GMGreaterThanZeroCommands { jetPackMode.target = entity->GetObjectID(); jetPackMode.bUse = false; jetPackMode.Send(UNASSIGNED_SYSTEM_ADDRESS); + if (auto* const physics = entity->GetComponent()) physics->SetJetPackMode(jetPackMode.bUse, jetPackMode.effectID, jetPackMode.bBypassChecks); character->SetIsFlying(false); } else { @@ -235,6 +237,7 @@ namespace GMGreaterThanZeroCommands { jetPackMode.fMaxAirspeed = maxAirSpeed; jetPackMode.fVertVel = verticalVelocity; jetPackMode.Send(UNASSIGNED_SYSTEM_ADDRESS); + if (auto* const physics = entity->GetComponent()) physics->SetJetPackMode(jetPackMode.bUse, jetPackMode.effectID, jetPackMode.bBypassChecks); character->SetIsFlying(true); } diff --git a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp index 1672577cb..c55bc7ada 100644 --- a/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp +++ b/tests/dGameTests/dComponentsTests/ReplicaConstructionTests.cpp @@ -173,6 +173,43 @@ TEST_F(ReplicaConstructionTest, CharacterActivityAndPvpChangesAreWrittenOnce) { player.SetCharacter(nullptr); } +// A player in jetpack mode is constructed in it: live player constructions carried the jetpack block (effect 167, +// flying 0, bypass checks 1) and a 0 after the pickup radius. LWOControllablePhysComponent::Deserialize reads both. +TEST_F(ReplicaConstructionTest, ControllablePhysicsConstructionCarriesJetPackMode) { + Entity player(1152921506064087006, info); + auto* const physics = player.AddComponent(-1); + + const auto construct = [physics]() { + RakNet::BitStream construction; + physics->Serialize(construction, true); + return Bits(construction); + }; + const auto bits = [](const RakNet::BitStream& stream) { return Bits(stream); }; + + // No jetpack: one 0 bit, then the stun immunities + const auto plain = construct(); + EXPECT_EQ(plain.substr(0, 2), "01"); + + physics->SetJetPackMode(true, 167, true); + RakNet::BitStream head; + head.Write1(); // jetpack + head.Write(167); // effect + head.Write0(); // flying + head.Write1(); // bypass checks + head.Write1(); // stun immunities + const auto jetpack = construct(); + EXPECT_EQ(jetpack.substr(0, head.GetNumberOfBitsUsed()), bits(head)); + // After the 7 immunity counts: no cheats, then the pickup radius and a 0 (not the jetpack flag) + const size_t afterImmunities = head.GetNumberOfBitsUsed() + 7 * 32; + EXPECT_EQ(jetpack.substr(afterImmunities, 2), "01"); + EXPECT_EQ(jetpack[afterImmunities + 2 + 32], '0'); + // The rest is what a player without a jetpack gets + EXPECT_EQ(jetpack.substr(head.GetNumberOfBitsUsed()), plain.substr(2)); + + physics->SetJetPackMode(false, -1, false); + EXPECT_EQ(construct(), plain); +} + // A guild change (docs/Guilds.md) is written once: the client reads guild id, a u8 length and 16-bit characters, and // redraws the name billboard every time it reads a name. TEST_F(ReplicaConstructionTest, CharacterGuildChangeIsWrittenOnce) {