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 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 14:48:11 -06:00
parent e786d2b1c5
commit 55a6d0babc
5 changed files with 91 additions and 0 deletions

View File

@@ -251,6 +251,19 @@ namespace ClientPackets {
ldfData.Insert<int64_t>(u"reputation", reputation);
ldfData.Insert<int32_t>(u"propertycloneid", propertyCloneID);
if (txfring) ldfData.Insert<bool>(u"txfring", true);
if (sendWorldKeys) {
ldfData.Insert<bool>(u"legoclub", legoClub);
ldfData.Insert<bool>(u"freetrial", freeTrial);
ldfData.Insert<bool>(u"requiresrename", requiresRename);
ldfData.Insert<uint64_t>(u"levelid", levelID);
ldfData.Insert<float>(u"position.x", position.x);
ldfData.Insert<float>(u"position.y", position.y);
ldfData.Insert<float>(u"position.z", position.z);
ldfData.Insert<float>(u"rotation.x", rotation.x);
ldfData.Insert<float>(u"rotation.y", rotation.y);
ldfData.Insert<float>(u"rotation.z", rotation.z);
ldfData.Insert<float>(u"rotation.w", rotation.w);
}
data.Write<uint32_t>(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:

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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<uint64_t>(zoneID.GetMapID()) | static_cast<uint64_t>(zoneID.GetInstanceID()) << 16 |
static_cast<uint64_t>(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<int32_t>(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<int32_t>(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;

View File

@@ -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 = "<obj/>";
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;