From b9d6b739d5628387e54603a16d9c5401abee9e39 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 04:03:43 -0500 Subject: [PATCH] fix(wire): PlaceModelResponse writes the model's rotation The client's PlaceModelResponse::Deserialize (0x00dc0170) reads the rotation as an optional w, x, y, z quaternion, but DLU wrote the 4-byte response after the rotation flag. With any rotation other than identity the client ran out of data. A live capture of a model turned 90 degrees shows the server echoing the rotation the client placed it with. Bytes only change when the rotation is not identity; a golden test pins the new layout. Co-Authored-By: Claude Opus 5.5 --- dGame/dGameMessages/PropertyMessages.cpp | 4 +-- dGame/dGameMessages/PropertyMessages.h | 7 +++-- docs/PacketArchitecture.md | 2 +- .../PropertyMessagesTests.cpp | 26 ++++++++++++++----- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/dGame/dGameMessages/PropertyMessages.cpp b/dGame/dGameMessages/PropertyMessages.cpp index 9e7e09db2..fee301d4b 100644 --- a/dGame/dGameMessages/PropertyMessages.cpp +++ b/dGame/dGameMessages/PropertyMessages.cpp @@ -298,9 +298,7 @@ namespace GameMessages { BitStreamUtils::WriteOptional(bitStream, position, NiPoint3Constant::ZERO); BitStreamUtils::WriteOptional(bitStream, propertyPlaqueID, LWOOBJID_EMPTY); BitStreamUtils::WriteOptional(bitStream, response, 0); - // WIRE BUG (kept, see the struct): the value written after rotation's flag is response, not rotation. - bitStream.Write(rotation != QuatUtils::IDENTITY); - if (rotation != QuatUtils::IDENTITY) bitStream.Write(response); + BitStreamUtils::WriteOptional(bitStream, rotation, QuatUtils::IDENTITY); } bool PlaceModelResponse::Deserialize(RakNet::BitStream& bitStream) { diff --git a/dGame/dGameMessages/PropertyMessages.h b/dGame/dGameMessages/PropertyMessages.h index 103fba34c..3b9f9dd62 100644 --- a/dGame/dGameMessages/PropertyMessages.h +++ b/dGame/dGameMessages/PropertyMessages.h @@ -134,10 +134,9 @@ namespace GameMessages { std::vector> models{}; // u32 count, then both IDs of each pair }; - // Server -> client. - // WIRE BUG (kept): DLU writes rotation's flag, but then writes response (4 bytes) instead of the quaternion - // (16 bytes) that the client's PlaceModelResponse::Deserialize (0x00dc0170) reads. Deserialize reads the - // client's layout, so a message with a non-identity rotation does not round trip. + // Server -> client. Laid out as the client's PlaceModelResponse::Deserialize (0x00dc0170) reads it: every field is + // optional and the rotation is a w, x, y, z quaternion. response is 14 when a model was placed and 16 when one was + // taken off the property (picked up or put away). struct PlaceModelResponse : public NetGameMsg { PlaceModelResponse() : NetGameMsg(MessageType::Game::PLACE_MODEL_RESPONSE) {} void Serialize(RakNet::BitStream& bitStream) const override; diff --git a/docs/PacketArchitecture.md b/docs/PacketArchitecture.md index 4cc8a4de9..5c0f9c099 100644 --- a/docs/PacketArchitecture.md +++ b/docs/PacketArchitecture.md @@ -306,7 +306,7 @@ the 1.10.64 client. | Message | DLU | Client / reference | |---|---|---| -| `PlaceModelResponse` | Writes a 4-byte `response` where the client expects the rotation. | The client reads a 16-byte quaternion when the rotation isn't identity (`0x00dc0170`). | +| `PlaceModelResponse` | Fixed (wire fix, see docs/BuildWorkflow.md): used to write a 4-byte `response` where the client expects the rotation. | The client reads a 16-byte w, x, y, z quaternion when the rotation isn't identity (`0x00dc0170`); a live server echoed the rotation the client placed the model with. | | `NotifyPetTamingPuzzleSelected` | Written as the client's `Serialize` (`0x00db6880`) writes it. | The client's own `Deserialize` (`0x00e3a7c0`) reads an extra `u32` its `Serialize` never writes. | | `SetBuildModeConfirmed` | Always sends the default flags. | The client has non-default flag fields. | | `NotifyNotEnoughInvSpace` | Sent with message ID `VEHICLE_NOTIFY_FINISHED_RACE` (1396). | Its ID is `NOTIFY_NOT_ENOUGH_INV_SPACE` (1516). | diff --git a/tests/dGameTests/dGameMessagesTests/PropertyMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/PropertyMessagesTests.cpp index 8bba893d7..24f00f0cf 100644 --- a/tests/dGameTests/dGameMessagesTests/PropertyMessagesTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/PropertyMessagesTests.cpp @@ -315,15 +315,17 @@ TEST_F(PropertyMessagesTests, PlaceModelResponseMatchesLegacy) { msg.propertyPlaqueID = plaque; msg.response = response; msg.rotation = rotation; - ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPlaceModelResponse(target, a, position, plaque, response, rotation); }, msg); - // The kept wire bug (response written where rotation belongs) only round trips with the default rotation. + // WIRE FIX: the legacy bytes wrote response where the rotation belongs; they only still match when + // the rotation is the identity (its flag is 0 and nothing follows). if (rotation == QuatUtils::IDENTITY) { - const auto copy = RoundTrip(msg); - EXPECT_EQ(copy.position, position); - EXPECT_EQ(copy.propertyPlaqueID, plaque); - EXPECT_EQ(copy.response, response); - ExpectTruncatedFails(msg); + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPlaceModelResponse(target, a, position, plaque, response, rotation); }, msg); } + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.position, position); + EXPECT_EQ(copy.propertyPlaqueID, plaque); + EXPECT_EQ(copy.response, response); + EXPECT_EQ(copy.rotation, rotation); + ExpectTruncatedFails(msg); } } } @@ -331,6 +333,16 @@ TEST_F(PropertyMessagesTests, PlaceModelResponseMatchesLegacy) { } } +// WIRE FIX golden bytes: a placed model turned 90 degrees, as a live server answered it. The rotation goes on the wire +// as w, x, y, z after its flag (0x00dc0170), not the 4-byte response DLU used to repeat there. +TEST_F(PropertyMessagesTests, PlaceModelResponseWritesRotation) { + GameMessages::PlaceModelResponse msg; + msg.response = 14; + msg.rotation = NiQuaternion(0.5f, 0.5f, 0.5f, 0.5f); // glm: w, x, y, z + // flags 0 (position), 0 (plaque), 1 + 0e 00 00 00, 1 + four 0.5f (00 00 00 3f) + EXPECT_PACKET_EQ(FromHex("21 c0 00 00 10 00 00 03 f0 00 00 03 f0 00 00 03 f0 00 00 03 f0", 164), Payload(msg)); +} + TEST_F(PropertyMessagesTests, UgcEquipMessagesMatchLegacy) { for (const auto target : g_Targets) { for (const auto id : g_Targets) {