226 lines
8.7 KiB
C++
226 lines
8.7 KiB
C++
|
|
#include "engine/project.h"
|
|||
|
|
|
|||
|
|
#include <algorithm>
|
|||
|
|
#include <cstdlib>
|
|||
|
|
#include <filesystem>
|
|||
|
|
#include <fstream>
|
|||
|
|
|
|||
|
|
#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<std::string>& parts, size_t i, float& out)
|
|||
|
|
{
|
|||
|
|
if (i < parts.size()) out = text::ToFloat(parts[i], out);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void Take(const std::vector<std::string>& 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<std::string> 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<std::string> 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<std::string> AvailableProjects()
|
|||
|
|
{
|
|||
|
|
std::error_code ec;
|
|||
|
|
std::vector<std::string> 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;
|
|||
|
|
}
|