278 lines
11 KiB
C++
278 lines
11 KiB
C++
// Offline replay of a golden Mars::GlobalConsts::LoadFile trace through game::config.
|
|
//
|
|
// For every LoadFile record in $SOTS_M1_TRACE the test rebuilds the registered slots from
|
|
// the record's `side.<KEY>.before` values (the region model: one struct per key, typed by
|
|
// its "t" field), reads the file named in args from $SOTS_DATA_DIR (case-insensitively --
|
|
// the game's VFS is), runs apply() with the scale constant carried in args, and emits the
|
|
// results as replay input (TRACE_FORMAT.md section 8). tracecmp.py --replay then diffs them
|
|
// against the original's `after` values. SKIPs cleanly when either variable is unset.
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "game/config/config_loader.h"
|
|
#include "harness.h"
|
|
#include "mini_json.h"
|
|
#include "shim/trace/emitter.h"
|
|
|
|
namespace fs = std::filesystem;
|
|
using namespace game::config;
|
|
using shim::trace::Tv;
|
|
|
|
static const char* kHook = "Mars::GlobalConsts::LoadFile";
|
|
|
|
static std::string getenv_str(const char* k) {
|
|
const char* v = std::getenv(k);
|
|
return v ? v : "";
|
|
}
|
|
|
|
// Resolve `rel` (forward slashes, any case) under `root`, one component at a time.
|
|
static bool resolve_ci(const fs::path& root, const std::string& rel, fs::path& out) {
|
|
fs::path cur = root;
|
|
std::stringstream ss(rel);
|
|
std::string part;
|
|
while (std::getline(ss, part, '/')) {
|
|
if (part.empty()) continue;
|
|
bool found = false;
|
|
std::error_code ec;
|
|
for (const fs::directory_entry& e : fs::directory_iterator(cur, ec)) {
|
|
const std::string name = e.path().filename().string();
|
|
if (name.size() == part.size() && std::equal(name.begin(), name.end(), part.begin(), [](char a, char b) {
|
|
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
|
|
})) {
|
|
cur = e.path();
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) return false;
|
|
}
|
|
out = cur;
|
|
return true;
|
|
}
|
|
|
|
static bool read_file(const fs::path& p, std::string& out) {
|
|
std::ifstream f(p, std::ios::binary);
|
|
if (!f) return false;
|
|
std::ostringstream ss;
|
|
ss << f.rdbuf();
|
|
out = ss.str();
|
|
return true;
|
|
}
|
|
|
|
// ---- the region model, host side (mirrors src/shim/hooks/global_consts.cpp) ---------------
|
|
|
|
static Kind kind_of(const std::string& t) {
|
|
if (t == "int") return Kind::Int;
|
|
if (t == "float") return Kind::Float;
|
|
if (t == "fscaled") return Kind::FloatScaled;
|
|
if (t == "colour") return Kind::Colour;
|
|
if (t == "vec3") return Kind::Vec3;
|
|
if (t == "rect") return Kind::Rect;
|
|
if (t == "string") return Kind::String;
|
|
return Kind::Unknown;
|
|
}
|
|
|
|
static float fld(const minijson::Value* v, const char* name);
|
|
static std::int32_t ild(const minijson::Value* v, const char* name);
|
|
|
|
static float fval(const minijson::Value* v) {
|
|
if (!v) return 0;
|
|
if (v->kind == minijson::Value::String) {
|
|
if (v->str == "nan") return std::nanf("");
|
|
if (v->str == "inf") return HUGE_VALF;
|
|
if (v->str == "-inf") return -HUGE_VALF;
|
|
}
|
|
return static_cast<float>(v->number());
|
|
}
|
|
|
|
// A struct tv {t:{..,v:"int"}, v:{..}} -> slot kind + storage bytes.
|
|
static bool slot_from_tv(const std::string& key, const minijson::Value& tv, Slot& slot, std::vector<std::uint8_t>& store) {
|
|
const minijson::Value* t = tv.get("t");
|
|
if (!t || t->string() != "struct") return false;
|
|
const minijson::Value* v = tv.get("v");
|
|
if (!v || !v->isObject()) return false;
|
|
const minijson::Value* kind = v->get("t");
|
|
if (!kind) return false;
|
|
slot.key = key;
|
|
slot.kind = kind_of(kind->get("v") ? kind->get("v")->string() : "");
|
|
store.assign(width(slot.kind), 0);
|
|
if (slot.kind == Kind::String) {
|
|
// host side: the slot's storage is a std::string owned by the test (see main)
|
|
return true;
|
|
}
|
|
if (slot.kind == Kind::Vec3) {
|
|
float c[3] = {fld(v, "x"), fld(v, "y"), fld(v, "z")};
|
|
std::memcpy(store.data(), c, sizeof c);
|
|
} else if (slot.kind == Kind::Rect) {
|
|
std::int32_t c[4] = {ild(v, "x"), ild(v, "y"), ild(v, "w"), ild(v, "h")};
|
|
std::memcpy(store.data(), c, sizeof c);
|
|
} else if (slot.kind == Kind::Colour) {
|
|
float c[4] = {fval(v->get("r") ? v->get("r")->get("v") : nullptr), fval(v->get("g") ? v->get("g")->get("v") : nullptr),
|
|
fval(v->get("b") ? v->get("b")->get("v") : nullptr), fval(v->get("a") ? v->get("a")->get("v") : nullptr)};
|
|
std::memcpy(store.data(), c, sizeof c);
|
|
} else if (slot.kind == Kind::Int) {
|
|
const std::int32_t i = static_cast<std::int32_t>(v->get("v") && v->get("v")->get("v") ? v->get("v")->get("v")->number() : 0);
|
|
std::memcpy(store.data(), &i, sizeof i);
|
|
} else {
|
|
const float f = fval(v->get("v") ? v->get("v")->get("v") : nullptr);
|
|
std::memcpy(store.data(), &f, sizeof f);
|
|
}
|
|
slot.storage = store.data();
|
|
return true;
|
|
}
|
|
|
|
static float fld(const minijson::Value* v, const char* name) {
|
|
const minijson::Value* f = v ? v->get(name) : nullptr;
|
|
return fval(f ? f->get("v") : nullptr);
|
|
}
|
|
static std::int32_t ild(const minijson::Value* v, const char* name) {
|
|
const minijson::Value* f = v ? v->get(name) : nullptr;
|
|
return static_cast<std::int32_t>(f && f->get("v") ? f->get("v")->number() : 0);
|
|
}
|
|
|
|
static Tv describe(const Slot& s) {
|
|
Tv out = shim::trace::tv::struct_();
|
|
out.add("t", shim::trace::tv::str(kind_name(s.kind)));
|
|
if (s.kind == Kind::String) {
|
|
const std::string* str = static_cast<const std::string*>(s.storage);
|
|
out.add("v", shim::trace::tv::str(str->data(), str->size()));
|
|
} else if (s.kind == Kind::Vec3) {
|
|
float c[3];
|
|
std::memcpy(c, s.storage, sizeof c);
|
|
out.add("x", shim::trace::tv::f32(c[0]));
|
|
out.add("y", shim::trace::tv::f32(c[1]));
|
|
out.add("z", shim::trace::tv::f32(c[2]));
|
|
} else if (s.kind == Kind::Rect) {
|
|
std::int32_t c[4];
|
|
std::memcpy(c, s.storage, sizeof c);
|
|
out.add("x", shim::trace::tv::i32(c[0]));
|
|
out.add("y", shim::trace::tv::i32(c[1]));
|
|
out.add("w", shim::trace::tv::i32(c[2]));
|
|
out.add("h", shim::trace::tv::i32(c[3]));
|
|
} else if (s.kind == Kind::Colour) {
|
|
float c[4];
|
|
std::memcpy(c, s.storage, sizeof c);
|
|
out.add("r", shim::trace::tv::f32(c[0]));
|
|
out.add("g", shim::trace::tv::f32(c[1]));
|
|
out.add("b", shim::trace::tv::f32(c[2]));
|
|
out.add("a", shim::trace::tv::f32(c[3]));
|
|
} else if (s.kind == Kind::Int) {
|
|
std::int32_t i;
|
|
std::memcpy(&i, s.storage, sizeof i);
|
|
out.add("v", shim::trace::tv::i32(i));
|
|
} else {
|
|
float f;
|
|
std::memcpy(&f, s.storage, sizeof f);
|
|
out.add("v", shim::trace::tv::f32(f));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
const std::string trace = getenv_str("SOTS_M1_TRACE");
|
|
const std::string data = getenv_str("SOTS_DATA_DIR");
|
|
if (trace.empty() || data.empty()) {
|
|
std::printf("SKIP: set SOTS_M1_TRACE (golden LoadFile log) and SOTS_DATA_DIR (game data root)\n");
|
|
return 0;
|
|
}
|
|
const std::string out_path = argc > 1 ? argv[1] : "m1-replay-impl.jsonl";
|
|
|
|
std::ifstream in(trace);
|
|
if (!in) {
|
|
std::printf("FAIL: cannot open %s\n", trace.c_str());
|
|
return 1;
|
|
}
|
|
std::FILE* out = std::fopen(out_path.c_str(), "wb");
|
|
if (!out) {
|
|
std::printf("FAIL: cannot write %s\n", out_path.c_str());
|
|
return 1;
|
|
}
|
|
|
|
unsigned calls = 0, files_missing = 0;
|
|
std::string line;
|
|
while (std::getline(in, line)) {
|
|
minijson::Value rec;
|
|
if (!minijson::Parser(line).parse(rec)) continue;
|
|
const minijson::Value* hook = rec.get("hook");
|
|
if (!hook || hook->string() != kHook) continue;
|
|
const minijson::Value* args = rec.get("args");
|
|
const minijson::Value* side = rec.get("side");
|
|
if (!args || !args->isArray() || args->arr.empty() || !side || !side->isObject()) continue;
|
|
|
|
std::string file;
|
|
double scale = 0;
|
|
for (const minijson::Value& a : args->arr) {
|
|
const minijson::Value* n = a.get("n");
|
|
const std::string name = n ? n->string() : "";
|
|
if (name == "file" && a.get("v")) file = a.get("v")->string();
|
|
if (name == "scale" && a.get("v")) scale = a.get("v")->number();
|
|
}
|
|
|
|
// Slots from the `before` snapshots, in the record's (map) order.
|
|
std::vector<Slot> slots;
|
|
std::vector<std::vector<std::uint8_t>> stores;
|
|
std::vector<std::string> strings; // String slots' storage on the host
|
|
slots.reserve(side->obj.size());
|
|
stores.reserve(side->obj.size());
|
|
strings.reserve(side->obj.size());
|
|
for (const auto& kv : side->obj) {
|
|
const minijson::Value* before = kv.second.get("before");
|
|
Slot s;
|
|
stores.emplace_back();
|
|
if (!before || !slot_from_tv(kv.first, *before, s, stores.back())) {
|
|
std::printf("FAIL: call %g region %s is not a typed struct\n", rec.get("call_id")->number(), kv.first.c_str());
|
|
return 1;
|
|
}
|
|
if (s.kind == Kind::String) {
|
|
const minijson::Value* bv = before->get("v");
|
|
const minijson::Value* txt = bv ? bv->get("v") : nullptr;
|
|
strings.push_back(txt && txt->get("v") ? txt->get("v")->string() : "");
|
|
s.storage = &strings.back();
|
|
}
|
|
slots.push_back(std::move(s));
|
|
}
|
|
const ExternWriter ext = [](Slot& sl, std::string_view v) { *static_cast<std::string*>(sl.storage) = std::string(v); };
|
|
|
|
shim::trace::Record r;
|
|
r.hook = kHook;
|
|
r.call_id = static_cast<std::uint32_t>(rec.get("call_id")->number());
|
|
r.mode = shim::trace::Mode::Trace;
|
|
|
|
fs::path path;
|
|
std::string text;
|
|
if (resolve_ci(data, file, path) && read_file(path, text)) {
|
|
apply(file, text, slots, scale, nullptr, ext);
|
|
} else {
|
|
++files_missing;
|
|
std::printf(" note: %s not under %s (leaving defaults, as the original does)\n", file.c_str(), data.c_str());
|
|
}
|
|
for (const Slot& s : slots) {
|
|
shim::trace::SideEntry e;
|
|
e.name = s.key;
|
|
e.before_kind = shim::trace::SideEntry::Absent;
|
|
e.after = describe(s);
|
|
r.side.push_back(std::move(e));
|
|
}
|
|
shim::trace::Buf b;
|
|
shim::trace::emit_record(b, r);
|
|
std::fwrite(b.data(), 1, b.size(), out);
|
|
++calls;
|
|
}
|
|
std::fclose(out);
|
|
std::printf("replayed %u LoadFile calls (%u files missing) -> %s\n", calls, files_missing, out_path.c_str());
|
|
if (calls == 0) {
|
|
std::printf("FAIL: no %s records in %s\n", kHook, trace.c_str());
|
|
return 1;
|
|
}
|
|
const int rc = tracetest::run_tracecmp(trace, "--hook " + std::string(kHook) + " --replay " + out_path);
|
|
if (rc == tracetest::kSkipped) return 0;
|
|
std::printf("tracecmp --replay exit %d\n", rc);
|
|
return rc == 0 ? 0 : 1;
|
|
}
|