216 lines
6.9 KiB
C++
216 lines
6.9 KiB
C++
|
|
#include "engine/catalog.h"
|
|||
|
|
|
|||
|
|
#include <algorithm>
|
|||
|
|
#include <filesystem>
|
|||
|
|
#include <fstream>
|
|||
|
|
|
|||
|
|
#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 <id> ключ=значение ...
|
|||
|
|
bool ParseEntity(const std::vector<std::string>& 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<EntityDesc> parsed;
|
|||
|
|
std::string line;
|
|||
|
|
int lineNo = 0;
|
|||
|
|
|
|||
|
|
while (std::getline(in, line))
|
|||
|
|
{
|
|||
|
|
++lineNo;
|
|||
|
|
const std::vector<std::string> 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).
|