sots-engine/tests/game_data/dump_catalog.cpp

527 lines
22 KiB
C++

// dump_catalog <data-root> <out.json>
//
// Loads the whole catalog and writes one JSON document for
// tests/game_data/oracle/compare.py:
// * every weapon / section with its typed fields (`typed`) and the raw block
// rendered in the reference reader's dict shape (`raw`): keys folded,
// repeats -> lists, barewords typed;
// * the tech tree (nodes, edges, groups), the turret table, id registries;
// * the cross_check() report.
// Strings are cp1252 in the files; they are decoded to Unicode here because
// the oracle JSON was written from decoded text.
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include "game/data/catalog.h"
#include "mars/parse/value.h"
using namespace game::data;
namespace {
// ---- minimal JSON writer --------------------------------------------------
std::string json_string(std::string_view bytes) {
static const unsigned cp1252[32] = {
0x20AC, 0x0081, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160,
0x2039, 0x0152, 0x008D, 0x017D, 0x008F, 0x0090, 0x2018, 0x2019, 0x201C, 0x201D, 0x2022,
0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x009D, 0x017E, 0x0178};
std::string out = "\"";
for (unsigned char c : bytes) {
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (c < 0x20 || c > 0x7e) {
unsigned cp = c;
if (c >= 0x80 && c <= 0x9f) cp = cp1252[c - 0x80];
char buf[8];
std::snprintf(buf, sizeof buf, "\\u%04x", cp);
out += buf;
} else {
out += static_cast<char>(c);
}
}
}
out += '"';
return out;
}
std::string json_double(double v) {
char buf[64];
std::snprintf(buf, sizeof buf, "%.17g", v);
std::string s(buf);
if (s.find_first_of(".eEn") == std::string::npos) s += ".0"; // keep it a float for Python
return s;
}
struct J {
std::string s;
};
J jnull() { return {"null"}; }
J jbool(bool b) { return {b ? "true" : "false"}; }
J jint(long long v) { return {std::to_string(v)}; }
J jnum(double v) { return {json_double(v)}; }
J jstr(std::string_view v) { return {json_string(v)}; }
template <class T>
J jopt(const std::optional<T>& o);
template <>
J jopt(const std::optional<double>& o) { return o ? jnum(*o) : jnull(); }
template <>
J jopt(const std::optional<std::int64_t>& o) { return o ? jint(*o) : jnull(); }
template <>
J jopt(const std::optional<int>& o) { return o ? jint(*o) : jnull(); }
template <>
J jopt(const std::optional<bool>& o) { return o ? jbool(*o) : jnull(); }
template <>
J jopt(const std::optional<std::string>& o) { return o ? jstr(*o) : jnull(); }
struct JArr {
std::string s = "[";
bool first = true;
JArr& push(const J& v) {
if (!first) s += ',';
first = false;
s += v.s;
return *this;
}
J done() const { return {s + "]"}; }
};
struct JObj {
std::string s = "{";
bool first = true;
JObj& set(std::string_view key, const J& v) {
if (!first) s += ',';
first = false;
s += json_string(key);
s += ':';
s += v.s;
return *this;
}
J done() const { return {s + "}"}; }
};
J jstrs(const std::vector<std::string>& v) {
JArr a;
for (const auto& s : v) a.push(jstr(s));
return a.done();
}
// ---- raw block in the reference dict shape --------------------------------
J typed_bareword(const std::string& tok) {
using mars::parse::ScalarKind;
switch (mars::parse::classify(tok)) {
case ScalarKind::Int:
if (auto v = mars::parse::as_int(tok)) return jint(*v);
return jstr(tok);
case ScalarKind::Float: return jnum(*mars::parse::as_double(tok));
case ScalarKind::Bool: return jbool(*mars::parse::as_bool(tok));
case ScalarKind::Text: break;
}
return jstr(tok);
}
J raw_json(const Block& b) {
struct Slot { std::string key; std::vector<std::string> vals; };
std::vector<Slot> slots;
auto add = [&](const std::string& key, const std::string& val) {
for (Slot& s : slots)
if (s.key == key) {
s.vals.push_back(val);
return;
}
slots.push_back({key, {val}});
};
// merge attrs and sub-blocks in file order
std::size_t ai = 0, bi = 0;
while (ai < b.attrs.size() || bi < b.blocks.size()) {
bool take_attr = bi >= b.blocks.size() || (ai < b.attrs.size() && b.attrs[ai].ord < b.blocks[bi].ord);
if (take_attr) {
const Attr& a = b.attrs[ai++];
add(fold(a.key), a.quoted ? jstr(a.value).s : typed_bareword(a.value).s);
} else {
const Block& sub = b.blocks[bi++];
add(fold(sub.name), raw_json(sub).s);
}
}
for (const std::string& it : b.items) add("_items", jstr(it).s);
JObj o;
for (const Slot& s : slots) {
if (s.vals.size() == 1) {
o.set(s.key, {s.vals[0]});
} else {
JArr a;
for (const auto& v : s.vals) a.push({v});
o.set(s.key, a.done());
}
}
return o.done();
}
// ---- typed views ----------------------------------------------------------
J rangetable_json(const RangeTable& t) {
JObj o;
auto band = [&](const RangeTable::Band& b, const char* p) {
std::string pre(p);
o.set(pre + "_range", jopt(b.range));
o.set(pre + "_range_dev", jopt(b.deviation));
o.set(pre + "_range_dam", jopt(b.damage));
};
band(t.point_blank, "pb");
band(t.effective, "eff");
band(t.maximum, "max");
return o.done();
}
J planet_json(const PlanetDamage& p) {
return JObj().set("dam_pop", jopt(p.pop)).set("dam_infra", jopt(p.infra)).set("dam_terra", jopt(p.terra)).done();
}
J weapon_json(const WeaponDef& w) {
JObj t;
t.set("name", jstr(w.name))
.set("weaponclass", jstr(w.weapon_class))
.set("weaponfamily", jstr(w.weapon_family))
.set("weapondamagetype", jstr(w.weapon_damage_type))
.set("requires", jstrs(w.requires))
.set("compatible_section", jstrs(w.compatible_section))
.set("exclusive_species", jstr(w.exclusive_species))
.set("cost", jopt(w.cost))
.set("turretsize", jstr(w.turret_size))
.set("turretclass", jstr(w.turret_class))
.set("trackspeed_mod", jopt(w.track_speed_mod))
.set("burst_volleys", jopt(w.burst_volleys))
.set("recharge_time", jopt(w.recharge_time))
.set("volley_period", jopt(w.volley_period))
.set("volley_duration", jopt(w.volley_duration))
.set("buildup_delay", jopt(w.buildup_delay))
.set("solution_tolerance", jopt(w.solution_tolerance))
.set("range", jopt(w.range))
.set("range_planet", jopt(w.range_planet))
.set("muzzle_speed", jopt(w.muzzle_speed))
.set("hpbonus", jopt(w.hpbonus))
.set("dam_est", jopt(w.dam_est))
.set("hidden", jopt(w.hidden))
.set("pinpoint", jopt(w.pinpoint))
.set("blindfire", jopt(w.blindfire))
.set("secondary_pd", jopt(w.secondary_pd))
.set("model1", jstr(w.model1))
.set("model2", jstr(w.model2))
.set("model3", jstr(w.model3))
.set("muzzle_effect", jstr(w.muzzle_effect))
.set("muzzle_sound", jstr(w.muzzle_sound))
.set("icon_file", jstr(w.icon_file))
.set("icon_rect", jstr(w.icon_rect))
.set("fc_requires_los", jopt(w.fc.requires_los))
.set("fc_requires_inrange", jopt(w.fc.requires_inrange))
.set("fc_requires_enemycolony", jopt(w.fc.requires_enemycolony))
.set("fc_manual_target", jopt(w.fc.manual_target))
.set("fc_manual_toggle", jopt(w.fc.manual_toggle))
.set("fc_manual_launch", jopt(w.fc.manual_launch))
.set("fc_controllable", jopt(w.fc.controllable))
.set("fc_holdsfire", jopt(w.fc.holdsfire))
.set("fc_explicit_target", jopt(w.fc.explicit_target))
.set("fc_exclusive_launch", jopt(w.fc.exclusive_launch))
.set("fc_targets_expire", jopt(w.fc.targets_expire))
.set("rating_frate", jopt(w.ratings.fire_rate))
.set("rating_dam", jopt(w.ratings.damage))
.set("rating_acc", jopt(w.ratings.accuracy))
.set("rating_range", jopt(w.ratings.range))
.set("behavior_kind", jstr(w.behavior_kind))
.set("planet_damage", planet_json(w.planet_damage))
.set("rangetable", w.rangetable ? rangetable_json(*w.rangetable) : jnull());
if (w.bolt) {
JObj b;
b.set("rangetable", rangetable_json(w.bolt->rangetable))
.set("dam_pop", jopt(w.bolt->planet.pop))
.set("dam_infra", jopt(w.bolt->planet.infra))
.set("dam_terra", jopt(w.bolt->planet.terra))
.set("mass", jopt(w.bolt->mass))
.set("beam_origin", jopt(w.bolt->beam_origin))
.set("beam_length", jopt(w.bolt->beam_length))
.set("ricochet_mod", jopt(w.bolt->ricochet_mod))
.set("effect", jstr(w.bolt->effect))
.set("impact_effect", jstr(w.bolt->impact_effect))
.set("expire_effect", jstr(w.bolt->expire_effect));
t.set("bolt", b.done());
} else {
t.set("bolt", jnull());
}
JObj o;
o.set("stem", jstr(w.stem))
.set("file", jstr(w.file))
.set("scope", jstr(w.scope == WeaponScope::Player ? "player" : "NPC"))
.set("id", jopt(w.id))
.set("display_name", jopt(w.display_name))
.set("typed", t.done())
.set("raw", raw_json(w.raw));
return o.done();
}
J section_json(const ShipSectionDef& s) {
JObj t;
t.set("model", jstr(s.model))
.set("dam_model", jstr(s.dam_model))
.set("requires", jstrs(s.requires))
.set("section_type", jstr(s.section_type_text))
.set("section_type_enum", jstr(section_type_name(s.section_type)))
.set("section_class", jstr(s.section_class_text))
.set("section_class_enum", jstr(section_class_name(s.section_class)))
.set("design_class", jstr(s.design_class))
.set("entity_class", jstr(s.entity_class))
.set("health", jopt(s.health))
.set("mass", jopt(s.mass))
.set("cost", jopt(s.cost))
.set("cpoints", jopt(s.cpoints))
.set("crew", jopt(s.crew))
.set("command_cost", jopt(s.command_cost))
.set("maintenance_cost", jopt(s.maintenance_cost))
.set("command_quota", jopt(s.command_quota))
.set("socket_fore", jstr(s.socket_fore))
.set("socket_aft", jstr(s.socket_aft))
.set("dam_socket_fore", jstr(s.dam_socket_fore))
.set("dam_socket_aft", jstr(s.dam_socket_aft))
.set("ftlspeed", jopt(s.ftlspeed))
.set("nodespeed", jopt(s.nodespeed))
.set("range", jopt(s.range))
.set("scanrange", jopt(s.scanrange))
.set("tacticalsensorrange", jopt(s.tactical_sensor_range))
.set("engine_techera", jstr(s.engine_techera))
.set("exclude", jstrs(s.exclude))
.set("explicit_command_section", jstr(s.explicit_command_section))
.set("explicit_engine_section", jstr(s.explicit_engine_section))
.set("explicit_section", jopt(s.explicit_section))
.set("autonomous", jopt(s.autonomous))
.set("nodesign", jopt(s.nodesign));
{
JArr groups;
for (const OptionGroup& g : s.options)
groups.push(JObj().set("members", jstrs(g.members)).set("scalar", jbool(g.scalar)).done());
t.set("option", groups.done());
t.set("optiondef", s.optiondef ? jstrs(s.optiondef->members) : jnull());
}
{
JArr banks;
for (const BankDef& b : s.banks) {
JArr mounts;
for (const MountDef& m : b.mounts)
mounts.push(JObj()
.set("node", jstr(m.node))
.set("min_azimuth", jopt(m.min_azimuth))
.set("max_azimuth", jopt(m.max_azimuth))
.set("min_inclination", jopt(m.min_inclination))
.set("max_inclination", jopt(m.max_inclination))
.set("home_azimuth", jopt(m.home_azimuth))
.set("home_inclination", jopt(m.home_inclination))
.done());
banks.push(JObj()
.set("turretclass", jstr(b.turret_class))
.set("turretsize", jstr(b.turret_size))
.set("weapon", jstr(b.weapon))
.set("showturrets", jopt(b.show_turrets))
.set("invincible", jopt(b.invincible))
.set("repeated_turret_spec", jbool(b.repeated_turret_spec))
.set("mount", mounts.done())
.done());
}
t.set("bank", banks.done());
}
if (s.netforcelimits) {
const NetForceLimits& n = *s.netforcelimits;
t.set("netforcelimits", JObj()
.set("force_forward", jopt(n.force_forward))
.set("force_right", jopt(n.force_right))
.set("force_up", jopt(n.force_up))
.set("torque_yaw", jopt(n.torque_yaw))
.set("torque_pitch", jopt(n.torque_pitch))
.set("torque_roll", jopt(n.torque_roll))
.set("speed", jopt(n.speed))
.set("rotspeed", jopt(n.rotspeed))
.done());
} else {
t.set("netforcelimits", jnull());
}
{
JArr th;
for (const ThrusterDef& d : s.thrusters)
th.push(JObj().set("node", jstr(d.node)).set("effect", jstr(d.effect)).set("idle_effect", jstr(d.idle_effect)).done());
t.set("thruster", th.done());
}
JObj o;
o.set("race", jstr(s.race))
.set("species", jstr(species_name(s.species)))
.set("stem", jstr(s.stem))
.set("file", jstr(s.file))
.set("id", jopt(s.id))
.set("display_name", jopt(s.display_name))
.set("description", jopt(s.description))
.set("unlocked_by", jstrs(s.unlocked_by))
.set("typed", t.done())
.set("raw", raw_json(s.raw));
return o.done();
}
J tech_json(const TechTree& t) {
JArr nodes;
for (const TechNode& n : t.nodes) {
JArr allows;
for (std::size_t ei : n.allows) allows.push(jstr(t.edges[ei].to));
nodes.push(JObj()
.set("name", jstr(n.name))
.set("display_name", jopt(n.display_name))
.set("description", jopt(n.description))
.set("family", n.family.empty() ? jnull() : jstr(n.family))
.set("family_inferred", jstr(n.family_inferred))
.set("type", n.type.empty() ? jnull() : jstr(n.type))
.set("threat", jopt(n.threat))
.set("group", n.group.empty() ? jnull() : jstr(n.group))
.set("option_cost", jopt(n.option_cost))
.set("unlock_explicitly", jopt(n.unlock_explicitly))
.set("requires", jstrs(n.requires))
.set("benefits_inc", jstrs(n.benefits_inc))
.set("benefits_dec", jstrs(n.benefits_dec))
.set("sections", jstrs(n.sections))
.set("weapons", jstrs(n.weapon_files))
.set("allows", allows.done())
.done());
}
JArr edges;
for (const AllowsEdge& e : t.edges) {
JObj pct, all;
for (int i = 0; i < kSpeciesCount; ++i) {
auto s = static_cast<Species>(i);
all.set(species_name(s), jint(e.percent(s)));
if (e.pct_written[static_cast<std::size_t>(i)]) pct.set(species_name(s), jint(e.percent(s)));
}
edges.push(JObj()
.set("from", jstr(e.from))
.set("to", jstr(e.to))
.set("rp", jopt(e.rp))
.set("pct", pct.done())
.set("pct_effective", all.done())
.set("unparsed", jstrs(e.unparsed))
.done());
}
JObj groups;
for (const auto& kv : t.groups) groups.set(kv.first, jstrs(kv.second));
return JObj().set("nodes", nodes.done()).set("edges", edges.done()).set("groups", groups.done()).done();
}
J turrets_json(const TurretTable& t) {
JArr rows;
for (const TurretRow& r : t.rows())
rows.push(JObj()
.set("mount_size", jstr(r.mount_size))
.set("weapon_size", jstr(r.weapon_size))
.set("class", jstr(r.turret_class))
.set("health", jopt(r.health))
.set("track_speed", jopt(r.track_speed))
.set("azimuth_scale", jopt(r.azimuth_scale))
.set("inclination_scale", jopt(r.inclination_scale))
.set("model", jstr(r.model))
.done());
return rows.done();
}
J refs_json(const std::vector<CrossCheck::Ref>& v) {
JArr a;
for (const auto& r : v) a.push(JArr().push(jstr(r.from)).push(jstr(r.ref)).done());
return a.done();
}
J registry_json(const IdRegistry& r) {
JArr ids;
for (const IdEntry& e : r.entries()) ids.push(JArr().push(jint(e.id)).push(jstr(e.name)).done());
JArr del;
for (int d : r.deleted()) del.push(jint(d));
return JObj().set("ids", ids.done()).set("deleted", del.done()).done();
}
J cross_json(const CrossCheck& x) {
JArr gaps;
for (const auto& g : x.manifest_ids_without_file)
gaps.push(JObj().set("scope", jstr(g.scope)).set("id", jint(g.id)).set("name", jstr(g.name)).done());
return JObj()
.set("weapon_requires_dangling", refs_json(x.weapon_requires_dangling))
.set("weapon_requires_case_mismatch", refs_json(x.weapon_requires_case_mismatch))
.set("weapons_without_requires", jstrs(x.weapons_without_requires))
.set("section_requires_dangling", refs_json(x.section_requires_dangling))
.set("section_requires_case_mismatch", refs_json(x.section_requires_case_mismatch))
.set("section_option_dangling", refs_json(x.section_option_dangling))
.set("tech_ship_section_dangling", refs_json(x.tech_ship_section_dangling))
.set("tech_weapon_file_dangling", refs_json(x.tech_weapon_file_dangling))
.set("tech_requires_dangling", refs_json(x.tech_requires_dangling))
.set("tech_allows_dangling", refs_json(x.tech_allows_dangling))
.set("tech_allows_unparsed", refs_json(x.tech_allows_unparsed))
.set("bank_weapon_dangling", refs_json(x.bank_weapon_dangling))
.set("manifest_ids_without_file", gaps.done())
.set("files_without_manifest_id", refs_json(x.files_without_manifest_id))
.set("weapon_turret_pairs_without_row", refs_json(x.weapon_turret_pairs_without_row))
.set("bank_turret_pairs_without_row", refs_json(x.bank_turret_pairs_without_row))
.set("unresolved_weapon_names", refs_json(x.unresolved_weapon_names))
.set("missing_techname", jstrs(x.missing_techname))
.set("missing_techdesc", jstrs(x.missing_techdesc))
.set("missing_sectionname", jstrs(x.missing_sectionname))
.set("missing_sectiondesc", jstrs(x.missing_sectiondesc))
.set("tech_roots", jstrs(x.tech_roots))
.set("strings_available", jbool(x.strings_available))
.set("dangling_count", jint(static_cast<long long>(x.dangling_count())))
.done();
}
} // namespace
int main(int argc, char** argv) {
if (argc != 3) {
std::fprintf(stderr, "usage: dump_catalog <data-root> <out.json>\n");
return 2;
}
Catalog cat = load_catalog(argv[1]);
JArr weapons;
for (const WeaponDef& w : cat.weapons) weapons.push(weapon_json(w));
JArr sections;
for (const ShipSectionDef& s : cat.sections) sections.push(section_json(s));
JObj manifests;
manifests.set("Weapons", registry_json(cat.weapon_ids));
for (const auto& kv : cat.section_ids) manifests.set(kv.first, registry_json(kv.second));
JArr problems;
for (const Problem& p : cat.problems)
problems.push(JObj()
.set("kind", jstr(problem_kind_name(p.kind)))
.set("file", jstr(p.file))
.set("line", jint(p.line))
.set("key", jstr(p.key))
.set("message", jstr(p.message))
.done());
JObj doc;
doc.set("weapons", weapons.done())
.set("sections", sections.done())
.set("tech", tech_json(cat.tech))
.set("turrets", turrets_json(cat.turrets))
.set("manifests", manifests.done())
.set("strings_loaded", jbool(cat.strings_loaded))
.set("string_count", jint(static_cast<long long>(cat.strings.size())))
.set("races", jstrs(cat.races))
.set("problems", problems.done())
.set("cross_check", cross_json(cat.cross_check()));
std::ofstream out(argv[2], std::ios::binary);
if (!out) {
std::fprintf(stderr, "cannot write %s\n", argv[2]);
return 1;
}
out << doc.done().s << '\n';
std::printf("dumped %zu weapons, %zu sections, %zu techs, %zu edges, %zu problems -> %s\n", cat.weapons.size(),
cat.sections.size(), cat.tech.nodes.size(), cat.tech.edges.size(), cat.problems.size(), argv[2]);
return 0;
}