summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCaladius <Caladius@users.noreply.github.com>2025-10-15 12:38:14 -0400
committerGitHub <noreply@github.com>2025-10-15 12:38:14 -0400
commit894670b9008381ca4ea366736e6243bcdb3dad94 (patch)
treee02f28f38963acaa78fa22d8e1438965545aecfe
parent3b88dd05ed4e1c9c1a30af4b767db1e1e91d0b3d (diff)
[Enhancement] Time Splits (#1266)
* Adds support for Timesplits * clanged * Fix Popout Crash * wip file saving; fixes Bow and Red Potion splits * Add Active Row Highlighting * Fix D-Pad Bottle Collection * Cleanup and Add Boss Splits. * Add names to Boss Icons * clang v2 * Linker fix * Move time formatting to ShipUtils (this also cleaned up DisplayOverlay). * clang v3 * Adds Saving/Loading splits from file. * Update inventory range to exclude heart piece. * Update column name to not be abbreviated * Adds Magic Meters and Double Defense * clang cuz ya boi alwayz fogetz * Fix Fairy Splits with CS Skip Enabled * Move songColorMap to ShipUtils along with the function to get the color. Fixed Song Icon sizing. * Adds default Window size and allows resizing. * Allow scrolling of splits list * Adds Entrance Splits and cleans up the UI. Adds new splitType to the object so I added a small solution to handle older files that may not have it for testing. * UI Update and Entrance Filters. * Adds split list to the options window to fill out the space. * Fix ID mismatching. * Add Split Comparing Functionality * clang * Compare Split text display corrections * Adds auto scrolling to follow Active Splt if list is bigger than the window. Also locks the Header Row when visible to always be visible at the top instead of scrolling with the list. * Adds Tooltips for options * Fix time coloring for splits with no previous data. * Fix crash and ID mismatch * Feedback Updates * Extern is needed since my definition is in the cpp file it seems.
-rw-r--r--mm/2s2h/BenGui/BenGui.cpp11
-rw-r--r--mm/2s2h/BenGui/BenMenu.cpp7
-rw-r--r--mm/2s2h/Enhancements/Trackers/DisplayOverlay.cpp12
-rw-r--r--mm/2s2h/Enhancements/Trackers/TimeSplits/TimeSplitsActions.cpp404
-rw-r--r--mm/2s2h/Enhancements/Trackers/TimeSplits/Timesplits.cpp305
-rw-r--r--mm/2s2h/Enhancements/Trackers/TimeSplits/Timesplits.h86
-rw-r--r--mm/2s2h/Enhancements/Trackers/TimeSplits/TimesplitsSettings.cpp741
-rw-r--r--mm/2s2h/Enhancements/Trackers/TimeSplits/TimesplitsSettings.h10
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor.cpp15
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor.h3
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor_HookTable.h3
-rw-r--r--mm/2s2h/ShipUtils.cpp31
-rw-r--r--mm/2s2h/ShipUtils.h5
-rw-r--r--mm/src/code/z_parameter.c4
-rw-r--r--mm/src/overlays/actors/ovl_Boss_01/z_boss_01.c2
-rw-r--r--mm/src/overlays/actors/ovl_Boss_02/z_boss_02.c1
-rw-r--r--mm/src/overlays/actors/ovl_Boss_03/z_boss_03.c2
-rw-r--r--mm/src/overlays/actors/ovl_Boss_07/z_boss_07.c3
-rw-r--r--mm/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c1
19 files changed, 1634 insertions, 12 deletions
diff --git a/mm/2s2h/BenGui/BenGui.cpp b/mm/2s2h/BenGui/BenGui.cpp
index 4ebf42c56..f34b874f8 100644
--- a/mm/2s2h/BenGui/BenGui.cpp
+++ b/mm/2s2h/BenGui/BenGui.cpp
@@ -23,6 +23,8 @@
#include "Enhancements/Trackers/ItemTracker.h"
#include "Enhancements/Trackers/ItemTrackerSettings.h"
#include "Enhancements/Trackers/DisplayOverlay.h"
+#include "Enhancements/Trackers//TimeSplits/Timesplits.h"
+#include "Enhancements/Trackers/TimeSplits/TimesplitsSettings.h"
#include "BenMenu.h"
#include "BenMenuBar.h"
#include "DeveloperTools/HookDebugger.h"
@@ -56,6 +58,8 @@ std::shared_ptr<Rando::CheckTracker::SettingsWindow> mRandoCheckTrackerSettingsW
std::shared_ptr<ItemTrackerWindow> mItemTrackerWindow;
std::shared_ptr<ItemTrackerSettingsWindow> mItemTrackerSettingsWindow;
std::shared_ptr<DisplayOverlayWindow> mDisplayOverlayWindow;
+std::shared_ptr<TimesplitsWindow> mTimesplitsWindow;
+std::shared_ptr<TimesplitsSettingsWindow> mTimesplitsSettingsWindow;
UIWidgets::Colors GetMenuThemeColor() {
return mBenMenu->GetMenuThemeColor();
@@ -140,6 +144,13 @@ void SetupGuiElements() {
mDisplayOverlayWindow = std::make_shared<DisplayOverlayWindow>("gWindows.DisplayOverlay", "Display Overlay");
gui->AddGuiWindow(mDisplayOverlayWindow);
+ mTimesplitsWindow = std::make_shared<TimesplitsWindow>("gWindows.Timesplits", "Time Splits Window");
+ gui->AddGuiWindow(mTimesplitsWindow);
+
+ mTimesplitsSettingsWindow = std::make_shared<TimesplitsSettingsWindow>(
+ "gWindows.Timesplits.Settings", "Time Splits Settings Window", ImVec2(567, 97));
+ gui->AddGuiWindow(mTimesplitsSettingsWindow);
+
mNotificationWindow = std::make_shared<Notification::Window>("gWindows.Notifications", "Notifications Window");
gui->AddGuiWindow(mNotificationWindow);
mNotificationWindow->Show();
diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp
index 2cfc503e8..2faa343bf 100644
--- a/mm/2s2h/BenGui/BenMenu.cpp
+++ b/mm/2s2h/BenGui/BenMenu.cpp
@@ -1620,6 +1620,13 @@ void BenMenu::AddEnhancements() {
.CVar("gWindows.ItemTrackerSettings")
.WindowName("Item Tracker Settings");
+ // Timesplit Settings
+ path = { "Enhancements", "Time Splits", SECTION_COLUMN_1 };
+ AddSidebarEntry("Enhancements", "Time Splits", 1);
+ AddWidget(path, "Popout Timesplits Settings", WIDGET_WINDOW_BUTTON)
+ .CVar("gWindows.Timesplits.Settings")
+ .WindowName("Time Splits Settings Window");
+
// Audio Editor
path = { "Enhancements", "Audio Editor", SECTION_COLUMN_1 };
AddSidebarEntry("Enhancements", "Audio Editor", 1);
diff --git a/mm/2s2h/Enhancements/Trackers/DisplayOverlay.cpp b/mm/2s2h/Enhancements/Trackers/DisplayOverlay.cpp
index ea8f0e244..4da589058 100644
--- a/mm/2s2h/Enhancements/Trackers/DisplayOverlay.cpp
+++ b/mm/2s2h/Enhancements/Trackers/DisplayOverlay.cpp
@@ -18,22 +18,12 @@ void Boss07_Wrath_DeathCutscene(Boss07*, PlayState*);
float windowScale = 1.0f;
ImVec4 windowBG = ImVec4(0, 0, 0, 0.5f);
-
-std::string formatTimeDisplay(uint32_t value) {
- uint32_t sec = value / 10;
- uint32_t hh = sec / 3600;
- uint32_t mm = (sec - hh * 3600) / 60;
- uint32_t ss = sec - hh * 3600 - mm * 60;
- uint32_t ds = value % 10;
- return fmt::format("{}:{:0>2}:{:0>2}.{}", hh, mm, ss, ds);
-}
-
static constexpr ImVec4 tintColor = {};
void DrawInGameTimer(uint32_t timer, ImVec4 color = ImVec4(1, 1, 1, 1)) {
float windowScale = MAX(CVarGetFloat("gDisplayOverlay.Scale", 1.0f), 1.0f);
- std::string timerStr = formatTimeDisplay(timer);
+ std::string timerStr = Ship_FormatTimeDisplay(timer);
uint16_t textureIndex = 0;
for (const auto c : timerStr) {
if (c == ':' || c == '.') {
diff --git a/mm/2s2h/Enhancements/Trackers/TimeSplits/TimeSplitsActions.cpp b/mm/2s2h/Enhancements/Trackers/TimeSplits/TimeSplitsActions.cpp
new file mode 100644
index 000000000..9ead65be8
--- /dev/null
+++ b/mm/2s2h/Enhancements/Trackers/TimeSplits/TimeSplitsActions.cpp
@@ -0,0 +1,404 @@
+#include "Timesplits.h"
+#include "public/bridge/consolevariablebridge.h"
+#include "2s2h/GameInteractor/GameInteractor.h"
+#include "2s2h/ShipInit.hpp"
+#include "Context.h"
+#include "Window.h"
+#include "2s2h/BenGui/UIWidgets.hpp"
+#include <fstream>
+#include <filesystem>
+
+#include "assets/archives/icon_item_static/icon_item_static_yar.h"
+
+using json = nlohmann::json;
+
+extern "C" {
+#include "variables.h"
+#include "overlays/actors/ovl_Bg_Dy_Yoseizo/z_bg_dy_yoseizo.h"
+uint64_t GetUnixTimestamp();
+}
+
+#define CVAR_NAME "gSettings.TimeSplits.Enable"
+#define CVAR CVarGetInteger(CVAR_NAME, 0)
+
+nlohmann::json TimesplitObject_to_json(const TimesplitObject& split) {
+ return nlohmann::json{
+ { "splitId", split.splitId },
+ { "splitName", split.splitName },
+ { "splitCurrentTime", split.splitCurrentTime },
+ { "splitPreviousBest", split.splitPreviousBest },
+ { "splitStatus", SPLIT_INACTIVE },
+ { "splitType", split.splitType },
+ };
+}
+
+TimesplitObject json_to_TimesplitObject(const nlohmann::json& jsonSplit) {
+ TimesplitObject split;
+ split.splitId = jsonSplit["splitId"];
+ split.splitName = jsonSplit["splitName"].get<std::string>();
+ split.splitCurrentTime = jsonSplit["splitCurrentTime"];
+ split.splitPreviousBest = jsonSplit["splitPreviousBest"];
+ split.splitStatus = jsonSplit["splitStatus"];
+ if (jsonSplit.contains("splitType")) {
+ split.splitType = jsonSplit["splitType"];
+ } else {
+ split.splitType = SPLIT_TYPE_NORMAL;
+ }
+
+ return split;
+}
+
+uint32_t GetCurrentActiveSplit(std::vector<TimesplitObject> list) {
+ for (size_t i = 0; i < splitList.size(); i++) {
+ if (splitList[i].splitStatus == SPLIT_ACTIVE) {
+ return (uint32_t)i;
+ }
+ }
+ return -1;
+}
+
+TimesplitObject GetSplitObjectBySceneId(uint32_t sceneId) {
+ TimesplitObject splitObject;
+ for (auto& list : sceneObjectList) {
+ if (list.splitId == sceneId) {
+ splitObject = list;
+ splitObject.splitType = SPLIT_TYPE_SCENE;
+ break;
+ }
+ }
+ return splitObject;
+}
+
+TimesplitObject GetSplitObjectById(uint32_t itemId) {
+ TimesplitObject splitObject;
+ for (auto& list : splitObjectList) {
+ if (list.splitId == itemId) {
+ splitObject = list;
+ }
+ }
+ return splitObject;
+}
+
+void HandlePopUpContext(uint32_t popupId) {
+ if (shouldPopUpOpen && ImGui::BeginPopup("ItemSubMenu")) {
+ std::vector<uint32_t> itemList;
+
+ for (auto& item : itemSubMenuList) {
+ if (item.first == popupId) {
+ itemList = item.second;
+ break;
+ }
+ }
+
+ if (itemList.size() == 0) {
+ ImGui::EndPopup();
+ return;
+ }
+
+ uint32_t slotIndex = 0;
+ for (auto& list : itemList) {
+ SplitsPushImageButtonStyle();
+ if (ImGui::ImageButton(
+ std::to_string(list).c_str(),
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(GetItemImageById(list)),
+ GetItemImageSizeById(list) * 1.5f, ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0),
+ Ship_GetItemColorTint(list))) {
+ AddSplitEntryById(list);
+ ImGui::CloseCurrentPopup();
+ shouldPopUpOpen = false;
+ }
+ UIWidgets::Tooltip(GetSplitObjectById(list).splitName.c_str());
+ SplitsPopImageButtonStyle();
+
+ if (slotIndex == 4) {
+ slotIndex = -1;
+ } else {
+ ImGui::SameLine();
+ }
+ slotIndex++;
+ }
+ ImGui::EndPopup();
+ }
+}
+
+void HandleDragAndDrop(size_t i) {
+ if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
+ ImGui::SetDragDropPayload("SPLIT_DRAG", &i, sizeof(size_t));
+ ImGui::ImageButton(std::to_string(splitList[i].splitId).c_str(),
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? GetItemImageById(splitList[i].splitId)
+ : gPauseUnusedCursorTex),
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? GetItemImageSizeById(splitList[i].splitId)
+ : ImVec2(32.0f, 32.0f),
+ ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0),
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? Ship_GetItemColorTint(splitList[i].splitId)
+ : ImVec4(1, 1, 1, 1));
+ ImGui::EndDragDropSource();
+ }
+
+ if (ImGui::BeginDragDropTarget()) {
+ if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("SPLIT_DRAG")) {
+ size_t srcIndex = *(const size_t*)payload->Data;
+ if (srcIndex != i && srcIndex < splitList.size()) {
+ auto item = splitList[srcIndex];
+ splitList.erase(splitList.begin() + srcIndex);
+
+ if (srcIndex < i) {
+ i--;
+ }
+
+ splitList.insert(splitList.begin() + i, item);
+ }
+ }
+ ImGui::EndDragDropTarget();
+ }
+}
+
+void CheckSplitsCompleted(uint32_t index) {
+ if (index == splitList.size() - 1) {
+ gSaveContext.save.shipSaveInfo.fileCompletedAt = GetUnixTimestamp();
+ } else {
+ splitList[index + 1].splitStatus = SPLIT_ACTIVE;
+ }
+}
+
+void AddSplitEntryBySceneId(uint32_t sceneId) {
+ TimesplitObject splitObject = GetSplitObjectBySceneId(sceneId);
+
+ if (splitList.size() == 0) {
+ splitObject.splitStatus = SPLIT_ACTIVE;
+ }
+ splitList.push_back(splitObject);
+}
+
+void AddSplitEntryById(uint32_t itemId) {
+ TimesplitObject splitObject = GetSplitObjectById(itemId);
+
+ if (splitList.size() == 0) {
+ splitObject.splitStatus = SPLIT_ACTIVE;
+ }
+ splitList.push_back(splitObject);
+}
+
+void RemoveSplitEntry(uint32_t splitId, uint32_t index) {
+ uint32_t activeIndex = GetCurrentActiveSplit(splitList);
+
+ if (activeIndex != -1) {
+ if (splitList[activeIndex].splitId == splitId) {
+ CheckSplitsCompleted(activeIndex);
+ }
+ }
+
+ splitList.erase(splitList.begin() + index);
+}
+
+void SkipSplitEntry(uint32_t index) {
+ if (splitList[index].splitStatus == SPLIT_ACTIVE) {
+ CheckSplitsCompleted(index);
+ }
+ splitList[index].splitStatus = SPLIT_SKIPPED;
+}
+
+void UpdateSplitBests() {
+ for (auto& splits : splitList) {
+ if (splits.splitCurrentTime < splits.splitPreviousBest || splits.splitPreviousBest == 0) {
+ splits.splitPreviousBest = splits.splitCurrentTime;
+ }
+ }
+}
+
+void UpdateSplitStatusBySceneId(uint32_t sceneId) {
+ uint32_t activeIndex = GetCurrentActiveSplit(splitList);
+
+ if (activeIndex == -1) {
+ return;
+ }
+
+ if (splitList[activeIndex].splitType == SPLIT_TYPE_SCENE && splitList[activeIndex].splitId == sceneId) {
+ splitList[activeIndex].splitCurrentTime =
+ ((GetUnixTimestamp() - gSaveContext.save.shipSaveInfo.fileCreatedAt) / 100);
+ splitList[activeIndex].splitStatus = SPLIT_COMPLETE;
+
+ if (activeIndex == splitList.size() - 1) {
+ CheckSplitsCompleted(activeIndex);
+ } else {
+ splitList[activeIndex + 1].splitStatus = SPLIT_ACTIVE;
+ }
+ }
+}
+
+void UpdateSplitStatusById(uint32_t itemId) {
+ uint32_t activeIndex = GetCurrentActiveSplit(splitList);
+
+ if (activeIndex == -1) {
+ return;
+ }
+
+ if (splitList[activeIndex].splitId == itemId) {
+ splitList[activeIndex].splitCurrentTime =
+ ((GetUnixTimestamp() - gSaveContext.save.shipSaveInfo.fileCreatedAt) / 100);
+ splitList[activeIndex].splitStatus = SPLIT_COMPLETE;
+
+ if (activeIndex == splitList.size() - 1) {
+ CheckSplitsCompleted(activeIndex);
+ } else {
+ splitList[activeIndex + 1].splitStatus = SPLIT_ACTIVE;
+ }
+ }
+}
+
+void GetSplitByActorId(int16_t actorId, uint32_t specialType = 0) {
+ uint32_t activeIndex = GetCurrentActiveSplit(splitList);
+
+ switch (actorId) {
+ case ACTOR_BOSS_01:
+ UpdateSplitStatusById(SPLIT_KILLED_ODOLWA);
+ break;
+ case ACTOR_BOSS_02:
+ UpdateSplitStatusById(SPLIT_KILLED_TWINMOLD);
+ break;
+ case ACTOR_BOSS_03:
+ UpdateSplitStatusById(SPLIT_KILLED_GYORG);
+ break;
+ case ACTOR_BOSS_07:
+ UpdateSplitStatusById(SPLIT_KILLED_MAJORA);
+ break;
+ case ACTOR_BOSS_HAKUGIN:
+ UpdateSplitStatusById(SPLIT_KILLED_GOHT);
+ break;
+ case ACTOR_BG_DY_YOSEIZO:
+ case ACTOR_EN_ELFGRP:
+ if (specialType == GREAT_FAIRY_TYPE_MAGIC) {
+ if (gSaveContext.save.saveInfo.playerData.isMagicAcquired != true) {
+ UpdateSplitStatusById(SPLIT_SINGLE_MAGIC);
+ }
+ } else if (specialType == GREAT_FAIRY_TYPE_WISDOM) {
+ if (gSaveContext.save.saveInfo.playerData.isDoubleMagicAcquired != true) {
+ UpdateSplitStatusById(SPLIT_DOUBLE_MAGIC);
+ }
+ } else if (specialType == GREAT_FAIRY_TYPE_COURAGE) {
+ if (gSaveContext.save.saveInfo.playerData.doubleDefense != true) {
+ UpdateSplitStatusById(SPLIT_DOUBLE_DEFENSE);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (activeIndex == -1) {
+ return;
+ }
+}
+
+void SplitLoadComparisonList() {
+ std::string filename = Ship::Context::GetPathRelativeToAppDirectory("2S2HTimeSplitData.json");
+ json compareFile;
+ json listArray = nlohmann::json::array();
+
+ std::ifstream inputFile(filename);
+ if (inputFile.is_open()) {
+ inputFile >> compareFile;
+ inputFile.close();
+ }
+
+ if (compareFile.contains(savedLists[comparedIndex])) {
+ listArray = compareFile[savedLists[comparedIndex]];
+ comparisonList.clear();
+
+ for (auto& data : listArray) {
+ comparisonList.push_back(json_to_TimesplitObject(data));
+ }
+ }
+}
+
+void SplitSaveFileAction(uint32_t action, std::string listName) {
+ std::string filename = Ship::Context::GetPathRelativeToAppDirectory("2S2HTimeSplitData.json");
+ json saveFile;
+ json listArray = nlohmann::json::array();
+
+ std::ifstream inputFile(filename);
+ if (inputFile.is_open()) {
+ inputFile >> saveFile;
+ inputFile.close();
+ }
+
+ if (action == SPLIT_SAVE) {
+ for (auto& data : splitList) {
+ listArray.push_back(TimesplitObject_to_json(data));
+ }
+ saveFile[listName] = listArray;
+
+ std::ofstream outputFile(filename);
+ if (outputFile.is_open()) {
+ outputFile << saveFile.dump(4);
+ outputFile.close();
+ }
+ }
+
+ if (action == SPLIT_LOAD) {
+ if (saveFile.contains(listName)) {
+ listArray = saveFile[listName];
+ splitList.clear();
+
+ for (auto& data : listArray) {
+ splitList.push_back(json_to_TimesplitObject(data));
+ }
+ splitList[0].splitStatus = SPLIT_ACTIVE;
+ }
+ }
+
+ if (action == SPLIT_RETRIEVE) {
+ savedLists.clear();
+
+ for (auto& data : saveFile.items()) {
+ savedLists.push_back(data.key());
+ }
+ if (savedLists.size() == 0) {
+ savedLists.push_back("Create a List First");
+ }
+ }
+}
+
+void RegisterTimesplits() {
+ if (!std::filesystem::exists(Ship::Context::GetPathRelativeToAppDirectory("2S2HTimeSplitData.json"))) {
+ json initFile;
+ std::ofstream file(Ship::Context::GetPathRelativeToAppDirectory("2S2HTimeSplitData.json"));
+ file << initFile.dump(4);
+ file.close();
+ }
+
+ SplitSaveFileAction(SPLIT_RETRIEVE, "");
+
+ COND_HOOK(OnItemGive, CVAR, [](u8 item) {
+ if (item == ITEM_HEART_PIECE_2) {
+ item = ITEM_HEART_PIECE;
+ }
+ if (item == ITEM_LONGSHOT) {
+ item = ITEM_POTION_RED;
+ }
+ if (item >= ITEM_BOMBCHUS_20 && item <= ITEM_BOMBCHUS_5) {
+ item = ITEM_BOMBCHU;
+ }
+
+ UpdateSplitStatusById((uint32_t)item);
+ });
+
+ COND_HOOK(OnBottleContentsUpdate, CVAR, [](u8 item) { UpdateSplitStatusById((uint32_t)item); });
+ COND_HOOK(OnBossDefeated, CVAR, [](int16_t actorId) { GetSplitByActorId(actorId); });
+
+ COND_VB_SHOULD(VB_GIVE_ITEM_FROM_GREAT_FAIRY, CVAR, {
+ Actor* actor = va_arg(args, Actor*);
+
+ GetSplitByActorId(actor->id, GREAT_FAIRY_GET_TYPE(actor));
+ });
+
+ COND_VB_SHOULD(VB_GIVE_ITEM_FROM_STRAY_FAIRY_MANAGER, CVAR, {
+ Actor* actor = va_arg(args, Actor*);
+
+ GetSplitByActorId(actor->id, GREAT_FAIRY_GET_TYPE(actor));
+ });
+ COND_HOOK(OnSceneInit, CVAR, [](s8 sceneId, s8 spawnNum) { UpdateSplitStatusBySceneId(sceneId); });
+}
+
+static RegisterShipInitFunc initFunc(RegisterTimesplits, { CVAR_NAME });
diff --git a/mm/2s2h/Enhancements/Trackers/TimeSplits/Timesplits.cpp b/mm/2s2h/Enhancements/Trackers/TimeSplits/Timesplits.cpp
new file mode 100644
index 000000000..138201f2d
--- /dev/null
+++ b/mm/2s2h/Enhancements/Trackers/TimeSplits/Timesplits.cpp
@@ -0,0 +1,305 @@
+
+#include "Timesplits.h"
+#include "public/bridge/consolevariablebridge.h"
+#include "Context.h"
+#include "Window.h"
+#include "2s2h/BenGui/UIWidgets.hpp"
+
+extern "C" {
+#include "variables.h"
+uint64_t GetUnixTimestamp();
+}
+
+#include "ShipUtils.h"
+#include "interface/parameter_static/parameter_static.h"
+#include "assets/archives/icon_item_static/icon_item_static_yar.h"
+#include "GameInteractor/GameInteractor.h"
+
+#define BLANK_SPLIT "--:--:--.-"
+
+// ImVec4 Colors
+#define COLOR_WHITE ImVec4(1.00f, 1.00f, 1.00f, 1.00f)
+#define COLOR_GREY ImVec4(0.78f, 0.78f, 0.78f, 1.00f)
+#define COLOR_GREEN ImVec4(0.10f, 1.00f, 0.10f, 1.00f)
+#define COLOR_RED ImVec4(1.00f, 0.00f, 0.00f, 1.00f)
+#define COLOR_LIGHTGREEN ImVec4(0.42f, 0.86f, 0.38f, 1.0f)
+#define COLOR_LIGHTRED ImVec4(0.87f, 0.40f, 0.40f, 1.0f)
+
+std::vector<TimesplitObject> splitList;
+std::vector<TimesplitObject> comparisonList;
+ImGuiTableFlags tableColumnFlags = ImGuiTableColumnFlags_None;
+ImVec4 splitOpacity = { 0, 0, 0, 0.5f };
+
+uint32_t GetTotalTime() {
+ return ((GetUnixTimestamp() - gSaveContext.save.shipSaveInfo.fileCreatedAt) / 100);
+}
+
+SplitTextObject GetCurrentTimeTextDisplay(TimesplitObject split) {
+ SplitTextObject textDisplay;
+
+ switch (split.splitStatus) {
+ case SPLIT_INACTIVE:
+ case SPLIT_SKIPPED:
+ textDisplay.timeDisplay = 0;
+ textDisplay.colorDisplay = COLOR_GREY;
+ return textDisplay;
+ case SPLIT_ACTIVE:
+ textDisplay.timeDisplay = GetTotalTime();
+ textDisplay.colorDisplay = COLOR_WHITE;
+ return textDisplay;
+ case SPLIT_COMPLETE:
+ textDisplay.timeDisplay = split.splitCurrentTime;
+ if (split.splitCurrentTime > split.splitPreviousBest) {
+ textDisplay.colorDisplay = COLOR_RED;
+ } else if (split.splitCurrentTime == split.splitPreviousBest) {
+ textDisplay.colorDisplay = COLOR_WHITE;
+ } else if (split.splitCurrentTime < split.splitPreviousBest) {
+ textDisplay.colorDisplay = COLOR_GREEN;
+ }
+ if (split.splitPreviousBest == 0) {
+ textDisplay.colorDisplay = COLOR_WHITE;
+ }
+ return textDisplay;
+ default:
+ break;
+ }
+}
+
+SplitTextObject GetComparisonTimeTextDisplay(TimesplitObject split, TimesplitObject splitCompare) {
+ SplitTextObject textDisplay;
+
+ textDisplay.timeDisplay = splitCompare.splitPreviousBest;
+ if (splitCompare.splitPreviousBest == 0) {
+ textDisplay.timeDisplay = splitCompare.splitPreviousBest;
+ textDisplay.colorDisplay = COLOR_GREY;
+ } else {
+ if (split.splitStatus == SPLIT_ACTIVE) {
+ if (GetTotalTime() <= splitCompare.splitPreviousBest) {
+ textDisplay.colorDisplay = COLOR_LIGHTGREEN;
+ } else {
+ textDisplay.colorDisplay = COLOR_LIGHTRED;
+ }
+ } else {
+ if (split.splitCurrentTime <= splitCompare.splitPreviousBest) {
+ textDisplay.colorDisplay = COLOR_LIGHTRED;
+ } else {
+ textDisplay.colorDisplay = COLOR_LIGHTGREEN;
+ }
+ }
+ }
+
+ return textDisplay;
+}
+
+SplitTextObject GetTimeDiffTextDisplay(TimesplitObject split) {
+ SplitTextObject textDisplay;
+
+ switch (split.splitStatus) {
+ case SPLIT_INACTIVE:
+ case SPLIT_SKIPPED:
+ textDisplay.timeDisplay = split.splitPreviousBest;
+ textDisplay.colorDisplay = COLOR_GREY;
+ return textDisplay;
+ case SPLIT_ACTIVE:
+ if (GetTotalTime() > split.splitPreviousBest) {
+ textDisplay.timeDisplay = GetTotalTime() - split.splitPreviousBest;
+ textDisplay.colorDisplay = COLOR_RED;
+ } else if (GetTotalTime() == split.splitPreviousBest) {
+ textDisplay.timeDisplay = GetTotalTime();
+ textDisplay.colorDisplay = COLOR_WHITE;
+ } else if (GetTotalTime() < split.splitPreviousBest) {
+ textDisplay.timeDisplay = split.splitPreviousBest - GetTotalTime();
+ textDisplay.colorDisplay = COLOR_GREEN;
+ }
+ if (split.splitPreviousBest == 0) {
+ textDisplay.colorDisplay = COLOR_WHITE;
+ }
+ return textDisplay;
+ case SPLIT_COMPLETE:
+ if (split.splitCurrentTime > split.splitPreviousBest) {
+ textDisplay.timeDisplay = split.splitCurrentTime - split.splitPreviousBest;
+ textDisplay.colorDisplay = COLOR_RED;
+ } else if (split.splitCurrentTime == split.splitPreviousBest) {
+ textDisplay.timeDisplay = split.splitCurrentTime;
+ textDisplay.colorDisplay = COLOR_WHITE;
+ } else if (split.splitCurrentTime < split.splitPreviousBest) {
+ textDisplay.timeDisplay = split.splitPreviousBest - split.splitCurrentTime;
+ textDisplay.colorDisplay = COLOR_GREEN;
+ }
+ if (split.splitPreviousBest == 0) {
+ textDisplay.colorDisplay = COLOR_WHITE;
+ }
+ return textDisplay;
+ default:
+ break;
+ }
+}
+
+void TableCellCenteredText(ImVec4 color, const char* text) {
+ float textHeight = ImGui::GetTextLineHeight();
+ float offsetY = (32.0f - textHeight + 10.0f) * 0.5f;
+ ImGui::SetCursorPosY(ImGui::GetCursorPosY() + offsetY);
+ ImGui::TextColored(color, text);
+}
+
+void SplitsPushImageButtonStyle() {
+ ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 1.0f, 1.0f, 0.0f));
+ ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 1.0f, 1.0f, 0.2f));
+ ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 1.0f, 1.0f, 0.1f));
+}
+
+void SplitsPopImageButtonStyle() {
+ ImGui::PopStyleColor(3);
+}
+
+void DrawSplitsList(bool isMain) {
+ float columnSizeMultiplier = isMain ? 1.0f : 1.5f;
+ ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0));
+ ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 7.0f);
+ ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(10, 0));
+
+ if (isMain) {
+ ImGui::Begin("Timesplits", nullptr,
+ ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoDocking |
+ ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollWithMouse |
+ ImGuiWindowFlags_NoScrollbar);
+ }
+
+ if (ImGui::BeginChild("SplitChild", ImVec2(0, 0), true, ImGuiWindowFlags_NoScrollbar)) {
+ if (ImGui::BeginTable("Splits", 5,
+ ImGuiTableFlags_ScrollY | ImGuiTableFlags_Hideable | ImGuiTableFlags_Reorderable)) {
+ ImGui::TableSetupColumn("Item Image",
+ ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHeaderLabel,
+ (28.0f * columnSizeMultiplier));
+ ImGui::TableSetupColumn("Item Name");
+ ImGui::TableSetupColumn("Current Time");
+ ImGui::TableSetupColumn("+/-");
+ ImGui::TableSetupColumn("Previous Best ");
+ if (tableColumnFlags != ImGuiTableColumnFlags_None) {
+ ImGui::TableSetupScrollFreeze(0, 1);
+ ImGui::TableHeadersRow();
+ }
+
+ for (size_t i = 0; i < splitList.size(); i++) {
+ ImGui::PushID(i);
+
+ // Item Image Column
+ ImGui::TableNextColumn();
+
+ if (CVarGetInteger("gSettings.TimeSplits.Highlight", 0) && splitList[i].splitStatus == SPLIT_ACTIVE) {
+ ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, IM_COL32(47, 79, 90, 255));
+ }
+
+ SplitsPushImageButtonStyle();
+ if (ImGui::ImageButton(
+ std::to_string(i).c_str(),
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? GetItemImageById(splitList[i].splitId)
+ : gPauseUnusedCursorTex),
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? GetItemImageSizeById(splitList[i].splitId)
+ : ImVec2(32.0f, 32.0f),
+ ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0),
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? Ship_GetItemColorTint(splitList[i].splitId)
+ : ImVec4(1, 1, 1, 1))) {
+ SkipSplitEntry(i);
+ };
+ SplitsPopImageButtonStyle();
+
+ // Item Name Column
+ ImGui::TableNextColumn();
+ TableCellCenteredText(COLOR_WHITE, splitList[i].splitName.c_str());
+
+ // Current Time Column
+ ImGui::TableNextColumn();
+ TableCellCenteredText(
+ GetCurrentTimeTextDisplay(splitList[i]).colorDisplay,
+ !gPlayState ? BLANK_SPLIT
+ : Ship_FormatTimeDisplay(GetCurrentTimeTextDisplay(splitList[i]).timeDisplay).c_str());
+
+ // +/- Column
+ ImGui::TableNextColumn();
+ TableCellCenteredText(
+ GetTimeDiffTextDisplay(splitList[i]).colorDisplay,
+ !gPlayState ? BLANK_SPLIT
+ : Ship_FormatTimeDisplay(GetTimeDiffTextDisplay(splitList[i]).timeDisplay).c_str());
+ if (CVarGetInteger("gSettings.TimeSplits.Compare", 0) && comparisonList.size() != 0) {
+ !gPlayState ? ImGui::TextColored(COLOR_WHITE, BLANK_SPLIT)
+ : i < comparisonList.size()
+ ? ImGui::TextColored(
+ GetComparisonTimeTextDisplay(splitList[i], comparisonList[i]).colorDisplay,
+ Ship_FormatTimeDisplay(
+ GetComparisonTimeTextDisplay(splitList[i], comparisonList[i]).timeDisplay)
+ .c_str())
+ : ImGui::TextColored(COLOR_GREY, "No Data");
+ }
+
+ // Previous Best Column
+ ImGui::TableNextColumn();
+ TableCellCenteredText(COLOR_WHITE, Ship_FormatTimeDisplay(splitList[i].splitPreviousBest).c_str());
+ if (CVarGetInteger("gSettings.TimeSplits.Compare", 0) && comparisonList.size() != 0) {
+ ImGui::TextColored(COLOR_GREY,
+ i < comparisonList.size()
+ ? Ship_FormatTimeDisplay(comparisonList[i].splitPreviousBest).c_str()
+ : "No Data");
+ }
+
+ ImGui::PopID();
+
+ if (CVarGetInteger("gSettings.TimeSplits.Follow", 0) && splitList[i].splitStatus == SPLIT_ACTIVE) {
+ ImGui::SetScrollHereY();
+ }
+ }
+
+ ImGui::EndTable();
+ }
+ }
+ ImGui::EndChild();
+
+ if (isMain) {
+ ImGui::End();
+ }
+
+ ImGui::PopStyleVar(2);
+ ImGui::PopStyleColor(1);
+}
+
+void UpdateSplitSettings(uint32_t settingName) {
+ switch (settingName) {
+ case SPLIT_HEADERS:
+ tableColumnFlags = CVarGetInteger("gSettings.TimeSplits.ShowHeaders", 0)
+ ? ImGuiTableColumnFlags_NoHeaderLabel
+ : ImGuiTableColumnFlags_None;
+ break;
+ case SPLIT_OPACITY:
+ splitOpacity.w = CVarGetInteger("gSettings.TimeSplits.Opacity", 0) ? 0 : 0.5f;
+ break;
+ case SPLIT_COMPARE:
+ if (CVarGetInteger("gSettings.TimeSplits.Compare", 0)) {
+ SplitSaveFileAction(SPLIT_RETRIEVE, "");
+ if (savedLists.size() != 0) {
+ SplitLoadComparisonList();
+ }
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+void TimesplitsWindow::Draw() {
+ if (!CVarGetInteger("gSettings.TimeSplits.Enable", 0)) {
+ return;
+ }
+ ImGui::PushStyleColor(ImGuiCol_WindowBg, splitOpacity);
+ DrawSplitsList(true);
+ ImGui::PopStyleColor();
+}
+
+void TimesplitsWindow::InitElement() {
+ UpdateSplitSettings(SPLIT_HEADERS);
+ UpdateSplitSettings(SPLIT_OPACITY);
+ UpdateSplitSettings(SPLIT_COMPARE);
+}
+
+// void TimesplitsWindow::DrawElement() {
+// }
diff --git a/mm/2s2h/Enhancements/Trackers/TimeSplits/Timesplits.h b/mm/2s2h/Enhancements/Trackers/TimeSplits/Timesplits.h
new file mode 100644
index 000000000..20a7f2bc6
--- /dev/null
+++ b/mm/2s2h/Enhancements/Trackers/TimeSplits/Timesplits.h
@@ -0,0 +1,86 @@
+#include "GuiWindow.h"
+#include <vector>
+#include <map>
+
+class TimesplitsWindow : public Ship::GuiWindow {
+ public:
+ using GuiWindow::GuiWindow;
+
+ void InitElement() override;
+ void DrawElement() override{};
+ void Draw() override;
+ void UpdateElement() override{};
+};
+
+typedef enum ExtendedSplitIds {
+ SPLIT_KILLED_ODOLWA = 256,
+ SPLIT_KILLED_GOHT,
+ SPLIT_KILLED_GYORG,
+ SPLIT_KILLED_TWINMOLD,
+ SPLIT_KILLED_MAJORA,
+ SPLIT_SINGLE_MAGIC,
+ SPLIT_DOUBLE_MAGIC,
+ SPLIT_DOUBLE_DEFENSE,
+};
+
+typedef enum SplitSettings {
+ SPLIT_HEADERS,
+ SPLIT_OPACITY,
+ SPLIT_HIGHLIGHT,
+ SPLIT_COMPARE,
+};
+
+typedef enum SplitFileActions {
+ SPLIT_SAVE,
+ SPLIT_LOAD,
+ SPLIT_RETRIEVE,
+};
+
+typedef enum SplitStatus { SPLIT_INACTIVE, SPLIT_ACTIVE, SPLIT_COMPLETE, SPLIT_SKIPPED };
+typedef enum SplitTypes { SPLIT_TYPE_NORMAL, SPLIT_TYPE_SCENE };
+
+typedef struct {
+ uint32_t splitId;
+ std::string splitName;
+ uint32_t splitCurrentTime;
+ uint32_t splitPreviousBest;
+ uint8_t splitStatus;
+ uint32_t splitType;
+} TimesplitObject;
+
+typedef struct {
+ uint32_t timeDisplay;
+ ImVec4 colorDisplay;
+} SplitTextObject;
+
+typedef struct {
+ uint32_t startIndex;
+ uint32_t endIndex;
+} IndexRangeObject;
+
+extern std::vector<TimesplitObject> splitList;
+extern std::vector<TimesplitObject> comparisonList;
+extern std::vector<TimesplitObject> splitObjectList;
+extern std::vector<TimesplitObject> sceneObjectList;
+extern std::vector<std::string> savedLists;
+extern std::map<uint32_t, std::vector<uint32_t>> itemSubMenuList;
+extern std::map<uint32_t, ImVec4> songColorMap;
+extern uint32_t comparedIndex;
+extern bool shouldPopUpOpen;
+extern TimesplitObject GetSplitObjectById(uint32_t itemId);
+extern ImVec2 GetItemImageSizeById(uint32_t itemId);
+extern void TableCellCenteredText(ImVec4 color, const char* text);
+extern const char* GetItemImageById(uint32_t itemId);
+extern void SplitsPushImageButtonStyle();
+extern void SplitsPopImageButtonStyle();
+extern void HandlePopUpContext(uint32_t popupId);
+extern void HandleDragAndDrop(size_t i);
+extern void UpdateSplitBests();
+extern void UpdateSplitSettings(uint32_t settingName);
+extern void SkipSplitEntry(uint32_t index);
+extern void AddSplitEntryBySceneId(uint32_t sceneId);
+extern void AddSplitEntryById(uint32_t itemId);
+extern void RemoveSplitEntry(uint32_t splitId, uint32_t index);
+extern void SplitSaveFileAction(uint32_t action, std::string listName);
+extern void DrawSplitsList(bool isMain);
+extern void SplitLoadComparisonList();
diff --git a/mm/2s2h/Enhancements/Trackers/TimeSplits/TimesplitsSettings.cpp b/mm/2s2h/Enhancements/Trackers/TimeSplits/TimesplitsSettings.cpp
new file mode 100644
index 000000000..0e2978b2f
--- /dev/null
+++ b/mm/2s2h/Enhancements/Trackers/TimeSplits/TimesplitsSettings.cpp
@@ -0,0 +1,741 @@
+
+#include "TimesplitsSettings.h"
+#include "Timesplits.h"
+#include "public/bridge/consolevariablebridge.h"
+#include "Context.h"
+#include "Window.h"
+#include "2s2h/BenGui/UIWidgets.hpp"
+
+extern "C" {
+#include "variables.h"
+}
+
+#include "interface/parameter_static/parameter_static.h"
+#include "assets/archives/icon_item_24_static/icon_item_24_static_yar.h"
+#include "assets/archives/icon_item_static/icon_item_static_yar.h"
+#include "assets/interface/icon_item_dungeon_static/icon_item_dungeon_static.h"
+#include "assets/interface/icon_item_field_static/icon_item_field_static.h"
+#include "assets/objects/gameplay_keep/gameplay_keep.h"
+#include "GameInteractor/GameInteractor.h"
+
+IndexRangeObject sceneRange = { 0, 98 };
+uint32_t sceneFilterIndex = 0;
+
+std::vector<TimesplitObject> sceneObjectList = {
+ // Misc. Areas
+ { SCENE_LOST_WOODS, "Lost Woods (Intro)" },
+ { SCENE_OPENINGDAN, "Before Clock Town" },
+ { SCENE_KAKUSIANA, "Lone Peak Shrine & Grottos" },
+ { SCENE_YOUSEI_IZUMI, "Fairy's Fountain" },
+ { SCENE_KYOJINNOMA, "Giants' Chamber" },
+ // Clock Town Areas
+ { SCENE_TOWN, "East Clock Town" },
+ { SCENE_ICHIBA, "West Clock Town" },
+ { SCENE_BACKTOWN, "North Clock Town" },
+ { SCENE_CLOCKTOWER, "South Clock Town" },
+ { SCENE_INSIDETOWER, "Clock Tower Interior" },
+ { SCENE_OKUJOU, "Clock Tower Rooftop" },
+ { SCENE_ALLEY, "Laundry Pool" },
+ { SCENE_YADOYA, "Stock Pot Inn" },
+ { SCENE_DOUJOU, "Swordsman's School" },
+ { SCENE_AYASHIISHOP, "Curiosity Shop" },
+ { SCENE_8ITEMSHOP, "Trading Post" },
+ { SCENE_BOMYA, "Bomb Shop" },
+ { SCENE_BOWLING, "Honey & Darling's Shop" },
+ { SCENE_SONCHONOIE, "The Mayor's Residence" },
+ { SCENE_DEKUTES, "Deku Scrub Playground" },
+ { SCENE_TAKARAKUJI, "Lottery Shop" },
+ { SCENE_TAKARAYA, "Treasure Chest Shop" },
+ { SCENE_MILK_BAR, "Milk Bar" },
+ { SCENE_SYATEKI_MIZU, "Town Shooting Gallery" },
+ { SCENE_POSTHOUSE, "Post Office" },
+ // Termina Field Region
+ { SCENE_00KEIKOKU, "Termina Field" },
+ { SCENE_F01, "Romani Ranch" },
+ { SCENE_TENMON_DAI, "Astral Observatory" },
+ { SCENE_ROMANYMAE, "Milk Road" },
+ { SCENE_KOEPONARACE, "Gorman Track" },
+ { SCENE_OMOYA, "Mama's House & Barn" },
+ { SCENE_F01_B, "Doggy Racetrack" },
+ { SCENE_F01C, "Cucco Shack" },
+ // Woodfall Region
+ { SCENE_24KEMONOMITI, "Road to Southern Swamp" },
+ { SCENE_20SICHITAI, "Southern Swamp (poison)" },
+ { SCENE_20SICHITAI2, "Southern Swamp (Clear)" },
+ { SCENE_22DEKUCITY, "Deku Palace" },
+ { SCENE_21MITURINMAE, "Woodfall" },
+ { SCENE_MITURIN, "Woodfall Temple" },
+ { SCENE_MITURIN_BS, "Odolwa's Lair" },
+ { SCENE_26SARUNOMORI, "Woods of Mystery" },
+ { SCENE_WITCH_SHOP, "Magic Hags' Potion Shop" },
+ { SCENE_MAP_SHOP, "Tourist Information" },
+ { SCENE_DANPEI, "Deku Shrine" },
+ { SCENE_DEKU_KING, "Deku King's Chamber" },
+ { SCENE_SYATEKI_MORI, "Swamp Shooting Gallery" },
+ { SCENE_KINSTA1, "Swamp Spider House" },
+ // Snowhead Region
+ { SCENE_13HUBUKINOMITI, "Path to Mountain Village" },
+ { SCENE_10YUKIYAMANOMURA, "Mountain Village (winter)" },
+ { SCENE_10YUKIYAMANOMURA2, "Mountain Village (spring)" },
+ { SCENE_17SETUGEN, "Path to Goron Village (winter)" },
+ { SCENE_17SETUGEN2, "Path to Goron Village (spring)" },
+ { SCENE_11GORONNOSATO, "Goron Village (winter)" },
+ { SCENE_11GORONNOSATO2, "Goron Village (spring)" },
+ { SCENE_GORONRACE, "Goron Racetrack" },
+ { SCENE_14YUKIDAMANOMITI, "Path to Snowhead" },
+ { SCENE_12HAKUGINMAE, "Snowhead" },
+ { SCENE_HAKUGIN, "Snowhead Temple" },
+ { SCENE_HAKUGIN_BS, "Goht's Lair" },
+ { SCENE_KAJIYA, "Mountain Smithy" },
+ { SCENE_GORON_HAKA, "Goron Graveyard" },
+ { SCENE_16GORON_HOUSE, "Goron Shrine" },
+ { SCENE_GORONSHOP, "Goron Shop" },
+ // Great Bay Region
+ { SCENE_30GYOSON, "Great Bay Coast" },
+ { SCENE_31MISAKI, "Zora Cape" },
+ { SCENE_SINKAI, "Pinnacle Rock" },
+ { SCENE_KAIZOKU, "Pirates' Fortress" },
+ { SCENE_TORIDE, "Pirates' Fortress Moat" },
+ { SCENE_PIRATE, "Pirates' Fortress Interior" },
+ { SCENE_SEA, "Great Bay Temple" },
+ { SCENE_SEA_BS, "Gyorg's Lair" },
+ { SCENE_35TAKI, "Waterfall Rapids" },
+ { SCENE_FISHERMAN, "Fisherman's Hut" },
+ { SCENE_LABO, "Marine Research Lab" },
+ { SCENE_KINDAN2, "Oceanside Spider House" },
+ { SCENE_33ZORACITY, "Zora Hall" },
+ // Ikana Region
+ { SCENE_IKANAMAE, "Road to Ikana" },
+ { SCENE_IKANA, "Ikana Canyon" },
+ { SCENE_BOTI, "Ikana Graveyard" },
+ { SCENE_HAKASHITA, "Beneath the Graveyard" },
+ { SCENE_F40, "Stone Tower" },
+ { SCENE_F41, "Inverted Stone Tower" },
+ { SCENE_INISIE_N, "Stone Tower Temple" },
+ { SCENE_INISIE_BS, "Twinmold's Lair" },
+ { SCENE_INISIE_R, "Inverted Stone Tower Temple" },
+ { SCENE_CASTLE, "Ancient Castle of Ikana" },
+ { SCENE_IKNINSIDE, "Igos du Ikana's Lair" },
+ { SCENE_DANPEI2TEST, "Beneath Graveyard and Dampe's House" },
+ { SCENE_REDEAD, "Beneath the Well" },
+ { SCENE_SECOM, "Sakon's Hideout" },
+ { SCENE_TOUGITES, "Ghost Hut" },
+ { SCENE_MUSICHOUSE, "Music Box House" },
+ { SCENE_RANDOM, "Secret Shrine" },
+ // The Moon
+ { SCENE_SOUGEN, "The Moon" },
+ { SCENE_LAST_DEKU, "Moon Deku Trial" },
+ { SCENE_LAST_GORON, "Moon Goron Trial" },
+ { SCENE_LAST_ZORA, "Moon Zora Trial" },
+ { SCENE_LAST_LINK, "Moon Link Trial" },
+ { SCENE_LAST_BS, "Majora's Lair" },
+};
+
+std::vector<TimesplitObject> splitObjectList = {
+ // clang-format off
+ { ITEM_HEART_PIECE, "Piece of Heart" },
+
+ // Inventory
+ { ITEM_OCARINA_OF_TIME, "Ocarina of Time" },
+ { ITEM_BOW, "Bow" },
+ { ITEM_ARROW_FIRE, "Fire Arrow" },
+ { ITEM_ARROW_ICE, "Ice Arrow" },
+ { ITEM_ARROW_LIGHT, "Light Arrow" },
+ { ITEM_MOONS_TEAR, "Moon's Tear" },
+ { ITEM_BOMB, "Bomb" },
+ { ITEM_BOMBCHU, "Bombchu" },
+ { ITEM_DEKU_STICK, "Deku Stick" },
+ { ITEM_DEKU_NUT, "Deku Nut" },
+ { ITEM_MAGIC_BEANS, "Magic Bean" },
+ { ITEM_ROOM_KEY, "Room Key" },
+ { ITEM_POWDER_KEG, "Powder Keg" },
+ { ITEM_PICTOGRAPH_BOX, "Pictograph" },
+ { ITEM_LENS_OF_TRUTH, "Lens of Truth" },
+ { ITEM_HOOKSHOT, "Hookshot" },
+ { ITEM_SWORD_GREAT_FAIRY, "Great Fairy Sword" },
+ { ITEM_LETTER_TO_KAFEI, "Letter to Kafei" },
+ { ITEM_BOTTLE, "Empty Bottle" },
+
+ // Masks
+ { ITEM_MASK_POSTMAN, "Postman's Hat" },
+ { ITEM_MASK_ALL_NIGHT, "All-Night Mask" },
+ { ITEM_MASK_BLAST, "Blast Mask" },
+ { ITEM_MASK_STONE, "Stone Mask" },
+ { ITEM_MASK_GREAT_FAIRY, "Great Fairy's Mask" },
+ { ITEM_MASK_DEKU, "Deku Mask" },
+ { ITEM_MASK_KEATON, "Keaton Mask" },
+ { ITEM_MASK_BREMEN, "Bremen Mask" },
+ { ITEM_MASK_BUNNY, "Bunny Hood" },
+ { ITEM_MASK_DON_GERO, "Don Gero's Mask" },
+ { ITEM_MASK_SCENTS, "Mask of Scents" },
+ { ITEM_MASK_GORON, "Goron Mask" },
+ { ITEM_MASK_ROMANI, "Romani's Mask" },
+ { ITEM_MASK_CIRCUS_LEADER, "Circus Leader's Mask" },
+ { ITEM_MASK_KAFEIS_MASK, "Kafei's Mask" },
+ { ITEM_MASK_COUPLE, "Couple's Mask" },
+ { ITEM_MASK_TRUTH, "Mask of Truth" },
+ { ITEM_MASK_ZORA, "Zora Mask" },
+ { ITEM_MASK_KAMARO, "Kamaro's Mask" },
+ { ITEM_MASK_GIBDO, "Gibdo Mask" },
+ { ITEM_MASK_GARO, "Garo's Mask" },
+ { ITEM_MASK_CAPTAIN, "Captain's Hat" },
+ { ITEM_MASK_GIANT, "Giant's Mask" },
+ { ITEM_MASK_FIERCE_DEITY, "Fierce Deity's Mask" },
+
+ // Songs
+ { ITEM_SONG_TIME, "Song of Time" },
+ { ITEM_SONG_HEALING, "Song of Healing" },
+ { ITEM_SONG_EPONA, "Epona's Song" },
+ { ITEM_SONG_SOARING, "Song of Soaring" },
+ { ITEM_SONG_STORMS, "Song of Storms" },
+ { ITEM_SONG_SONATA, "Sonata of Awakening" },
+ { ITEM_SONG_LULLABY, "Goron Lullaby" },
+ { ITEM_SONG_NOVA, "New Wave Bossa Nova" },
+ { ITEM_SONG_ELEGY, "Elegy of Emptiness" },
+ { ITEM_SONG_OATH, "Oath to Order" },
+
+ // Quest
+ { ITEM_REMAINS_ODOLWA, "Odolwa's Remains" },
+ { ITEM_REMAINS_GOHT, "Goht's Remains" },
+ { ITEM_REMAINS_GYORG, "Gyorg's Remains" },
+ { ITEM_REMAINS_TWINMOLD, "Twinmold's Remains" },
+ { ITEM_SWORD_KOKIRI, "Kokiri Sword" },
+ { ITEM_SHIELD_HERO, "Hero's Shield" },
+ { ITEM_WALLET_ADULT, "Adult Wallet" },
+ { ITEM_BOMBERS_NOTEBOOK, "Bombers' Notebook" },
+
+ // Quest II
+ { SPLIT_SINGLE_MAGIC, "Single Magic" },
+ { SPLIT_DOUBLE_DEFENSE, "Double Defense" },
+
+ // Dungeon Bosses
+ { SPLIT_KILLED_ODOLWA, "Odolwa" },
+ { SPLIT_KILLED_GOHT, "Goht" },
+ { SPLIT_KILLED_GYORG, "Gyorg" },
+ { SPLIT_KILLED_TWINMOLD, "Twinmold" },
+ { SPLIT_KILLED_MAJORA, "Majora" },
+
+
+ // Upgrade Items
+ { ITEM_QUIVER_30, "Quiver" },
+ { ITEM_QUIVER_40, "Large Quiver" },
+ { ITEM_QUIVER_50, "Largest Quiver" },
+ { ITEM_BOMB_BAG_20, "Bomb Bag" },
+ { ITEM_BOMB_BAG_30, "Big Bomb Bag" },
+ { ITEM_BOMB_BAG_40, "Biggest Bomb Bag" },
+ { ITEM_SONG_LULLABY_INTRO, "Goron Lullaby Intro" },
+ { ITEM_SWORD_RAZOR, "Razor Sword" },
+ { ITEM_SWORD_GILDED, "Gilded Sword" },
+ { ITEM_SHIELD_MIRROR, "Mirror Shield" },
+ { ITEM_WALLET_GIANT, "Giant Wallet" },
+ { SPLIT_DOUBLE_MAGIC, "Double Magic" },
+
+ // Trade Items
+ { ITEM_DEED_LAND, "Land Title Deed" },
+ { ITEM_DEED_SWAMP, "Swamp Title Deed" },
+ { ITEM_DEED_MOUNTAIN, "Mountain Title Deed" },
+ { ITEM_DEED_OCEAN, "Ocean Title Deed" },
+ { ITEM_LETTER_MAMA, "Letter to Mama" },
+ { ITEM_PENDANT_OF_MEMORIES, "Pendant of Memories" },
+
+ // Bottled Items
+ { ITEM_POTION_RED, "Red Potion" },
+ { ITEM_POTION_GREEN, "Green Potion" },
+ { ITEM_POTION_BLUE, "Blue Potion" },
+ { ITEM_FAIRY, "Bottled Fairy" },
+ { ITEM_DEKU_PRINCESS, "Bottled Deku Princess" },
+ { ITEM_MILK_BOTTLE, "Bottled Full Milk" },
+ { ITEM_MILK_HALF, "Bottled Half Milk" },
+ { ITEM_FISH, "Bottled Fish" },
+ { ITEM_BUG, "Bottled Bug" },
+ { ITEM_BLUE_FIRE, "Bottled Blue Fire" },
+ { ITEM_POE, "Bottled Poe" },
+ { ITEM_BIG_POE, "Bottled Big Poe" },
+ { ITEM_SPRING_WATER, "Spring Water" },
+ { ITEM_HOT_SPRING_WATER, "Hot Spring Water" },
+ { ITEM_ZORA_EGG, "Bottled Zora Egg" },
+ { ITEM_GOLD_DUST, "Bottled Gold Dust" },
+ { ITEM_MUSHROOM, "Bottled Mushroom" },
+ { ITEM_SEAHORSE, "Bottled Seahorse" },
+ { ITEM_CHATEAU, "Chateau Romani" },
+
+ // clang-format on
+};
+
+std::map<uint32_t, std::vector<uint32_t>> itemSubMenuList = {
+ // clang-format off
+ { ITEM_BOW, { ITEM_BOW, ITEM_QUIVER_40, ITEM_QUIVER_50 } },
+ { ITEM_BOMB, { ITEM_BOMB_BAG_20, ITEM_BOMB_BAG_30, ITEM_BOMB_BAG_40 } },
+ { ITEM_MOONS_TEAR, { ITEM_MOONS_TEAR, ITEM_DEED_LAND, ITEM_DEED_SWAMP, ITEM_DEED_MOUNTAIN, ITEM_DEED_OCEAN } },
+ { ITEM_ROOM_KEY, { ITEM_ROOM_KEY, ITEM_LETTER_MAMA } },
+ { ITEM_LETTER_TO_KAFEI, { ITEM_LETTER_TO_KAFEI, ITEM_PENDANT_OF_MEMORIES } },
+ { ITEM_BOTTLE, { ITEM_POTION_RED, ITEM_POTION_GREEN, ITEM_POTION_BLUE, ITEM_FAIRY, ITEM_DEKU_PRINCESS,
+ ITEM_MILK_BOTTLE, ITEM_MILK_HALF, ITEM_FISH, ITEM_BUG, ITEM_BLUE_FIRE, ITEM_POE,
+ ITEM_BIG_POE, ITEM_SPRING_WATER, ITEM_HOT_SPRING_WATER, ITEM_ZORA_EGG, ITEM_GOLD_DUST,
+ ITEM_MUSHROOM, ITEM_SEAHORSE, ITEM_CHATEAU } },
+ { ITEM_SONG_LULLABY, { ITEM_SONG_LULLABY_INTRO, ITEM_SONG_LULLABY } },
+ { ITEM_SWORD_KOKIRI, { ITEM_SWORD_KOKIRI, ITEM_SWORD_RAZOR, ITEM_SWORD_GILDED } },
+ { ITEM_SHIELD_HERO, { ITEM_SHIELD_HERO, ITEM_SHIELD_MIRROR } },
+ { ITEM_WALLET_ADULT, { ITEM_WALLET_ADULT, ITEM_WALLET_GIANT } },
+ { SPLIT_SINGLE_MAGIC, { SPLIT_SINGLE_MAGIC, SPLIT_DOUBLE_MAGIC } },
+};
+
+static std::vector<const char*> sceneAreaNameMap = {
+ "All Scenes",
+ "Miscellaneous",
+ "Clock Town",
+ "Termina Field & Romani's Ranch",
+ "Woodfall Region",
+ "Snowhead Region",
+ "Great Bay Region",
+ "Ikana Region",
+ "The Moon",
+};
+
+static std::unordered_map<const char*, IndexRangeObject> sceneAreaRangeMap = {
+ { "All Scenes", { SCENE_LOST_WOODS, SCENE_LAST_BS } },
+ { "Miscellaneous", { SCENE_LOST_WOODS, SCENE_KYOJINNOMA } },
+ { "Clock Town", { SCENE_TOWN, SCENE_POSTHOUSE } },
+ { "Termina Field & Romani's Ranch", { SCENE_00KEIKOKU, SCENE_F01C } },
+ { "Woodfall Region", { SCENE_24KEMONOMITI, SCENE_KINSTA1 } },
+ { "Snowhead Region", { SCENE_13HUBUKINOMITI, SCENE_GORONSHOP } },
+ { "Great Bay Region", { SCENE_30GYOSON, SCENE_33ZORACITY } },
+ { "Ikana Region", { SCENE_IKANAMAE, SCENE_RANDOM } },
+ { "The Moon", { SCENE_SOUGEN, SCENE_LAST_BS } },
+};
+// clang-format on
+
+IndexRangeObject GetSceneIndexRange(uint32_t start, uint32_t end) {
+ IndexRangeObject setRange = { 0, 0 };
+
+ for (size_t i = 0; i < sceneObjectList.size(); i++) {
+ if (sceneObjectList[i].splitId == start) {
+ setRange.startIndex = static_cast<int>(i);
+ }
+ if (sceneObjectList[i].splitId == end) {
+ setRange.endIndex = static_cast<int>(i);
+ }
+ }
+
+ return setRange;
+}
+
+IndexRangeObject GetIndexRange(uint32_t start, uint32_t end) {
+ IndexRangeObject setRange = { 0, 0 };
+
+ for (size_t i = 0; i < splitObjectList.size(); i++) {
+ if (splitObjectList[i].splitId == start) {
+ setRange.startIndex = static_cast<int>(i);
+ }
+ if (splitObjectList[i].splitId == end) {
+ setRange.endIndex = static_cast<int>(i);
+ }
+ }
+
+ return setRange;
+}
+
+bool shouldPopUpOpen = false;
+uint32_t popupItem = 0;
+const char* popupTooltip = "";
+IndexRangeObject range = GetIndexRange((uint32_t)ITEM_OCARINA_OF_TIME, (uint32_t)ITEM_BOTTLE);
+const char* listName = "Inventory";
+uint32_t listColumns = 6;
+TexturePtr itemImage;
+std::string listInputName;
+std::vector<std::string> savedLists;
+uint32_t selectedIndex = 0;
+uint32_t comparedIndex = 0;
+
+const char* GetItemImageById(uint32_t itemId) {
+ switch (itemId) {
+ case SPLIT_KILLED_ODOLWA:
+ case SPLIT_KILLED_GOHT:
+ case SPLIT_KILLED_GYORG:
+ case SPLIT_KILLED_TWINMOLD:
+ case SPLIT_KILLED_MAJORA:
+ return gDungeonMapSkullTex;
+ case SPLIT_SINGLE_MAGIC:
+ return (const char*)gItemIcons[ITEM_MAGIC_JAR_SMALL];
+ case SPLIT_DOUBLE_MAGIC:
+ return (const char*)gItemIcons[ITEM_MAGIC_JAR_BIG];
+ case SPLIT_DOUBLE_DEFENSE:
+ return (const char*)gItemIcons[ITEM_HEART_CONTAINER];
+ default:
+ if (itemId <= ITEM_NONE) {
+ return (const char*)gItemIcons[itemId];
+ }
+ break;
+ }
+}
+
+ImVec2 GetItemImageSizeById(uint32_t itemId) {
+ float defaultImageSize = 32.0f;
+ if ((itemId >= ITEM_SONG_SONATA && itemId <= ITEM_SONG_SUN) || itemId == ITEM_SONG_LULLABY_INTRO) {
+ return ImVec2(defaultImageSize / 1.5f, defaultImageSize);
+ } else {
+ return ImVec2(defaultImageSize, defaultImageSize);
+ }
+}
+
+void DrawOptions() {
+ if (ImGui::BeginTable("Options", 3)) {
+ ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
+ ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
+ ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
+
+ ImGui::TableNextColumn();
+ UIWidgets::CVarCheckbox("Enable Time Splits", "gSettings.TimeSplits.Enable",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ });
+ UIWidgets::Tooltip("Enables the Time Split system, splits will not occur with this unchecked.");
+
+ ImGui::TableNextColumn();
+ if (UIWidgets::CVarCheckbox("Show Headers", "gSettings.TimeSplits.ShowHeaders",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ UpdateSplitSettings(SPLIT_HEADERS);
+ };
+ UIWidgets::Tooltip("Shows the column names in the Split List.");
+
+ ImGui::TableNextColumn();
+ if (UIWidgets::CVarCheckbox("Hide Background", "gSettings.TimeSplits.Opacity",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ UpdateSplitSettings(SPLIT_OPACITY);
+ };
+ UIWidgets::Tooltip("Hides the background of the Splits List.\n"
+ "Note: The background will display if the window extrudes from the main game window.");
+
+ ImGui::TableNextColumn();
+ UIWidgets::CVarCheckbox("Highlight Active Split", "gSettings.TimeSplits.Highlight",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ });
+ UIWidgets::Tooltip("Highlights the row with the current Active Split.");
+
+ ImGui::TableNextColumn();
+ UIWidgets::CVarCheckbox("Follow Active Split", "gSettings.TimeSplits.Follow",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ });
+ UIWidgets::Tooltip("Forces the Split List window to keep the Active Split visible.\n"
+ "Note: This prevents user scrolling, disable to restore control.");
+
+ ImGui::EndTable();
+ }
+
+ UIWidgets::CVarCheckbox("Compare Splits", "gSettings.TimeSplits.Compare",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ });
+ UIWidgets::Tooltip("Enables Split Comparisons between lists, this will integrate within the Split List.");
+
+ if (CVarGetInteger("gSettings.TimeSplits.Compare", 0)) {
+ ImGui::SameLine();
+ UIWidgets::PushStyleCombobox(UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)));
+ ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x);
+ if (ImGui::BeginCombo("##compareSplits", savedLists[comparedIndex].c_str())) {
+ for (int i = 0; i < savedLists.size(); i++) {
+ if (ImGui::Selectable(savedLists[i].c_str())) {
+ comparedIndex = i;
+ SplitLoadComparisonList();
+ break;
+ }
+ }
+ ImGui::EndCombo();
+ }
+ ImGui::PopItemWidth();
+ UIWidgets::PopStyleCombobox();
+ }
+}
+
+void DrawActionButtons() {
+ if (ImGui::BeginTable("Action Buttons", 2)) {
+ ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
+ ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch);
+
+ ImGui::TableNextColumn();
+ UIWidgets::InputString("New List", &listInputName,
+ {
+ .labelPosition = UIWidgets::LabelPosition::None,
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ .placeholder = "Enter new list name",
+ });
+
+ ImGui::TableNextColumn();
+ if (UIWidgets::Button("Create List", {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ SplitSaveFileAction(SPLIT_SAVE, listInputName);
+ SplitSaveFileAction(SPLIT_RETRIEVE, "");
+ }
+
+ ImGui::TableNextColumn();
+ UIWidgets::PushStyleCombobox(UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)));
+ ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x);
+ if (ImGui::BeginCombo("##savedSplits", savedLists[selectedIndex].c_str())) {
+ for (int i = 0; i < savedLists.size(); i++) {
+ if (ImGui::Selectable(savedLists[i].c_str())) {
+ selectedIndex = i;
+ break;
+ }
+ }
+ ImGui::EndCombo();
+ }
+ ImGui::PopItemWidth();
+ UIWidgets::PopStyleCombobox();
+
+ ImGui::TableNextColumn();
+ if (UIWidgets::Button("Save Splits", {
+ .size = { (ImGui::GetContentRegionAvail().x * 0.5f), 0 },
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ if (savedLists[0] != "Create a List First") {
+ SplitSaveFileAction(SPLIT_SAVE, savedLists[selectedIndex]);
+ }
+ }
+ ImGui::SameLine();
+ if (UIWidgets::Button("Load Splits", {
+ .size = { (ImGui::GetContentRegionAvail().x), 0 },
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ if (savedLists[0] != "Create a List First") {
+ SplitSaveFileAction(SPLIT_LOAD, savedLists[selectedIndex]);
+ }
+ }
+
+ ImGui::TableNextColumn();
+ if (UIWidgets::Button("New Attempt", {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ if (splitList.size() == 0) {
+ return;
+ }
+
+ for (auto& splits : splitList) {
+ splits.splitStatus = SPLIT_INACTIVE;
+ }
+ splitList[0].splitStatus = SPLIT_ACTIVE;
+ }
+
+ ImGui::TableNextColumn();
+ if (UIWidgets::Button("Update Splits",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ UpdateSplitBests();
+ }
+
+ ImGui::EndTable();
+ }
+}
+
+void DrawEntranceList() {
+ UIWidgets::PushStyleCombobox(UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)));
+ ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x);
+ if (ImGui::BeginCombo("##SceneFilter", sceneAreaNameMap[sceneFilterIndex])) {
+ for (int i = 0; i < sceneAreaNameMap.size(); i++) {
+ if (ImGui::Selectable(sceneAreaNameMap[i])) {
+ sceneFilterIndex = i;
+ sceneRange = GetSceneIndexRange(sceneAreaRangeMap.at(sceneAreaNameMap[i]).startIndex,
+ sceneAreaRangeMap.at(sceneAreaNameMap[i]).endIndex);
+ break;
+ }
+ }
+ ImGui::EndCombo();
+ }
+ ImGui::PopItemWidth();
+ UIWidgets::PopStyleCombobox();
+
+ if (ImGui::BeginChild("Entrance List")) {
+ if (ImGui::BeginTable("Entrances", 2)) {
+ for (int i = sceneRange.startIndex; i <= sceneRange.endIndex; i++) {
+ ImGui::TableNextColumn();
+ ImGui::PushID(sceneObjectList[i].splitId);
+ SplitsPushImageButtonStyle();
+
+ if (ImGui::ImageButton(
+ std::to_string(sceneObjectList[i].splitId).c_str(),
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(gPauseUnusedCursorTex),
+ ImVec2(32.0f, 32.0f))) {
+ AddSplitEntryBySceneId(sceneObjectList[i].splitId);
+ };
+ ImGui::SameLine();
+ TableCellCenteredText(UIWidgets::ColorValues.at(UIWidgets::Colors::White),
+ sceneObjectList[i].splitName.c_str());
+
+ SplitsPopImageButtonStyle();
+ ImGui::PopID();
+ }
+
+ ImGui::EndTable();
+ }
+ ImGui::EndChild();
+ }
+}
+
+void DrawItemList(const char* tableName, IndexRangeObject range, uint32_t tableSize) {
+ if (ImGui::BeginTable(tableName, tableSize)) {
+ for (int i = range.startIndex; i <= range.endIndex; i++) {
+ ImGui::TableNextColumn();
+ SplitsPushImageButtonStyle();
+ if (ImGui::ImageButton(std::to_string(splitObjectList[i].splitId).c_str(),
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(
+ GetItemImageById(splitObjectList[i].splitId)),
+ GetItemImageSizeById(splitObjectList[i].splitId) * 1.5f, ImVec2(0, 0), ImVec2(1, 1),
+ ImVec4(0, 0, 0, 0), Ship_GetItemColorTint(splitObjectList[i].splitId))) {
+ if (itemSubMenuList.contains(splitObjectList[i].splitId)) {
+ shouldPopUpOpen = true;
+ popupItem = splitObjectList[i].splitId;
+ ImGui::OpenPopup("ItemSubMenu");
+ } else {
+ AddSplitEntryById(splitObjectList[i].splitId);
+ }
+ }
+ UIWidgets::Tooltip(splitObjectList[i].splitName.c_str());
+ if (listName == "Bosses") {
+ ImGui::SameLine();
+ TableCellCenteredText(UIWidgets::ColorValues.at(UIWidgets::Colors::White),
+ splitObjectList[i].splitName.c_str());
+ }
+
+ SplitsPopImageButtonStyle();
+ }
+ HandlePopUpContext(popupItem);
+ ImGui::EndTable();
+ }
+}
+
+void TimesplitsSettingsWindow::DrawElement() {
+ bool shouldRemoveEntry = false;
+ uint32_t entryId = 0, entryIndex = 0;
+
+ UIWidgets::PushStyleTabs(UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)));
+ if (ImGui::BeginTabBar("Timesplit Settings Tabs")) {
+ if (ImGui::BeginTabItem("List Options")) {
+ DrawOptions();
+ DrawActionButtons();
+ ImGui::SeparatorText("Current Splits");
+ DrawSplitsList(false);
+ ImGui::EndTabItem();
+ }
+ if (ImGui::BeginTabItem("Split Entries")) {
+ if (ImGui::BeginTable("Split Settings", 3)) {
+ ImGui::TableSetupColumn("Preview",
+ ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHeaderLabel, 88.0f);
+ ImGui::TableSetupColumn("Item Categories",
+ ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHeaderLabel, 120.0f);
+ ImGui::TableSetupColumn("Item Grids",
+ ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoHeaderLabel);
+
+ ImGui::TableNextColumn();
+ ImGui::BeginDisabled();
+ UIWidgets::Button("Preview", {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ });
+ ImGui::EndDisabled();
+ ImGui::BeginChild("Preview List");
+ for (size_t i = 0; i < splitList.size(); i++) {
+ ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ((ImGui::GetContentRegionAvail().x - 50.0f) * 0.5f));
+ ImGui::PushID(i);
+ SplitsPushImageButtonStyle();
+ if (ImGui::ImageButton(
+ std::to_string(i).c_str(),
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? GetItemImageById(splitList[i].splitId)
+ : gPauseUnusedCursorTex),
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? GetItemImageSizeById(splitList[i].splitId)
+ : ImVec2(32.0f, 32.0f),
+ ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0),
+ splitList[i].splitType == SPLIT_TYPE_NORMAL ? Ship_GetItemColorTint(splitList[i].splitId)
+ : ImVec4(1, 1, 1, 1))) {
+ shouldRemoveEntry = true;
+ entryId = splitList[i].splitId;
+ entryIndex = i;
+ };
+ UIWidgets::Tooltip(splitList[i].splitName.c_str());
+
+ HandleDragAndDrop(i);
+ SplitsPopImageButtonStyle();
+ ImGui::PopID();
+ }
+ ImGui::EndChild();
+
+ ImGui::TableNextColumn();
+ if (UIWidgets::Button("Inventory",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ range = GetIndexRange((uint32_t)ITEM_OCARINA_OF_TIME, (uint32_t)ITEM_BOTTLE);
+ listName = "Inventory";
+ listColumns = 6;
+ }
+ if (UIWidgets::Button("Masks",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ range = GetIndexRange((uint32_t)ITEM_MASK_POSTMAN, (uint32_t)ITEM_MASK_FIERCE_DEITY);
+ listName = "Masks";
+ listColumns = 6;
+ }
+ if (UIWidgets::Button("Songs",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ range = GetIndexRange((uint32_t)ITEM_SONG_TIME, (uint32_t)ITEM_SONG_OATH);
+ listName = "Songs";
+ listColumns = 5;
+ }
+ if (UIWidgets::Button("Quest",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ range = GetIndexRange((uint32_t)ITEM_REMAINS_ODOLWA, (uint32_t)ITEM_BOMBERS_NOTEBOOK);
+ listName = "Quest";
+ listColumns = 4;
+ }
+ if (UIWidgets::Button("Bosses",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ range = GetIndexRange((uint32_t)SPLIT_KILLED_ODOLWA, (uint32_t)SPLIT_KILLED_MAJORA);
+ listName = "Bosses";
+ listColumns = 1;
+ }
+ if (UIWidgets::Button("Entrances",
+ {
+ .color = UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5)),
+ })) {
+ listName = "Entrances";
+ }
+ ImGui::TableNextColumn();
+ if (listName != "Entrances") {
+ DrawItemList(listName, range, listColumns);
+ if (listName == "Quest") {
+ DrawItemList("Quest II",
+ GetIndexRange((uint32_t)SPLIT_SINGLE_MAGIC, (uint32_t)SPLIT_DOUBLE_DEFENSE), 3);
+ }
+ } else {
+ DrawEntranceList();
+ }
+
+ ImGui::EndTable();
+ }
+ ImGui::EndTabItem();
+ }
+ ImGui::EndTabBar();
+ }
+ UIWidgets::PopStyleTabs();
+
+ if (shouldRemoveEntry) {
+ RemoveSplitEntry(entryId, entryIndex);
+ shouldRemoveEntry = false;
+ }
+}
+
+void TimesplitsSettingsWindow::InitElement() {
+}
diff --git a/mm/2s2h/Enhancements/Trackers/TimeSplits/TimesplitsSettings.h b/mm/2s2h/Enhancements/Trackers/TimeSplits/TimesplitsSettings.h
new file mode 100644
index 000000000..c06708a1b
--- /dev/null
+++ b/mm/2s2h/Enhancements/Trackers/TimeSplits/TimesplitsSettings.h
@@ -0,0 +1,10 @@
+#include "GuiWindow.h"
+
+class TimesplitsSettingsWindow : public Ship::GuiWindow {
+ public:
+ using GuiWindow::GuiWindow;
+
+ void InitElement() override;
+ void DrawElement() override;
+ void UpdateElement() override{};
+}; \ No newline at end of file
diff --git a/mm/2s2h/GameInteractor/GameInteractor.cpp b/mm/2s2h/GameInteractor/GameInteractor.cpp
index fe6cb7fb1..97422c41b 100644
--- a/mm/2s2h/GameInteractor/GameInteractor.cpp
+++ b/mm/2s2h/GameInteractor/GameInteractor.cpp
@@ -179,6 +179,14 @@ void GameInteractor_ExecuteOnPlayerPostLimbDraw(Player* player, s32 limbIndex) {
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnPlayerPostLimbDraw>(player, limbIndex);
}
+void GameInteractor_ExecuteOnBossDefeated(s16 actorId) {
+ SPDLOG_DEBUG("GameInteractor_ExecuteOnBossDefeated: actorId: {}", actorId);
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnBossDefeated>(actorId);
+ GameInteractor::Instance->ExecuteHooksForID<GameInteractor::OnBossDefeated>(actorId, actorId);
+ GameInteractor::Instance->ExecuteHooksForPtr<GameInteractor::OnBossDefeated>((uintptr_t)actorId, actorId);
+ GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnBossDefeated>(actorId);
+}
+
void GameInteractor_ExecuteOnSceneFlagSet(s16 sceneId, FlagType flagType, u32 flag) {
SPDLOG_DEBUG("OnSceneFlagSet: sceneId: {}, flagType: {}, flag: {}", sceneId, (u32)flagType, flag);
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSceneFlagSet>(sceneId, flagType, flag);
@@ -258,6 +266,13 @@ void GameInteractor_ExecuteOnItemGive(u8 item) {
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnItemGive>(item);
}
+void GameInteractor_ExecuteOnBottleContentsUpdate(u8 item) {
+ SPDLOG_DEBUG("OnBottleContentsUpdate: item: {}", item);
+ GameInteractor::Instance->ExecuteHooks<GameInteractor::OnBottleContentsUpdate>(item);
+ GameInteractor::Instance->ExecuteHooksForID<GameInteractor::OnBottleContentsUpdate>(item, item);
+ GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnBottleContentsUpdate>(item);
+}
+
bool GameInteractor_Should(GIVanillaBehavior flag, uint32_t result, ...) {
// Only the external function can use the Variadic Function syntax
// To pass the va args to the next caller must be done using va_list and reading the args into it
diff --git a/mm/2s2h/GameInteractor/GameInteractor.h b/mm/2s2h/GameInteractor/GameInteractor.h
index 9bc8a868d..a4d67350b 100644
--- a/mm/2s2h/GameInteractor/GameInteractor.h
+++ b/mm/2s2h/GameInteractor/GameInteractor.h
@@ -731,6 +731,7 @@ void GameInteractor_ExecuteOnActorDraw(Actor* actor);
void GameInteractor_ExecuteOnActorKill(Actor* actor);
void GameInteractor_ExecuteOnActorDestroy(Actor* actor);
void GameInteractor_ExecuteOnPlayerPostLimbDraw(Player* player, s32 limbIndex);
+void GameInteractor_ExecuteOnBossDefeated(s16 actorId);
void GameInteractor_ExecuteOnSceneFlagSet(s16 sceneId, FlagType flagType, u32 flag);
void GameInteractor_ExecuteOnSceneFlagUnset(s16 sceneId, FlagType flagType, u32 flag);
@@ -748,6 +749,8 @@ void GameInteractor_ExecuteOnOpenText(u16* textId, bool* loadFromMessageTable);
bool GameInteractor_ShouldItemGive(u8 item);
void GameInteractor_ExecuteOnItemGive(u8 item);
+void GameInteractor_ExecuteOnBottleContentsUpdate(u8 item);
+
bool GameInteractor_Should(GIVanillaBehavior flag, uint32_t result, ...);
#define REGISTER_VB_SHOULD(flag, body) \
GameInteractor::Instance->RegisterGameHookForID<GameInteractor::ShouldVanillaBehavior>( \
diff --git a/mm/2s2h/GameInteractor/GameInteractor_HookTable.h b/mm/2s2h/GameInteractor/GameInteractor_HookTable.h
index 745fc08d0..688b6091a 100644
--- a/mm/2s2h/GameInteractor/GameInteractor_HookTable.h
+++ b/mm/2s2h/GameInteractor/GameInteractor_HookTable.h
@@ -31,6 +31,7 @@ DEFINE_HOOK(OnActorDraw, (Actor * actor))
DEFINE_HOOK(OnActorKill, (Actor * actor))
DEFINE_HOOK(OnActorDestroy, (Actor * actor))
DEFINE_HOOK(OnPlayerPostLimbDraw, (Player * player, s32 limbIndex))
+DEFINE_HOOK(OnBossDefeated, (s16 actorId))
DEFINE_HOOK(OnSceneFlagSet, (s16 sceneId, FlagType flagType, u32 flag))
DEFINE_HOOK(OnSceneFlagUnset, (s16 sceneId, FlagType flagType, u32 flag))
@@ -48,4 +49,6 @@ DEFINE_HOOK(OnOpenText, (u16 * textId, bool* loadFromMessageTable))
DEFINE_HOOK(ShouldItemGive, (u8 item, bool* should))
DEFINE_HOOK(OnItemGive, (u8 item))
+DEFINE_HOOK(OnBottleContentsUpdate, (u8 item))
+
DEFINE_HOOK(ShouldVanillaBehavior, (GIVanillaBehavior flag, bool* should, va_list originalArgs))
diff --git a/mm/2s2h/ShipUtils.cpp b/mm/2s2h/ShipUtils.cpp
index bc26f3790..2edb19927 100644
--- a/mm/2s2h/ShipUtils.cpp
+++ b/mm/2s2h/ShipUtils.cpp
@@ -45,7 +45,7 @@ std::unordered_map<s16, const char*> sceneNames = {
#undef DEFINE_SCENE_UNSET
// These textures are not in existing lists that we iterate over.
-std::array<const char*, 19> miscellaneousTextures = {
+std::array<const char*, 21> miscellaneousTextures = {
gArcheryScoreIconTex,
gBarrelTrackerIcon,
gChestTrackerIcon,
@@ -65,12 +65,32 @@ std::array<const char*, 19> miscellaneousTextures = {
gTimerClockIconTex,
gWorldMapOwlFaceTex,
gameplay_keep_Tex_053140,
+ gDungeonMapSkullTex,
+ gPauseUnusedCursorTex,
};
std::array<const char*, 11> digitList = { gCounterDigit0Tex, gCounterDigit1Tex, gCounterDigit2Tex, gCounterDigit3Tex,
gCounterDigit4Tex, gCounterDigit5Tex, gCounterDigit6Tex, gCounterDigit7Tex,
gCounterDigit8Tex, gCounterDigit9Tex, gCounterColonTex };
+std::map<uint32_t, ImVec4> itemColorMap = {
+ { ITEM_SONG_SONATA, ImVec4(0.588f, 1.0f, 0.392f, 1.0f) },
+ { ITEM_SONG_LULLABY, ImVec4(1.0f, 0.313f, 0.156f, 1.0f) },
+ { ITEM_SONG_NOVA, ImVec4(0.392f, 0.588f, 1.0f, 1.0f) },
+ { ITEM_SONG_ELEGY, ImVec4(1.0f, 0.627f, 0.0f, 1.0f) },
+ { ITEM_SONG_OATH, ImVec4(1.0f, 0.392f, 1.0f, 1.0f) },
+ { ITEM_SONG_LULLABY_INTRO, ImVec4(1.0f, 0.313f, 0.156f, 1.0f) },
+};
+
+ImVec4 Ship_GetItemColorTint(uint32_t itemId) {
+ auto findColor = itemColorMap.find(itemId);
+ if (findColor != itemColorMap.end()) {
+ return findColor->second;
+ } else {
+ return ImVec4(1, 1, 1, 1);
+ }
+}
+
extern "C" const char* Ship_GetSceneName(s16 sceneId) {
if (sceneNames.contains(sceneId)) {
return sceneNames[sceneId];
@@ -79,6 +99,15 @@ extern "C" const char* Ship_GetSceneName(s16 sceneId) {
return "Unknown";
}
+std::string Ship_FormatTimeDisplay(uint32_t value) {
+ uint32_t sec = value / 10;
+ uint32_t hh = sec / 3600;
+ uint32_t mm = (sec - hh * 3600) / 60;
+ uint32_t ss = sec - hh * 3600 - mm * 60;
+ uint32_t ds = value % 10;
+ return fmt::format("{}:{:0>2}:{:0>2}.{}", hh, mm, ss, ds);
+}
+
constexpr f32 fourByThree = 4.0f / 3.0f;
// Gets the additional ratio of the screen compared to the original 4:3 ratio, clamping to 1 if smaller
diff --git a/mm/2s2h/ShipUtils.h b/mm/2s2h/ShipUtils.h
index 6161f5a9d..492a7640e 100644
--- a/mm/2s2h/ShipUtils.h
+++ b/mm/2s2h/ShipUtils.h
@@ -8,9 +8,14 @@
#ifdef __cplusplus
#include <string>
#include <array>
+#include <map>
+#include <imgui.h>
void LoadGuiTextures();
std::string convertEnumToReadableName(const std::string& input);
extern std::array<const char*, 11> digitList;
+extern std::string Ship_FormatTimeDisplay(uint32_t value);
+extern std::map<uint32_t, ImVec4> itemColorMap;
+extern ImVec4 Ship_GetItemColorTint(uint32_t itemId);
extern "C" {
#endif
diff --git a/mm/src/code/z_parameter.c b/mm/src/code/z_parameter.c
index 5c409fd46..bd313cdce 100644
--- a/mm/src/code/z_parameter.c
+++ b/mm/src/code/z_parameter.c
@@ -4901,6 +4901,8 @@ void Inventory_Dpad_UpdateBottleItem(PlayState* play, u8 item, u8 btn) {
if (item == ITEM_HOT_SPRING_WATER) {
Interface_StartBottleTimer(60, DPAD_GET_CUR_FORM_BTN_SLOT(btn) - SLOT_BOTTLE_1);
}
+
+ GameInteractor_ExecuteOnBottleContentsUpdate(item);
}
// #endregion
@@ -4916,6 +4918,8 @@ void Inventory_UpdateBottleItem(PlayState* play, u8 item, u8 btn) {
if (item == ITEM_HOT_SPRING_WATER) {
Interface_StartBottleTimer(60, GET_CUR_FORM_BTN_SLOT(btn) - SLOT_BOTTLE_1);
}
+
+ GameInteractor_ExecuteOnBottleContentsUpdate(item);
}
s32 Inventory_ConsumeFairy(PlayState* play) {
diff --git a/mm/src/overlays/actors/ovl_Boss_01/z_boss_01.c b/mm/src/overlays/actors/ovl_Boss_01/z_boss_01.c
index 9dbcb5e9a..cd4fcca5c 100644
--- a/mm/src/overlays/actors/ovl_Boss_01/z_boss_01.c
+++ b/mm/src/overlays/actors/ovl_Boss_01/z_boss_01.c
@@ -35,6 +35,7 @@
#include "overlays/actors/ovl_Item_B_Heart/z_item_b_heart.h"
#include "2s2h/BenPort.h"
+#include "2s2h/GameInteractor/GameInteractor.h"
#define FLAGS \
(ACTOR_FLAG_ATTENTION_ENABLED | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_CULLING_DISABLED | \
@@ -2046,6 +2047,7 @@ void Boss01_UpdateDamage(Boss01* this, PlayState* play) {
Boss01_SetupDeathCutscene(this, play);
Actor_PlaySfx(&this->actor, NA_SE_EN_MIBOSS_DEAD_OLD);
Enemy_StartFinishingBlow(play, &this->actor);
+ GameInteractor_ExecuteOnBossDefeated(this->actor.id); // 2S2H Time Splits
} else {
Boss01_SetupDamaged(this, play, this->actor.colChkInfo.damageEffect);
Audio_PlaySfx_AtPos(&sOdolwaDamageSfxPos, NA_SE_EN_MIBOSS_DAMAGE_OLD);
diff --git a/mm/src/overlays/actors/ovl_Boss_02/z_boss_02.c b/mm/src/overlays/actors/ovl_Boss_02/z_boss_02.c
index 9b0805475..9059a2d51 100644
--- a/mm/src/overlays/actors/ovl_Boss_02/z_boss_02.c
+++ b/mm/src/overlays/actors/ovl_Boss_02/z_boss_02.c
@@ -1148,6 +1148,7 @@ void func_809DBFB4(Boss02* this, PlayState* play) {
if (otherTwinmold->unk_0144 >= 10) {
SEQCMD_STOP_SEQUENCE(SEQ_PLAYER_BGM_MAIN, 1);
+ GameInteractor_ExecuteOnBossDefeated(this->actor.id); // 2S2H Time Splits
} else {
otherTwinmold->unk_0195 = 1;
}
diff --git a/mm/src/overlays/actors/ovl_Boss_03/z_boss_03.c b/mm/src/overlays/actors/ovl_Boss_03/z_boss_03.c
index 65931ffc6..8360897ca 100644
--- a/mm/src/overlays/actors/ovl_Boss_03/z_boss_03.c
+++ b/mm/src/overlays/actors/ovl_Boss_03/z_boss_03.c
@@ -57,6 +57,7 @@
#include "objects/gameplay_keep/gameplay_keep.h"
#include "objects/object_water_effect/object_water_effect.h"
#include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h"
+#include "2s2h/GameInteractor/GameInteractor.h"
#define FLAGS \
(ACTOR_FLAG_ATTENTION_ENABLED | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_CULLING_DISABLED | \
@@ -1885,6 +1886,7 @@ void Boss03_UpdateCollision(Boss03* this, PlayState* play) {
Boss03_PlayUnderwaterSfx(&D_809E9848, NA_SE_EN_KONB_DEAD_OLD);
Boss03_PlayUnderwaterSfx(&D_809E9848, NA_SE_EN_KONB_DEAD2_OLD);
Boss03_SetupDeathCutscene(this, play);
+ GameInteractor_ExecuteOnBossDefeated(this->actor.id); // 2S2H Time Splits
Enemy_StartFinishingBlow(play, &this->actor);
} else {
Boss03_SetupDamaged(this, play);
diff --git a/mm/src/overlays/actors/ovl_Boss_07/z_boss_07.c b/mm/src/overlays/actors/ovl_Boss_07/z_boss_07.c
index f2f4317b9..8c42cb333 100644
--- a/mm/src/overlays/actors/ovl_Boss_07/z_boss_07.c
+++ b/mm/src/overlays/actors/ovl_Boss_07/z_boss_07.c
@@ -29,6 +29,7 @@
#include "2s2h/BenPort.h"
#include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h"
+#include "2s2h/GameInteractor/GameInteractor.h"
#define FLAGS \
(ACTOR_FLAG_ATTENTION_ENABLED | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_CULLING_DISABLED | \
@@ -1768,6 +1769,8 @@ void Boss07_Wrath_SetupDeathCutscene(Boss07* this, PlayState* play) {
}
this->damagedTimer = 1000;
+
+ GameInteractor_ExecuteOnBossDefeated(this->actor.id); // 2S2H Time Splits
}
void Boss07_Wrath_DeathCutscene(Boss07* this, PlayState* play) {
diff --git a/mm/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c b/mm/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c
index 22ece3117..8cfcbd121 100644
--- a/mm/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c
+++ b/mm/src/overlays/actors/ovl_Boss_Hakugin/z_boss_hakugin.c
@@ -2309,6 +2309,7 @@ void BossHakugin_CutsceneStart(BossHakugin* this, PlayState* play) {
CutsceneManager_StartWithPlayerCs(this->actor.csId, &this->actor);
if (this->actor.colChkInfo.health == 0) {
BossHakugin_SetupDeathCutsceneRun(this);
+ GameInteractor_ExecuteOnBossDefeated(this->actor.id); // 2S2H Time Splits
} else if (this->nextCutsceneType == GOHT_NEXT_CUTSCENE_TYPE_INTRO) {
BossHakugin_SetupIntroCutsceneThaw(this);
} else {