Load metadata without LoadFile() on startup (#5817)

* Load metadata without `LoadFile` on startup.
Implement file type entry.

* Fix non-Windows?

* clang
This commit is contained in:
Malkierian
2025-11-13 19:05:51 -07:00
committed by GitHub
parent c005248129
commit 1e1a47c263
5 changed files with 135 additions and 81 deletions

View File

@@ -251,6 +251,7 @@ void SavePreset(std::string& presetName) {
fs::create_directory(presetFolder);
}
presets[presetName].presetValues["presetName"] = presetName;
presets[presetName].presetValues["fileType"] = FILE_TYPE_PRESET;
std::ofstream file(
fmt::format("{}/{}.json", Ship::Context::GetInstance()->LocateFileAcrossAppDirs("presets"), presetName));
file << presets[presetName].presetValues.dump(4);

View File

@@ -343,6 +343,7 @@ const char* SpoilerLog_Write() {
jsonData.clear();
jsonData["version"] = (char*)gBuildVersion;
jsonData["fileType"] = FILE_TYPE_SPOILER;
jsonData["git_branch"] = (char*)gGitBranch;
jsonData["git_commit"] = (char*)gGitCommitHash;
jsonData["seed"] = ctx->GetSeedString();

View File

@@ -156,6 +156,10 @@ SaveManager::SaveManager() {
}
void SaveManager::LoadRandomizer() {
if (gSaveContext.ship.quest.id != QUEST_RANDOMIZER) {
return;
}
auto randoContext = Rando::Context::GetInstance();
SaveManager::Instance->LoadArray("itemLocations", RC_MAX, [&](size_t i) {
SaveManager::Instance->LoadStruct("", [&]() {
@@ -255,9 +259,10 @@ void SaveManager::LoadRandomizer() {
}
void SaveManager::SaveRandomizer(SaveContext* saveContext, int sectionID, bool fullSave) {
if (saveContext->ship.quest.id != QUEST_RANDOMIZER)
if (saveContext->ship.quest.id != QUEST_RANDOMIZER) {
return;
}
auto randoContext = Rando::Context::GetInstance();
SaveManager::Instance->SaveArray("itemLocations", RC_MAX, [&](size_t i) {
@@ -455,11 +460,122 @@ void SaveManager::Init() {
// Load files to initialize metadata
for (int fileNum = 0; fileNum < MaxFiles; fileNum++) {
if (std::filesystem::exists(GetFileName(fileNum))) {
LoadFile(fileNum);
saveBlock = nlohmann::json::object();
OTRGlobals::Instance->gRandoContext->ClearItemLocations();
StartupCheckAndInitMeta(fileNum);
}
}
saveBlock = nlohmann::json::object();
}
void SaveManager::StartupCheckAndInitMeta(int fileNum) {
saveMtx.lock();
SPDLOG_INFO("Init Meta - fileNum: {}", fileNum);
std::filesystem::path fileName = GetFileName(fileNum);
std::ifstream input(fileName);
bool deleteRando = false;
nlohmann::json metaSaveBlock = nlohmann::json::object();
input >> metaSaveBlock;
input.close();
saveMtx.unlock();
if (!metaSaveBlock.contains("version")) {
SPDLOG_ERROR("Save at " + fileName.string() + " contains no version");
assert(false);
return;
}
if (metaSaveBlock["sections"].contains("randomizer")) {
if (!metaSaveBlock.contains("fileType") || metaSaveBlock["fileType"] == FILE_TYPE_SAVE_VANILLA) {
SohGui::RegisterPopup(
"Loading old file",
"The file in slot " + std::to_string(fileNum + 1) +
" appears to contain randomizer data, but is a very old format or is empty.\n" +
"The randomizer data has been removed, and this file will be treated as a vanilla "
"file.\nIf this was a vanilla file, it still is, and you shouldn't see this "
"message again.\n" +
"If this was a randomizer file, the file will not work, and should be deleted.");
metaSaveBlock["sections"].erase(metaSaveBlock["sections"].find("randomizer"));
metaSaveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA;
saveMtx.lock();
std::ofstream output(GetFileName(fileNum));
output << metaSaveBlock.dump(1);
output.close();
saveMtx.unlock();
}
s16 major = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"];
s16 minor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"];
s16 patch = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"];
// block loading outdated rando save
if (!(major == gBuildVersionMajor && minor == gBuildVersionMinor && patch == gBuildVersionPatch)) {
std::string newFileName =
Ship::Context::GetPathRelativeToAppDirectory("Save") +
("/file" + std::to_string(fileNum + 1) + "-" + std::to_string(GetUnixTimestamp()) + ".bak");
#if defined(__SWITCH__) || defined(__WIIU__)
copy_file(fileName.c_str(), newFileName.c_str());
std::filesystem::remove(fileName);
#else
std::filesystem::rename(fileName, newFileName);
#endif
SohGui::RegisterPopup("Outdated Randomizer Save",
"The SoH version in the file in slot " + std::to_string(fileNum + 1) +
" does not match the currently running version.\n" +
"Non-matching rando saves are unsupported, and the file has been renamed to\n" +
" " + newFileName + "\n" +
"If this was not in error, the file should be deleted.");
return;
}
}
bool isRando = metaSaveBlock["fileType"] == FILE_TYPE_SAVE_RANDO;
fileMetaInfo[fileNum].valid = true;
nlohmann::json& baseBlock = metaSaveBlock["sections"]["base"]["data"];
fileMetaInfo[fileNum].deaths = baseBlock["deaths"];
for (int i = 0; i < ARRAY_COUNT(fileMetaInfo[fileNum].playerName); i++) {
fileMetaInfo[fileNum].playerName[i] = baseBlock["playerName"][i];
}
fileMetaInfo[fileNum].healthCapacity = baseBlock["healthCapacity"];
fileMetaInfo[fileNum].questItems = baseBlock["inventory"]["questItems"];
for (int i = 0; i < ARRAY_COUNT(fileMetaInfo[fileNum].inventoryItems); i++) {
fileMetaInfo[fileNum].inventoryItems[i] = baseBlock["inventory"]["items"][i];
}
fileMetaInfo[fileNum].equipment = baseBlock["inventory"]["equipment"];
fileMetaInfo[fileNum].upgrades = baseBlock["inventory"]["upgrades"];
fileMetaInfo[fileNum].isMagicAcquired = baseBlock["isMagicAcquired"];
fileMetaInfo[fileNum].isDoubleMagicAcquired = baseBlock["isDoubleMagicAcquired"];
fileMetaInfo[fileNum].rupees = baseBlock["rupees"];
fileMetaInfo[fileNum].gsTokens = baseBlock["inventory"]["gsTokens"];
fileMetaInfo[fileNum].isDoubleDefenseAcquired = baseBlock["isDoubleDefenseAcquired"];
fileMetaInfo[fileNum].gregFound = false;
fileMetaInfo[fileNum].filenameLanguage = baseBlock["filenameLanguage"];
fileMetaInfo[fileNum].hasWallet = !isRando;
fileMetaInfo[fileNum].defense = baseBlock["inventory"]["defenseHearts"];
fileMetaInfo[fileNum].health = baseBlock["health"];
fileMetaInfo[fileNum].requiresOriginal = !baseBlock["isMasterQuest"];
fileMetaInfo[fileNum].requiresMasterQuest = baseBlock["isMasterQuest"];
fileMetaInfo[fileNum].randoSave = isRando;
if (isRando) {
nlohmann::json& randoBlock = metaSaveBlock["sections"]["randomizer"]["data"];
for (int i = 0; i < ARRAY_COUNT(fileMetaInfo[fileNum].seedHash); i++) {
fileMetaInfo[fileNum].seedHash[i] = randoBlock["seed"][i];
}
fileMetaInfo[fileNum].gregFound =
(int16_t)baseBlock["randomizerInf"][RAND_INF_GREG_FOUND >> 4] & (1 << (RAND_INF_GREG_FOUND & 0xF));
fileMetaInfo[fileNum].hasWallet =
(int16_t)baseBlock["randomizerInf"][RAND_INF_HAS_WALLET >> 4] & (1 << (RAND_INF_HAS_WALLET & 0xF));
fileMetaInfo[fileNum].requiresMasterQuest = randoBlock["masterQuestDungeonCount"] > 0;
// If the file is not marked as Master Quest, it could still theoretically be a rando save with all 12 MQ
// dungeons, in which case we don't actually require a vanilla OTR.
fileMetaInfo[fileNum].requiresOriginal = randoBlock["masterQuestDungeonCount"] < 12;
}
fileMetaInfo[fileNum].buildVersionMajor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"];
fileMetaInfo[fileNum].buildVersionMinor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"];
fileMetaInfo[fileNum].buildVersionPatch = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"];
SohUtils::CopyStringToCharArray(fileMetaInfo[fileNum].buildVersion,
metaSaveBlock["sections"]["sohStats"]["data"]["buildVersion"],
ARRAY_COUNT(fileMetaInfo[fileNum].buildVersion));
}
void SaveManager::InitMeta(int fileNum) {
@@ -539,7 +655,6 @@ void SaveManager::InitFileNormal() {
gSaveContext.ship.filenameLanguage =
(gSaveContext.language == LANGUAGE_JPN) ? NAME_LANGUAGE_NTSC_JPN : NAME_LANGUAGE_NTSC_ENG;
}
gSaveContext.n64ddFlag = 0;
gSaveContext.healthCapacity = 0x30;
gSaveContext.health = 0x30;
gSaveContext.magicLevel = 0;
@@ -715,7 +830,6 @@ void SaveManager::InitFileDebug() {
gSaveContext.ship.filenameLanguage =
(gSaveContext.language == LANGUAGE_JPN) ? NAME_LANGUAGE_NTSC_JPN : NAME_LANGUAGE_NTSC_ENG;
}
gSaveContext.n64ddFlag = 0;
gSaveContext.healthCapacity = 0xE0;
gSaveContext.health = 0xE0;
gSaveContext.magicLevel = 0;
@@ -836,7 +950,6 @@ void SaveManager::InitFileMaxed() {
gSaveContext.ship.filenameLanguage =
(gSaveContext.language == LANGUAGE_JPN) ? NAME_LANGUAGE_NTSC_JPN : NAME_LANGUAGE_NTSC_ENG;
}
gSaveContext.n64ddFlag = 0;
gSaveContext.healthCapacity = 0x140;
gSaveContext.health = 0x140;
gSaveContext.magicLevel = 2;
@@ -1004,6 +1117,11 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se
SPDLOG_INFO("Save File - fileNum: {}", fileNum);
// Needed for first time save, hasn't changed in forever anyway
saveBlock["version"] = 1;
if (IS_RANDO) {
saveBlock["fileType"] = FILE_TYPE_SAVE_RANDO;
} else {
saveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA;
}
if (sectionID == SECTION_ID_BASE) {
for (auto& sectionHandlerPair : sectionSaveHandlers) {
auto& saveFuncInfo = sectionHandlerPair.second;
@@ -1123,62 +1241,20 @@ void SaveManager::LoadFile(int fileNum) {
std::ifstream input(fileName);
try {
bool deleteRando = false;
saveBlock = nlohmann::json::object();
input >> saveBlock;
input.close();
if (!saveBlock.contains("version")) {
SPDLOG_ERROR("Save at " + fileName.string() + " contains no version");
assert(false);
}
if (saveBlock.contains("fileType") && saveBlock["fileType"] == FILE_TYPE_SAVE_RANDO) {
gSaveContext.ship.quest.id = QUEST_RANDOMIZER;
}
switch (saveBlock["version"].get<int>()) {
case 1:
for (auto& block : saveBlock["sections"].items()) {
bool oldVanilla =
block.value()["data"].empty() || block.value()["data"].contains("aat0") ||
block.value()["data"]["entrances"].empty() ||
SohUtils::IsStringEmpty(saveBlock["sections"]["sohStats"]["data"]["buildVersion"]);
std::string sectionName = block.key();
if (sectionName == "randomizer") {
bool hasStats = saveBlock["sections"].contains("sohStats");
if (oldVanilla || !hasStats) { // Vanilla "rando" data
SohGui::RegisterPopup(
"Loading old file",
"The file in slot " + std::to_string(fileNum + 1) +
" appears to contain randomizer data, but is a very old format or is empty.\n" +
"The randomizer data has been removed, and this file will be treated as a vanilla "
"file.\nIf this was a vanilla file, it still is, and you shouldn't see this "
"message again.\n" +
"If this was a randomizer file, the file will not work, and should be deleted.");
deleteRando = true;
continue;
}
s16 major = saveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"];
s16 minor = saveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"];
s16 patch = saveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"];
// block loading outdated rando save
if (!(major == gBuildVersionMajor && minor == gBuildVersionMinor &&
patch == gBuildVersionPatch)) {
input.close();
std::string newFileName = Ship::Context::GetPathRelativeToAppDirectory("Save") +
("/file" + std::to_string(fileNum + 1) + "-" +
std::to_string(GetUnixTimestamp()) + ".bak");
#if defined(__SWITCH__) || defined(__WIIU__)
copy_file(fileName.c_str(), newFileName.c_str());
std::filesystem::remove(fileName);
#else
std::filesystem::rename(fileName, newFileName);
#endif
SohGui::RegisterPopup(
"Outdated Randomizer Save",
"The SoH version in the file in slot " + std::to_string(fileNum + 1) +
" does not match the currently running version.\n" +
"Non-matching rando saves are unsupported, and the file has been renamed to\n" +
" " + newFileName + "\n" +
"If this was not in error, the file should be deleted.");
saveMtx.unlock();
return;
}
}
int sectionVersion = block.value()["version"];
if (sectionName == "randomizer" && sectionVersion != 1) {
sectionVersion = 1;
@@ -1214,12 +1290,6 @@ void SaveManager::LoadFile(int fileNum) {
assert(false);
break;
}
input.close();
if (deleteRando) {
saveBlock["sections"].erase(saveBlock["sections"].find("randomizer"));
SaveFile(fileNum);
deleteRando = false;
}
InitMeta(fileNum);
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnLoadFile>(fileNum);
} catch (const std::exception& e) {
@@ -1335,11 +1405,6 @@ void SaveManager::LoadBaseVersion1() {
SaveManager::Instance->LoadData("deaths", gSaveContext.deaths);
SaveManager::Instance->LoadArray("playerName", ARRAY_COUNT(gSaveContext.playerName),
[](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.playerName[i]); });
int isRando = 0;
SaveManager::Instance->LoadData("n64ddFlag", isRando);
if (isRando) {
gSaveContext.ship.quest.id = QUEST_RANDOMIZER;
}
SaveManager::Instance->LoadData("healthCapacity", gSaveContext.healthCapacity);
SaveManager::Instance->LoadData("health", gSaveContext.health);
SaveManager::Instance->LoadData("magicLevel", gSaveContext.magicLevel);
@@ -1479,11 +1544,6 @@ void SaveManager::LoadBaseVersion2() {
SaveManager::Instance->LoadData("deaths", gSaveContext.deaths);
SaveManager::Instance->LoadArray("playerName", ARRAY_COUNT(gSaveContext.playerName),
[](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.playerName[i]); });
int isRando = 0;
SaveManager::Instance->LoadData("n64ddFlag", isRando);
if (isRando) {
gSaveContext.ship.quest.id = QUEST_RANDOMIZER;
}
SaveManager::Instance->LoadData("healthCapacity", gSaveContext.healthCapacity);
SaveManager::Instance->LoadData("health", gSaveContext.health);
SaveManager::Instance->LoadData("magicLevel", gSaveContext.magicLevel);
@@ -1695,11 +1755,6 @@ void SaveManager::LoadBaseVersion3() {
SaveManager::Instance->LoadData("deaths", gSaveContext.deaths);
SaveManager::Instance->LoadArray("playerName", ARRAY_COUNT(gSaveContext.playerName),
[](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.playerName[i]); });
int isRando = 0;
SaveManager::Instance->LoadData("n64ddFlag", isRando);
if (isRando) {
gSaveContext.ship.quest.id = QUEST_RANDOMIZER;
}
SaveManager::Instance->LoadData("healthCapacity", gSaveContext.healthCapacity);
SaveManager::Instance->LoadData("health", gSaveContext.health);
SaveManager::Instance->LoadData("magicLevel", gSaveContext.magicLevel);
@@ -1915,11 +1970,6 @@ void SaveManager::LoadBaseVersion4() {
SaveManager::Instance->LoadData("deaths", gSaveContext.deaths);
SaveManager::Instance->LoadArray("playerName", ARRAY_COUNT(gSaveContext.playerName),
[](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.playerName[i]); });
int isRando = 0;
SaveManager::Instance->LoadData("n64ddFlag", isRando);
if (isRando) {
gSaveContext.ship.quest.id = QUEST_RANDOMIZER;
}
SaveManager::Instance->LoadData("healthCapacity", gSaveContext.healthCapacity);
SaveManager::Instance->LoadData("health", gSaveContext.health);
SaveManager::Instance->LoadData("magicLevel", gSaveContext.magicLevel);
@@ -2098,7 +2148,6 @@ void SaveManager::SaveBase(SaveContext* saveContext, int sectionID, bool fullSav
SaveManager::Instance->SaveArray("playerName", ARRAY_COUNT(saveContext->playerName), [&](size_t i) {
SaveManager::Instance->SaveData("", saveContext->playerName[i]);
});
SaveManager::Instance->SaveData("n64ddFlag", saveContext->ship.quest.id == QUEST_RANDOMIZER);
SaveManager::Instance->SaveData("healthCapacity", saveContext->healthCapacity);
SaveManager::Instance->SaveData("health", saveContext->health);
SaveManager::Instance->SaveData("magicLevel", saveContext->magicLevel);

View File

@@ -161,6 +161,7 @@ class SaveManager {
void SaveFileThreaded(int fileNum, SaveContext* saveContext, int sectionID);
void InitMeta(int slotNum);
void StartupCheckAndInitMeta(int slotNum);
static void InitFileImpl(bool isDebug);
static void InitFileNormal();
static void InitFileDebug();

View File

@@ -2,6 +2,8 @@
#include <string>
#include <stdint.h>
typedef enum FileType { FILE_TYPE_SAVE_VANILLA, FILE_TYPE_SAVE_RANDO, FILE_TYPE_PRESET, FILE_TYPE_SPOILER } FileType;
namespace SohUtils {
const std::string& GetSceneName(int32_t scene);