Версия профиля и миграция (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 <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Итог забега снимается ДО проверки улучшений: та начинает новую вылазку и
|
// Итог забега снимается ДО проверки улучшений: та начинает новую вылазку и
|
||||||
// обнуляет счётчик этажей. Печатать его после значило бы отчитаться
|
// обнуляет счётчик этажей. Печатать его после значило бы отчитаться
|
||||||
// цифрой, которую испортил измеряющий.
|
// цифрой, которую испортил измеряющий.
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)] = {};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user