fix: send reputation in the character data as live did, as LDF type 8

Live sent the reputation key as a 64-bit number (LDF type 8) in all 451
character data packets looked at; the server sent it as an object ID
(type 9). It now goes out as type 8 with the other keys live sent; the
packet without them keeps type 9, and both are read.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 15:54:54 -06:00
parent 9b386caf1e
commit 76930028db
2 changed files with 66 additions and 1 deletions

View File

@@ -248,7 +248,9 @@ namespace ClientPackets {
ldfData.Insert<u16string>(u"name", name);
ldfData.Insert<int32_t>(u"gmlevel", static_cast<int32_t>(gmLevel));
ldfData.Insert<int32_t>(u"chatmode", chatMode);
ldfData.Insert<int64_t>(u"reputation", reputation);
// Live sent reputation as LDF type 8 (451 of 451 character data packets); the old packet has it as type 9
if (sendWorldKeys) ldfData.Insert<uint64_t>(u"reputation", static_cast<uint64_t>(reputation));
else ldfData.Insert<int64_t>(u"reputation", reputation);
ldfData.Insert<int32_t>(u"propertycloneid", propertyCloneID);
if (txfring) ldfData.Insert<bool>(u"txfring", true);
if (sendWorldKeys) {
@@ -373,6 +375,7 @@ namespace ClientPackets {
uint64_t value{};
VALIDATE_READ(data.Read(value));
if (key == u"levelid") { levelID = value; sendWorldKeys = true; }
else if (key == u"reputation") reputation = static_cast<int64_t>(value);
break;
}
case LDF_TYPE_FLOAT: {

View File

@@ -1,3 +1,5 @@
#include "ZCompression.h"
#include "LDFFormat.h"
#include "ChatPackets.h"
#include "ClientPackets.h"
#include "GameDependencies.h"
@@ -365,6 +367,66 @@ TEST_F(WorldPacketsTests, CreateCharacterSendsTheWorldKeys) {
EXPECT_FALSE(without.requiresRename);
}
// Live sent reputation as LDF type 8 (a 64-bit number), not as an object ID (type 9), in every character data packet.
// The entries are sorted by key: reputation follows propertycloneid.
TEST_F(WorldPacketsTests, CreateCharacterSendsReputationAsLdfType8) {
const auto reputationType = [](const ClientPackets::CreateCharacter& packet) {
RakNet::BitStream bitStream;
packet.Serialize(bitStream);
// The LDF is compressed: size + 9, the compressed flag, the size, the compressed size, the data
uint32_t sizePlusHeader{};
uint8_t compressed{};
uint32_t uncompressedSize{};
uint32_t compressedSize{};
EXPECT_TRUE(bitStream.Read(sizePlusHeader));
EXPECT_TRUE(bitStream.Read(compressed));
EXPECT_TRUE(bitStream.Read(uncompressedSize));
EXPECT_TRUE(bitStream.Read(compressedSize));
std::vector<uint8_t> compressedData(compressedSize);
EXPECT_TRUE(bitStream.ReadAlignedBytes(compressedData.data(), compressedSize));
std::vector<uint8_t> uncompressed(uncompressedSize);
int32_t error{};
EXPECT_EQ(ZCompression::Decompress(compressedData.data(), compressedSize, uncompressed.data(), uncompressedSize, error), static_cast<int32_t>(uncompressedSize));
RakNet::BitStream data(uncompressed.data(), uncompressed.size(), false);
uint32_t count{};
EXPECT_TRUE(data.Read(count));
for (uint32_t i = 0; i < count; i++) {
uint8_t keyBytes{};
EXPECT_TRUE(data.Read(keyBytes));
std::u16string key(keyBytes / 2, u'\0');
for (auto& character : key) EXPECT_TRUE(data.Read(character));
uint8_t type{};
EXPECT_TRUE(data.Read(type));
if (key == u"reputation") {
int64_t value{};
EXPECT_TRUE(data.Read(value));
EXPECT_EQ(value, 300);
return static_cast<int32_t>(type);
}
switch (type) {
case LDF_TYPE_S32: case LDF_TYPE_U32: case LDF_TYPE_FLOAT: data.IgnoreBytes(4); break;
case LDF_TYPE_BOOLEAN: data.IgnoreBytes(1); break;
case LDF_TYPE_U64: case LDF_TYPE_OBJID: data.IgnoreBytes(8); break;
case LDF_TYPE_UTF_16: { uint32_t length{}; EXPECT_TRUE(data.Read(length)); data.IgnoreBytes(length * 2); break; }
case LDF_TYPE_UTF_8: { uint32_t length{}; EXPECT_TRUE(data.Read(length)); data.IgnoreBytes(length); break; }
default: ADD_FAILURE() << "LDF type " << static_cast<int32_t>(type); return -1;
}
}
return -1;
};
ClientPackets::CreateCharacter packet;
packet.xmlData = "<obj/>";
packet.reputation = 300;
EXPECT_EQ(reputationType(packet), 8);
EXPECT_EQ(RoundTrip(packet).reputation, 300);
// The packet from before the world keys keeps its type
packet.sendWorldKeys = false;
EXPECT_EQ(reputationType(packet), 9);
EXPECT_EQ(RoundTrip(packet).reputation, 300);
}
TEST_F(WorldPacketsTests, CharacterListMatchesLegacy) {
FakeCharacter first;
FakeCharacter second;