From 31db9e4adeb99b6f61c9b60ae55459a974ba7d3d Mon Sep 17 00:00:00 2001 From: Glought Date: Fri, 2 Jan 2026 06:39:42 -0800 Subject: [PATCH] Added "firstInput" stat and repurposed the "fileCreatedAt" stat. (#6070) - The "firstInput" stat is set on first input in game. Used for RTA Timing. - The "fileCreatedAt" stat is now set when then save file is created (After the player is done entering the file name). Useful for seeding non rando randomizers like Mirrorworld, Enemy Randomizer, extraTraps, etc. --- soh/include/z64save.h | 1 + soh/soh/Enhancements/gameplaystats.cpp | 5 ++++- soh/soh/Enhancements/gameplaystats.h | 4 ++-- soh/soh/Network/Anchor/JsonConversions.hpp | 2 ++ soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp | 1 + soh/soh/SaveManager.cpp | 2 ++ soh/src/code/z_play.c | 4 ++-- 7 files changed, 14 insertions(+), 5 deletions(-) diff --git a/soh/include/z64save.h b/soh/include/z64save.h index 0abfe32d82..086dbccba4 100644 --- a/soh/include/z64save.h +++ b/soh/include/z64save.h @@ -97,6 +97,7 @@ typedef struct { /* */ u32 entrancesDiscovered[SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT]; /* */ u32 scenesDiscovered[SAVEFILE_SCENES_DISCOVERED_IDX_COUNT]; /* */ bool rtaTiming; + /* */ uint64_t firstInput; /* */ uint64_t fileCreatedAt; } SohStats; diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp index ca360543fa..628ce24a58 100644 --- a/soh/soh/Enhancements/gameplaystats.cpp +++ b/soh/soh/Enhancements/gameplaystats.cpp @@ -299,6 +299,7 @@ void LoadStatsVersion1() { SaveManager::Instance->LoadData("", gSaveContext.ship.stats.dungeonKeys[i]); }); SaveManager::Instance->LoadData("rtaTiming", gSaveContext.ship.stats.rtaTiming); + SaveManager::Instance->LoadData("firstInput", gSaveContext.ship.stats.firstInput); SaveManager::Instance->LoadData("fileCreatedAt", gSaveContext.ship.stats.fileCreatedAt); SaveManager::Instance->LoadData("playTimer", gSaveContext.ship.stats.playTimer); SaveManager::Instance->LoadData("pauseTimer", gSaveContext.ship.stats.pauseTimer); @@ -348,6 +349,7 @@ void SaveStats(SaveContext* saveContext, int sectionID, bool fullSave) { SaveManager::Instance->SaveData("", saveContext->ship.stats.dungeonKeys[i]); }); SaveManager::Instance->SaveData("rtaTiming", saveContext->ship.stats.rtaTiming); + SaveManager::Instance->SaveData("firstInput", saveContext->ship.stats.firstInput); SaveManager::Instance->SaveData("fileCreatedAt", saveContext->ship.stats.fileCreatedAt); SaveManager::Instance->SaveData("playTimer", saveContext->ship.stats.playTimer); SaveManager::Instance->SaveData("pauseTimer", saveContext->ship.stats.pauseTimer); @@ -694,7 +696,8 @@ void InitStats(bool isDebug) { gSaveContext.ship.stats.dungeonKeys[dungeon] = isDebug ? 8 : 0; } gSaveContext.ship.stats.rtaTiming = CVarGetInteger(CVAR_GAMEPLAY_STATS("RTATiming"), 0); - gSaveContext.ship.stats.fileCreatedAt = 0; + gSaveContext.ship.stats.fileCreatedAt = GetUnixTimestamp(); + gSaveContext.ship.stats.firstInput = 0; gSaveContext.ship.stats.playTimer = 0; gSaveContext.ship.stats.pauseTimer = 0; for (int timestamp = 0; timestamp < ARRAY_COUNT(gSaveContext.ship.stats.itemTimestamp); timestamp++) { diff --git a/soh/soh/Enhancements/gameplaystats.h b/soh/soh/Enhancements/gameplaystats.h index 1d60b5dfca..f3cdeaa5d6 100644 --- a/soh/soh/Enhancements/gameplaystats.h +++ b/soh/soh/Enhancements/gameplaystats.h @@ -22,9 +22,9 @@ char* GameplayStats_GetCurrentTime(); #define GAMEPLAYSTAT_TOTAL_TIME \ (gSaveContext.ship.stats.rtaTiming \ ? (!gSaveContext.ship.stats.gameComplete \ - ? (!gSaveContext.ship.stats.fileCreatedAt \ + ? (!gSaveContext.ship.stats.firstInput \ ? 0 \ - : ((GetUnixTimestamp() - gSaveContext.ship.stats.fileCreatedAt) / 100)) \ + : ((GetUnixTimestamp() - gSaveContext.ship.stats.firstInput) / 100)) \ : (gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] \ ? gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] \ : gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED])) \ diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp index b99b784964..f9357c142a 100644 --- a/soh/soh/Network/Anchor/JsonConversions.hpp +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -93,12 +93,14 @@ inline void from_json(const json& j, Inventory& inventory) { inline void to_json(json& j, const SohStats& sohStats) { j = json{ { "entrancesDiscovered", sohStats.entrancesDiscovered }, + { "firstInput", sohStats.firstInput }, { "fileCreatedAt", sohStats.fileCreatedAt }, }; } inline void from_json(const json& j, SohStats& sohStats) { j.at("entrancesDiscovered").get_to(sohStats.entrancesDiscovered); + j.at("firstInput").get_to(sohStats.firstInput); j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt); } diff --git a/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp index deb3fedc32..9e9169779e 100644 --- a/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp +++ b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp @@ -189,6 +189,7 @@ void Anchor::HandlePacket_UpdateTeamState(nlohmann::json payload) { gSaveContext.gsFlags[i] = loadedData.gsFlags[i]; } + gSaveContext.ship.stats.firstInput = loadedData.ship.stats.firstInput; gSaveContext.ship.stats.fileCreatedAt = loadedData.ship.stats.fileCreatedAt; // Restore master sword state diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 5059deade8..dc5072d27e 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -1631,6 +1631,7 @@ void SaveManager::LoadBaseVersion2() { SaveManager::Instance->LoadData("", gSaveContext.ship.stats.dungeonKeys[i]); }); SaveManager::Instance->LoadData("rtaTiming", gSaveContext.ship.stats.rtaTiming); + SaveManager::Instance->LoadData("firstInput", gSaveContext.ship.stats.firstInput); SaveManager::Instance->LoadData("fileCreatedAt", gSaveContext.ship.stats.fileCreatedAt); SaveManager::Instance->LoadData("playTimer", gSaveContext.ship.stats.playTimer); SaveManager::Instance->LoadData("pauseTimer", gSaveContext.ship.stats.pauseTimer); @@ -1848,6 +1849,7 @@ void SaveManager::LoadBaseVersion3() { SaveManager::Instance->LoadData("", gSaveContext.ship.stats.dungeonKeys[i]); }); SaveManager::Instance->LoadData("rtaTiming", gSaveContext.ship.stats.rtaTiming); + SaveManager::Instance->LoadData("firstInput", gSaveContext.ship.stats.firstInput); SaveManager::Instance->LoadData("fileCreatedAt", gSaveContext.ship.stats.fileCreatedAt); SaveManager::Instance->LoadData("playTimer", gSaveContext.ship.stats.playTimer); SaveManager::Instance->LoadData("pauseTimer", gSaveContext.ship.stats.pauseTimer); diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 89583e5984..38a040a8ad 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -781,10 +781,10 @@ void Play_Update(PlayState* play) { } // Start RTA timing on first non-c-up input after intro cutscene - if (!gSaveContext.ship.stats.fileCreatedAt && !Player_InCsMode(play) && + if (!gSaveContext.ship.stats.firstInput && !Player_InCsMode(play) && ((input[0].press.button && input[0].press.button != 0x8) || input[0].rel.stick_x != 0 || input[0].rel.stick_y != 0)) { - gSaveContext.ship.stats.fileCreatedAt = GetUnixTimestamp(); + gSaveContext.ship.stats.firstInput = GetUnixTimestamp(); } } // #endregion