feat(capture): decode every LU packet with its own struct

The capture viewer's LU packet fields come from each struct's Deserialize and a member list generated from the
struct definitions (tools/gen_game_message_fields.py -> dNet/PacketFields.inc), replacing the hand-written list of
25. Secret members are never listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-30 04:23:32 -05:00
parent 951edfdde2
commit 19aa691ff7
5 changed files with 2390 additions and 154 deletions

View File

@@ -1,6 +1,11 @@
#include "PacketDecoder.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <deque>
#include <set>
#include <unordered_map>
#include <map>
#include <utility>
@@ -21,16 +26,30 @@
#include "WorldPackets.h"
#include "WorldRoutePacket.h"
#include "magic_enum.hpp"
#include "PacketJson.h"
#include "master/CDClientReload.h"
#include "master/DashboardMessages.h"
#include "master/DataChanged.h"
#include "master/InstanceMigration.h"
#include "master/LiveUpdate.h"
#include "master/MessageCapture.h"
#include "master/PlayerAction.h"
#include "master/Profiling.h"
#include "master/ServerTraffic.h"
#include "master/UgcModelsMade.h"
#include "master/WorldFiles.h"
#include "Profiler.h"
#include "TrafficStats.h"
#include "ZoneFileLog.h"
namespace {
using json = nlohmann::json;
using Fields = std::function<std::optional<json>(RakNet::BitStream&, bool fromClient)>;
using Redactor = std::function<bool(std::string&)>;
using Scrubber = std::function<bool(std::string&, bool anonymise)>;
struct Entry {
Fields fields;
// Packets with secrets or text to scrub (the fields of every packet come from PacketFields.inc)
struct Protection {
Redactor redact; // set for structs with secret fields
Scrubber scrub; // set for structs with account names, character names or typed text
};
@@ -73,19 +92,124 @@ namespace {
};
}
std::string Id(LWOOBJID id) { return std::to_string(id); }
json Point(const NiPoint3& p) { return json::array({ p.x, p.y, p.z }); }
json Rotation(const NiQuaternion& q) { return json::array({ q.x, q.y, q.z, q.w }); }
// The fields of every packet: its struct's own Deserialize, then its members by name (PacketFields.inc, written by
// tools/gen_game_message_fields.py from the structs)
using PacketJson::ToJson;
template<typename T>
Fields Make(std::function<void(const T&, json&)> fill) {
return [fill](RakNet::BitStream& stream, bool) -> std::optional<json> {
T packet;
if (!packet.Deserialize(stream)) return std::nullopt;
json out = json::object();
fill(packet, out);
json ToJson(const TrafficStats::Histogram& histogram) {
json buckets = json::array();
for (const auto& [bucket, count] : histogram.Sparse()) buckets.push_back(json::array({ bucket, count }));
return json{ {"count", std::to_string(histogram.Count())}, {"sumUs", std::to_string(histogram.Sum())}, {"buckets", buckets} };
}
template<typename T> json ToJson(const std::optional<T>& value);
template<typename T> json ToJson(const std::vector<T>& values);
template<typename K, typename V> json ToJson(const std::map<K, V>& values);
template<typename A, typename B> json ToJson(const std::pair<A, B>& value);
template<typename T, size_t N> json ToJson(const std::array<T, N>& values);
template<typename T> json ToJson(const std::set<T>& values);
template<typename T> json ToJson(const std::deque<T>& values);
template<typename K, typename V> json ToJson(const std::unordered_map<K, V>& values);
struct Read {
std::optional<json> fields;
uint32_t unreadBits{};
};
using Reader = Read(*)(RakNet::BitStream&);
struct Entry {
ServiceType service;
uint32_t id;
const char* structName;
bool received; // read by the server from its client (has a Handle)
Reader read;
};
template<typename T> Read ReadWith(RakNet::BitStream& stream);
#include "PacketFields.inc"
template<typename T> json ToJson(const std::optional<T>& value) { return value ? ToJson(*value) : json(nullptr); }
template<typename T> json ToJson(const std::vector<T>& values) {
if constexpr (std::is_same_v<T, uint8_t> || std::is_same_v<T, char>) {
return json{ {"hex", PacketJson::Hex(std::string_view(reinterpret_cast<const char*>(values.data()), values.size()))} };
} else {
json out = json::array();
for (const auto& value : values) out.push_back(ToJson(value));
return out;
};
}
}
template<typename K, typename V> json ToJson(const std::map<K, V>& values) {
json out = json::array();
for (const auto& [key, value] : values) out.push_back(json::array({ ToJson(key), ToJson(value) }));
return out;
}
template<typename A, typename B> json ToJson(const std::pair<A, B>& value) { return json::array({ ToJson(value.first), ToJson(value.second) }); }
template<typename T, size_t N> json ToJson(const std::array<T, N>& values) {
json out = json::array();
for (const auto& value : values) out.push_back(ToJson(value));
return out;
}
template<typename T> json ToJson(const std::set<T>& values) {
json out = json::array();
for (const auto& value : values) out.push_back(ToJson(value));
return out;
}
template<typename T> json ToJson(const std::deque<T>& values) {
json out = json::array();
for (const auto& value : values) out.push_back(ToJson(value));
return out;
}
template<typename K, typename V> json ToJson(const std::unordered_map<K, V>& values) {
json out = json::array();
for (const auto& [key, value] : values) out.push_back(json::array({ ToJson(key), ToJson(value) }));
return out;
}
template<typename T> Read ReadWith(RakNet::BitStream& stream) {
T packet;
if (!packet.Deserialize(stream)) return {};
return { ToJson(packet), stream.GetNumberOfUnreadBits() };
}
// (service, ID) -> its structs, the ones the server reads from a client first
const std::map<std::pair<ServiceType, uint32_t>, std::vector<const Entry*>>& FieldIndex() {
static const auto index = [] {
std::map<std::pair<ServiceType, uint32_t>, std::vector<const Entry*>> out;
for (const auto& entry : Entries()) out[{ entry.service, entry.id }].push_back(&entry);
return out;
}();
return index;
}
/**
* The packet's fields from the first of its structs that reads the whole of it (a struct per direction can share
* an ID: the one for this direction is tried first). A struct that reads but leaves whole bytes unread is shown
* with "(unread bits)" when no struct reads it all.
*/
std::optional<json> ReadFields(ServiceType service, uint32_t id, bool fromClient, RakNet::BitStream& stream, bool& failed) {
const auto it = FieldIndex().find({ service, id });
if (it == FieldIndex().end()) return std::nullopt;
auto candidates = it->second;
std::stable_sort(candidates.begin(), candidates.end(), [fromClient](const Entry* a, const Entry* b) {
return (a->received == fromClient) > (b->received == fromClient);
});
const auto start = stream.GetReadOffset();
std::optional<json> partial;
for (const auto* entry : candidates) {
if (!entry->read) continue;
stream.SetReadOffset(start);
auto read = entry->read(stream);
if (!read.fields) continue;
if (read.unreadBits < 8) return read.fields;
if (!partial) {
partial = std::move(read.fields);
(*partial)["(unread bits)"] = read.unreadBits;
}
}
failed = !partial;
return partial;
}
// Reads T from a whole packet, lets `blank` clear its secrets, and writes it back in place
@@ -106,141 +230,23 @@ namespace {
using Key = std::pair<ServiceType, uint32_t>;
template<typename E> Key K(ServiceType service, E id) { return { service, static_cast<uint32_t>(id) }; }
const std::map<Key, Entry>& Registry() {
const std::map<Key, Protection>& Registry() {
using S = ServiceType;
static const std::map<Key, Entry> registry{
// Handshake: one ID, a struct per direction
{ K(S::COMMON, MessageType::Server::VERSION_CONFIRM), { [](RakNet::BitStream& stream, bool fromClient) -> std::optional<json> {
if (fromClient) {
CommonPackets::ClientVersionConfirm packet;
if (!packet.Deserialize(stream)) return std::nullopt;
return json{ {"netVersion", packet.netVersion}, {"serviceType", static_cast<int>(packet.serviceType)}, {"processID", packet.processID}, {"port", packet.port} };
}
CommonPackets::ServerVersionConfirm packet;
if (!packet.Deserialize(stream)) return std::nullopt;
return json{ {"netVersion", packet.netVersion}, {"serviceType", static_cast<int>(packet.serviceType)} };
} } },
// Auth: the username and password are blanked when recorded
{ K(S::AUTH, MessageType::Auth::LOGIN_REQUEST), {
Make<AuthPackets::LoginRequest>([](const auto& p, json& j) {
j = { {"username", p.username.GetAsString()}, {"localeID", static_cast<int>(p.localeID)}, {"clientOS", static_cast<int>(p.clientOS)},
{"memoryStats", p.memoryStats.GetAsString()}, {"videoCard", p.videoCard.GetAsString()} };
}),
Blank<AuthPackets::LoginRequest>([](auto& p) { p.username.string.clear(); p.password.string.clear(); }),
Scrub<AuthPackets::LoginRequest>([](auto& p) { X(p.username); }) } },
{ K(S::CLIENT, MessageType::Client::LOGIN_RESPONSE), {
Make<ClientPackets::LoginResponse>([](const auto& p, json& j) {
json stamps = json::array();
for (const auto& stamp : p.stamps.list) stamps.push_back({ {"type", static_cast<int>(stamp.type)}, {"value", stamp.value}, {"timestamp", stamp.timestamp} });
j = { {"responseCode", static_cast<int>(p.responseCode)}, {"worldServerIP", p.worldServerIP.string}, {"worldServerPort", p.worldServerPort},
{"errorMessage", p.errorMessage}, {"stamps", stamps} };
}),
Blank<ClientPackets::LoginResponse>([](auto& p) { p.userKey.string.clear(); p.cdnKey.string.clear(); }) } },
// World
{ K(S::WORLD, MessageType::World::VALIDATION), {
Make<WorldPackets::Validation>([](const auto& p, json& j) { j = { {"username", p.username.GetAsString()}, {"fdbChecksum", p.fdbChecksum.string} }; }),
Blank<WorldPackets::Validation>([](auto& p) { p.sessionKey.string.clear(); }),
Scrub<WorldPackets::Validation>([](auto& p) { X(p.username); }) } },
{ K(S::WORLD, MessageType::World::CHARACTER_CREATE_REQUEST), { Make<WorldPackets::CharacterCreateRequest>([](const auto& p, json& j) {
j = { {"name", p.name.GetAsString()}, {"firstNameIndex", p.firstNameIndex}, {"middleNameIndex", p.middleNameIndex}, {"lastNameIndex", p.lastNameIndex},
{"shirtColor", p.shirtColor}, {"shirtStyle", p.shirtStyle}, {"pantsColor", p.pantsColor}, {"hairStyle", p.hairStyle}, {"hairColor", p.hairColor},
{"eyebrows", p.eyebrows}, {"eyes", p.eyes}, {"mouth", p.mouth} };
}),
nullptr, Scrub<WorldPackets::CharacterCreateRequest>(nullptr, [](auto& p) { X(p.name); }) } },
{ K(S::WORLD, MessageType::World::LOGIN_REQUEST), { Make<WorldPackets::CharacterLoginRequest>([](const auto& p, json& j) { j = { {"playerID", Id(p.playerID)} }; }) } },
{ K(S::WORLD, MessageType::World::CHARACTER_DELETE_REQUEST), { Make<WorldPackets::CharacterDeleteRequest>([](const auto& p, json& j) { j = { {"objectID", Id(p.objectID)} }; }) } },
{ K(S::WORLD, MessageType::World::CHARACTER_RENAME_REQUEST), { Make<WorldPackets::CharacterRenameRequest>([](const auto& p, json& j) {
j = { {"objectID", Id(p.objectID)}, {"name", p.name.GetAsString()} };
}),
nullptr, Scrub<WorldPackets::CharacterRenameRequest>(nullptr, [](auto& p) { X(p.name); }) } },
{ K(S::WORLD, MessageType::World::LEVEL_LOAD_COMPLETE), { Make<WorldPackets::LevelLoadComplete>([](const auto& p, json& j) {
j = { {"mapID", p.mapID}, {"instanceID", p.instanceID}, {"cloneID", p.cloneID} };
}) } },
{ K(S::WORLD, MessageType::World::POSITION_UPDATE), { Make<WorldPackets::PositionUpdate>([](const auto& p, json& j) {
j = { {"position", Point(p.update.position)}, {"rotation", Rotation(p.update.rotation)}, {"onGround", p.update.onGround}, {"onRail", p.update.onRail} };
if (p.hasVelocity) j["velocity"] = Point(p.update.velocity);
if (p.hasLocalSpaceInfo) j["platform"] = Id(p.update.localSpaceInfo.objectId);
}) } },
{ K(S::WORLD, MessageType::World::GENERAL_CHAT_MESSAGE), { Make<WorldPackets::GeneralChatMessage>([](const auto& p, json& j) {
j = { {"chatChannel", p.chatChannel}, {"message", GeneralUtils::UTF16ToWTF8(p.message)} };
}),
nullptr, Scrub<WorldPackets::GeneralChatMessage>(nullptr, [](auto& p) { X(p.message); }) } },
{ K(S::WORLD, MessageType::World::ROUTE_PACKET), { Make<WorldPackets::RoutePacket>([](const auto& p, json& j) {
j = { {"routed", PacketDecoder::Name(p.routedService, p.routedMessageID)}, {"size", p.size} };
}) } },
// To the client
{ K(S::CLIENT, MessageType::Client::LOAD_STATIC_ZONE), { Make<ClientPackets::LoadStaticZone>([](const auto& p, json& j) {
j = { {"mapID", p.mapID}, {"instanceID", p.instanceID}, {"cloneID", p.cloneID}, {"mapChecksum", p.mapChecksum}, {"playerPosition", Point(p.playerPosition)},
{"instanceType", p.instanceType} };
}) } },
{ K(S::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE), { Make<ClientPackets::CharacterListResponse>([](const auto& p, json& j) {
json characters = json::array();
for (const auto& c : p.characters) {
characters.push_back({ {"objectID", Id(c.objectID)}, {"name", c.name.GetAsString()}, {"zoneID", c.zoneID}, {"equippedItems", c.equippedItems} });
}
j = { {"selectedCharacterIndex", p.selectedCharacterIndex}, {"characters", characters} };
}),
nullptr, Scrub<ClientPackets::CharacterListResponse>(nullptr, [](auto& p) { for (auto& c : p.characters) { X(c.name); X(c.unapprovedName); } }) } },
{ K(S::CLIENT, MessageType::Client::CREATE_CHARACTER), { Make<ClientPackets::CreateCharacter>([](const auto& p, json& j) {
j = { {"objectID", Id(p.objectID)}, {"templateID", p.templateID}, {"name", GeneralUtils::UTF16ToWTF8(p.name)}, {"gmLevel", static_cast<int>(p.gmLevel)},
{"reputation", p.reputation}, {"propertyCloneID", p.propertyCloneID}, {"xmlBytes", p.xmlData.size()} };
}),
nullptr, Scrub<ClientPackets::CreateCharacter>(nullptr, [](auto& p) { X(p.name); }) } },
{ K(S::CLIENT, MessageType::Client::CHARACTER_CREATE_RESPONSE), { Make<ClientPackets::CharacterCreateResponse>([](const auto& p, json& j) {
j = { {"response", static_cast<int>(p.response)} };
}) } },
{ K(S::CLIENT, MessageType::Client::TRANSFER_TO_WORLD), { Make<ClientPackets::TransferToWorld>([](const auto& p, json& j) {
j = { {"serverIP", p.serverIP.string}, {"serverPort", p.serverPort}, {"mythranShift", p.mythranShift} };
}) } },
// Chat
{ K(S::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE), { Make<ChatPackets::GeneralChatMessage>([](const auto& p, json& j) {
j = { {"playerID", Id(p.playerID)}, {"chatChannel", static_cast<int>(p.chatChannel)}, {"message", p.message.GetAsString()} };
}),
nullptr, Scrub<ChatPackets::GeneralChatMessage>(nullptr, [](auto& p) { X(p.senderName); X(p.message); }) } },
{ K(S::CHAT, MessageType::Chat::PRIVATE_CHAT_MESSAGE), { Make<ChatPackets::PrivateChatMessage>([](const auto& p, json& j) {
j = { {"playerID", Id(p.playerID)}, {"senderName", p.senderName.GetAsString()}, {"receiverName", p.receiverName.GetAsString()},
{"responseCode", p.responseCode}, {"message", p.message.GetAsString()} };
}),
nullptr, Scrub<ChatPackets::PrivateChatMessage>(nullptr, [](auto& p) { X(p.senderName); X(p.receiverName); X(p.message); }) } },
{ K(S::CHAT, MessageType::Chat::WORLD_ROUTE_PACKET), { Make<ChatPackets::WorldRoutePacket>([](const auto& p, json& j) {
j = { {"targetID", Id(p.targetID)}, {"bytes", p.routedData.size()} };
if (p.routedData.size() >= 8 && p.routedData[0] == ID_USER_PACKET_ENUM) {
uint16_t service;
uint32_t id;
std::memcpy(&service, p.routedData.data() + 1, sizeof(service));
std::memcpy(&id, p.routedData.data() + 3, sizeof(id));
j["routed"] = PacketDecoder::Name(static_cast<ServiceType>(service), id);
}
}) } },
// Between servers: session keys are blanked when recorded
{ K(S::MASTER, MessageType::Master::REQUEST_SESSION_KEY), { Make<MasterPackets::RequestSessionKey>([](const auto& p, json& j) { j = { {"username", p.username.GetAsString()} }; }),
nullptr, Scrub<MasterPackets::RequestSessionKey>([](auto& p) { X(p.username); }) } },
{ K(S::MASTER, MessageType::Master::SESSION_KEY_RESPONSE), {
Make<MasterPackets::SessionKeyResponse>([](const auto& p, json& j) { j = { {"username", p.username.GetAsString()} }; }),
Blank<MasterPackets::SessionKeyResponse>([](auto& p) { p.sessionKey = 0; }),
Scrub<MasterPackets::SessionKeyResponse>([](auto& p) { X(p.username); }) } },
{ K(S::MASTER, MessageType::Master::SET_SESSION_KEY), {
Make<MasterPackets::SetSessionKey>([](const auto& p, json& j) { j = { {"username", p.username.string} }; }),
Blank<MasterPackets::SetSessionKey>([](auto& p) { p.sessionKey = 0; }),
Scrub<MasterPackets::SetSessionKey>([](auto& p) { X(p.username); }) } },
{ K(S::MASTER, MessageType::Master::NEW_SESSION_ALERT), {
Make<MasterPackets::NewSessionAlert>([](const auto& p, json& j) { j = { {"username", p.username.string} }; }),
Blank<MasterPackets::NewSessionAlert>([](auto& p) { p.sessionKey = 0; }),
Scrub<MasterPackets::NewSessionAlert>([](auto& p) { X(p.username); }) } },
{ K(S::MASTER, MessageType::Master::REQUEST_ZONE_TRANSFER), { Make<MasterPackets::RequestZoneTransfer>([](const auto& p, json& j) {
j = { {"requestID", Id(p.requestID)}, {"zoneID", p.zoneID}, {"cloneID", p.cloneID}, {"mythranShift", p.mythranShift}, {"stamps", p.stamps.size()} };
}) } },
{ K(S::MASTER, MessageType::Master::REQUEST_ZONE_TRANSFER_RESPONSE), { Make<MasterPackets::RequestZoneTransferResponse>([](const auto& p, json& j) {
j = { {"requestID", Id(p.requestID)}, {"zoneID", p.zoneID}, {"zoneInstance", p.zoneInstance}, {"zoneClone", p.zoneClone}, {"serverPort", p.serverPort},
{"stamps", p.stamps.size()} };
}) } },
{ K(S::MASTER, MessageType::Master::PLAYER_ADDED), { Make<MasterPackets::PlayerAdded>([](const auto& p, json& j) { j = { {"zoneID", p.zoneID}, {"instanceID", p.instanceID} }; }) } },
{ K(S::MASTER, MessageType::Master::PLAYER_REMOVED), { Make<MasterPackets::PlayerRemoved>([](const auto& p, json& j) { j = { {"zoneID", p.zoneID}, {"instanceID", p.instanceID} }; }) } },
static const std::map<Key, Protection> registry{
{ K(S::AUTH, MessageType::Auth::LOGIN_REQUEST), { Blank<AuthPackets::LoginRequest>([](auto& p) { p.username.string.clear(); p.password.string.clear(); }), Scrub<AuthPackets::LoginRequest>([](auto& p) { X(p.username); }) } },
{ K(S::CLIENT, MessageType::Client::LOGIN_RESPONSE), { Blank<ClientPackets::LoginResponse>([](auto& p) { p.userKey.string.clear(); p.cdnKey.string.clear(); }), nullptr } },
{ K(S::WORLD, MessageType::World::VALIDATION), { Blank<WorldPackets::Validation>([](auto& p) { p.sessionKey.string.clear(); }), Scrub<WorldPackets::Validation>([](auto& p) { X(p.username); }) } },
{ K(S::WORLD, MessageType::World::CHARACTER_CREATE_REQUEST), { nullptr, Scrub<WorldPackets::CharacterCreateRequest>(nullptr, [](auto& p) { X(p.name); }) } },
{ K(S::WORLD, MessageType::World::CHARACTER_RENAME_REQUEST), { nullptr, Scrub<WorldPackets::CharacterRenameRequest>(nullptr, [](auto& p) { X(p.name); }) } },
{ K(S::WORLD, MessageType::World::GENERAL_CHAT_MESSAGE), { nullptr, Scrub<WorldPackets::GeneralChatMessage>(nullptr, [](auto& p) { X(p.message); }) } },
{ K(S::CLIENT, MessageType::Client::CHARACTER_LIST_RESPONSE), { nullptr, Scrub<ClientPackets::CharacterListResponse>(nullptr, [](auto& p) { for (auto& c : p.characters) { X(c.name); X(c.unapprovedName); } }) } },
{ K(S::CLIENT, MessageType::Client::CREATE_CHARACTER), { nullptr, Scrub<ClientPackets::CreateCharacter>(nullptr, [](auto& p) { X(p.name); }) } },
{ K(S::CHAT, MessageType::Chat::GENERAL_CHAT_MESSAGE), { nullptr, Scrub<ChatPackets::GeneralChatMessage>(nullptr, [](auto& p) { X(p.senderName); X(p.message); }) } },
{ K(S::CHAT, MessageType::Chat::PRIVATE_CHAT_MESSAGE), { nullptr, Scrub<ChatPackets::PrivateChatMessage>(nullptr, [](auto& p) { X(p.senderName); X(p.receiverName); X(p.message); }) } },
{ K(S::MASTER, MessageType::Master::REQUEST_SESSION_KEY), { nullptr, Scrub<MasterPackets::RequestSessionKey>([](auto& p) { X(p.username); }) } },
{ K(S::MASTER, MessageType::Master::SESSION_KEY_RESPONSE), { Blank<MasterPackets::SessionKeyResponse>([](auto& p) { p.sessionKey = 0; }), Scrub<MasterPackets::SessionKeyResponse>([](auto& p) { X(p.username); }) } },
{ K(S::MASTER, MessageType::Master::SET_SESSION_KEY), { Blank<MasterPackets::SetSessionKey>([](auto& p) { p.sessionKey = 0; }), Scrub<MasterPackets::SetSessionKey>([](auto& p) { X(p.username); }) } },
{ K(S::MASTER, MessageType::Master::NEW_SESSION_ALERT), { Blank<MasterPackets::NewSessionAlert>([](auto& p) { p.sessionKey = 0; }), Scrub<MasterPackets::NewSessionAlert>([](auto& p) { X(p.username); }) } },
};
return registry;
}
@@ -312,6 +318,27 @@ namespace {
}
}
namespace {
// Routed packets: the name of the packet they carry
void AddRoutedName(ServiceType service, uint32_t id, std::string_view bytes, json& fields) {
RakNet::BitStream stream(reinterpret_cast<unsigned char*>(const_cast<char*>(bytes.data())), static_cast<unsigned int>(bytes.size()), false);
stream.IgnoreBytes(8);
if (service == ServiceType::WORLD && id == static_cast<uint32_t>(MessageType::World::ROUTE_PACKET)) {
WorldPackets::RoutePacket packet;
if (packet.Deserialize(stream)) fields["routed"] = PacketDecoder::Name(packet.routedService, packet.routedMessageID);
} else if (service == ServiceType::CHAT && id == static_cast<uint32_t>(MessageType::Chat::WORLD_ROUTE_PACKET)) {
ChatPackets::WorldRoutePacket packet;
if (packet.Deserialize(stream) && packet.routedData.size() >= 8 && packet.routedData[0] == ID_USER_PACKET_ENUM) {
uint16_t routedService;
uint32_t routedId;
std::memcpy(&routedService, packet.routedData.data() + 1, sizeof(routedService));
std::memcpy(&routedId, packet.routedData.data() + 3, sizeof(routedId));
fields["routed"] = PacketDecoder::Name(static_cast<ServiceType>(routedService), routedId);
}
}
}
}
namespace PacketDecoder {
std::string Name(ServiceType service, uint32_t messageId) {
switch (service) {
@@ -344,7 +371,7 @@ namespace PacketDecoder {
int32_t lot{};
if (stream.Read(flag) && stream.Read(network) && stream.Read(object) && stream.Read(lot)) {
out.objectId = object;
out.fields = json{ {"networkID", network}, {"objectID", Id(object)}, {"lot", lot} };
out.fields = json{ {"networkID", network}, {"objectID", std::to_string(object)}, {"lot", lot} };
}
}
return out;
@@ -380,10 +407,8 @@ namespace PacketDecoder {
return out;
}
const auto it = Registry().find({ service, id });
if (it == Registry().end()) return out;
out.fields = it->second.fields(stream, fromClient);
out.failed = !out.fields.has_value();
out.fields = ReadFields(service, id, fromClient, stream, out.failed);
if (out.fields) AddRoutedName(service, id, bytes, *out.fields);
return out;
}
@@ -434,5 +459,7 @@ namespace PacketDecoder {
return it != Registry().end() && it->second.redact;
}
size_t RegisteredCount() { return Registry().size() + 1; }
size_t RegisteredCount() { return FieldIndex().size(); }
bool HasFields(ServiceType service, uint32_t messageId) { return FieldIndex().contains({ service, messageId }); }
}

View File

@@ -74,6 +74,9 @@ namespace PacketDecoder {
// How many (service, message) pairs have typed decoders, for tests
size_t RegisteredCount();
// Whether a (service, message) has a struct its fields are read with
bool HasFields(ServiceType service, uint32_t messageId);
}
#endif //!__PACKETDECODER__H__

1891
dNet/PacketFields.inc Normal file

File diff suppressed because it is too large Load Diff

118
dNet/PacketJson.h Normal file
View File

@@ -0,0 +1,118 @@
#ifndef __PACKETJSON__H__
#define __PACKETJSON__H__
#include <chrono>
#include <cstdint>
#include <string>
#include <type_traits>
#include "BitStream.h"
#include "BitStreamUtils.h"
#include "dCommonVars.h"
#include "Stamps.h"
#include "GeneralUtils.h"
#include "json.hpp"
#include "LDFFormat.h"
#include "magic_enum.hpp"
#include "NiPoint3.h"
#include "NiQuaternion.h"
/**
* How a packet struct's members are shown in the capture viewer: one ToJson overload per member type. The member
* lists are generated from the structs (tools/gen_game_message_fields.py); a member type without an overload here (or
* in the file that includes the generated list) fails the build. Bring them in with `using PacketJson::ToJson;`.
*/
namespace PacketJson {
using json = nlohmann::json;
inline std::string Hex(std::string_view bytes) {
static constexpr char digits[] = "0123456789abcdef";
std::string out;
out.reserve(bytes.size() * 2);
for (const unsigned char c : bytes) {
out += digits[c >> 4];
out += digits[c & 15];
}
return out;
}
inline json ToJson(bool value) { return value; }
inline json ToJson(float value) { return value; }
inline json ToJson(double value) { return value; }
// 64-bit values (object IDs) as strings: they don't fit in a JavaScript number
inline json ToJson(int64_t value) { return std::to_string(value); }
inline json ToJson(uint64_t value) { return std::to_string(value); }
inline json ToJson(int32_t value) { return value; }
inline json ToJson(uint32_t value) { return value; }
inline json ToJson(int16_t value) { return value; }
inline json ToJson(uint16_t value) { return value; }
inline json ToJson(uint8_t value) { return value; }
inline json ToJson(int8_t value) { return value; }
inline json ToJson(char value) { return static_cast<int>(value); }
inline json ToJson(const NiPoint3& point) { return json::array({ point.x, point.y, point.z }); }
inline json ToJson(const NiQuaternion& rotation) { return json::array({ rotation.x, rotation.y, rotation.z, rotation.w }); }
inline json ToJson(const std::u16string& text) { return GeneralUtils::UTF16ToWTF8(text); }
// Text, or its bytes in hex when it isn't text (skill behavior streams are std::string too)
inline json ToJson(const std::string& text) {
for (const unsigned char c : text) {
if (c < 0x20 && c != '\n' && c != '\r' && c != '\t') return json{ {"hex", Hex(text)} };
}
try {
(void)json(text).dump(); // throws when it isn't UTF-8
} catch (const json::exception&) {
return json{ {"hex", Hex(text)} };
}
return text;
}
// Fixed-size strings: the text up to the first null
// (Templates, so nothing converts to them: they have implicit constructors from a size)
template<typename T> requires std::is_same_v<T, LUString>
json ToJson(const T& text) { return ToJson(text.string.substr(0, text.string.find('\0'))); }
template<typename T> requires std::is_same_v<T, LUWString>
json ToJson(const T& text) { return ToJson(text.string.substr(0, text.string.find(u'\0'))); }
inline json ToJson(const RakNet::BitStream& stream) {
return json{ {"hex", Hex(std::string_view(reinterpret_cast<const char*>(stream.GetData()), stream.GetNumberOfBytesUsed()))} };
}
inline json ToJson(const LwoNameValue& config) {
json out = json::array();
for (const auto& [key, value] : config.values) {
if (value) out.push_back(value->GetString());
}
return out;
}
inline json ToJson(const json& value) { return value; }
inline json ToJson(const LWOZONEID& zone) {
return json{ {"mapID", zone.GetMapID()}, {"instanceID", zone.GetInstanceID()}, {"cloneID", zone.GetCloneID()} };
}
inline json ToJson(const Stamps& stamps) {
json out = json::array();
for (const auto& stamp : stamps.list) {
const auto name = magic_enum::enum_name(stamp.type);
out.push_back({ {"type", name.empty() ? std::to_string(static_cast<int>(stamp.type)) : std::string(name)}, {"value", stamp.value}, {"timestamp", std::to_string(stamp.timestamp)} });
}
return out;
}
template<typename Clock, typename Duration>
json ToJson(const std::chrono::time_point<Clock, Duration>& time) {
return std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(time.time_since_epoch()).count());
}
// Enums by name, with the number when it has none
template<typename E> requires std::is_enum_v<E>
json ToJson(E value) {
const auto name = magic_enum::enum_name(value);
const auto number = static_cast<std::underlying_type_t<E>>(value);
if (name.empty()) return number;
return std::string(name) + " (" + std::to_string(number) + ")";
}
}
#endif //!__PACKETJSON__H__