#include "engine/project.h" #include #include #include #include #include "engine/text_parse.h" namespace fs = std::filesystem; namespace { constexpr const char* MANIFEST = "project.txt"; constexpr const char* PROJECTS_DIR = "projects"; // Откуда смотрим. Игра запускается и из корня репозитория, и из build/bin — // оба способа рабочие, и требовать за это ключ было бы налогом на каждый запуск. const char* const BASES[] = {".", "..", "../..", "../../.."}; bool HasManifest(const fs::path& dir) { std::error_code ec; return fs::exists(dir / MANIFEST, ec); } // Число с плавающей точкой из лексемы по индексу; нет лексемы — прежнее значение // остаётся. Так частично заполненная строка не обнуляет незаданные поля. void Take(const std::vector& parts, size_t i, float& out) { if (i < parts.size()) out = text::ToFloat(parts[i], out); } void Take(const std::vector& parts, size_t i, int& out) { if (i < parts.size()) out = text::ToInt(parts[i], out); } } // namespace bool Project::Load(const std::string& projectDir, std::string* err) { auto fail = [&](const std::string& why) { if (err) *err = why; return false; }; const fs::path root(projectDir); const fs::path manifest = root / MANIFEST; std::ifstream in(manifest); if (!in) return fail("нет " + manifest.string()); *this = Project{}; dir = projectDir; std::string line; int lineNo = 0; std::string rooms = "rooms"; std::string catalog = "catalog/entities.txt"; while (std::getline(in, line)) { ++lineNo; const std::vector parts = text::Split(text::Trim(text::StripComment(line))); if (parts.empty()) continue; const std::string& key = parts[0]; if (key == "name") name = text::JoinFrom(parts, 1); else if (key == "rooms") rooms = text::JoinFrom(parts, 1); else if (key == "catalog") catalog = text::JoinFrom(parts, 1); else if (key == "startdepth") Take(parts, 1, startDepth); else if (key == "view") { // Неизвестный вид — это ошибка, а не повод молча остаться в // изометрии: проект, написанный под вид сверху, в изометрии // выглядит не «чуть иначе», а сломанным. if (parts.size() < 2 || !ParseViewMode(parts[1].c_str(), view)) return fail(manifest.string() + ": строка " + std::to_string(lineNo) + ": неизвестный вид отображения"); } else if (key == "danger") { Take(parts, 1, curve.dangerBase); Take(parts, 2, curve.dangerLinear); Take(parts, 3, curve.dangerQuad); } else if (key == "density") { Take(parts, 1, curve.densityPerFloor); Take(parts, 2, curve.densityCap); } else if (key == "loot") { Take(parts, 1, curve.lootBase); Take(parts, 2, curve.floorsPerDrop); Take(parts, 3, curve.floorsPerTier); } else if (key == "sight") { Take(parts, 1, curve.sightPerFloor); Take(parts, 2, curve.sightFloorAt); } else if (key == "xp") Take(parts, 1, curve.xpPerFloor); else if (key == "themes") { for (int i = 0; i < THEME_COUNT; ++i) Take(parts, size_t(i) + 1, curve.themeFrom[i]); } else { // Опечатка в ключе — самая дорогая ошибка в текстовом формате: // строка просто не действует, и это выглядит как «движок не // слушается». Поэтому она валит загрузку, а не пропускается. return fail(manifest.string() + ": строка " + std::to_string(lineNo) + ": неизвестный ключ " + key); } } roomsDir = (root / rooms).string(); catalogPath = (root / catalog).string(); std::error_code ec; if (!fs::is_directory(fs::path(roomsDir), ec)) return fail("нет каталога комнат " + roomsDir); if (!fs::exists(fs::path(catalogPath), ec)) return fail("нет каталога сущностей " + catalogPath); // Полосы отделки обязаны идти по возрастанию: тема с большим номером — это // «глубже». Перепутанные границы дали бы этаж, у которого тема зависит от // порядка сравнений, а не от глубины. for (int i = 1; i < THEME_COUNT; ++i) if (curve.themeFrom[i] < curve.themeFrom[i - 1]) return fail(manifest.string() + ": полосы отделки идут не по возрастанию"); if (startDepth < 1) startDepth = 1; return true; } void Project::Apply() const { g_proj = Projection::For(view); g_curve = curve; } std::string FindProjectDir(const std::string& wantedArg) { std::error_code ec; // Ключ важнее окружения, окружение важнее угадывания. Переменная нужна // терминалу редактора и запущенным из него агентам: инструмент, вызванный // изнутри открытого проекта, обязан говорить про ЭТОТ проект, а дописывать // --project в чужие команды строковой подстановкой — способ однажды // подставить его не туда. std::string wanted = wantedArg; if (wanted.empty()) { #ifdef _MSC_VER // getenv_s, а не getenv: под /W4 второй — предупреждение, а собираемся // мы без предупреждений, и глушить их ради одной строки нельзя. char buf[512] = {}; size_t len = 0; if (getenv_s(&len, buf, sizeof(buf), "TILE2D_PROJECT") == 0 && len > 1) wanted = buf; #else const char* env = std::getenv("TILE2D_PROJECT"); if (env && *env) wanted = env; #endif } // 1. Явный путь — как дали, так и берём. if (!wanted.empty()) { if (HasManifest(fs::path(wanted))) return wanted; // 2. Или это имя проекта: <база>/projects/<имя>. for (const char* b : BASES) { const fs::path p = fs::path(b) / PROJECTS_DIR / wanted; if (HasManifest(p)) return p.string(); } return {}; } // 3. Без имени: проект под ногами, потом единственный в projects/. for (const char* b : BASES) { if (HasManifest(fs::path(b))) return fs::path(b).string(); const fs::path pdir = fs::path(b) / PROJECTS_DIR; if (!fs::is_directory(pdir, ec)) continue; std::vector found; for (const fs::directory_entry& e : fs::directory_iterator(pdir, ec)) if (e.is_directory(ec) && HasManifest(e.path())) found.push_back(e.path().string()); // Несколько проектов и ни одного названного — выбирать за игрока // нельзя: молча взятый «первый попавшийся» читается как «движок открыл // не то». Пусть скажут, какой. if (found.size() == 1) return found[0]; if (found.size() > 1) return {}; } return {}; } std::vector AvailableProjects() { std::error_code ec; std::vector names; for (const char* b : BASES) { const fs::path pdir = fs::path(b) / PROJECTS_DIR; if (!fs::is_directory(pdir, ec)) continue; for (const fs::directory_entry& e : fs::directory_iterator(pdir, ec)) if (e.is_directory(ec) && HasManifest(e.path())) names.push_back(e.path().filename().string()); if (!names.empty()) break; // ближайший каталог projects/ и есть нужный } std::sort(names.begin(), names.end()); return names; }