#include "engine/catalog.h" #include #include #include #include "engine/text_parse.h" namespace fs = std::filesystem; namespace { // color=116,52,52 — три байта через запятую. Отдельным ключом, а не тремя: // цвет это одно свойство, и разносить его по трём ключам значило бы разрешить // написать два из трёх. bool ParseColor(const std::string& v, EntityDesc& out) { const size_t c1 = v.find(','); if (c1 == std::string::npos) return false; const size_t c2 = v.find(',', c1 + 1); if (c2 == std::string::npos) return false; out.r = uint8_t(text::ToInt(v.substr(0, c1), 180)); out.g = uint8_t(text::ToInt(v.substr(c1 + 1, c2 - c1 - 1), 180)); out.b = uint8_t(text::ToInt(v.substr(c2 + 1), 180)); return true; } // depth=3 с третьего этажа и глубже // depth=1..4 с первого по четвёртый включительно bool ParseDepth(const std::string& v, EntityDesc& out) { const size_t dots = v.find(".."); if (dots == std::string::npos) { out.minDepth = std::max(1, text::ToInt(v, 1)); out.maxDepth = 0; return true; } out.minDepth = std::max(1, text::ToInt(v.substr(0, dots), 1)); out.maxDepth = text::ToInt(v.substr(dots + 2), 0); return out.maxDepth <= 0 || out.maxDepth >= out.minDepth; } // entity ключ=значение ... bool ParseEntity(const std::vector& p, EntityDesc& out, std::string& why) { if (p.size() < 2) { why = "у записи нет id"; return false; } out.id = p[1]; out.name = p[1]; bool haveClass = false; for (size_t i = 2; i < p.size(); ++i) { std::string key, value; if (!text::SplitKeyValue(p[i], key, value) || key.empty() || value.empty()) { why = "не ключ=значение: " + p[i]; return false; } if (key == "class") { if (value == "monster") out.cls = EntityClass::MONSTER; else if (value == "prop") out.cls = EntityClass::PROP; else if (value == "loot") out.cls = EntityClass::LOOT; else { why = "неизвестный класс: " + value; return false; } haveClass = true; } else if (key == "name") out.name = value; else if (key == "sprite") out.sprite = (value == "-") ? std::string{} : value; else if (key == "radius") out.radius = text::ToFloat(value, 0.34f); else if (key == "danger") out.danger = text::ToFloat(value, 1.0f); else if (key == "tier") out.tier = text::ToInt(value, 0); else if (key == "color") { if (!ParseColor(value, out)) { why = "цвет не разбирается: " + value; return false; } } else if (key == "depth") { if (!ParseDepth(value, out)) { why = "глубина не разбирается: " + value; return false; } } else { // Молча глотать незнакомый ключ нельзя: опечатка в `dangre=3` // означала бы тварь с ценой по умолчанию и ровно ноль признаков // того, что что-то не так. why = "неизвестный ключ: " + key; return false; } } if (!haveClass) { why = "не указан class"; return false; } // Обстановка и предметы в бюджет опасности не входят по определению: // ящик не делает этаж страшнее, он делает его обжитее. if (out.cls != EntityClass::MONSTER) out.danger = 0.0f; return true; } } // namespace const char* EntityClassName(EntityClass cls) { switch (cls) { case EntityClass::MONSTER: return "MONSTER"; case EntityClass::PROP: return "PROP"; default: return "LOOT"; } } bool EntityCatalog::Load(const std::string& path, std::string* err) { std::ifstream in(path); if (!in) { if (err) *err = "cannot open catalog: " + path; return false; } // Разбираем в СВОЙ вектор и подменяем только на успехе: битая правка // каталога не должна оставлять редактор с половиной палитры. std::vector parsed; std::string line; int lineNo = 0; while (std::getline(in, line)) { ++lineNo; const std::vector p = text::Split(text::StripComment(line)); if (p.empty()) continue; if (p[0] != "entity") { if (err) *err = "line " + std::to_string(lineNo) + ": expected `entity`"; return false; } EntityDesc d; std::string why; if (!ParseEntity(p, d, why)) { if (err) *err = "line " + std::to_string(lineNo) + ": " + why; return false; } // Дубль id молча перекрыл бы предыдущую запись, и в редакторе появились // бы две одинаковые кнопки палитры с разным поведением. for (const EntityDesc& prev : parsed) { if (prev.id != d.id) continue; if (err) *err = "line " + std::to_string(lineNo) + ": duplicate id `" + d.id + "`"; return false; } parsed.push_back(std::move(d)); } if (parsed.empty()) { if (err) *err = "empty catalog: " + path; return false; } entries = std::move(parsed); return true; } int EntityCatalog::IndexOf(const std::string& id) const { for (size_t i = 0; i < entries.size(); ++i) if (entries[i].id == id) return int(i); return -1; } const EntityDesc* EntityCatalog::Find(const std::string& id) const { const int i = IndexOf(id); return (i >= 0) ? &entries[size_t(i)] : nullptr; } int EntityCatalog::CountOf(EntityClass cls) const { int n = 0; for (const EntityDesc& d : entries) if (d.cls == cls) ++n; return n; } bool EntityCatalog::HasAnyAt(EntityClass cls, int depth) const { for (const EntityDesc& d : entries) if (d.cls == cls && d.InDepth(depth)) return true; return false; } int EntityCatalog::DeepestIntroduction() const { int deepest = 1; for (const EntityDesc& d : entries) deepest = std::max(deepest, d.minDepth); return deepest; } // FindCatalogPath() здесь БОЛЬШЕ НЕТ — по той же причине, что и FindRoomsDir: // путь к содержимому называет проект (engine/project.h).