mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
fix(capture): replica components read as the client reads them, checked on live captures
Every live capture decoded: 83 of 60,659 constructions and 5 of 609,919 serializations now don't read exactly (was about 1 in 10 constructions, and the decode aborted on text that isn't UTF-8). - trigger component (header trigger bit): a bit and the trigger ID, last - BBB component on objects listing component 107 (characters) - local space info, buff immunities, phantom physics distance, skills in progress, a choice build's setting - moving platform: path when dirty, then subcomponents while a 1 bit comes before one (mover and simple mover) - pet: names under the dirty bit, on updates too; no item or model component - models: the plain model block, or the mutable one (behaviors, editing info) when the config has propertyObjectID or inInventory; the UGC block is the item's - zone file variants: markedAsPhantom (phantom physics) and renderDisabled (no render data) - compressed LDF: u32 uncompressed and compressed sizes, inflated and shown as entries - narrow text that isn't UTF-8 is shown byte by byte, and the capture tool prints invalid text with replacements instead of aborting Tests use live byte samples for each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -157,7 +157,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
const auto j = CaptureTools::RecordJson(record, i, start, fields, replica ? &*replica : nullptr);
|
const auto j = CaptureTools::RecordJson(record, i, start, fields, replica ? &*replica : nullptr);
|
||||||
std::printf("%7zu %10.3f %-12s %-18s %s%s\n", i, j["t"].get<double>() / 1000.0, (j.value("from", std::string()) + ">" + j.value("to", std::string())).c_str(),
|
std::printf("%7zu %10.3f %-12s %-18s %s%s\n", i, j["t"].get<double>() / 1000.0, (j.value("from", std::string()) + ">" + j.value("to", std::string())).c_str(),
|
||||||
j.value("source", std::string()).c_str(), j.value("name", std::string()).c_str(), fields && j.contains("fields") ? (" " + j["fields"].dump()).c_str() : "");
|
j.value("source", std::string()).c_str(), j.value("name", std::string()).c_str(), fields && j.contains("fields") ? (" " + j["fields"].dump(-1, ' ', false, json::error_handler_t::replace)).c_str() : "");
|
||||||
}
|
}
|
||||||
if (!cdServer.empty()) std::printf("%zu constructions, %zu whose components didn't read exactly\n", constructions, unmatched);
|
if (!cdServer.empty()) std::printf("%zu constructions, %zu whose components didn't read exactly\n", constructions, unmatched);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -11,11 +11,25 @@
|
|||||||
#include "GeneralUtils.h"
|
#include "GeneralUtils.h"
|
||||||
#include "magic_enum.hpp"
|
#include "magic_enum.hpp"
|
||||||
#include "MessageIdentifiers.h"
|
#include "MessageIdentifiers.h"
|
||||||
|
#include "ZCompression.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
using enum eReplicaComponentType;
|
using enum eReplicaComponentType;
|
||||||
|
|
||||||
|
bool IsUtf8(std::string_view text) {
|
||||||
|
for (size_t i = 0; i < text.size();) {
|
||||||
|
const auto byte = static_cast<uint8_t>(text[i]);
|
||||||
|
const size_t length = byte < 0x80 ? 1 : (byte >> 5) == 0x6 ? 2 : (byte >> 4) == 0xE ? 3 : (byte >> 3) == 0x1E ? 4 : 0;
|
||||||
|
if (length == 0 || i + length > text.size()) return false;
|
||||||
|
for (size_t k = 1; k < length; k++) {
|
||||||
|
if ((static_cast<uint8_t>(text[i + k]) & 0xC0) != 0x80) return false;
|
||||||
|
}
|
||||||
|
i += length;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Reads values in order; the first read past the end marks the reader as failed and every later read gives 0
|
// Reads values in order; the first read past the end marks the reader as failed and every later read gives 0
|
||||||
struct Reader {
|
struct Reader {
|
||||||
RakNet::BitStream& stream;
|
RakNet::BitStream& stream;
|
||||||
@@ -46,13 +60,28 @@ namespace {
|
|||||||
const auto length = Get<Length>();
|
const auto length = Get<Length>();
|
||||||
std::u16string text;
|
std::u16string text;
|
||||||
for (Length i = 0; ok && i < length; i++) text += static_cast<char16_t>(Get<uint16_t>());
|
for (Length i = 0; ok && i < length; i++) text += static_cast<char16_t>(Get<uint16_t>());
|
||||||
return GeneralUtils::UTF16ToWTF8(text);
|
return Printable(GeneralUtils::UTF16ToWTF8(text));
|
||||||
}
|
}
|
||||||
template<typename Length> std::string Text() {
|
template<typename Length> std::string Text() {
|
||||||
const auto length = Get<Length>();
|
const auto length = Get<Length>();
|
||||||
std::string text;
|
std::string text;
|
||||||
for (Length i = 0; ok && i < length; i++) text += static_cast<char>(Get<uint8_t>());
|
for (Length i = 0; ok && i < length; i++) text += static_cast<char>(Get<uint8_t>());
|
||||||
return text;
|
return Printable(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow text is shown as is when it is UTF-8, else byte by byte as Latin-1 (JSON only takes UTF-8)
|
||||||
|
static std::string Printable(const std::string& text) {
|
||||||
|
if (IsUtf8(text)) return text;
|
||||||
|
std::string out;
|
||||||
|
for (const auto c : text) {
|
||||||
|
const auto byte = static_cast<uint8_t>(c);
|
||||||
|
if (byte < 0x80) out += c;
|
||||||
|
else {
|
||||||
|
out += static_cast<char>(0xC0 | (byte >> 6));
|
||||||
|
out += static_cast<char>(0x80 | (byte & 0x3F));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,21 +112,38 @@ namespace {
|
|||||||
case 13: value = r.Text<uint32_t>(); break;
|
case 13: value = r.Text<uint32_t>(); break;
|
||||||
default: r.ok = false; break;
|
default: r.ok = false; break;
|
||||||
}
|
}
|
||||||
out.push_back(GeneralUtils::UTF16ToWTF8(key) + "=" + std::to_string(type) + ":" + value);
|
out.push_back(Reader::Printable(GeneralUtils::UTF16ToWTF8(key)) + "=" + std::to_string(type) + ":" + value);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// u32 size, u8 compressed, then the entries (or the compressed bytes, which are shown as their size)
|
/**
|
||||||
|
* u32 size, u8 compressed, then the entries (a u32 count and each entry). Compressed: u32 uncompressed size, u32
|
||||||
|
* compressed size and that many bytes of zlib data holding the entries, inflated here.
|
||||||
|
*/
|
||||||
json ReadLdf(Reader& r) {
|
json ReadLdf(Reader& r) {
|
||||||
const auto size = r.Get<uint32_t>();
|
const auto size = r.Get<uint32_t>();
|
||||||
const auto compressed = r.Get<uint8_t>();
|
const auto compressed = r.Get<uint8_t>();
|
||||||
if (compressed) {
|
if (!compressed) return ReadLdfEntries(r, r.Get<int32_t>());
|
||||||
|
const auto uncompressedSize = r.Get<uint32_t>();
|
||||||
const auto compressedSize = r.Get<uint32_t>();
|
const auto compressedSize = r.Get<uint32_t>();
|
||||||
for (uint32_t i = 0; r.ok && i < compressedSize; i++) r.Get<uint8_t>();
|
std::string data;
|
||||||
return json{ {"compressed", true}, {"size", size}, {"compressedSize", compressedSize} };
|
for (uint32_t i = 0; r.ok && i < compressedSize; i++) data += static_cast<char>(r.Get<uint8_t>());
|
||||||
|
json out{ {"compressed", true}, {"size", size}, {"uncompressedSize", uncompressedSize}, {"compressedSize", compressedSize} };
|
||||||
|
if (!r.ok || uncompressedSize > 1024 * 1024) return out;
|
||||||
|
std::string entries(uncompressedSize, '\0');
|
||||||
|
int32_t error = 0;
|
||||||
|
const auto inflated = ZCompression::Decompress(reinterpret_cast<const uint8_t*>(data.data()), compressedSize,
|
||||||
|
reinterpret_cast<uint8_t*>(entries.data()), uncompressedSize, error);
|
||||||
|
if (inflated != static_cast<int32_t>(uncompressedSize)) {
|
||||||
|
out["(did not inflate)"] = true;
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
return ReadLdfEntries(r, r.Get<int32_t>());
|
RakNet::BitStream stream(reinterpret_cast<unsigned char*>(entries.data()), uncompressedSize, false);
|
||||||
|
Reader inner{ stream };
|
||||||
|
out["entries"] = ReadLdfEntries(inner, inner.Get<int32_t>());
|
||||||
|
if (!inner.ok || stream.GetNumberOfUnreadBits() > 0) out["(entries did not read)"] = true;
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activity user info: object ID and 10 values each
|
// Activity user info: object ID and 10 values each
|
||||||
@@ -127,6 +173,39 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The object a character stands on and where on it (LWOBasePhysComponent's frame stats)
|
||||||
|
json LocalSpace(Reader& r) {
|
||||||
|
json out{ {"object", r.Id()}, {"position", r.Point()} };
|
||||||
|
if (r.Bit()) out["velocity"] = r.Point();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LWOBuffComponent::ReadBuffs: a u32 count and each buff
|
||||||
|
json Buffs(Reader& r) {
|
||||||
|
json buffs = json::array();
|
||||||
|
const auto count = r.Get<uint32_t>();
|
||||||
|
for (uint32_t i = 0; r.ok && i < count && i < 256; i++) {
|
||||||
|
json buff{ {"id", r.Get<uint32_t>()} };
|
||||||
|
if (r.Bit()) buff["timeMs"] = r.Get<uint32_t>();
|
||||||
|
for (const char* flag : { "cancelOnDeath", "cancelOnZone", "cancelOnDamaged", "cancelOnRemoveBuff", "cancelOnUi", "cancelOnLogout", "cancelOnUnequip", "cancelOnDamageAbsorbRanOut" }) buff[flag] = r.Bit();
|
||||||
|
const bool addedByTeammate = r.Bit();
|
||||||
|
buff["addedByTeammate"] = addedByTeammate;
|
||||||
|
buff["applyOnTeammates"] = r.Bit();
|
||||||
|
if (addedByTeammate) buff["source"] = r.Id();
|
||||||
|
buff["refCount"] = r.Get<uint32_t>();
|
||||||
|
buffs.push_back(buff);
|
||||||
|
}
|
||||||
|
return buffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModelBase(Reader& r, json& j) {
|
||||||
|
if (!r.Bit()) return;
|
||||||
|
j["pickable"] = r.Bit();
|
||||||
|
j["modelType"] = r.Get<uint32_t>();
|
||||||
|
j["originalPosition"] = r.Point();
|
||||||
|
j["originalRotation"] = r.RawRotation();
|
||||||
|
}
|
||||||
|
|
||||||
using ComponentReader = std::function<void(Reader&, json&, bool initial, const std::vector<eReplicaComponentType>& components)>;
|
using ComponentReader = std::function<void(Reader&, json&, bool initial, const std::vector<eReplicaComponentType>& components)>;
|
||||||
|
|
||||||
// One reader per component, each mirroring the component's Serialize(bIsInitialUpdate)
|
// One reader per component, each mirroring the component's Serialize(bIsInitialUpdate)
|
||||||
@@ -178,7 +257,7 @@ namespace {
|
|||||||
j["onRail"] = r.Bit();
|
j["onRail"] = r.Bit();
|
||||||
if (r.Bit()) j["velocity"] = r.Point();
|
if (r.Bit()) j["velocity"] = r.Point();
|
||||||
if (r.Bit()) j["angularVelocity"] = r.Point();
|
if (r.Bit()) j["angularVelocity"] = r.Point();
|
||||||
if (r.Bit()) NotRead(r, j, "localSpaceInfo");
|
if (r.Bit()) j["localSpace"] = LocalSpace(r);
|
||||||
if (!initial) j["teleporting"] = r.Bit();
|
if (!initial) j["teleporting"] = r.Bit();
|
||||||
}
|
}
|
||||||
} },
|
} },
|
||||||
@@ -203,7 +282,7 @@ namespace {
|
|||||||
j["onRail"] = r.Bit();
|
j["onRail"] = r.Bit();
|
||||||
if (r.Bit()) j["velocity"] = r.Point();
|
if (r.Bit()) j["velocity"] = r.Point();
|
||||||
if (r.Bit()) j["angularVelocity"] = r.Point();
|
if (r.Bit()) j["angularVelocity"] = r.Point();
|
||||||
if (r.Bit()) NotRead(r, j, "localSpaceInfo");
|
if (r.Bit()) j["localSpace"] = LocalSpace(r);
|
||||||
if (r.Bit()) {
|
if (r.Bit()) {
|
||||||
j["remoteInputX"] = r.Get<float>();
|
j["remoteInputX"] = r.Get<float>();
|
||||||
j["remoteInputY"] = r.Get<float>();
|
j["remoteInputY"] = r.Get<float>();
|
||||||
@@ -225,7 +304,10 @@ namespace {
|
|||||||
if (!(j["effectActive"] = r.Bit()).get<bool>()) return;
|
if (!(j["effectActive"] = r.Bit()).get<bool>()) return;
|
||||||
j["effectType"] = r.Get<uint32_t>();
|
j["effectType"] = r.Get<uint32_t>();
|
||||||
j["directionalMultiplier"] = r.Get<float>();
|
j["directionalMultiplier"] = r.Get<float>();
|
||||||
if (r.Bit()) return NotRead(r, j, "distanceInfo");
|
if (r.Bit()) {
|
||||||
|
j["minDistance"] = r.Get<float>();
|
||||||
|
j["maxDistance"] = r.Get<float>();
|
||||||
|
}
|
||||||
if ((j["directional"] = r.Bit()).get<bool>()) j["direction"] = r.Point();
|
if ((j["directional"] = r.Bit()).get<bool>()) j["direction"] = r.Point();
|
||||||
} },
|
} },
|
||||||
{ SOUND_TRIGGER, [](Reader& r, json& j, bool, const auto&) {
|
{ SOUND_TRIGGER, [](Reader& r, json& j, bool, const auto&) {
|
||||||
@@ -264,23 +346,8 @@ namespace {
|
|||||||
} },
|
} },
|
||||||
{ BUFF, [](Reader& r, json& j, bool initial, const auto&) {
|
{ BUFF, [](Reader& r, json& j, bool initial, const auto&) {
|
||||||
if (!initial) return;
|
if (!initial) return;
|
||||||
if (r.Bit()) {
|
if (r.Bit()) j["buffs"] = Buffs(r);
|
||||||
json buffs = json::array();
|
if (r.Bit()) j["immunities"] = Buffs(r);
|
||||||
const auto count = r.Get<uint32_t>();
|
|
||||||
for (uint32_t i = 0; r.ok && i < count && i < 256; i++) {
|
|
||||||
json buff{ {"id", r.Get<uint32_t>()} };
|
|
||||||
if (r.Bit()) buff["timeMs"] = r.Get<uint32_t>();
|
|
||||||
for (const char* flag : { "cancelOnDeath", "cancelOnZone", "cancelOnDamaged", "cancelOnRemoveBuff", "cancelOnUi", "cancelOnLogout", "cancelOnUnequip", "cancelOnDamageAbsorbRanOut" }) buff[flag] = r.Bit();
|
|
||||||
const bool addedByTeammate = r.Bit();
|
|
||||||
buff["addedByTeammate"] = addedByTeammate;
|
|
||||||
buff["applyOnTeammates"] = r.Bit();
|
|
||||||
if (addedByTeammate) buff["source"] = r.Id();
|
|
||||||
buff["refCount"] = r.Get<uint32_t>();
|
|
||||||
buffs.push_back(buff);
|
|
||||||
}
|
|
||||||
j["buffs"] = buffs;
|
|
||||||
}
|
|
||||||
if (r.Bit()) NotRead(r, j, "immunityBuffs");
|
|
||||||
} },
|
} },
|
||||||
{ DESTROYABLE, [](Reader& r, json& j, bool initial, const auto&) {
|
{ DESTROYABLE, [](Reader& r, json& j, bool initial, const auto&) {
|
||||||
if (initial && r.Bit()) {
|
if (initial && r.Bit()) {
|
||||||
@@ -320,14 +387,14 @@ namespace {
|
|||||||
if (r.Bit()) j["onThreatList"] = r.Bit();
|
if (r.Bit()) j["onThreatList"] = r.Bit();
|
||||||
} },
|
} },
|
||||||
{ COLLECTIBLE, [](Reader& r, json& j, bool, const auto&) { j["collectibleID"] = r.Get<int16_t>(); } },
|
{ COLLECTIBLE, [](Reader& r, json& j, bool, const auto&) { j["collectibleID"] = r.Get<int16_t>(); } },
|
||||||
{ PET, [](Reader& r, json& j, bool initial, const auto&) {
|
// LWOPetComponent::Deserialize: everything under the dirty bit, the names on updates too
|
||||||
if (r.Bit()) {
|
{ PET, [](Reader& r, json& j, bool, const auto&) {
|
||||||
|
if (!r.Bit()) return;
|
||||||
j["status"] = r.Get<uint32_t>();
|
j["status"] = r.Get<uint32_t>();
|
||||||
j["ability"] = r.Get<uint32_t>();
|
j["ability"] = r.Get<uint32_t>();
|
||||||
if (r.Bit()) j["interaction"] = r.Id();
|
if (r.Bit()) j["interaction"] = r.Id();
|
||||||
if (r.Bit()) j["owner"] = r.Id();
|
if (r.Bit()) j["owner"] = r.Id();
|
||||||
}
|
if (r.Bit()) {
|
||||||
if (initial && r.Bit()) {
|
|
||||||
j["moderationStatus"] = r.Get<uint32_t>();
|
j["moderationStatus"] = r.Get<uint32_t>();
|
||||||
j["name"] = r.WideText<uint8_t>();
|
j["name"] = r.WideText<uint8_t>();
|
||||||
j["ownerName"] = r.WideText<uint8_t>();
|
j["ownerName"] = r.WideText<uint8_t>();
|
||||||
@@ -398,7 +465,35 @@ namespace {
|
|||||||
if (initial && r.Bit()) j["networkSettings"] = ReadLdf(r);
|
if (initial && r.Bit()) j["networkSettings"] = ReadLdf(r);
|
||||||
} },
|
} },
|
||||||
{ SKILL, [](Reader& r, json& j, bool initial, const auto&) {
|
{ SKILL, [](Reader& r, json& j, bool initial, const auto&) {
|
||||||
if (initial && r.Bit()) NotRead(r, j, "skillsInProgress");
|
// LWOSkillComponent::Deserialize: the skills being cast and each one's running behaviors
|
||||||
|
if (!initial || !r.Bit()) return;
|
||||||
|
json skills = json::array();
|
||||||
|
const auto count = r.Get<uint32_t>();
|
||||||
|
for (uint32_t i = 0; r.ok && i < count && i < 256; i++) {
|
||||||
|
json skill{ {"skillUID", r.Get<uint32_t>()} };
|
||||||
|
skill["skillID"] = r.Get<uint32_t>();
|
||||||
|
skill["castType"] = r.Get<uint32_t>();
|
||||||
|
skill["cancelType"] = r.Get<uint32_t>();
|
||||||
|
json behaviors = json::array();
|
||||||
|
const auto behaviorCount = r.Get<uint32_t>();
|
||||||
|
for (uint32_t b = 0; r.ok && b < behaviorCount && b < 256; b++) {
|
||||||
|
json behavior{ {"behaviorHandle", r.Get<uint32_t>()} };
|
||||||
|
behavior["action"] = r.Get<uint32_t>();
|
||||||
|
behavior["waitTimeMs"] = r.Get<uint32_t>();
|
||||||
|
behavior["templateID"] = r.Get<uint32_t>();
|
||||||
|
behavior["caster"] = r.Id();
|
||||||
|
behavior["originator"] = r.Id();
|
||||||
|
behavior["target"] = r.Id();
|
||||||
|
behavior["usedMouse"] = r.Bit();
|
||||||
|
behavior["cooldown"] = r.Get<float>();
|
||||||
|
behavior["chargeTime"] = r.Get<float>();
|
||||||
|
behavior["imaginationCost"] = r.Get<float>();
|
||||||
|
behaviors.push_back(behavior);
|
||||||
|
}
|
||||||
|
skill["behaviors"] = behaviors;
|
||||||
|
skills.push_back(skill);
|
||||||
|
}
|
||||||
|
j["skillsInProgress"] = skills;
|
||||||
} },
|
} },
|
||||||
{ BASE_COMBAT_AI, [](Reader& r, json& j, bool, const auto&) {
|
{ BASE_COMBAT_AI, [](Reader& r, json& j, bool, const auto&) {
|
||||||
if (!r.Bit()) return;
|
if (!r.Bit()) return;
|
||||||
@@ -420,38 +515,58 @@ namespace {
|
|||||||
j["timeSinceStart"] = r.Get<float>();
|
j["timeSinceStart"] = r.Get<float>();
|
||||||
j["pausedTime"] = r.Get<float>();
|
j["pausedTime"] = r.Get<float>();
|
||||||
if (initial) {
|
if (initial) {
|
||||||
j["choiceBuild"] = r.Bit();
|
// LWOQuickBuildComponent::Deserialize: a choice build has a u32 of its settings after the bit
|
||||||
|
if ((j["choiceBuild"] = r.Bit()).get<bool>()) j["choiceBuildSetting"] = r.Get<uint32_t>();
|
||||||
j["activatorPosition"] = r.Point();
|
j["activatorPosition"] = r.Point();
|
||||||
j["repositionPlayer"] = r.Bit();
|
j["repositionPlayer"] = r.Bit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} },
|
} },
|
||||||
|
// LWOMovingPlatformComponent::Deserialize: the path when dirty, then each subcomponent (a 1 bit before
|
||||||
|
// each, a 0 bit after the last) as its type
|
||||||
{ MOVING_PLATFORM, [](Reader& r, json& j, bool, const auto&) {
|
{ MOVING_PLATFORM, [](Reader& r, json& j, bool, const auto&) {
|
||||||
if (!r.Bit()) {
|
const bool hasSubcomponents = r.Bit();
|
||||||
r.Bit();
|
if (r.Bit() && r.Bit()) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (r.Bit()) {
|
|
||||||
j["pathFlag"] = r.Bit();
|
|
||||||
j["pathName"] = r.WideText<uint16_t>();
|
j["pathName"] = r.WideText<uint16_t>();
|
||||||
j["startingWaypoint"] = r.Get<uint32_t>();
|
j["startingWaypoint"] = r.Get<uint32_t>();
|
||||||
j["reverse"] = r.Bit();
|
j["reverse"] = r.Bit();
|
||||||
}
|
}
|
||||||
if (!r.Bit()) return;
|
if (!hasSubcomponents) return;
|
||||||
|
json subcomponents = json::array();
|
||||||
|
while (r.ok && subcomponents.size() < 16 && r.Bit()) {
|
||||||
const auto type = r.Get<uint32_t>();
|
const auto type = r.Get<uint32_t>();
|
||||||
j["moverType"] = type;
|
json sub{ {"type", type} };
|
||||||
if (type == 5) return; // simple mover: nothing more
|
if (type == 4) {
|
||||||
if (!r.Bit()) return;
|
// LWOPlatformMover
|
||||||
j["state"] = r.Get<uint32_t>();
|
if (r.Bit()) {
|
||||||
j["desiredWaypoint"] = r.Get<int32_t>();
|
sub["state"] = r.Get<uint32_t>();
|
||||||
j["stopAtDesiredWaypoint"] = r.Bit();
|
sub["desiredWaypoint"] = r.Get<int32_t>();
|
||||||
j["reverse"] = r.Bit();
|
sub["stopAtDesiredWaypoint"] = r.Bit();
|
||||||
j["percentBetweenPoints"] = r.Get<float>();
|
sub["reverse"] = r.Bit();
|
||||||
j["position"] = r.Point();
|
sub["percentBetweenPoints"] = r.Get<float>();
|
||||||
j["currentWaypoint"] = r.Get<uint32_t>();
|
sub["position"] = r.Point();
|
||||||
j["nextWaypoint"] = r.Get<uint32_t>();
|
sub["currentWaypoint"] = r.Get<uint32_t>();
|
||||||
j["idleTimeElapsed"] = r.Get<float>();
|
sub["nextWaypoint"] = r.Get<uint32_t>();
|
||||||
j["moveTimeElapsed"] = r.Get<float>();
|
sub["idleTimeElapsed"] = r.Get<float>();
|
||||||
|
sub["moveTimeElapsed"] = r.Get<float>();
|
||||||
|
}
|
||||||
|
} else if (type == 5) {
|
||||||
|
// LWOPlatformSimpleMover
|
||||||
|
if (r.Bit() && r.Bit()) {
|
||||||
|
sub["startPosition"] = r.Point();
|
||||||
|
sub["startRotation"] = r.Rotation();
|
||||||
|
}
|
||||||
|
if (r.Bit()) {
|
||||||
|
sub["state"] = r.Get<uint32_t>();
|
||||||
|
sub["currentWaypoint"] = r.Get<uint32_t>();
|
||||||
|
sub["reverse"] = r.Bit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NotRead(r, sub, "subcomponent");
|
||||||
|
}
|
||||||
|
subcomponents.push_back(sub);
|
||||||
|
}
|
||||||
|
j["subcomponents"] = subcomponents;
|
||||||
} },
|
} },
|
||||||
{ SWITCH, [](Reader& r, json& j, bool, const auto&) { j["active"] = r.Bit(); } },
|
{ SWITCH, [](Reader& r, json& j, bool, const auto&) { j["active"] = r.Bit(); } },
|
||||||
{ VENDOR, [](Reader& r, json& j, bool, const auto&) {
|
{ VENDOR, [](Reader& r, json& j, bool, const auto&) {
|
||||||
@@ -535,24 +650,25 @@ namespace {
|
|||||||
}
|
}
|
||||||
} },
|
} },
|
||||||
{ LUP_EXHIBIT, [](Reader& r, json& j, bool, const auto&) { if (r.Bit()) j["exhibitLOT"] = r.Get<int32_t>(); } },
|
{ LUP_EXHIBIT, [](Reader& r, json& j, bool, const auto&) { if (r.Bit()) j["exhibitLOT"] = r.Get<int32_t>(); } },
|
||||||
{ MODEL, [](Reader& r, json& j, bool initial, const std::vector<eReplicaComponentType>& components) {
|
// LWOModelBehaviorComponent::Deserialize (the user-generated-content block before it is the item's)
|
||||||
if (std::find(components.begin(), components.end(), PET) == components.end() && r.Bit()) {
|
{ MODEL, [](Reader& r, json& j, bool, const auto&) { ModelBase(r, j); } },
|
||||||
j["modelID"] = r.Id();
|
// LWOMutableModelBehaviorComponent::Deserialize: the model's block, the behaviors, and on construction who
|
||||||
j["modelModerationStatus"] = r.Get<int32_t>();
|
// is editing it
|
||||||
if (r.Bit()) j["ugDescription"] = r.WideText<uint32_t>();
|
{ MUTABLE_MODEL_BEHAVIORS, [](Reader& r, json& j, bool initial, const auto&) {
|
||||||
}
|
ModelBase(r, j);
|
||||||
if (r.Bit()) {
|
|
||||||
j["pickable"] = r.Bit();
|
|
||||||
j["physicsType"] = r.Get<uint32_t>();
|
|
||||||
j["originalPosition"] = r.Point();
|
|
||||||
j["originalRotation"] = r.RawRotation();
|
|
||||||
}
|
|
||||||
if (r.Bit()) {
|
if (r.Bit()) {
|
||||||
j["behaviors"] = r.Get<uint32_t>();
|
j["behaviors"] = r.Get<uint32_t>();
|
||||||
j["paused"] = r.Bit();
|
j["paused"] = r.Bit();
|
||||||
}
|
}
|
||||||
if (initial && r.Bit()) NotRead(r, j, "editingInfo");
|
if (initial && r.Bit()) {
|
||||||
|
j["oldObjectID"] = r.Id();
|
||||||
|
j["editor"] = r.Id();
|
||||||
|
}
|
||||||
} },
|
} },
|
||||||
|
// LWOBBBComponent::Deserialize (characters carry one in live captures)
|
||||||
|
{ BBB, [](Reader& r, json& j, bool, const auto&) { if (r.Bit()) j["metadataSourceItem"] = r.Id(); } },
|
||||||
|
// LWOTriggerComponent::Deserialize, on objects whose header sets the trigger bit
|
||||||
|
{ TRIGGER, [](Reader& r, json& j, bool, const auto&) { if (r.Bit()) j["triggerID"] = r.Get<int32_t>(); } },
|
||||||
{ RENDER, [](Reader& r, json& j, bool initial, const auto&) {
|
{ RENDER, [](Reader& r, json& j, bool initial, const auto&) {
|
||||||
if (!initial) return;
|
if (!initial) return;
|
||||||
json effects = json::array();
|
json effects = json::array();
|
||||||
@@ -582,7 +698,7 @@ namespace {
|
|||||||
PHANTOM_PHYSICS, SOUND_TRIGGER, RACING_SOUND_TRIGGER, BUFF, DESTROYABLE, COLLECTIBLE, PET, POSSESSOR, LEVEL_PROGRESSION,
|
PHANTOM_PHYSICS, SOUND_TRIGGER, RACING_SOUND_TRIGGER, BUFF, DESTROYABLE, COLLECTIBLE, PET, POSSESSOR, LEVEL_PROGRESSION,
|
||||||
PLAYER_FORCED_MOVEMENT, CHARACTER, INVENTORY, SCRIPT, SKILL, BASE_COMBAT_AI, ITEM, QUICK_BUILD, MOVING_PLATFORM, SWITCH,
|
PLAYER_FORCED_MOVEMENT, CHARACTER, INVENTORY, SCRIPT, SKILL, BASE_COMBAT_AI, ITEM, QUICK_BUILD, MOVING_PLATFORM, SWITCH,
|
||||||
VENDOR, DONATION_VENDOR, ACHIEVEMENT_VENDOR, BOUNCER, SCRIPTED_ACTIVITY, SHOOTING_GALLERY, RACING_CONTROL, LUP_EXHIBIT,
|
VENDOR, DONATION_VENDOR, ACHIEVEMENT_VENDOR, BOUNCER, SCRIPTED_ACTIVITY, SHOOTING_GALLERY, RACING_CONTROL, LUP_EXHIBIT,
|
||||||
MODEL, RENDER, MINI_GAME_CONTROL,
|
MODEL, MUTABLE_MODEL_BEHAVIORS, RENDER, MINI_GAME_CONTROL, BBB,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -593,7 +709,14 @@ namespace {
|
|||||||
std::vector<eReplicaComponentType> Arrange(std::set<eReplicaComponentType> has, bool extraDestroyable) {
|
std::vector<eReplicaComponentType> Arrange(std::set<eReplicaComponentType> has, bool extraDestroyable) {
|
||||||
if (has.contains(DESTROYABLE)) has.insert(BUFF);
|
if (has.contains(DESTROYABLE)) has.insert(BUFF);
|
||||||
if (has.contains(CHARACTER)) has.insert({ POSSESSOR, LEVEL_PROGRESSION, PLAYER_FORCED_MOVEMENT });
|
if (has.contains(CHARACTER)) has.insert({ POSSESSOR, LEVEL_PROGRESSION, PLAYER_FORCED_MOVEMENT });
|
||||||
if (has.contains(PET)) has.erase(MODEL);
|
// The client drops a pet's model and item components (ObjectLoader2::DoObjectComponentLoad)
|
||||||
|
if (has.contains(PET)) {
|
||||||
|
has.erase(MODEL);
|
||||||
|
has.erase(MUTABLE_MODEL_BEHAVIORS);
|
||||||
|
has.erase(ITEM);
|
||||||
|
}
|
||||||
|
// Objects listing component 107 (characters) write a BBB component's data after the others in live captures
|
||||||
|
if (has.contains(CRAFTING)) has.insert(BBB);
|
||||||
// Collectibles get one; a quick build without one writes the same empty bits itself in the same place
|
// Collectibles get one; a quick build without one writes the same empty bits itself in the same place
|
||||||
const bool destroyable = has.contains(DESTROYABLE) || has.contains(COLLECTIBLE) || has.contains(QUICK_BUILD) || extraDestroyable;
|
const bool destroyable = has.contains(DESTROYABLE) || has.contains(COLLECTIBLE) || has.contains(QUICK_BUILD) || extraDestroyable;
|
||||||
eReplicaComponentType slot = MINI_GAME_CONTROL;
|
eReplicaComponentType slot = MINI_GAME_CONTROL;
|
||||||
@@ -610,17 +733,25 @@ namespace {
|
|||||||
|
|
||||||
std::set<eReplicaComponentType> Registered(LOT lot, const ReplicaDecoder::ComponentTable& table) {
|
std::set<eReplicaComponentType> Registered(LOT lot, const ReplicaDecoder::ComponentTable& table) {
|
||||||
// BBB models (LOT 14) are made up in code: simple physics, model, render and a destroyable after them
|
// BBB models (LOT 14) are made up in code: simple physics, model, render and a destroyable after them
|
||||||
if (lot == 14) return { SIMPLE_PHYSICS, MODEL, RENDER };
|
if (lot == 14) return { SIMPLE_PHYSICS, ITEM, MODEL, RENDER };
|
||||||
const auto it = table.find(lot);
|
const auto it = table.find(lot);
|
||||||
if (it == table.end()) return {};
|
if (it == table.end()) return {};
|
||||||
return { it->second.begin(), it->second.end() };
|
return { it->second.begin(), it->second.end() };
|
||||||
}
|
}
|
||||||
|
|
||||||
// The layouts to try for an object, the registry's own first
|
// The layouts to try for an object, the registry's own first
|
||||||
std::vector<std::vector<eReplicaComponentType>> Candidates(LOT lot, const ReplicaDecoder::ComponentTable& table) {
|
/**
|
||||||
const auto has = Registered(lot, table);
|
* `mutableModel`: the object's config has propertyObjectID or inInventory set, for which the client makes a model's
|
||||||
|
* mutable component instead of the plain one (ObjectLoader2::LoadModelBehaviorsComponent)
|
||||||
|
*/
|
||||||
|
std::vector<std::vector<eReplicaComponentType>> Candidates(LOT lot, const ReplicaDecoder::ComponentTable& table, bool mutableModel) {
|
||||||
|
auto has = Registered(lot, table);
|
||||||
|
if (mutableModel && has.contains(MODEL)) {
|
||||||
|
has.erase(MODEL);
|
||||||
|
has.insert(MUTABLE_MODEL_BEHAVIORS);
|
||||||
|
}
|
||||||
// Models get a destroyable too (Entity::Initialize), BBB models always
|
// Models get a destroyable too (Entity::Initialize), BBB models always
|
||||||
const bool model = has.contains(MODEL) && !has.contains(PET);
|
const bool model = (has.contains(MODEL) || has.contains(MUTABLE_MODEL_BEHAVIORS)) && !has.contains(PET);
|
||||||
std::vector<std::vector<eReplicaComponentType>> out{ Arrange(has, lot == 14 || model) };
|
std::vector<std::vector<eReplicaComponentType>> out{ Arrange(has, lot == 14 || model) };
|
||||||
auto add = [&out](std::vector<eReplicaComponentType> candidate) {
|
auto add = [&out](std::vector<eReplicaComponentType> candidate) {
|
||||||
if (std::find(out.begin(), out.end(), candidate) == out.end()) out.push_back(std::move(candidate));
|
if (std::find(out.begin(), out.end(), candidate) == out.end()) out.push_back(std::move(candidate));
|
||||||
@@ -638,6 +769,23 @@ namespace {
|
|||||||
auto withoutScript = has;
|
auto withoutScript = has;
|
||||||
withoutScript.erase(SCRIPT);
|
withoutScript.erase(SCRIPT);
|
||||||
add(Arrange(withoutScript, false));
|
add(Arrange(withoutScript, false));
|
||||||
|
// A simple physics object the zone file sets markedAsPhantom on gets phantom physics instead
|
||||||
|
// (LWOSimplePhysicsComponent::Allocator)
|
||||||
|
if (has.contains(SIMPLE_PHYSICS)) {
|
||||||
|
auto phantom = has;
|
||||||
|
phantom.erase(SIMPLE_PHYSICS);
|
||||||
|
phantom.insert(PHANTOM_PHYSICS);
|
||||||
|
add(Arrange(phantom, false));
|
||||||
|
add(Arrange(phantom, true));
|
||||||
|
}
|
||||||
|
// The client makes no FX component (the render data) when the zone file sets renderDisabled, as on trigger
|
||||||
|
// volumes (ObjectLoader2::LoadRenderComponent)
|
||||||
|
if (has.contains(RENDER)) {
|
||||||
|
auto withoutRender = has;
|
||||||
|
withoutRender.erase(RENDER);
|
||||||
|
add(Arrange(withoutRender, false));
|
||||||
|
add(Arrange(withoutRender, true));
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,10 +833,12 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reads the components in order from `start`; true when they read the stream exactly
|
// Reads the components in order from `start`; true when they read the stream exactly
|
||||||
bool ReadComponents(RakNet::BitStream& stream, BitSize_t start, const std::vector<eReplicaComponentType>& components, bool initial, json& out) {
|
bool ReadComponents(RakNet::BitStream& stream, BitSize_t start, std::vector<eReplicaComponentType> components, bool trigger, bool initial, json& out) {
|
||||||
stream.SetReadOffset(start);
|
stream.SetReadOffset(start);
|
||||||
out = json::array();
|
out = json::array();
|
||||||
Reader r{ stream };
|
Reader r{ stream };
|
||||||
|
// Made for objects with a trigger after all the others (ObjectLoader2::DoObjectComponentLoad), so read last
|
||||||
|
if (trigger) components.push_back(TRIGGER);
|
||||||
for (const auto type : components) {
|
for (const auto type : components) {
|
||||||
json fields = json::object();
|
json fields = json::object();
|
||||||
const auto reader = Readers().find(type);
|
const auto reader = Readers().find(type);
|
||||||
@@ -705,6 +855,18 @@ namespace {
|
|||||||
return OnlyPadding(stream);
|
return OnlyPadding(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether a construction's config (entries as "key=type:value") names a property or says it is in an inventory
|
||||||
|
bool MutableModel(const json& config) {
|
||||||
|
const auto& entries = config.is_object() ? config.value("entries", json::array()) : config;
|
||||||
|
if (!entries.is_array()) return false;
|
||||||
|
for (const auto& entry : entries) {
|
||||||
|
if (!entry.is_string()) continue;
|
||||||
|
const auto text = entry.get<std::string>();
|
||||||
|
if (text.starts_with("propertyObjectID=") || text == "inInventory=7:1") return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
json ReadParentChild(Reader& r) {
|
json ReadParentChild(Reader& r) {
|
||||||
json out = json::object();
|
json out = json::object();
|
||||||
if (!r.Bit()) return out;
|
if (!r.Bit()) return out;
|
||||||
@@ -734,7 +896,7 @@ namespace ReplicaDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<eReplicaComponentType> ComponentsOf(LOT lot, const ComponentTable& table) {
|
std::vector<eReplicaComponentType> ComponentsOf(LOT lot, const ComponentTable& table) {
|
||||||
return Candidates(lot, table).front();
|
return Candidates(lot, table, false).front();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<json> Session::Decode(std::string_view bytes, uint64_t connection) {
|
std::optional<json> Session::Decode(std::string_view bytes, uint64_t connection) {
|
||||||
@@ -773,7 +935,7 @@ namespace ReplicaDecoder {
|
|||||||
out["parentChild"] = ReadParentChild(r);
|
out["parentChild"] = ReadParentChild(r);
|
||||||
json components;
|
json components;
|
||||||
const auto start = stream.GetReadOffset();
|
const auto start = stream.GetReadOffset();
|
||||||
if (!r.ok || !ReadComponents(stream, start, it->second.components, false, components)) {
|
if (!r.ok || !ReadComponents(stream, start, it->second.components, it->second.trigger, false, components)) {
|
||||||
out["(layout did not match)"] = true;
|
out["(layout did not match)"] = true;
|
||||||
out["(rest)"] = RestHex(stream);
|
out["(rest)"] = RestHex(stream);
|
||||||
}
|
}
|
||||||
@@ -792,7 +954,8 @@ namespace ReplicaDecoder {
|
|||||||
out["name"] = r.WideText<uint8_t>();
|
out["name"] = r.WideText<uint8_t>();
|
||||||
out["timeSinceCreatedMs"] = r.Get<uint32_t>();
|
out["timeSinceCreatedMs"] = r.Get<uint32_t>();
|
||||||
if (r.Bit()) out["config"] = ReadLdf(r);
|
if (r.Bit()) out["config"] = ReadLdf(r);
|
||||||
out["trigger"] = r.Bit();
|
const bool trigger = r.Bit();
|
||||||
|
out["trigger"] = trigger;
|
||||||
if (r.Bit()) out["spawner"] = r.Id();
|
if (r.Bit()) out["spawner"] = r.Id();
|
||||||
if (r.Bit()) out["spawnerNode"] = r.Get<uint32_t>();
|
if (r.Bit()) out["spawnerNode"] = r.Get<uint32_t>();
|
||||||
if (r.Bit()) out["scale"] = r.Get<float>();
|
if (r.Bit()) out["scale"] = r.Get<float>();
|
||||||
@@ -805,13 +968,13 @@ namespace ReplicaDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto start = stream.GetReadOffset();
|
const auto start = stream.GetReadOffset();
|
||||||
const auto candidates = Candidates(lot, m_Table);
|
const auto candidates = Candidates(lot, m_Table, out.contains("config") && MutableModel(out["config"]));
|
||||||
std::vector<eReplicaComponentType> chosen = candidates.front();
|
std::vector<eReplicaComponentType> chosen = candidates.front();
|
||||||
json components;
|
json components;
|
||||||
bool matched = false;
|
bool matched = false;
|
||||||
for (const auto& candidate : candidates) {
|
for (const auto& candidate : candidates) {
|
||||||
json attempt;
|
json attempt;
|
||||||
if (ReadComponents(stream, start, candidate, true, attempt)) {
|
if (ReadComponents(stream, start, candidate, trigger, true, attempt)) {
|
||||||
chosen = candidate;
|
chosen = candidate;
|
||||||
components = std::move(attempt);
|
components = std::move(attempt);
|
||||||
matched = true;
|
matched = true;
|
||||||
@@ -820,13 +983,13 @@ namespace ReplicaDecoder {
|
|||||||
}
|
}
|
||||||
if (!matched) {
|
if (!matched) {
|
||||||
// Shown as the registry says, as far as it reads, with the rest as bytes
|
// Shown as the registry says, as far as it reads, with the rest as bytes
|
||||||
ReadComponents(stream, start, chosen, true, components);
|
ReadComponents(stream, start, chosen, trigger, true, components);
|
||||||
out["(layout did not match)"] = true;
|
out["(layout did not match)"] = true;
|
||||||
out["(rest)"] = RestHex(stream);
|
out["(rest)"] = RestHex(stream);
|
||||||
}
|
}
|
||||||
if (!m_Table.contains(lot) && lot != 14) out["(LOT not in ComponentsRegistry)"] = true;
|
if (!m_Table.contains(lot) && lot != 14) out["(LOT not in ComponentsRegistry)"] = true;
|
||||||
out["components"] = components;
|
out["components"] = components;
|
||||||
m_Objects[{ connection, network }] = Object{ objectId, lot, chosen };
|
m_Objects[{ connection, network }] = Object{ objectId, lot, chosen, trigger };
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ namespace ReplicaDecoder {
|
|||||||
LWOOBJID objectId{};
|
LWOOBJID objectId{};
|
||||||
LOT lot{};
|
LOT lot{};
|
||||||
std::vector<eReplicaComponentType> components;
|
std::vector<eReplicaComponentType> components;
|
||||||
|
bool trigger{}; // its trigger component is read after the others
|
||||||
};
|
};
|
||||||
|
|
||||||
const ComponentTable& m_Table;
|
const ComponentTable& m_Table;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "PlayerForcedMovementComponent.h"
|
#include "PlayerForcedMovementComponent.h"
|
||||||
#include "PossessorComponent.h"
|
#include "PossessorComponent.h"
|
||||||
#include "ReplicaDecoder.h"
|
#include "ReplicaDecoder.h"
|
||||||
|
#include "ZCompression.h"
|
||||||
#include "SimplePhysicsComponent.h"
|
#include "SimplePhysicsComponent.h"
|
||||||
#include "SkillComponent.h"
|
#include "SkillComponent.h"
|
||||||
#include "User.h"
|
#include "User.h"
|
||||||
@@ -146,8 +147,8 @@ TEST_F(ReplicaDecoderTest, ModelWithTheDestroyableTheRegistryDoesNotList) {
|
|||||||
info.lot = 6000;
|
info.lot = 6000;
|
||||||
Entity model(288300744895900200, info);
|
Entity model(288300744895900200, info);
|
||||||
model.AddComponent<SimplePhysicsComponent>(-1);
|
model.AddComponent<SimplePhysicsComponent>(-1);
|
||||||
|
// The model component writes the item's user-generated-content block itself, where the client reads the item
|
||||||
model.AddComponent<ModelComponent>(-1)->LoadBehaviors();
|
model.AddComponent<ModelComponent>(-1)->LoadBehaviors();
|
||||||
model.AddComponent<ItemComponent>(-1);
|
|
||||||
auto* const destroyable = model.AddComponent<DestroyableComponent>(-1);
|
auto* const destroyable = model.AddComponent<DestroyableComponent>(-1);
|
||||||
destroyable->SetIsSmashable(true);
|
destroyable->SetIsSmashable(true);
|
||||||
|
|
||||||
@@ -190,3 +191,179 @@ TEST_F(ReplicaDecoderTest, LiveConstructionHeader) {
|
|||||||
EXPECT_EQ((*constructed)["lot"], 13006);
|
EXPECT_EQ((*constructed)["lot"], 13006);
|
||||||
EXPECT_EQ((*constructed)["timeSinceCreatedMs"], 3814335);
|
EXPECT_EQ((*constructed)["timeSinceCreatedMs"], 3814335);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::string FromHex(std::string_view hex) {
|
||||||
|
std::string bytes;
|
||||||
|
for (size_t i = 0; i + 1 < hex.size(); i += 2) bytes += static_cast<char>(std::stoi(std::string(hex.substr(i, 2)), nullptr, 16));
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json DecodeLive(std::string_view hex, LOT lot, std::vector<eReplicaComponentType> registry) {
|
||||||
|
const ReplicaDecoder::ComponentTable table{ { lot, std::move(registry) } };
|
||||||
|
ReplicaDecoder::Session session(table);
|
||||||
|
const auto decoded = session.Decode(FromHex(hex), 1);
|
||||||
|
return decoded ? *decoded : nlohmann::json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live constructions (2014 captures): each reads to its last whole byte
|
||||||
|
|
||||||
|
// A trigger object (the header's trigger bit set) has a trigger component read after all the others: a 1 bit and the
|
||||||
|
// trigger ID, -1 in every live capture. This trigger volume has no render data: its zone file sets renderDisabled.
|
||||||
|
TEST_F(ReplicaDecoderTest, LiveTriggerIsReadLast) {
|
||||||
|
const auto constructed = DecodeLive("24818c061f8000002000020a0b000000734cf8803de3f04008040000080000000773fe3d00928d12ec38dcef343c926e0c200000000a010e6bd000000002c617e3fbfffffffe", 5652, { RENDER, PHANTOM_PHYSICS });
|
||||||
|
EXPECT_FALSE(constructed.contains("(layout did not match)")) << constructed.dump();
|
||||||
|
EXPECT_EQ(constructed["trigger"], true);
|
||||||
|
EXPECT_EQ(Names(constructed), "PHANTOM_PHYSICS TRIGGER ");
|
||||||
|
EXPECT_EQ((*FieldsOf(constructed, "TRIGGER"))["triggerID"], -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A phantom physics effect (a gravity scale, 4) with its amount
|
||||||
|
TEST_F(ReplicaDecoderTest, LivePhantomPhysicsEffect) {
|
||||||
|
const auto constructed = DecodeLive("24da8d409f0000002000023f09000000445b780031126020040420000800000004675ecd009b85b16c4cfa9514363a7b4c10000000086db54bf000000005b390e3fc10000003373130f4ffffffff80", 4734, { RENDER, PHANTOM_PHYSICS });
|
||||||
|
EXPECT_FALSE(constructed.contains("(layout did not match)")) << constructed.dump();
|
||||||
|
const auto* physics = FieldsOf(constructed, "PHANTOM_PHYSICS");
|
||||||
|
ASSERT_TRUE(physics);
|
||||||
|
EXPECT_EQ((*physics)["effectType"], 4);
|
||||||
|
EXPECT_FLOAT_EQ((*physics)["directionalMultiplier"].get<float>(), 0.05f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A phantom physics effect with its distance range (min and max)
|
||||||
|
TEST_F(ReplicaDecoderTest, LivePhantomPhysicsDistance) {
|
||||||
|
const auto constructed = DecodeLive("24ac038f86800000200002710680000040d88400398e03a00004000008000000009891de442d7ad85434aaaa9c30000000000000000000000000000803fc000000000001210a0001007e0000908522b93642588a88c107b330c1ffffffff80", 3554, { RENDER, PHANTOM_PHYSICS });
|
||||||
|
EXPECT_FALSE(constructed.contains("(layout did not match)")) << constructed.dump();
|
||||||
|
const auto* physics = FieldsOf(constructed, "PHANTOM_PHYSICS");
|
||||||
|
ASSERT_TRUE(physics);
|
||||||
|
EXPECT_TRUE(physics->contains("minDistance"));
|
||||||
|
EXPECT_TRUE(physics->contains("maxDistance"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A moving platform: its mover subcomponent, then the 0 bit that ends the subcomponent list
|
||||||
|
TEST_F(ReplicaDecoderTest, LiveMovingPlatform) {
|
||||||
|
const auto constructed = DecodeLive("248d800e0000000020000216848000006f3786801da2c3b0000400000800000005d45238fc80000000080000000000000000000000000000000000000000000000041000000346f43d874d0ca28689b34f82177a657ee056e67a8f0a6c7f7f99a679a0800000109000000ffffffff0000000000000000000000000000000000000000000000000000000000000000000000000", 2349, { RENDER, SIMPLE_PHYSICS, MOVING_PLATFORM, PLATFORM_BOUNDARY });
|
||||||
|
EXPECT_FALSE(constructed.contains("(layout did not match)")) << constructed.dump();
|
||||||
|
const auto* platform = FieldsOf(constructed, "MOVING_PLATFORM");
|
||||||
|
ASSERT_TRUE(platform);
|
||||||
|
ASSERT_EQ((*platform)["subcomponents"].size(), 1u);
|
||||||
|
EXPECT_EQ((*platform)["subcomponents"][0]["type"], 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An enemy mid-attack: the skill it is casting with its running behaviors
|
||||||
|
TEST_F(ReplicaDecoderTest, LiveSkillsInProgress) {
|
||||||
|
const auto constructed = DecodeLive("24dff38ba48300002000021612000000200a80001c9f33a0000000000808000000800000000414000002a3720b896d24b48794199a820000000000000000000000000001007e4000000000000000000000000000000000000000000000000000000000000000000000002020000000001007e00000000000000000000000000000000000000000000200fc0000000000000000040000002c000000480800000008000001f81800000000000000000000080000001000000471e0000465c8000090000000ba48300002000020ba48300002000020000000000000000000000000000000000000000000000000", 9260, { RENDER, SIMPLE_PHYSICS, SCRIPT, DESTROYABLE, SKILL });
|
||||||
|
EXPECT_FALSE(constructed.contains("(layout did not match)")) << constructed.dump();
|
||||||
|
const auto* skill = FieldsOf(constructed, "SKILL");
|
||||||
|
ASSERT_TRUE(skill);
|
||||||
|
ASSERT_FALSE((*skill)["skillsInProgress"].empty());
|
||||||
|
EXPECT_FALSE((*skill)["skillsInProgress"][0]["behaviors"].empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// An object the zone file sets markedAsPhantom on: phantom physics where its LOT lists simple physics
|
||||||
|
TEST_F(ReplicaDecoderTest, LiveMarkedAsPhantom) {
|
||||||
|
const auto constructed = DecodeLive("24a302e403000000200002390e0000002cdd22009a506004040420000800000005e050b8f89da600ec2e412b3439e8626c300000000defb533f00000000e2850f3f8800000000000000000000000000000000000000000000000000000000000000000000000404000000000200fc00000000000000000000000000000000000000000000401f8000000000000000008000001b80000008000000000", 7282, { RENDER, SIMPLE_PHYSICS, DESTROYABLE, SKILL });
|
||||||
|
EXPECT_FALSE(constructed.contains("(layout did not match)")) << constructed.dump();
|
||||||
|
EXPECT_EQ(Names(constructed), "PHANTOM_PHYSICS BUFF DESTROYABLE SKILL RENDER ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compressed LDF (as live sent item and script settings): u32 size, a 1 byte, u32 uncompressed and compressed sizes,
|
||||||
|
// then zlib data holding the entries, shown inflated
|
||||||
|
TEST_F(ReplicaDecoderTest, CompressedLdfIsInflated) {
|
||||||
|
RakNet::BitStream entries;
|
||||||
|
entries.Write<int32_t>(1);
|
||||||
|
const std::u16string key = u"name";
|
||||||
|
entries.Write<uint8_t>(key.size() * 2);
|
||||||
|
for (const auto c : key) entries.Write<uint16_t>(c);
|
||||||
|
entries.Write<uint8_t>(1); // i32
|
||||||
|
entries.Write<int32_t>(42);
|
||||||
|
std::vector<uint8_t> compressed(ZCompression::GetMaxCompressedLength(entries.GetNumberOfBytesUsed()));
|
||||||
|
const auto compressedSize = ZCompression::Compress(entries.GetData(), entries.GetNumberOfBytesUsed(), compressed.data(), compressed.size());
|
||||||
|
ASSERT_GT(compressedSize, 0);
|
||||||
|
|
||||||
|
RakNet::BitStream packet;
|
||||||
|
packet.Write<uint8_t>(ID_REPLICA_MANAGER_CONSTRUCTION);
|
||||||
|
packet.Write1();
|
||||||
|
packet.Write<uint16_t>(5);
|
||||||
|
packet.Write<int64_t>(288300744895900300);
|
||||||
|
packet.Write<int32_t>(7000);
|
||||||
|
packet.Write<uint8_t>(0); // name
|
||||||
|
packet.Write<uint32_t>(0);
|
||||||
|
packet.Write1(); // config
|
||||||
|
packet.Write<uint32_t>(1 + 4 + 4 + compressedSize);
|
||||||
|
packet.Write<uint8_t>(1);
|
||||||
|
packet.Write<uint32_t>(entries.GetNumberOfBytesUsed());
|
||||||
|
packet.Write<uint32_t>(compressedSize);
|
||||||
|
for (int32_t i = 0; i < compressedSize; i++) packet.Write<uint8_t>(compressed[i]);
|
||||||
|
for (int i = 0; i < 7; i++) packet.Write0(); // trigger, spawner, spawner node, scale, world state, GM level, parent/child
|
||||||
|
|
||||||
|
const ReplicaDecoder::ComponentTable table{ { 7000, {} } };
|
||||||
|
ReplicaDecoder::Session session(table);
|
||||||
|
const auto constructed = session.Decode(Bytes(packet), 1);
|
||||||
|
ASSERT_TRUE(constructed);
|
||||||
|
EXPECT_FALSE(constructed->contains("(layout did not match)")) << constructed->dump();
|
||||||
|
ASSERT_TRUE(constructed->contains("config")) << constructed->dump();
|
||||||
|
EXPECT_EQ((*constructed)["config"].value("entries", json()), json::array({ "name=1:42" })) << constructed->dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow text that isn't UTF-8 is shown byte by byte, so the viewer's JSON always writes
|
||||||
|
TEST_F(ReplicaDecoderTest, TextThatIsNotUtf8StillWrites) {
|
||||||
|
RakNet::BitStream packet;
|
||||||
|
packet.Write<uint8_t>(ID_REPLICA_MANAGER_CONSTRUCTION);
|
||||||
|
packet.Write1();
|
||||||
|
packet.Write<uint16_t>(6);
|
||||||
|
packet.Write<int64_t>(288300744895900301);
|
||||||
|
packet.Write<int32_t>(7001);
|
||||||
|
packet.Write<uint8_t>(0);
|
||||||
|
packet.Write<uint32_t>(0);
|
||||||
|
packet.Write1(); // config: one narrow string entry with a byte that isn't UTF-8
|
||||||
|
RakNet::BitStream entries;
|
||||||
|
entries.Write<int32_t>(1);
|
||||||
|
entries.Write<uint8_t>(2);
|
||||||
|
entries.Write<uint16_t>(u'k');
|
||||||
|
entries.Write<uint8_t>(13);
|
||||||
|
entries.Write<uint32_t>(2);
|
||||||
|
entries.Write<uint8_t>(0x10);
|
||||||
|
entries.Write<uint8_t>(0xE9);
|
||||||
|
packet.Write<uint32_t>(1 + entries.GetNumberOfBytesUsed());
|
||||||
|
packet.Write<uint8_t>(0);
|
||||||
|
for (uint32_t i = 0; i < entries.GetNumberOfBytesUsed(); i++) packet.Write<uint8_t>(entries.GetData()[i]);
|
||||||
|
for (int i = 0; i < 7; i++) packet.Write0();
|
||||||
|
|
||||||
|
const ReplicaDecoder::ComponentTable table{ { 7001, {} } };
|
||||||
|
ReplicaDecoder::Session session(table);
|
||||||
|
const auto constructed = session.Decode(Bytes(packet), 1);
|
||||||
|
ASSERT_TRUE(constructed);
|
||||||
|
EXPECT_NO_THROW(constructed->dump());
|
||||||
|
EXPECT_EQ((*constructed)["config"][0], "k=13:\x10\xC3\xA9");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A model outside a property (a model reward in the world): the client makes the plain model component, which reads
|
||||||
|
// only the model's block, no behaviors; not in an inventory, it is smashable, with its destroyable last
|
||||||
|
TEST_F(ReplicaDecoderTest, LiveModelOutsideAProperty) {
|
||||||
|
const auto constructed = DecodeLive("248a802380000000200002458b800000375d87004f800000004f0000000d4000001e2718d898180432997219d2185219721842186a190a002c8620708318a94331501d8460cbd737a4f061606cd6e700430005b78231d2f2f3b0000400000800000000800000000414000002546af7863532ba87960375840000000000000000000000000001007f8b1700000000000000000000400000000546af7863532ba87960375840001007e000000000000000000000000000000000", 6027, { RENDER, SIMPLE_PHYSICS, ITEM, MODEL });
|
||||||
|
EXPECT_FALSE(constructed.contains("(layout did not match)")) << constructed.dump();
|
||||||
|
EXPECT_EQ(Names(constructed), "SIMPLE_PHYSICS ITEM MODEL RENDER DESTROYABLE ");
|
||||||
|
EXPECT_FALSE(FieldsOf(constructed, "MODEL")->contains("behaviors"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// DLU's models: a property model (propertyObjectID in its settings) writes its behaviors, which the client reads with
|
||||||
|
// the mutable model component; any other writes only the model's block
|
||||||
|
TEST_F(ReplicaDecoderTest, ModelBehaviorsOnlyOnPropertyModels) {
|
||||||
|
const ReplicaDecoder::ComponentTable table{ { 6001, { SIMPLE_PHYSICS, ITEM, MODEL } } };
|
||||||
|
for (const bool onProperty : { false, true }) {
|
||||||
|
info.lot = 6001;
|
||||||
|
info.settings.values.clear();
|
||||||
|
if (onProperty) info.settings.Insert<bool>(u"propertyObjectID", true);
|
||||||
|
Entity model(288300744895900210, info);
|
||||||
|
model.AddComponent<SimplePhysicsComponent>(-1);
|
||||||
|
model.AddComponent<ModelComponent>(-1)->LoadBehaviors();
|
||||||
|
ReplicaDecoder::Session session(table);
|
||||||
|
const auto constructed = session.Decode(Construction(model, 4), 1);
|
||||||
|
ASSERT_TRUE(constructed);
|
||||||
|
EXPECT_FALSE(constructed->contains("(layout did not match)")) << constructed->dump();
|
||||||
|
const auto* fields = FieldsOf(*constructed, onProperty ? "MUTABLE_MODEL_BEHAVIORS" : "MODEL");
|
||||||
|
ASSERT_TRUE(fields) << Names(*constructed);
|
||||||
|
EXPECT_EQ(fields->contains("behaviors"), onProperty);
|
||||||
|
}
|
||||||
|
info.settings.values.clear();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user