summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 {