diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index bedfa5091..60b1c84b7 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -8,6 +8,8 @@ #include "soh/Enhancements/randomizer/fishsanity.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ImGuiUtils.h" +#include "soh/Notification/Notification.h" extern "C" { #include "macros.h" @@ -837,6 +839,20 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l Randomizer_Item_Give(gPlayState, item00->itemEntry); } } + + if (item00->itemEntry.modIndex == MOD_NONE) { + Notification::Emit({ + .itemIcon = GetTextureForItemId(item00->itemEntry.itemId), + .message = "You found ", + .suffix = SohUtils::GetItemName(item00->itemEntry.itemId), + }); + } else if (item00->itemEntry.modIndex == MOD_RANDOMIZER) { + Notification::Emit({ + .message = "You found ", + .suffix = Rando::StaticData::RetrieveItem((RandomizerGet)item00->itemEntry.getItemId).GetName().english, + }); + } + // This is typically called when you close the text box after getting an item, in case a previous // function hid the interface. Interface_ChangeAlpha(gSaveContext.unk_13EE); diff --git a/soh/soh/ImGuiUtils.cpp b/soh/soh/ImGuiUtils.cpp index ac6b7dc70..11ad464e8 100644 --- a/soh/soh/ImGuiUtils.cpp +++ b/soh/soh/ImGuiUtils.cpp @@ -93,12 +93,36 @@ std::map itemMapping = { ITEM_MAP_ENTRY(ITEM_WALLET_GIANT), ITEM_MAP_ENTRY(ITEM_SEEDS), ITEM_MAP_ENTRY(ITEM_FISHING_POLE), + ITEM_MAP_ENTRY(ITEM_SONG_MINUET), + ITEM_MAP_ENTRY(ITEM_SONG_BOLERO), + ITEM_MAP_ENTRY(ITEM_SONG_SERENADE), + ITEM_MAP_ENTRY(ITEM_SONG_REQUIEM), + ITEM_MAP_ENTRY(ITEM_SONG_NOCTURNE), + ITEM_MAP_ENTRY(ITEM_SONG_PRELUDE), + ITEM_MAP_ENTRY(ITEM_SONG_LULLABY), + ITEM_MAP_ENTRY(ITEM_SONG_EPONA), + ITEM_MAP_ENTRY(ITEM_SONG_SARIA), + ITEM_MAP_ENTRY(ITEM_SONG_SUN), + ITEM_MAP_ENTRY(ITEM_SONG_TIME), + ITEM_MAP_ENTRY(ITEM_SONG_STORMS), + ITEM_MAP_ENTRY(ITEM_MEDALLION_FOREST), + ITEM_MAP_ENTRY(ITEM_MEDALLION_FIRE), + ITEM_MAP_ENTRY(ITEM_MEDALLION_WATER), + ITEM_MAP_ENTRY(ITEM_MEDALLION_SPIRIT), + ITEM_MAP_ENTRY(ITEM_MEDALLION_SHADOW), + ITEM_MAP_ENTRY(ITEM_MEDALLION_LIGHT), + ITEM_MAP_ENTRY(ITEM_KOKIRI_EMERALD), + ITEM_MAP_ENTRY(ITEM_GORON_RUBY), + ITEM_MAP_ENTRY(ITEM_ZORA_SAPPHIRE), + ITEM_MAP_ENTRY(ITEM_STONE_OF_AGONY), + ITEM_MAP_ENTRY(ITEM_GERUDO_CARD), + ITEM_MAP_ENTRY(ITEM_SKULL_TOKEN), + ITEM_MAP_ENTRY(ITEM_HEART_CONTAINER), + ITEM_MAP_ENTRY(ITEM_HEART_PIECE), ITEM_MAP_ENTRY(ITEM_KEY_BOSS), ITEM_MAP_ENTRY(ITEM_COMPASS), ITEM_MAP_ENTRY(ITEM_DUNGEON_MAP), ITEM_MAP_ENTRY(ITEM_KEY_SMALL), - ITEM_MAP_ENTRY(ITEM_HEART_CONTAINER), - ITEM_MAP_ENTRY(ITEM_HEART_PIECE), ITEM_MAP_ENTRY(ITEM_MAGIC_SMALL), ITEM_MAP_ENTRY(ITEM_MAGIC_LARGE) }; @@ -156,6 +180,14 @@ std::array vanillaSongMapping = { { VANILLA_SONG_MAP_ENTRY(QUEST_SONG_PRELUDE, 255, 240, 100), } }; +const char* GetTextureForItemId(uint32_t itemId) { + auto it = itemMapping.find(itemId); + if (it != itemMapping.end()) { + return it->second.name.c_str(); + } + return nullptr; +} + void RegisterImGuiItemIcons() { for (const auto& entry : itemMapping) { Ship::Context::GetInstance()->GetWindow()->GetGui()->LoadGuiTexture(entry.second.name, entry.second.texturePath, ImVec4(1, 1, 1, 1)); diff --git a/soh/soh/ImGuiUtils.h b/soh/soh/ImGuiUtils.h index fe6608df6..c5545ba74 100644 --- a/soh/soh/ImGuiUtils.h +++ b/soh/soh/ImGuiUtils.h @@ -19,6 +19,7 @@ extern "C" { #include "textures/parameter_static/parameter_static.h" } +const char* GetTextureForItemId(uint32_t itemId); void RegisterImGuiItemIcons(); typedef struct { diff --git a/soh/soh/Notification/Notification.cpp b/soh/soh/Notification/Notification.cpp new file mode 100644 index 000000000..b743fae82 --- /dev/null +++ b/soh/soh/Notification/Notification.cpp @@ -0,0 +1,139 @@ + +#include "Notification.h" +#include +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" +#include "macros.h" +#include "variables.h" +} + +namespace Notification { + +static uint32_t nextId = 0; +static std::vector notifications = {}; + +void Window::Draw() { + auto vp = ImGui::GetMainViewport(); + + const float margin = 30.0f; + const float padding = 10.0f; + + int position = CVarGetInteger(CVAR_SETTING("Notifications.Position"), 0); + + // Top Left + ImVec2 basePosition; + switch (position) { + case 0: // Top Left + basePosition = ImVec2(vp->Pos.x + margin, vp->Pos.y + margin); + break; + case 1: // Top Right + basePosition = ImVec2(vp->Pos.x + vp->Size.x - margin, vp->Pos.y + margin); + break; + case 2: // Bottom Left + basePosition = ImVec2(vp->Pos.x + margin, vp->Pos.y + vp->Size.y - margin); + break; + case 3: // Bottom Right + basePosition = ImVec2(vp->Pos.x + vp->Size.x - margin, vp->Pos.y + vp->Size.y - margin); + break; + case 4: // Hidden + return; + } + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, CVarGetFloat(CVAR_SETTING("Notifications.BgOpacity"), 0.5f))); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + + for (int index = 0; index < notifications.size(); ++index) { + auto& notification = notifications[index]; + int inverseIndex = -ABS(index - (notifications.size() - 1)); + + ImGui::SetNextWindowViewport(vp->ID); + if (notification.remainingTime < 4.0f) { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, (notification.remainingTime - 1) / 3.0f); + } else { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 1.0f); + } + + ImGui::Begin(("notification#" + std::to_string(notification.id)).c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar + ); + + ImGui::SetWindowFontScale(CVarGetFloat(CVAR_SETTING("Notifications.Size"), 1.8f)); // Make this adjustable + + ImVec2 notificationPos; + switch (position) { + case 0: // Top Left + notificationPos = ImVec2(basePosition.x, basePosition.y + ((ImGui::GetWindowSize().y + padding) * inverseIndex)); + break; + case 1: // Top Right + notificationPos = ImVec2(basePosition.x - ImGui::GetWindowSize().x, basePosition.y + ((ImGui::GetWindowSize().y + padding) * inverseIndex)); + break; + case 2: // Bottom Left + notificationPos = ImVec2(basePosition.x, basePosition.y - ((ImGui::GetWindowSize().y + padding) * (inverseIndex + 1))); + break; + case 3: // Bottom Right + notificationPos = ImVec2(basePosition.x - ImGui::GetWindowSize().x, basePosition.y - ((ImGui::GetWindowSize().y + padding) * (inverseIndex + 1))); + break; + } + + ImGui::SetWindowPos(notificationPos); + + if (notification.itemIcon != nullptr) { + ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(notification.itemIcon), ImVec2(24, 24)); + ImGui::SameLine(); + } + if (!notification.prefix.empty()) { + ImGui::TextColored(notification.prefixColor, "%s", notification.prefix.c_str()); + ImGui::SameLine(); + } + ImGui::TextColored(notification.messageColor, "%s", notification.message.c_str()); + if (!notification.suffix.empty()) { + ImGui::SameLine(); + ImGui::TextColored(notification.suffixColor, "%s", notification.suffix.c_str()); + } + + ImGui::End(); + ImGui::PopStyleVar(); + } + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); +} + + +void Window::UpdateElement() { + for (int index = 0; index < notifications.size(); ++index) { + auto& notification = notifications[index]; + + // decrement remainingTime + notification.remainingTime -= ImGui::GetIO().DeltaTime; + + // remove notification if it has expired + if (notification.remainingTime <= 0) { + notifications.erase(notifications.begin() + index); + --index; + } + } +} + +void Emit(Options notification) { + notification.id = nextId++; + if (notification.remainingTime == 0.0f) { + notification.remainingTime = CVarGetFloat(CVAR_SETTING("Notifications.Duration"), 10.0f); + } + notifications.push_back(notification); + Audio_PlaySoundGeneral(NA_SE_SY_METRONOME, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); +} + +} // namespace Notification diff --git a/soh/soh/Notification/Notification.h b/soh/soh/Notification/Notification.h new file mode 100644 index 000000000..670e86ba3 --- /dev/null +++ b/soh/soh/Notification/Notification.h @@ -0,0 +1,37 @@ +#ifndef NOTIFICATION_H +#define NOTIFICATION_H +#ifdef __cplusplus + +#include +#include + +namespace Notification { + +struct Options { + uint32_t id = 0; + const char* itemIcon = nullptr; + std::string prefix = ""; + ImVec4 prefixColor = ImVec4(0.5f, 0.5f, 1.0f, 1.0f); + std::string message = ""; + ImVec4 messageColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); + std::string suffix = ""; + ImVec4 suffixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f); + float remainingTime = 0.0f; // Seconds +}; + +class Window : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override {}; + void DrawElement() override {}; + void Draw() override; + void UpdateElement() override; +}; + +void Emit(Options notification); + +} // namespace Notification + +#endif // __cplusplus +#endif // NOTIFICATION_H diff --git a/soh/soh/SohGui.cpp b/soh/soh/SohGui.cpp index 90db44501..87e4b2dae 100644 --- a/soh/soh/SohGui.cpp +++ b/soh/soh/SohGui.cpp @@ -37,6 +37,7 @@ #include "Enhancements/cosmetics/authenticGfxPatches.h" #include "Enhancements/resolution-editor/ResolutionEditor.h" #include "Enhancements/debugger/MessageViewer.h" +#include "soh/Notification/Notification.h" bool isBetaQuestEnabled = false; @@ -132,6 +133,7 @@ namespace SohGui { std::shared_ptr mRandomizerSettingsWindow; std::shared_ptr mAdvancedResolutionSettingsWindow; std::shared_ptr mModalWindow; + std::shared_ptr mNotificationWindow; void SetupGuiElements() { auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); @@ -141,9 +143,9 @@ namespace SohGui { if (gui->GetMenuBar() && !gui->GetMenuBar()->IsVisible()) { #if defined(__SWITCH__) || defined(__WIIU__) - gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Press - to access enhancements menu"); + Notification::Emit({ .message = "Press - to access enhancements menu", .remainingTime = 10.0f }); #else - gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Press F1 to access enhancements menu"); + Notification::Emit({ .message = "Press F1 to access enhancements menu", .remainingTime = 10.0f }); #endif } @@ -210,12 +212,16 @@ namespace SohGui { mModalWindow = std::make_shared(CVAR_WINDOW("ModalWindow"), "Modal Window"); gui->AddGuiWindow(mModalWindow); mModalWindow->Show(); + mNotificationWindow = std::make_shared(CVAR_WINDOW("Notifications"), "Notifications Window"); + gui->AddGuiWindow(mNotificationWindow); + mNotificationWindow->Show(); } void Destroy() { auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); gui->RemoveAllGuiWindows(); + mNotificationWindow = nullptr; mModalWindow = nullptr; mAdvancedResolutionSettingsWindow = nullptr; mRandomizerSettingsWindow = nullptr; diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index 7f7c949bf..cf940ff78 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -15,6 +15,7 @@ #include "Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/presets.h" #include "soh/Enhancements/mods.h" +#include "soh/Notification/Notification.h" #include "Enhancements/cosmetics/authenticGfxPatches.h" #ifdef ENABLE_REMOTE_CONTROL #include "soh/Network/CrowdControl/CrowdControl.h" @@ -564,6 +565,35 @@ void DrawSettingsMenu() { ImGui::EndMenu(); } + + UIWidgets::Spacer(0); + + if (ImGui::BeginMenu("Notifications")) { + static const char* notificationPosition[] = { + "Top Left", + "Top Right", + "Bottom Left", + "Bottom Right", + "Hidden", + }; + + ImGui::Text("Position"); + UIWidgets::EnhancementCombobox(CVAR_SETTING("Notifications.Position"), notificationPosition, 0); + UIWidgets::EnhancementSliderFloat("Duration: %.0f seconds", "##NotificationDuration", CVAR_SETTING("Notifications.Duration"), 3.0f, 30.0f, "", 10.0f, false, false, false); + UIWidgets::EnhancementSliderFloat("BG Opacity: %.1f %%", "##NotificaitonBgOpacity", CVAR_SETTING("Notifications.BgOpacity"), 0.0f, 1.0f, "", 0.5f, true, false, false); + UIWidgets::EnhancementSliderFloat("Size: %.1f", "##NotificaitonSize", CVAR_SETTING("Notifications.Size"), 1.0f, 5.0f, "", 1.8f, false, false, false); + + UIWidgets::Spacer(0); + + if (ImGui::Button("Test Notification", ImVec2(-1.0f, 0.0f))) { + Notification::Emit({ + .message = (gPlayState != NULL ? SohUtils::GetSceneName(gPlayState->sceneNum) : "Hyrule") + " looks beautiful today!", + }); + } + + ImGui::EndMenu(); + } + ImGui::EndMenu(); } } diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp index d5f1e8418..faa171231 100644 --- a/soh/soh/util.cpp +++ b/soh/soh/util.cpp @@ -30,7 +30,7 @@ std::vector sceneNames = { "Phantom Ganon's Lair", "Volvagia's Lair", "Morpha's Lair", - "Twinrova's Lair & Nabooru's Mini-Boss Room", + "Twinrova's Lair", "Bongo Bongo's Lair", "Ganondorf's Lair", "Tower Collapse Exterior", @@ -79,34 +79,34 @@ std::vector sceneNames = { "Castle Hedge Maze (Day)", "Castle Hedge Maze (Night)", "Cutscene Map", - "Damp�'s Grave & Windmill", + "Dampe's Grave & Windmill", "Fishing Pond", "Castle Courtyard", "Bombchu Bowling Alley", "Ranch House & Silo", "Guard House", "Granny's Potion Shop", - "Ganon's Tower Collapse & Battle Arena", + "Ganon's Tower Collapse & Arena", "House of Skulltula", - "Spot 00 - Hyrule Field", - "Spot 01 - Kakariko Village", - "Spot 02 - Graveyard", - "Spot 03 - Zora's River", - "Spot 04 - Kokiri Forest", - "Spot 05 - Sacred Forest Meadow", - "Spot 06 - Lake Hylia", - "Spot 07 - Zora's Domain", - "Spot 08 - Zora's Fountain", - "Spot 09 - Gerudo Valley", - "Spot 10 - Lost Woods", - "Spot 11 - Desert Colossus", - "Spot 12 - Gerudo's Fortress", - "Spot 13 - Haunted Wasteland", - "Spot 15 - Hyrule Castle", - "Spot 16 - Death Mountain Trail", - "Spot 17 - Death Mountain Crater", - "Spot 18 - Goron City", - "Spot 20 - Lon Lon Ranch", + "Hyrule Field", + "Kakariko Village", + "Graveyard", + "Zora's River", + "Kokiri Forest", + "Sacred Forest Meadow", + "Lake Hylia", + "Zora's Domain", + "Zora's Fountain", + "Gerudo Valley", + "Lost Woods", + "Desert Colossus", + "Gerudo's Fortress", + "Haunted Wasteland", + "Hyrule Castle", + "Death Mountain Trail", + "Death Mountain Crater", + "Goron City", + "Lon Lon Ranch", "Ganon's Castle Exterior", "Jungle Gym", "Ganondorf Test Room",