// 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..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 #include #include #include #include #include #include #include #include #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(a)) == std::tolower(static_cast(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(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& 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(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(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(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 slots; std::vector> stores; std::vector 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(sl.storage) = std::string(v); }; shim::trace::Record r; r.hook = kHook; r.call_id = static_cast(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; }