diff options
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 { |
