From 7fb1e404f00df8a14c95c49d8a62271ead19ebf6 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 08:21:22 -0500 Subject: [PATCH] fix(wire): write SetStatusImmunity flags in the client's order WIRE FIX. The 1.10.64 client writes and reads the immunity flags in alphabetical order after the u32 state (GameMessage::SetStatusImmunity:: Serialize @ 0x00d8f140; the field offsets are named by the Flash export at 0x00d8f410): BasicAttack, DOT, ImaginationGain, ImaginationLoss, Interrupt, Knockback, PullToPoint, QuickbuildInterrupt, Speed. DLU wrote them in declaration order, so e.g. a knockback immunity reached the client as an imagination-gain immunity. Co-Authored-By: Claude Opus 5.5 --- dGame/dGameMessages/CombatMessages.cpp | 17 +++++---- dGame/dGameMessages/CombatMessages.h | 5 ++- docs/PacketArchitecture.md | 2 +- .../CombatMessagesTests.cpp | 37 +++++++++++++++++-- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/dGame/dGameMessages/CombatMessages.cpp b/dGame/dGameMessages/CombatMessages.cpp index 9702cac60..521d14a3d 100644 --- a/dGame/dGameMessages/CombatMessages.cpp +++ b/dGame/dGameMessages/CombatMessages.cpp @@ -273,29 +273,30 @@ namespace GameMessages { } void SetStatusImmunity::Serialize(RakNet::BitStream& bitStream) const { + // The client's order (0x00d8f140): alphabetical. bitStream.Write(StateChangeType); bitStream.Write(bImmuneToBasicAttack); bitStream.Write(bImmuneToDOT); - bitStream.Write(bImmuneToKnockback); - bitStream.Write(bImmuneToInterrupt); - bitStream.Write(bImmuneToSpeed); bitStream.Write(bImmuneToImaginationGain); bitStream.Write(bImmuneToImaginationLoss); - bitStream.Write(bImmuneToQuickbuildInterrupt); + bitStream.Write(bImmuneToInterrupt); + bitStream.Write(bImmuneToKnockback); bitStream.Write(bImmuneToPullToPoint); + bitStream.Write(bImmuneToQuickbuildInterrupt); + bitStream.Write(bImmuneToSpeed); } bool SetStatusImmunity::Deserialize(RakNet::BitStream& bitStream) { VALIDATE_READ(bitStream.Read(StateChangeType)); VALIDATE_READ(bitStream.Read(bImmuneToBasicAttack)); VALIDATE_READ(bitStream.Read(bImmuneToDOT)); - VALIDATE_READ(bitStream.Read(bImmuneToKnockback)); - VALIDATE_READ(bitStream.Read(bImmuneToInterrupt)); - VALIDATE_READ(bitStream.Read(bImmuneToSpeed)); VALIDATE_READ(bitStream.Read(bImmuneToImaginationGain)); VALIDATE_READ(bitStream.Read(bImmuneToImaginationLoss)); - VALIDATE_READ(bitStream.Read(bImmuneToQuickbuildInterrupt)); + VALIDATE_READ(bitStream.Read(bImmuneToInterrupt)); + VALIDATE_READ(bitStream.Read(bImmuneToKnockback)); VALIDATE_READ(bitStream.Read(bImmuneToPullToPoint)); + VALIDATE_READ(bitStream.Read(bImmuneToQuickbuildInterrupt)); + VALIDATE_READ(bitStream.Read(bImmuneToSpeed)); return true; } diff --git a/dGame/dGameMessages/CombatMessages.h b/dGame/dGameMessages/CombatMessages.h index 2e0d2d2e0..1d0df4b96 100644 --- a/dGame/dGameMessages/CombatMessages.h +++ b/dGame/dGameMessages/CombatMessages.h @@ -173,8 +173,9 @@ namespace GameMessages { }; // Server -> client. - // NOTE: DLU writes the flags in this order; the client reads them in alphabetical order - // (GameMessage::SetStatusImmunity::Serialize @ 00d8f140 in 1.10.64). Kept as DLU sent it in this conversion. + // WIRE FIX: the flags go on the wire in alphabetical order, as the client's Serialize writes them + // (GameMessage::SetStatusImmunity::Serialize @ 00d8f140 in 1.10.64; field offsets named by its Flash export + // @ 00d8f410). DLU used to write them in the declaration order below. struct SetStatusImmunity : public NetGameMsg { SetStatusImmunity() : NetGameMsg(MessageType::Game::SET_STATUS_IMMUNITY) {} void Serialize(RakNet::BitStream& bitStream) const override; diff --git a/docs/PacketArchitecture.md b/docs/PacketArchitecture.md index c69fb1c87..d40b0d426 100644 --- a/docs/PacketArchitecture.md +++ b/docs/PacketArchitecture.md @@ -312,7 +312,7 @@ the 1.10.64 client. | `NotifyNotEnoughInvSpace` | Fixed (wire fix): used to be sent with message ID `VEHICLE_NOTIFY_FINISHED_RACE` (1396). | Its ID is `NOTIFY_NOT_ENOUGH_INV_SPACE` (1516, `0x00545c90`); payload read at `0x00d8b850`. | | `MoveInventoryBatch` | Now follows the client layout. | | | `UnEquipInventory` | The trailing optional `replacementObjectID` is never read. | The client can send it. | -| `SetStatusImmunity` | Writes the flags in DLU's order. | The client reads DOT, ImaginationGain, ImaginationLoss, Interrupt, Knockback, PullToPoint, QuickbuildInterrupt, Speed, BasicAttack (`0x00d8f140`). | +| `SetStatusImmunity` | Fixed (wire fix): used to write the flags in DLU's order (BasicAttack, DOT, Knockback, Interrupt, Speed, ImaginationGain, ImaginationLoss, QuickbuildInterrupt, PullToPoint). | The client uses alphabetical order: BasicAttack, DOT, ImaginationGain, ImaginationLoss, Interrupt, Knockback, PullToPoint, QuickbuildInterrupt, Speed (`0x00d8f140`; field offsets named by `0x00d8f410`). | | `RequestDie` | Read with the `Die` layout. | Starts with one `bDieAccepted` bit and has a mandatory `lootOwnerID` (`0x00e02d90`). | | `SetCurrency` | `sourceTradeID` is an optional `int32_t`. | lu_packets has an object ID (8 bytes). DLU only ever sends 0 (flag bit 0), so no bytes differ today. | | `FireEventClientSide` | Never writes `param1`/`param2` (both flag bits 0), whatever the caller passed: `RocketEquipped` loses the clone ID. | Optional `i64 param1` (default 0) and `i32 param2` (default -1). | diff --git a/tests/dGameTests/dGameMessagesTests/CombatMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/CombatMessagesTests.cpp index 21e812899..45172ef16 100644 --- a/tests/dGameTests/dGameMessagesTests/CombatMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/CombatMessagesTests.cpp @@ -264,9 +264,7 @@ TEST_F(CombatMessagesTests, StunsAndImmunitiesMatchLegacy) { status.bImmuneToImaginationLoss = Bit(mask, 6); status.bImmuneToQuickbuildInterrupt = Bit(mask, 7); status.bImmuneToPullToPoint = Bit(mask, 8); - ExpectSameAsLegacy([&](const SystemAddress& a) { - LegacyGameMessages::SendSetStatusImmunity(target, state, a, Bit(mask, 0), Bit(mask, 1), Bit(mask, 2), Bit(mask, 3), Bit(mask, 4), Bit(mask, 5), Bit(mask, 6), Bit(mask, 7), Bit(mask, 8)); - }, status); + // WIRE FIX: the legacy bytes use DLU's flag order; see SetStatusImmunityUsesClientOrder. EXPECT_EQ(RoundTrip(status).bImmuneToPullToPoint, Bit(mask, 8)); } } @@ -379,3 +377,36 @@ TEST_F(CombatMessagesTests, GoldenBytes) { remove.uiBuffID = 3; EXPECT_PACKET_EQ(FromHex("20 60 00 00 00", 35), Payload(remove)); } + +// WIRE FIX: the client writes and reads the immunity flags in alphabetical order after the u32 state +// (GameMessage::SetStatusImmunity::Serialize @ 0x00d8f140). Setting one flag at a time must set exactly that bit. +TEST_F(CombatMessagesTests, SetStatusImmunityUsesClientOrder) { + using Flag = bool GameMessages::SetStatusImmunity::*; + const std::array clientOrder = { + &GameMessages::SetStatusImmunity::bImmuneToBasicAttack, + &GameMessages::SetStatusImmunity::bImmuneToDOT, + &GameMessages::SetStatusImmunity::bImmuneToImaginationGain, + &GameMessages::SetStatusImmunity::bImmuneToImaginationLoss, + &GameMessages::SetStatusImmunity::bImmuneToInterrupt, + &GameMessages::SetStatusImmunity::bImmuneToKnockback, + &GameMessages::SetStatusImmunity::bImmuneToPullToPoint, + &GameMessages::SetStatusImmunity::bImmuneToQuickbuildInterrupt, + &GameMessages::SetStatusImmunity::bImmuneToSpeed, + }; + for (size_t i = 0; i < clientOrder.size(); i++) { + GameMessages::SetStatusImmunity msg; + msg.StateChangeType = eStateChangeType::POP; + msg.*clientOrder[i] = true; + RakNet::BitStream bitStream; + msg.Serialize(bitStream); + ASSERT_EQ(bitStream.GetNumberOfBitsUsed(), 32 + 9); + uint32_t state{}; + ASSERT_TRUE(bitStream.Read(state)); + EXPECT_EQ(state, 1u); + for (size_t bit = 0; bit < clientOrder.size(); bit++) { + bool value{}; + ASSERT_TRUE(bitStream.Read(value)); + EXPECT_EQ(value, bit == i) << "flag " << i << ", bit " << bit; + } + } +}