sots-engine/src/game/data/techtree.cpp

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