summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMalkierian <malkierian@gmail.com>2025-11-13 19:05:51 -0700
committerGitHub <noreply@github.com>2025-11-13 19:05:51 -0700
commit1e1a47c263b4e8f7edb2d4049d7bca01149d7613 (patch)
tree63771fbadc8c436b6ce40885aca79d4e7dafdcd7
parentc005248129d6247f7ba6a55cb6db53ab867ee956 (diff)
Load metadata without `LoadFile()` on startup (#5817)
* Load metadata without `LoadFile` on startup. Implement file type entry. * Fix non-Windows? * clang
-rw-r--r--soh/soh/Enhancements/Presets/Presets.cpp1
-rw-r--r--soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp1
-rw-r--r--soh/soh/SaveManager.cpp211
-rw-r--r--soh/soh/SaveManager.h1
-rw-r--r--soh/soh/util.h2
5 files changed, 135 insertions, 81 deletions
diff --git a/soh/soh/Enhancements/Presets/Presets.cpp b/soh/soh/Enhancements/Presets/Presets.cpp
index 267f093ab..7671ee18e 100644
--- a/soh/soh/Enhancements/Presets/Presets.cpp
+++ b/soh/soh/Enhancements/Presets/Presets.cpp
@@ -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);
diff --git a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp
index b3e71e8f0..85225e2b1 100644
--- a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp
@@ -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();
diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp
index c17ab0a54..ab6350758 100644
--- a/soh/soh/SaveManager.cpp
+++ b/soh/soh/SaveManager.cpp
@@ -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);
diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h
index 6d7ef3b33..42c608844 100644
--- a/soh/soh/SaveManager.h
+++ b/soh/soh/SaveManager.h
@@ -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();
diff --git a/soh/soh/util.h b/soh/soh/util.h
index 9a31d4be4..f704076b4 100644
--- a/soh/soh/util.h
+++ b/soh/soh/util.h
@@ -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);