fix(character): the character data packet sends txfring=true, as live

LWOInventoryComponent_Client::LoadConfigData (0x00cde200) reads txfring
from the character data; without it the client runs its inventory-full
tips (msgPlayerReady 0x00bde5e0) on every world load. Of 226 captured
live character data packets, 222 had the key, all true; the server never
wrote it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-02 10:33:01 -05:00
parent 0b5a79cf96
commit df3f46b43b
4 changed files with 24 additions and 0 deletions

View File

@@ -250,6 +250,7 @@ namespace ClientPackets {
ldfData.Insert<int32_t>(u"chatmode", chatMode);
ldfData.Insert<int64_t>(u"reputation", reputation);
ldfData.Insert<int32_t>(u"propertycloneid", propertyCloneID);
if (txfring) ldfData.Insert<bool>(u"txfring", true);
data.Write<uint32_t>(ldfData.values.size());
for (const auto& toSerialize : ldfData | std::views::values) toSerialize->WriteToPacket(data);
@@ -307,6 +308,7 @@ namespace ClientPackets {
if (decompressedSize != static_cast<int32_t>(uncompressedSize)) return false;
RakNet::BitStream data(uncompressed.data(), uncompressed.size(), false);
txfring = false; // the client's value without the key
uint32_t count{};
VALIDATE_READ(data.Read(count));
for (uint32_t i = 0; i < count; i++) {
@@ -343,6 +345,12 @@ namespace ClientPackets {
if (key == u"xmlData") xmlData = value;
break;
}
case LDF_TYPE_BOOLEAN: {
uint8_t value{};
VALIDATE_READ(data.Read(value));
if (key == u"txfring") txfring = value != 0;
break;
}
default:
return false; // CreateCharacter never uses other types
}

View File

@@ -185,6 +185,10 @@ namespace ClientPackets {
int32_t chatMode{}; // DLU sends the GM level
int64_t reputation{};
int32_t propertyCloneID{};
// LWOInventoryComponent_Client::LoadConfigData (0x00cde200) reads it; live sent true in every character data
// 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;
CreateCharacter() : LUBitStream(ServiceType::CLIENT, MessageType::Client::CREATE_CHARACTER) {}
void Serialize(RakNet::BitStream& bitStream) const override;

View File

@@ -639,6 +639,7 @@ json ToJson(const ClientPackets::CreateCharacter& m) {
j["chatMode"] = ToJson(m.chatMode);
j["reputation"] = ToJson(m.reputation);
j["propertyCloneID"] = ToJson(m.propertyCloneID);
j["txfring"] = ToJson(m.txfring);
return j;
}
json ToJson(const ClientPackets::ChatModerationString& m) {

View File

@@ -309,6 +309,7 @@ TEST_F(WorldPacketsTests, CreateCharacterMatchesLegacy) {
packet.chatMode = static_cast<int32_t>(gm);
packet.reputation = reputation;
packet.propertyCloneID = cloneID;
packet.txfring = false; // the legacy sender never wrote it
ExpectSameSend([&](const SystemAddress& a) { LegacyWorldPackets::SendCreateCharacter(a, reputation, objectID, xml, name, gm, cloneID); }, packet);
const auto copy = RoundTrip(packet);
@@ -329,6 +330,16 @@ TEST_F(WorldPacketsTests, CreateCharacterMatchesLegacy) {
ExpectTruncatedFails(packet);
}
// Live sent txfring=true in every character data packet (222 of the 222 that had the key, of 226)
TEST_F(WorldPacketsTests, CreateCharacterSendsTxfring) {
ClientPackets::CreateCharacter packet;
EXPECT_TRUE(packet.txfring);
EXPECT_TRUE(RoundTrip(packet).txfring);
packet.txfring = false;
EXPECT_FALSE(RoundTrip(packet).txfring);
}
TEST_F(WorldPacketsTests, CharacterListMatchesLegacy) {
FakeCharacter first;
FakeCharacter second;