From d60f9e0ce385044d7aafa3699ada5f81d9970731 Mon Sep 17 00:00:00 2001 From: "z.kirill" Date: Thu, 13 Aug 2026 21:16:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F=20=D0=B8=20=D0=BC=D0=B8?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20(E1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Формат профиля ломался дважды за три вехи — когда сумка стала общей и когда появились улучшения. Оба раза старый файл читался молча и частично, а игрок видел это как «схрон куда-то делся». Впереди аффиксы, которые сломают формат третий раз, поэтому версия заводится до них, а не после. Правило чтения несимметрично намеренно: файл СТАРШЕ игры — читается с миграцией (версии нет = формат 1) файл НОВЕЕ игры — не читается вовсе, с объяснением в сообщении Второе важнее первого. Молча обнулить профиль, записанный более новой сборкой, значит стереть десятки часов; отказ читать всего лишь просит не запускать старую сборку. Проверяется обеими сторонами в --descend: поддельный профиль формата 1 читается и сохраняет глубину и схрон, поддельный профиль из будущего отвергается. Co-Authored-By: Claude Opus 5 --- src/debug/harness.cpp | 51 +++++++++++++++++++++++++++++++++++++++++++ src/sim/profile.cpp | 24 ++++++++++++++++++++ src/sim/profile.h | 20 +++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/src/debug/harness.cpp b/src/debug/harness.cpp index 547e533..bf3e44f 100644 --- a/src/debug/harness.cpp +++ b/src/debug/harness.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -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; + } + } + // Итог забега снимается ДО проверки улучшений: та начинает новую вылазку и // обнуляет счётчик этажей. Печатать его после значило бы отчитаться // цифрой, которую испортил измеряющий. diff --git a/src/sim/profile.cpp b/src/sim/profile.cpp index 0f52bfd..8885616 100644 --- a/src/sim/profile.cpp +++ b/src/sim/profile.cpp @@ -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 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; } diff --git a/src/sim/profile.h b/src/sim/profile.h index d22ba53..86f78cf 100644 --- a/src/sim/profile.h +++ b/src/sim/profile.h @@ -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)] = {};