Add simple notifications system

This commit is contained in:
Garrett Cox 2024-10-20 20:08:31 -05:00
parent 4663bd152a
commit 92c791b7c0
8 changed files with 287 additions and 26 deletions

View File

@ -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);

View File

@ -93,12 +93,36 @@ std::map<uint32_t, ItemMapEntry> 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<SongMapEntry, 12> 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));

View File

@ -19,6 +19,7 @@ extern "C" {
#include "textures/parameter_static/parameter_static.h"
}
const char* GetTextureForItemId(uint32_t itemId);
void RegisterImGuiItemIcons();
typedef struct {

View File

@ -0,0 +1,139 @@
#include "Notification.h"
#include <libultraship/libultraship.h>
#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<Options> 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

View File

@ -0,0 +1,37 @@
#ifndef NOTIFICATION_H
#define NOTIFICATION_H
#ifdef __cplusplus
#include <string>
#include <libultraship/libultraship.h>
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

View File

@ -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<RandomizerSettingsWindow> mRandomizerSettingsWindow;
std::shared_ptr<AdvancedResolutionSettings::AdvancedResolutionSettingsWindow> mAdvancedResolutionSettingsWindow;
std::shared_ptr<SohModalWindow> mModalWindow;
std::shared_ptr<Notification::Window> 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<SohModalWindow>(CVAR_WINDOW("ModalWindow"), "Modal Window");
gui->AddGuiWindow(mModalWindow);
mModalWindow->Show();
mNotificationWindow = std::make_shared<Notification::Window>(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;

View File

@ -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();
}
}

View File

@ -30,7 +30,7 @@ std::vector<std::string> 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<std::string> sceneNames = {
"Castle Hedge Maze (Day)",
"Castle Hedge Maze (Night)",
"Cutscene Map",
"Damp<EFBFBD>'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",