From 55a6d0babcb5591faca4bad6e96faf215457e150 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 14:48:11 -0600 Subject: [PATCH] fix: the character data packet carries the world keys live sent Every live character data packet (451 of 451) had legoclub=1, freetrial=0, requiresrename=0, levelid and position.x/y/z + rotation.x/y/z/w next to the keys already sent. LWOCharacterComponent::LoadConfigData (0x00c70f00) reads requiresrename, legoclub and freetrial. requiresrename is the character's refused-name state, levelid the zone id (map, instance << 16, clone << 32) and the position and rotation are where the player is put in the world. Co-Authored-By: Claude Opus 5.5 --- dNet/ClientPackets.cpp | 36 +++++++++++++++++++ dNet/ClientPackets.h | 10 ++++++ dNet/PacketFields.inc | 7 ++++ dWorldServer/WorldServer.cpp | 13 +++++++ .../dNetTests/WorldPacketsTests.cpp | 25 +++++++++++++ 5 files changed, 91 insertions(+) diff --git a/dNet/ClientPackets.cpp b/dNet/ClientPackets.cpp index 424803d8e..72f6f1340 100644 --- a/dNet/ClientPackets.cpp +++ b/dNet/ClientPackets.cpp @@ -251,6 +251,19 @@ namespace ClientPackets { ldfData.Insert(u"reputation", reputation); ldfData.Insert(u"propertycloneid", propertyCloneID); if (txfring) ldfData.Insert(u"txfring", true); + if (sendWorldKeys) { + ldfData.Insert(u"legoclub", legoClub); + ldfData.Insert(u"freetrial", freeTrial); + ldfData.Insert(u"requiresrename", requiresRename); + ldfData.Insert(u"levelid", levelID); + ldfData.Insert(u"position.x", position.x); + ldfData.Insert(u"position.y", position.y); + ldfData.Insert(u"position.z", position.z); + ldfData.Insert(u"rotation.x", rotation.x); + ldfData.Insert(u"rotation.y", rotation.y); + ldfData.Insert(u"rotation.z", rotation.z); + ldfData.Insert(u"rotation.w", rotation.w); + } data.Write(ldfData.values.size()); for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data); @@ -309,6 +322,8 @@ namespace ClientPackets { RakNet::BitStream data(uncompressed.data(), uncompressed.size(), false); txfring = false; // the client's value without the key + sendWorldKeys = false; + legoClub = false; uint32_t count{}; VALIDATE_READ(data.Read(count)); for (uint32_t i = 0; i < count; i++) { @@ -349,6 +364,27 @@ namespace ClientPackets { uint8_t value{}; VALIDATE_READ(data.Read(value)); if (key == u"txfring") txfring = value != 0; + else if (key == u"legoclub") legoClub = value != 0; + else if (key == u"freetrial") freeTrial = value != 0; + else if (key == u"requiresrename") requiresRename = value != 0; + break; + } + case LDF_TYPE_U64: { + uint64_t value{}; + VALIDATE_READ(data.Read(value)); + if (key == u"levelid") { levelID = value; sendWorldKeys = true; } + break; + } + case LDF_TYPE_FLOAT: { + float value{}; + VALIDATE_READ(data.Read(value)); + if (key == u"position.x") position.x = value; + else if (key == u"position.y") position.y = value; + else if (key == u"position.z") position.z = value; + else if (key == u"rotation.x") rotation.x = value; + else if (key == u"rotation.y") rotation.y = value; + else if (key == u"rotation.z") rotation.z = value; + else if (key == u"rotation.w") rotation.w = value; break; } default: diff --git a/dNet/ClientPackets.h b/dNet/ClientPackets.h index c40a39315..b48c09b63 100644 --- a/dNet/ClientPackets.h +++ b/dNet/ClientPackets.h @@ -27,6 +27,7 @@ #include "MessageType/Client.h" #include "MessageType/Game.h" #include "NiPoint3.h" +#include "NiQuaternion.h" #include "Stamps.h" enum class eCharacterCreationResponse : uint8_t; @@ -189,6 +190,15 @@ namespace ClientPackets { // packet, and with it false the client shows inventory-full tips on every world load. Not written when false // (the client's default). bool txfring = true; + // The keys below were in every live character data packet. LWOCharacterComponent::LoadConfigData (0x00c70f00) + // reads requiresrename, legoclub and freetrial. Not written when sendWorldKeys is false. + bool sendWorldKeys = true; + bool legoClub = true; + bool freeTrial = false; + bool requiresRename = false; // The name was refused and a new one is asked for + uint64_t levelID{}; // map id, instance id << 16, clone id << 32 + NiPoint3 position{}; + NiQuaternion rotation = QuatUtils::IDENTITY; CreateCharacter() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CREATE_CHARACTER) {} void Serialize(RakNet::BitStream& bitStream) const override; diff --git a/dNet/PacketFields.inc b/dNet/PacketFields.inc index 61f6e6169..c7903f44f 100644 --- a/dNet/PacketFields.inc +++ b/dNet/PacketFields.inc @@ -640,6 +640,13 @@ json ToJson(const ClientPackets::CreateCharacter& m) { j["reputation"] = ToJson(m.reputation); j["propertyCloneID"] = ToJson(m.propertyCloneID); j["txfring"] = ToJson(m.txfring); + j["sendWorldKeys"] = ToJson(m.sendWorldKeys); + j["legoClub"] = ToJson(m.legoClub); + j["freeTrial"] = ToJson(m.freeTrial); + j["requiresRename"] = ToJson(m.requiresRename); + j["levelID"] = ToJson(m.levelID); + j["position"] = ToJson(m.position); + j["rotation"] = ToJson(m.rotation); return j; } json ToJson(const ClientPackets::ChatModerationString& m) { diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index cd8111967..b747f612f 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1018,6 +1018,17 @@ void HandleMasterPacket(Packet* packet) { } } +// What every live character data packet also carried: the world the player loads into, where it stands there and +// whether its name was refused (LWOCharacterComponent::LoadConfigData 0x00c70f00 reads requiresrename) +void SetWorldKeys(ClientPackets::CreateCharacter& createCharacter, const Entity& player, const Character& character) { + const auto& zoneID = Game::zoneManager->GetZoneID(); + createCharacter.levelID = static_cast(zoneID.GetMapID()) | static_cast(zoneID.GetInstanceID()) << 16 | + static_cast(zoneID.GetCloneID()) << 32; + createCharacter.position = player.GetPosition(); + createCharacter.rotation = player.GetRotation(); + createCharacter.requiresRename = character.GetNameRejected(); +} + // Creates the player's entity and sends the client everything in the world: after the client loaded the zone // (LEVEL_LOAD_COMPLETE), or at once when it kept its scene (an experimental seamless migration) // A player already in the world whose client reloaded the zone: its character again (from its current save), every @@ -1036,6 +1047,7 @@ void ReloadPlayerInPlace(const SystemAddress& sysAddr, Entity& player, Character createCharacter.chatMode = static_cast(character.GetGMLevel()); createCharacter.reputation = characterComponent->GetReputation(); createCharacter.propertyCloneID = character.GetPropertyCloneID(); + SetWorldKeys(createCharacter, player, character); createCharacter.Send(sysAddr); ClientPackets::ServerStates serverStates; serverStates.Send(sysAddr); @@ -1193,6 +1205,7 @@ void LoadPlayer(const SystemAddress& sysAddr) { createCharacter.chatMode = static_cast(c->GetGMLevel()); createCharacter.reputation = characterComponent->GetReputation(); createCharacter.propertyCloneID = c->GetPropertyCloneID(); + SetWorldKeys(createCharacter, *player, *c); createCharacter.Send(sysAddr); LOG("Sent CreateCharacter for ID: %llu", player->GetObjectID()); ClientPackets::ServerStates serverStates; diff --git a/tests/dGameTests/dNetTests/WorldPacketsTests.cpp b/tests/dGameTests/dNetTests/WorldPacketsTests.cpp index e613f0b5f..e1634db71 100644 --- a/tests/dGameTests/dNetTests/WorldPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/WorldPacketsTests.cpp @@ -310,6 +310,7 @@ TEST_F(WorldPacketsTests, CreateCharacterMatchesLegacy) { packet.reputation = reputation; packet.propertyCloneID = cloneID; packet.txfring = false; // the legacy sender never wrote it + packet.sendWorldKeys = false; // nor the world keys ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCreateCharacter(a, reputation, objectID, xml, name, gm, cloneID); }, packet); const auto copy = RoundTrip(packet); @@ -340,6 +341,30 @@ TEST_F(WorldPacketsTests, CreateCharacterSendsTxfring) { EXPECT_FALSE(RoundTrip(packet).txfring); } +// Live sent legoclub=1, freetrial=0, requiresrename=0, levelid and position / rotation in 451 of 451 character data packets +TEST_F(WorldPacketsTests, CreateCharacterSendsTheWorldKeys) { + ClientPackets::CreateCharacter packet; + packet.xmlData = ""; + packet.requiresRename = true; + packet.levelID = 1200ULL | 7ULL << 16 | 99ULL << 32; + packet.position = NiPoint3(1.5f, -2.0f, 3.25f); + packet.rotation = NiQuaternion(0.5f, 0.1f, 0.2f, 0.3f); + const auto copy = RoundTrip(packet); + EXPECT_TRUE(copy.sendWorldKeys); + EXPECT_TRUE(copy.legoClub); + EXPECT_FALSE(copy.freeTrial); + EXPECT_TRUE(copy.requiresRename); + EXPECT_EQ(copy.levelID, packet.levelID); + EXPECT_EQ(copy.position, packet.position); + EXPECT_EQ(copy.rotation, packet.rotation); + + packet.sendWorldKeys = false; + const auto without = RoundTrip(packet); + EXPECT_FALSE(without.sendWorldKeys); + EXPECT_FALSE(without.legoClub); // the client's value without the key + EXPECT_FALSE(without.requiresRename); +} + TEST_F(WorldPacketsTests, CharacterListMatchesLegacy) { FakeCharacter first; FakeCharacter second;