Версия профиля и миграция (E1)
Формат профиля ломался дважды за три вехи — когда сумка стала общей и когда появились улучшения. Оба раза старый файл читался молча и частично, а игрок видел это как «схрон куда-то делся». Впереди аффиксы, которые сломают формат третий раз, поэтому версия заводится до них, а не после. Правило чтения несимметрично намеренно: файл СТАРШЕ игры — читается с миграцией (версии нет = формат 1) файл НОВЕЕ игры — не читается вовсе, с объяснением в сообщении Второе важнее первого. Молча обнулить профиль, записанный более новой сборкой, значит стереть десятки часов; отказ читать всего лишь просит не запускать старую сборку. Проверяется обеими сторонами в --descend: поддельный профиль формата 1 читается и сохраняет глубину и схрон, поддельный профиль из будущего отвергается. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4382af2322
commit
d60f9e0ce3
@ -2,6 +2,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <cstdarg>
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
// Итог забега снимается ДО проверки улучшений: та начинает новую вылазку и
|
||||
// обнуляет счётчик этажей. Печатать его после значило бы отчитаться
|
||||
// цифрой, которую испортил измеряющий.
|
||||
|
||||
@ -102,6 +102,11 @@ bool Profile::Load(const std::string& path, std::string* err)
|
||||
p.ResetLoadouts();
|
||||
p.stash.clear();
|
||||
|
||||
// Версии нет — значит файл записан до того, как она появилась: формат 1,
|
||||
// с посумочными строками у каждого бойца. Он читается той же процедурой,
|
||||
// лишние лексемы в строке agent просто игнорируются.
|
||||
p.version = 1;
|
||||
|
||||
std::string line;
|
||||
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));
|
||||
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] == "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; }
|
||||
@ -163,6 +185,8 @@ bool Profile::Load(const std::string& path, std::string* err)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Прочитанный файл мог быть старее — записан он будет уже текущей версией.
|
||||
p.version = PROFILE_VERSION;
|
||||
*this = std::move(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -82,8 +82,28 @@ inline float UpgradeCost(int currentLevel) { return 150.0f * float(currentLevel
|
||||
const char* UpgradeName(Upgrade u);
|
||||
const char* UpgradeEffect(Upgrade u);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Версия формата профиля.
|
||||
//
|
||||
// Заведена не «на будущее»: формат уже ломался дважды за три вехи — когда сумка
|
||||
// стала общей и когда появились улучшения. Оба раза старый файл читался молча и
|
||||
// частично, а игрок видел это как «схрон куда-то делся».
|
||||
//
|
||||
// 1 — до общей сумки: у каждого бойца свои шесть ячеек в строке agent
|
||||
// 2 — общая сумка, улучшения отряда
|
||||
//
|
||||
// Правило чтения: файл СТАРШЕ игры читается с миграцией, файл НОВЕЕ игры не
|
||||
// читается вовсе. Второе важнее первого: молча обнулить профиль, который
|
||||
// записала более новая сборка, — это стереть десятки часов, а отказ читать
|
||||
// всего лишь просит не запускать старую сборку.
|
||||
// -----------------------------------------------------------------------------
|
||||
constexpr int PROFILE_VERSION = 2;
|
||||
|
||||
struct Profile
|
||||
{
|
||||
// Какой версией записан прочитанный файл. Новый профиль сразу текущей.
|
||||
int version = PROFILE_VERSION;
|
||||
|
||||
// Уровни улучшений, по одному на каждое. Живут в профиле, а не в игре:
|
||||
// это то, что переживает вылазку.
|
||||
int upgrade[int(Upgrade::COUNT)] = {};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user