202 lines
7.1 KiB
C++
202 lines
7.1 KiB
C++
#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<char>(c - 32);
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::string_view> split_ws(std::string_view s) {
|
|
std::vector<std::string_view> 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<Problem>* 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<std::size_t>(s)] = static_cast<int>(*n);
|
|
e.pct_written[static_cast<std::size_t>(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<std::string>* 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<const AllowsEdge*> TechTree::edges_to(std::string_view name) const {
|
|
std::vector<const AllowsEdge*> out;
|
|
for (const AllowsEdge& e : edges)
|
|
if (iequals(e.to, name)) out.push_back(&e);
|
|
return out;
|
|
}
|
|
|
|
std::vector<const AllowsEdge*> TechTree::edges_from(std::string_view name) const {
|
|
std::vector<const AllowsEdge*> out;
|
|
for (const AllowsEdge& e : edges)
|
|
if (iequals(e.from, name)) out.push_back(&e);
|
|
return out;
|
|
}
|
|
|
|
std::vector<const TechNode*> TechTree::roots() const {
|
|
std::vector<const TechNode*> 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<TechTree> load_tech_tree(const mars::parse::Document& doc, std::string file) {
|
|
Loaded<TechTree> 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<std::string>{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<TechTree> parse_tech_tree(std::string_view text, std::string file) {
|
|
auto parsed = mars::parse::parse_blocks(text);
|
|
if (!parsed.ok()) {
|
|
Loaded<TechTree> out;
|
|
out.problems.push_back({Problem::Kind::Syntax, file, parsed.error().line, "", parsed.error().message});
|
|
return out;
|
|
}
|
|
Loaded<TechTree> 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
|