mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-28 07:27:14 +00:00
Merge exp/property-replay: property models, info and behaviours in capture replays
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
|
||||
#include "BinaryPathFinder.h"
|
||||
#include "CaptureBundle.h"
|
||||
#include "CaptureProperty.h"
|
||||
#include "CaptureTools.h"
|
||||
#include "ClientAssets.h"
|
||||
#include "Database.h"
|
||||
#include "DashboardRoutes.h"
|
||||
#include "Game.h"
|
||||
@@ -112,6 +114,20 @@ namespace {
|
||||
}
|
||||
|
||||
const nlohmann::json* ReplicaFields(size_t i) const { return i < replica.size() && replica[i] ? &*replica[i] : nullptr; }
|
||||
|
||||
// The properties the capture saw (CaptureProperty), read once after the replica pass, on a worker thread
|
||||
std::once_flag propertyOnce;
|
||||
std::atomic<bool> propertyBuilt{};
|
||||
nlohmann::json property;
|
||||
|
||||
const nlohmann::json& Property() {
|
||||
Decode();
|
||||
std::call_once(propertyOnce, [this] {
|
||||
property = CaptureProperty::Build(bundle.records, startUs, [this](size_t i) { return ReplicaFields(i); });
|
||||
propertyBuilt = true;
|
||||
});
|
||||
return property;
|
||||
}
|
||||
};
|
||||
std::shared_ptr<Cached> g_Cache;
|
||||
|
||||
@@ -686,6 +702,46 @@ namespace CaptureReplay {
|
||||
{"worlds", WorldsJson(*cached)}, {"idleSeconds", 3}, {"bucket", 1}, {"interval", 1}, {"truncated", false} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/inspector/sessions/:id/property", Perm(PERMISSION),
|
||||
"The properties the capture saw, per world (zone, instance, clone): {worlds: [{zone, zoneName, instance, clone, saved, info, counts, "
|
||||
"models: [{object, lot, spawner, ugcId, behaviors, spans: [{from, to, i, position, rotation}]}], events}]}. From the captured replica "
|
||||
"packets and property game messages; `saved` is the property saved for that zone and clone now (its id for links and UGC meshes)",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto session = FindSession(context, reply);
|
||||
if (!session) return;
|
||||
const auto cached = Load(*session, reply);
|
||||
if (!cached) return;
|
||||
// On the main thread: zone names and the saved property of each world the capture was on
|
||||
std::map<std::pair<uint32_t, uint32_t>, json> saved;
|
||||
std::map<uint32_t, std::string> zoneNames;
|
||||
for (const auto& record : cached->bundle.records) {
|
||||
const auto& h = record.header;
|
||||
if (h.source != static_cast<uint8_t>(eCaptureSource::WORLD) || !h.zoneId || saved.contains({ h.zoneId, h.cloneId })) continue;
|
||||
saved[{ h.zoneId, h.cloneId }] = nullptr;
|
||||
if (!zoneNames.contains(h.zoneId)) zoneNames[h.zoneId] = GameText::ZoneName(h.zoneId);
|
||||
const auto info = h.cloneId ? Database::Get()->GetPropertyInfo(h.zoneId, h.cloneId) : std::nullopt;
|
||||
if (!info) continue;
|
||||
saved[{ h.zoneId, h.cloneId }] = { {"id", std::to_string(info->id)}, {"name", info->name}, {"ownerId", std::to_string(info->ownerId)},
|
||||
{"ownerName", CharacterName(info->ownerId)} };
|
||||
}
|
||||
Workers::Reply(reply, context, cached->propertyBuilt, [cached, saved = std::move(saved), zoneNames = std::move(zoneNames)](HTTPReply& out) {
|
||||
json worlds = cached->Property()["worlds"];
|
||||
for (auto& world : worlds) {
|
||||
const auto zone = world["zone"].get<uint32_t>(), clone = world["clone"].get<uint32_t>();
|
||||
const auto name = zoneNames.find(zone);
|
||||
world["zoneName"] = name == zoneNames.end() ? std::string{} : name->second;
|
||||
const auto found = saved.find({ zone, clone });
|
||||
world["saved"] = found == saved.end() ? json(nullptr) : found->second;
|
||||
// Names from the locale, in the viewer's language (read once at startup, so workers may read them)
|
||||
for (auto& model : world["models"]) model["name"] = ClientAssets::ItemName(model["lot"].get<LOT>());
|
||||
for (auto& event : world["events"]) {
|
||||
if (event.contains("lot")) event["name"] = ClientAssets::ItemName(event["lot"].get<LOT>());
|
||||
}
|
||||
}
|
||||
JsonSuccess(out, { {"worlds", worlds} });
|
||||
});
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/inspector/sessions/:id/worlds", Perm(PERMISSION),
|
||||
"Which world server each captured character was on, in time order: one entry per move to another zone or instance (zone 0 is character select)",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
|
||||
@@ -383,6 +383,26 @@ namespace WorldView {
|
||||
JsonReply(reply, eHTTPStatusCode::OK, { {"instances", instances} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/world3d/property_models", Perm("players_history"),
|
||||
"The models placed on a property now, for replays of recorded positions there (which don't record models): "
|
||||
"{property: {id, name, ownerId, ownerName} or null, models: [{id, lot, ugcId, position, rotation}]}. Query: ?zone=&clone=",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto zone = static_cast<uint32_t>(std::max<int64_t>(0, QueryInt(context, "zone", 0)));
|
||||
const auto clone = static_cast<uint32_t>(std::max<int64_t>(0, QueryInt(context, "clone", 0)));
|
||||
const auto info = zone && clone ? Database::Get()->GetPropertyInfo(zone, clone) : std::nullopt;
|
||||
if (!info) return JsonReply(reply, eHTTPStatusCode::OK, { {"property", nullptr}, {"models", nlohmann::json::array()} });
|
||||
nlohmann::json models = nlohmann::json::array();
|
||||
for (const auto& model : Database::Get()->GetPropertyModels(info->id)) {
|
||||
models.push_back({ {"id", std::to_string(model.id)}, {"lot", model.lot}, {"name", ClientAssets::ItemName(model.lot)}, {"ugcId", std::to_string(model.ugcId)},
|
||||
{"position", {model.position.x, model.position.y, model.position.z}},
|
||||
{"rotation", {model.rotation.x, model.rotation.y, model.rotation.z, model.rotation.w}} });
|
||||
}
|
||||
JsonReply(reply, eHTTPStatusCode::OK, {
|
||||
{"property", { {"id", std::to_string(info->id)}, {"name", info->name}, {"ownerId", std::to_string(info->ownerId)}, {"ownerName", CharacterName(info->ownerId)} }},
|
||||
{"models", models}
|
||||
});
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/world3d/history", Perm("players_history"),
|
||||
"Where players went in a zone between two times, for replays: per character its samples as [seconds after from, x, y, z, ...]. "
|
||||
"Query: ?zone=&instance= (0: all)&from=&to= (Unix seconds, at most 7 days apart). Long ranges are thinned to about 3000 samples per player. Audited",
|
||||
|
||||
Reference in New Issue
Block a user