// game::data -- the research graph (TechTree/MasterTechList.tech). // // tech { // name "IND_Waldo" family "IND" type P threat N group TORPS option_cost 1.2 // requires "TECH" | "GRP_" -- prerequisite beyond the allows edge // allows "CHILD RP:cost Human:% Zuul:% Hiver:% Tarkas:% Liir:% Morrigi:%" // strategy { inc TECHBEN_X dec TECHBEN_Y } // ship { section STEM ... } -- "new section" notice (not a build gate) // weapon { filename "Weapons/x.weapon" } // } // // `allows` is a directed edge parent -> child with the research-point cost of // the child when reached through this parent and a per-species availability // percentage. A species not named on the edge gets the engine default of 100 // (data-parsers.md; the file never writes it). Species are indexed in the // engine's species order (common.h), so `_NPC` has a slot and is always 100. #pragma once #include #include #include #include #include #include #include #include "game/data/block.h" #include "game/data/common.h" #include "mars/parse/blocks.h" namespace game::data { constexpr int kDefaultAllowPercent = 100; struct AllowsEdge { std::string from, to; std::optional rp; std::array pct{}; // effective values (default 100) std::array pct_written{}; // which ones the file spelled out std::vector unparsed; // tokens that were neither RP: nor a species std::string text; // the allows string as written int line = 0; AllowsEdge() { pct.fill(kDefaultAllowPercent); pct_written.fill(false); } int percent(Species s) const { return pct[static_cast(s)]; } }; // Parse one allows string. `problems` (optional) receives an Unparsed entry // for leftover tokens / missing RP. AllowsEdge parse_allows(std::string_view text, std::string from, int line = 0, std::vector* problems = nullptr, std::string_view file = {}); struct TechNode { std::string name; std::string family; // as written (often absent) std::string family_inferred; // upper-cased name prefix before '_' std::string type; // "P" on project techs std::optional threat; std::string group; // as written; group membership is matched upper-cased std::optional option_cost; std::optional unlock_explicitly; std::vector requires_tech; std::vector benefits_inc, benefits_dec; // TECHBEN_* tokens, kept as data std::vector sections; // ship{section} stems std::vector weapon_files; // weapon{filename} std::vector allows; // indices into TechTree::edges std::optional display_name, description; // TECHNAME_/TECHDESC_ (catalog fills) Block raw; int line = 0; bool is_root() const { return starts_with_fold(name.size() >= 5 ? name.substr(name.size() - 5) : name, "_ROOT"); } }; inline bool is_group_ref(std::string_view token) { return starts_with_fold(token, "GRP_"); } inline std::string_view group_of_ref(std::string_view token) { return token.substr(4); } // after GRP_ class TechTree { public: std::vector nodes; std::vector edges; // upper-cased group name -> member tech names, first-seen order std::vector>> groups; const TechNode* find(std::string_view name) const; // case-insensitive const std::vector* group_members(std::string_view name_or_ref) const; // "TORPS" or "GRP_Torps" std::vector edges_to(std::string_view name) const; std::vector edges_from(std::string_view name) const; std::vector roots() const; // nodes no edge points at // A `requires_tech` token is satisfiable if it names a tech or a non-empty group. bool requirement_exists(std::string_view token) const; void rebuild_index(); private: std::unordered_map by_name_; // folded }; Loaded load_tech_tree(const mars::parse::Document& doc, std::string file = {}); Loaded parse_tech_tree(std::string_view text, std::string file = {}); } // namespace game::data