Версия профиля и миграция (E1)

Формат профиля ломался дважды за три вехи — когда сумка стала общей и когда
появились улучшения. Оба раза старый файл читался молча и частично, а игрок
видел это как «схрон куда-то делся». Впереди аффиксы, которые сломают формат
третий раз, поэтому версия заводится до них, а не после.

Правило чтения несимметрично намеренно:

  файл СТАРШЕ игры  — читается с миграцией (версии нет = формат 1)
  файл НОВЕЕ игры   — не читается вовсе, с объяснением в сообщении

Второе важнее первого. Молча обнулить профиль, записанный более новой сборкой,
значит стереть десятки часов; отказ читать всего лишь просит не запускать
старую сборку.

Проверяется обеими сторонами в --descend: поддельный профиль формата 1
читается и сохраняет глубину и схрон, поддельный профиль из будущего
отвергается.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
z.kirill 2026-08-13 21:16:08 +03:00
parent 4382af2322
commit d60f9e0ce3
3 changed files with 95 additions and 0 deletions

View File

@ -2,6 +2,7 @@
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <fstream>
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
@ -906,6 +907,56 @@ int RunDescend(const RoomLibrary& rooms, const EntityCatalog& catalog, uint32_t
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// -------------------------------------------------------------------------
// Версия профиля. Формат ломался дважды за три вехи, и оба раза старый файл
// читался молча и частично. Проверяются оба направления: старый читается с
// миграцией, файл из будущего НЕ читается вовсе.
// -------------------------------------------------------------------------
{
const std::filesystem::path old = std::filesystem::temp_directory_path() / "t2d_v1.txt";
{
std::ofstream f(old);
// Формат 1: версии нет, у бойца шесть посумочных лексем.
f << "maxdepth 7\nruns 3\nxp 4200\n";
f << "agent 0 rifle vest charm bandage stimpack - - - -\n";
f << "stash saber plate\n";
}
Profile v1;
std::string why;
const bool readOld = v1.Load(old.string(), &why);
const std::filesystem::path future =
std::filesystem::temp_directory_path() / "t2d_future.txt";
{
std::ofstream f(future);
f << "version " << (PROFILE_VERSION + 1) << "\nmaxdepth 99\n";
}
Profile vNext;
std::string whyFuture;
const bool readFuture = vNext.Load(future.string(), &whyFuture);
std::error_code ec;
std::filesystem::remove(old, ec);
std::filesystem::remove(future, ec);
std::printf("\nпрофиль: старый формат %s (глубина %d, схрон %d), из будущего %s\n",
readOld ? "прочитан" : "НЕ прочитан", v1.maxDepth, v1.StashCount(),
readFuture ? "ПРОЧИТАН" : "отвергнут");
if (!readOld || v1.maxDepth != 7 || v1.StashCount() != 2)
{
std::printf(" [FAIL] миграция со старого формата потеряла данные: %s\n", why.c_str());
++bad;
}
if (readFuture)
{
std::printf(" [FAIL] профиль новее игры прочитан — запись поверх сотрёт чужое\n");
++bad;
}
}
// Итог забега снимается ДО проверки улучшений: та начинает новую вылазку и // Итог забега снимается ДО проверки улучшений: та начинает новую вылазку и
// обнуляет счётчик этажей. Печатать его после значило бы отчитаться // обнуляет счётчик этажей. Печатать его после значило бы отчитаться
// цифрой, которую испортил измеряющий. // цифрой, которую испортил измеряющий.

View File

@ -102,6 +102,11 @@ bool Profile::Load(const std::string& path, std::string* err)
p.ResetLoadouts(); p.ResetLoadouts();
p.stash.clear(); p.stash.clear();
// Версии нет — значит файл записан до того, как она появилась: формат 1,
// с посумочными строками у каждого бойца. Он читается той же процедурой,
// лишние лексемы в строке agent просто игнорируются.
p.version = 1;
std::string line; std::string line;
int lineNo = 0; int lineNo = 0;
@ -111,6 +116,23 @@ bool Profile::Load(const std::string& path, std::string* err)
const std::vector<std::string> t = text::Split(text::StripComment(line)); const std::vector<std::string> t = text::Split(text::StripComment(line));
if (t.empty()) continue; if (t.empty()) continue;
if (t[0] == "version" && t.size() >= 2)
{
p.version = text::ToInt(t[1], 1);
if (p.version > PROFILE_VERSION)
{
// Читать профиль из будущего нельзя даже частично: в нём есть
// то, чего эта сборка не знает, и сохранение поверх стёрло бы
// это молча.
if (err)
*err = "профиль версии " + std::to_string(p.version) +
", а игра понимает только " + std::to_string(PROFILE_VERSION) +
" — обновите сборку";
return false;
}
continue;
}
if (t[0] == "maxdepth" && t.size() >= 2) { p.maxDepth = std::max(1, text::ToInt(t[1], 1)); continue; } if (t[0] == "maxdepth" && t.size() >= 2) { p.maxDepth = std::max(1, text::ToInt(t[1], 1)); continue; }
if (t[0] == "runs" && t.size() >= 2) { p.runs = text::ToInt(t[1], 0); continue; } if (t[0] == "runs" && t.size() >= 2) { p.runs = text::ToInt(t[1], 0); continue; }
if (t[0] == "extractions" && t.size() >= 2) { p.extractions = text::ToInt(t[1], 0); continue; } if (t[0] == "extractions" && t.size() >= 2) { p.extractions = text::ToInt(t[1], 0); continue; }
@ -163,6 +185,8 @@ bool Profile::Load(const std::string& path, std::string* err)
return false; return false;
} }
// Прочитанный файл мог быть старее — записан он будет уже текущей версией.
p.version = PROFILE_VERSION;
*this = std::move(p); *this = std::move(p);
return true; return true;
} }

View File

@ -82,8 +82,28 @@ inline float UpgradeCost(int currentLevel) { return 150.0f * float(currentLevel
const char* UpgradeName(Upgrade u); const char* UpgradeName(Upgrade u);
const char* UpgradeEffect(Upgrade u); const char* UpgradeEffect(Upgrade u);
// -----------------------------------------------------------------------------
// Версия формата профиля.
//
// Заведена не «на будущее»: формат уже ломался дважды за три вехи — когда сумка
// стала общей и когда появились улучшения. Оба раза старый файл читался молча и
// частично, а игрок видел это как «схрон куда-то делся».
//
// 1 — до общей сумки: у каждого бойца свои шесть ячеек в строке agent
// 2 — общая сумка, улучшения отряда
//
// Правило чтения: файл СТАРШЕ игры читается с миграцией, файл НОВЕЕ игры не
// читается вовсе. Второе важнее первого: молча обнулить профиль, который
// записала более новая сборка, — это стереть десятки часов, а отказ читать
// всего лишь просит не запускать старую сборку.
// -----------------------------------------------------------------------------
constexpr int PROFILE_VERSION = 2;
struct Profile struct Profile
{ {
// Какой версией записан прочитанный файл. Новый профиль сразу текущей.
int version = PROFILE_VERSION;
// Уровни улучшений, по одному на каждое. Живут в профиле, а не в игре: // Уровни улучшений, по одному на каждое. Живут в профиле, а не в игре:
// это то, что переживает вылазку. // это то, что переживает вылазку.
int upgrade[int(Upgrade::COUNT)] = {}; int upgrade[int(Upgrade::COUNT)] = {};