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 0abfe32d8..086dbccba 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 ca360543f..628ce24a5 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 1d60b5dfc..f3cdeaa5d 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 b99b78496..f9357c142 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 deb3fedc3..9e9169779 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 5059deade..dc5072d27 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 89583e598..38a040a8a 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