#include "game/data/techtree.h" #include "game/data/fields.h" #include "mars/parse/value.h" #include "mars/text/value.h" namespace game::data { using detail::Fields; namespace { std::string upper(std::string_view s) { std::string out(s); for (char& c : out) if (c >= 'a' && c <= 'z') c = static_cast(c - 32); return out; } std::vector split_ws(std::string_view s) { std::vector out; std::size_t i = 0; while (i < s.size()) { while (i < s.size() && mars::text::is_space(s[i])) ++i; std::size_t start = i; while (i < s.size() && !mars::text::is_space(s[i])) ++i; if (i > start) out.push_back(s.substr(start, i - start)); } return out; } } // namespace AllowsEdge parse_allows(std::string_view text, std::string from, int line, std::vector* problems, std::string_view file) { AllowsEdge e; e.from = std::move(from); e.text = std::string(text); e.line = line; auto toks = split_ws(text); if (toks.empty()) { if (problems) problems->push_back({Problem::Kind::Unparsed, std::string(file), line, "allows", "empty allows string"}); return e; } e.to = std::string(toks[0]); for (std::size_t i = 1; i < toks.size(); ++i) { std::string_view t = toks[i]; auto colon = t.find(':'); if (colon == std::string_view::npos) { e.unparsed.emplace_back(t); continue; } std::string_view key = t.substr(0, colon); std::string_view val = t.substr(colon + 1); auto n = mars::parse::as_int(val); if (!n) { e.unparsed.emplace_back(t); continue; } if (iequals(key, "RP")) { e.rp = *n; continue; } Species s = parse_species(key); if (s == Species::Unknown) { e.unparsed.emplace_back(t); continue; } e.pct[static_cast(s)] = static_cast(*n); e.pct_written[static_cast(s)] = true; } if (problems) { if (!e.rp) problems->push_back({Problem::Kind::Unparsed, std::string(file), line, "allows", "allows `" + e.text + "` has no RP: cost"}); for (const std::string& u : e.unparsed) problems->push_back({Problem::Kind::Unparsed, std::string(file), line, "allows", "allows `" + e.text + "`: unrecognised token `" + u + "`"}); } return e; } const TechNode* TechTree::find(std::string_view name) const { auto it = by_name_.find(fold(name)); return it == by_name_.end() ? nullptr : &nodes[it->second]; } const std::vector* TechTree::group_members(std::string_view name_or_ref) const { std::string_view g = is_group_ref(name_or_ref) ? group_of_ref(name_or_ref) : name_or_ref; for (const auto& kv : groups) if (iequals(kv.first, g)) return &kv.second; return nullptr; } std::vector TechTree::edges_to(std::string_view name) const { std::vector out; for (const AllowsEdge& e : edges) if (iequals(e.to, name)) out.push_back(&e); return out; } std::vector TechTree::edges_from(std::string_view name) const { std::vector out; for (const AllowsEdge& e : edges) if (iequals(e.from, name)) out.push_back(&e); return out; } std::vector TechTree::roots() const { std::vector out; for (const TechNode& n : nodes) if (edges_to(n.name).empty()) out.push_back(&n); return out; } bool TechTree::requirement_exists(std::string_view token) const { if (is_group_ref(token)) { const auto* m = group_members(token); return m && !m->empty(); } return find(token) != nullptr; } void TechTree::rebuild_index() { by_name_.clear(); for (std::size_t i = 0; i < nodes.size(); ++i) by_name_[fold(nodes[i].name)] = i; // last wins } Loaded load_tech_tree(const mars::parse::Document& doc, std::string file) { Loaded out; TechTree tree; auto techs = doc.root.blocks("tech"); if (techs.empty()) { out.problems.push_back({Problem::Kind::MissingBlock, file, 0, "tech", "no tech{} blocks"}); return out; } for (const mars::parse::Node* node : techs) { TechNode t; t.raw = snapshot(*node); t.line = node->line; Fields f(t.raw, file, out.problems); t.name = f.required_str("name"); if (t.name.empty()) continue; t.family = f.str("family"); auto us = t.name.find('_'); t.family_inferred = upper(us == std::string::npos ? std::string_view(t.name) : std::string_view(t.name).substr(0, us)); t.type = f.str("type"); t.threat = f.opt_int("threat"); t.group = f.str("group"); t.option_cost = f.opt_double("option_cost"); t.unlock_explicitly = f.opt_bool("unlock_explicitly"); t.requires = f.strs("requires"); for (const Block* s : t.raw.blocks_named("strategy")) { for (const std::string& v : s->strs("inc")) t.benefits_inc.push_back(v); for (const std::string& v : s->strs("dec")) t.benefits_dec.push_back(v); } for (const Block* s : t.raw.blocks_named("ship")) for (const std::string& v : s->strs("section")) t.sections.push_back(v); for (const Block* w : t.raw.blocks_named("weapon")) for (const std::string& v : w->strs("filename")) t.weapon_files.push_back(v); for (const Attr* a : t.raw.all("allows")) { AllowsEdge e = parse_allows(a->value, t.name, a->line, &out.problems, file); t.allows.push_back(tree.edges.size()); tree.edges.push_back(std::move(e)); } if (!t.group.empty()) { std::string g = upper(t.group); bool found = false; for (auto& kv : tree.groups) if (kv.first == g) { kv.second.push_back(t.name); found = true; } if (!found) tree.groups.emplace_back(g, std::vector{t.name}); } for (const TechNode& n : tree.nodes) if (iequals(n.name, t.name)) { out.problems.push_back({Problem::Kind::Duplicate, file, t.line, t.name, "tech name repeats: " + t.name}); break; } tree.nodes.push_back(std::move(t)); } tree.rebuild_index(); out.value = std::move(tree); return out; } Loaded parse_tech_tree(std::string_view text, std::string file) { auto parsed = mars::parse::parse_blocks(text); if (!parsed.ok()) { Loaded out; out.problems.push_back({Problem::Kind::Syntax, file, parsed.error().line, "", parsed.error().message}); return out; } Loaded out = load_tech_tree(parsed.value(), file); for (const auto& d : parsed.value().warnings) out.problems.push_back({Problem::Kind::Syntax, file, d.line, "", "recovered: " + d.message}); return out; } } // namespace game::data