From 027e23d2e76a20bdf50b122b26ec2a21fc8f635a Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Wed, 5 Aug 2026 00:19:28 -0700 Subject: [PATCH 01/15] include save/stage services --- mods/randomizer/src/mod.cpp | 6 +++++- mods/randomizer/src/session.hpp | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mods/randomizer/src/mod.cpp b/mods/randomizer/src/mod.cpp index 60a37daedf..d026780452 100644 --- a/mods/randomizer/src/mod.cpp +++ b/mods/randomizer/src/mod.cpp @@ -10,6 +10,8 @@ IMPORT_SERVICE(HookService, svc_hook); IMPORT_SERVICE(UiService, svc_ui); IMPORT_SERVICE(ResourceService, svc_res); IMPORT_SERVICE(ConfigService, svc_config); +IMPORT_SERVICE(SaveService, svc_save); +IMPORT_SERVICE(StageService, svc_stage); extern "C" { @@ -21,7 +23,9 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { svc_hook, svc_ui, svc_res, - svc_config + svc_config, + svc_save, + svc_stage, }); if (result != MOD_OK) { return mods::set_error(error, result, "failed to initialize session"); diff --git a/mods/randomizer/src/session.hpp b/mods/randomizer/src/session.hpp index 098488bf74..c19e21229a 100644 --- a/mods/randomizer/src/session.hpp +++ b/mods/randomizer/src/session.hpp @@ -6,6 +6,8 @@ #include "mods/svc/hook.h" #include "mods/svc/ui.h" #include "mods/svc/resource.h" +#include "mods/svc/save.h" +#include "mods/svc/stage.h" namespace randomizer::session { struct ServiceManager { @@ -16,6 +18,8 @@ struct ServiceManager { const UiService* ui; const ResourceService* resource; const ConfigService* config; + const SaveService* save; + const StageService* stage; }; extern ServiceManager svc_mng; From 0ed984a86360fd9d99bb0e8160d16403cf914078 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Wed, 5 Aug 2026 00:43:23 -0700 Subject: [PATCH 02/15] use new log fmt wrappers --- mods/randomizer/CMakeLists.txt | 4 ++-- mods/randomizer/src/randomizer_context.cpp | 4 ++-- mods/randomizer/src/session.hpp | 12 ------------ mods/randomizer/src/tools.cpp | 5 +++-- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/mods/randomizer/CMakeLists.txt b/mods/randomizer/CMakeLists.txt index eeecd5b1f7..e91e45a3f1 100644 --- a/mods/randomizer/CMakeLists.txt +++ b/mods/randomizer/CMakeLists.txt @@ -105,13 +105,13 @@ foreach (RANDOMIZER_FILE IN LISTS RANDOMIZER_DATA) endforeach () add_mod(randomizer - FEATURES game + FEATURES game fmt SOURCES src/mod.cpp ${RANDOMIZER_GENERATOR_SOURCES} ${RANDOMIZER_SOURCES} MOD_JSON mod.json RES_DIR res ) -target_link_libraries(randomizer PRIVATE yaml-cpp::yaml-cpp base64pp fmt::fmt randomizer_embeds) +target_link_libraries(randomizer PRIVATE yaml-cpp::yaml-cpp base64pp randomizer_embeds) string(LENGTH "${CMAKE_CURRENT_SOURCE_DIR}/" RANDOMIZER_SOURCE_PATH_SIZE) target_compile_definitions(randomizer PRIVATE diff --git a/mods/randomizer/src/randomizer_context.cpp b/mods/randomizer/src/randomizer_context.cpp index 1c34958465..1f75849277 100644 --- a/mods/randomizer/src/randomizer_context.cpp +++ b/mods/randomizer/src/randomizer_context.cpp @@ -15,6 +15,7 @@ #include "../generator/utility/string.hpp" #include +#include #include "custom_flow_ids.hpp" #include "d/actor/d_a_alink.h" @@ -24,7 +25,6 @@ #include "d/d_meter2_info.h" #include "d/d_msg_class.h" #include "d/d_msg_flow.h" -#include "fmt/format.h" #include "m_Do/m_Do_audio.h" std::optional RandomizerContext::WriteToFile() { @@ -154,7 +154,7 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha this->mHash = hash; if (!std::filesystem::exists(this->GetSeedDataPath())) { - randomizer::session::LogError(fmt::format("Failed to load Hash: {}", hash).c_str()); + mods::log::error("Failed to load Hash: {}", hash); mHash.clear(); return std::nullopt; } diff --git a/mods/randomizer/src/session.hpp b/mods/randomizer/src/session.hpp index c19e21229a..fdf295c779 100644 --- a/mods/randomizer/src/session.hpp +++ b/mods/randomizer/src/session.hpp @@ -25,16 +25,4 @@ struct ServiceManager { extern ServiceManager svc_mng; ModResult initialize(const ServiceManager& services); - -inline void LogError(const char* msg) { - svc_mng.log->error(svc_mng.mod_ctx, msg); -} - -inline void LogDebug(const char* msg) { - svc_mng.log->debug(svc_mng.mod_ctx, msg); -} - -inline void LogWarn(const char* msg) { - svc_mng.log->warn(svc_mng.mod_ctx, msg); -} } \ No newline at end of file diff --git a/mods/randomizer/src/tools.cpp b/mods/randomizer/src/tools.cpp index 1236aae7e8..f9e794222b 100644 --- a/mods/randomizer/src/tools.cpp +++ b/mods/randomizer/src/tools.cpp @@ -6,7 +6,6 @@ #include "d/d_item.h" #include "d/d_item_data.h" #include "f_op/f_op_actor_mng.h" -#include "fmt/format.h" #include "item_ids.h" #include "randomizer_context.hpp" #include "session.hpp" @@ -14,6 +13,8 @@ #include "utilities.h" #include "verify_item_functions.h" +#include + bool playerIsInRoomStage(s32 room, const char* stage) { // Only check room if it is valid @@ -713,7 +714,7 @@ int getStageSaveId(int id) { case 42: // F_SP102 (Title Screen / King Bulblin 1) return 0xFF; default: - randomizer::session::LogWarn(fmt::format("Failed to find Save Id for ID: {}" , id).c_str()); + mods::log::warn("Failed to find Save Id for ID: {}" , id); return -1; } } From 3160d1c37020e98570e8899429c473e46da01e30 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Wed, 5 Aug 2026 03:04:06 -0700 Subject: [PATCH 03/15] ui: config_store/seed_generation --- mods/randomizer/CMakeLists.txt | 2 + mods/randomizer/src/ui/config_store.cpp | 45 ++++++++ mods/randomizer/src/ui/config_store.hpp | 15 +++ .../src/ui/rando_seed_generation.cpp | 104 ++++++++++++++++++ .../src/ui/rando_seed_generation.hpp | 10 ++ 5 files changed, 176 insertions(+) create mode 100644 mods/randomizer/src/ui/config_store.cpp create mode 100644 mods/randomizer/src/ui/config_store.hpp create mode 100644 mods/randomizer/src/ui/rando_seed_generation.cpp create mode 100644 mods/randomizer/src/ui/rando_seed_generation.hpp diff --git a/mods/randomizer/CMakeLists.txt b/mods/randomizer/CMakeLists.txt index e91e45a3f1..d040aa7a9f 100644 --- a/mods/randomizer/CMakeLists.txt +++ b/mods/randomizer/CMakeLists.txt @@ -85,6 +85,8 @@ set(RANDOMIZER_SOURCES src/verify_item_functions.cpp src/paths.cpp src/session.cpp + src/ui/config_store.cpp + src/ui/rando_seed_generation.cpp ) # Embed the generator's YAML data into the mod library. Paths are relative to this diff --git a/mods/randomizer/src/ui/config_store.cpp b/mods/randomizer/src/ui/config_store.cpp new file mode 100644 index 0000000000..f14867efc9 --- /dev/null +++ b/mods/randomizer/src/ui/config_store.cpp @@ -0,0 +1,45 @@ +#include "config_store.hpp" + +#include + +#include "../../generator/seedgen/seed.hpp" +#include "../paths.hpp" + +namespace randomizer::ui { + +seedgen::config::Config& GetRandomizerConfig() { + static seedgen::config::Config s_config{paths::GetRandomizerSettingsPath(), + paths::GetRandomizerPreferencesPath()}; + return s_config; +} + +void SaveRandomizerConfig() { + GetRandomizerConfig().WriteToFile(paths::GetRandomizerSettingsPath(), + paths::GetRandomizerPreferencesPath()); +} + +seedgen::settings::Setting* FindSetting(const std::string& key) { + if (key.empty()) { + return nullptr; + } + auto& settings = GetRandomizerConfig().GetSettings(); + auto& map = settings.GetMap(); + auto it = map.find(key); + if (it == map.end()) { + mods::log::error("randomizer: failed to get settings key: {}", key); + return nullptr; + } + return &it->second; +} + +bool TryCreateRandomSeed() { + auto& config = GetRandomizerConfig(); + if (config.GetSeed().empty()) { + config.SetSeed(seedgen::seed::GenerateSeed()); + SaveRandomizerConfig(); + return true; + } + return false; +} + +} // namespace randomizer::ui diff --git a/mods/randomizer/src/ui/config_store.hpp b/mods/randomizer/src/ui/config_store.hpp new file mode 100644 index 0000000000..c0cd805a71 --- /dev/null +++ b/mods/randomizer/src/ui/config_store.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "../../generator/seedgen/config.hpp" +#include "../../generator/seedgen/settings.hpp" + +namespace randomizer::ui { + +seedgen::config::Config& GetRandomizerConfig(); +void SaveRandomizerConfig(); +seedgen::settings::Setting* FindSetting(const std::string& key); +bool TryCreateRandomSeed(); + +} // namespace randomizer::ui diff --git a/mods/randomizer/src/ui/rando_seed_generation.cpp b/mods/randomizer/src/ui/rando_seed_generation.cpp new file mode 100644 index 0000000000..5daf9735d7 --- /dev/null +++ b/mods/randomizer/src/ui/rando_seed_generation.cpp @@ -0,0 +1,104 @@ +#include "rando_seed_generation.hpp" + +#include + +#include "../session.hpp" +#include "../randomizer_context.hpp" + +#include "m_Do/m_Do_audio.h" + +#include +#include +#include + +namespace randomizer::ui { +enum class SeedGenerateStatus { + Ready, + Generating, + Success, + Error, +}; + +UiDialogHandle seedGenDialog{0}; +static std::atomic seedGenStatus = SeedGenerateStatus::Ready; +static std::string generationStatusMsg{}; + +void OnDialogActionOK(ModContext* ctx, UiDialogHandle dialogHandle, void*) { + mDoAud_seStartMenu(Z2SE_SY_MENU_BACK); + session::svc_mng.ui->dialog_close(ctx, dialogHandle); +} + +static void StartSeedGeneration() { + if (GenerateAndWriteSeed(generationStatusMsg)) { + seedGenStatus.store(SeedGenerateStatus::Success); + } else { + seedGenStatus.store(SeedGenerateStatus::Error); + } + + mods::log::debug("{}", generationStatusMsg); +} + +static ModResult buildDialog() { + UiDialogDesc desc = UI_DIALOG_DESC_INIT; + desc.title = "Randomizer"; + desc.body_rml = "Generating Seed..."; + desc.icon = "verifying"; + desc.variant = UI_DIALOG_NORMAL; + + UiDialogAction action = { + .label = "OK", + .on_pressed = OnDialogActionOK, + .user_data = nullptr, + .keep_open = false, + }; + desc.actions = &action; + desc.action_count = 1; + + if (session::svc_mng.ui->dialog_push(session::svc_mng.mod_ctx, &desc, &seedGenDialog) != MOD_OK) { + mods::log::error("Failed to push dialog"); + return MOD_ERROR; + } + + return MOD_OK; +} + +void GenerateRandomizerSeed() { + if (buildDialog() != MOD_OK) { + return; + } + + // Start generation thread + seedGenStatus.store(SeedGenerateStatus::Generating); + std::thread rando_gen_thread(StartSeedGeneration); + rando_gen_thread.detach(); +} + +ModResult UpdateSeedGenerationDialog() { + if (seedGenDialog == 0) { + return MOD_OK; + } + + auto curSeedGenStatus = seedGenStatus.load(); + + // Change the modal text if we've finished attempting to generate + if (curSeedGenStatus == SeedGenerateStatus::Success || + curSeedGenStatus == SeedGenerateStatus::Error) + { + auto* ctx = session::svc_mng.mod_ctx; + auto* ui_svc = session::svc_mng.ui; + + if (curSeedGenStatus == SeedGenerateStatus::Success) { + mDoAud_seStartMenu(Z2SE_SY_FILE_SAVE_OK); + ui_svc->dialog_set_icon(ctx, seedGenDialog, "celebration"); + } else { + mDoAud_seStartMenu(Z2SE_SYS_RESULT_WRONG); + ui_svc->dialog_set_icon(ctx, seedGenDialog, "error"); + } + + ui_svc->dialog_set_body(ctx, seedGenDialog, generationStatusMsg.c_str()); + seedGenStatus.store(SeedGenerateStatus::Ready); + } + + return MOD_OK; +} +} \ No newline at end of file diff --git a/mods/randomizer/src/ui/rando_seed_generation.hpp b/mods/randomizer/src/ui/rando_seed_generation.hpp new file mode 100644 index 0000000000..92df6e6937 --- /dev/null +++ b/mods/randomizer/src/ui/rando_seed_generation.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace randomizer::ui { + +void GenerateRandomizerSeed(); +ModResult UpdateSeedGenerationDialog(); + +} From 8d677bf517d23ee92bfe847b8303268299223353 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Wed, 5 Aug 2026 04:43:55 -0700 Subject: [PATCH 04/15] ui: wip porting main ui --- mods/randomizer/CMakeLists.txt | 2 + mods/randomizer/src/mod.cpp | 7 ++ mods/randomizer/src/ui/config_store.cpp | 15 +-- mods/randomizer/src/ui/rando_config.cpp | 118 ++++++++++++++++++++++++ mods/randomizer/src/ui/rando_config.hpp | 26 ++++++ mods/randomizer/src/ui/ui.cpp | 81 ++++++++++++++++ mods/randomizer/src/ui/ui.hpp | 8 ++ 7 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 mods/randomizer/src/ui/rando_config.cpp create mode 100644 mods/randomizer/src/ui/rando_config.hpp create mode 100644 mods/randomizer/src/ui/ui.cpp create mode 100644 mods/randomizer/src/ui/ui.hpp diff --git a/mods/randomizer/CMakeLists.txt b/mods/randomizer/CMakeLists.txt index d040aa7a9f..f41c9f1eac 100644 --- a/mods/randomizer/CMakeLists.txt +++ b/mods/randomizer/CMakeLists.txt @@ -87,6 +87,8 @@ set(RANDOMIZER_SOURCES src/session.cpp src/ui/config_store.cpp src/ui/rando_seed_generation.cpp + src/ui/rando_config.cpp + src/ui/ui.cpp ) # Embed the generator's YAML data into the mod library. Paths are relative to this diff --git a/mods/randomizer/src/mod.cpp b/mods/randomizer/src/mod.cpp index d026780452..4421193432 100644 --- a/mods/randomizer/src/mod.cpp +++ b/mods/randomizer/src/mod.cpp @@ -2,6 +2,7 @@ #include "mods/svc/log.h" #include "session.hpp" +#include "ui/ui.hpp" DEFINE_MOD(); IMPORT_SERVICE(HostService, svc_host); @@ -31,11 +32,17 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { return mods::set_error(error, result, "failed to initialize session"); } + result = randomizer::ui::initialize(); + if (result != MOD_OK) { + return mods::set_error(error, result, "failed to initialize ui"); + } + svc_log->info(mod_ctx, "randomizer initialized"); return MOD_OK; } MOD_EXPORT ModResult mod_update(ModError*) { + randomizer::ui::update(); return MOD_OK; } diff --git a/mods/randomizer/src/ui/config_store.cpp b/mods/randomizer/src/ui/config_store.cpp index f14867efc9..b0a31a7d78 100644 --- a/mods/randomizer/src/ui/config_store.cpp +++ b/mods/randomizer/src/ui/config_store.cpp @@ -18,22 +18,9 @@ void SaveRandomizerConfig() { paths::GetRandomizerPreferencesPath()); } -seedgen::settings::Setting* FindSetting(const std::string& key) { - if (key.empty()) { - return nullptr; - } - auto& settings = GetRandomizerConfig().GetSettings(); - auto& map = settings.GetMap(); - auto it = map.find(key); - if (it == map.end()) { - mods::log::error("randomizer: failed to get settings key: {}", key); - return nullptr; - } - return &it->second; -} - bool TryCreateRandomSeed() { auto& config = GetRandomizerConfig(); + if (config.GetSeed().empty()) { config.SetSeed(seedgen::seed::GenerateSeed()); SaveRandomizerConfig(); diff --git a/mods/randomizer/src/ui/rando_config.cpp b/mods/randomizer/src/ui/rando_config.cpp new file mode 100644 index 0000000000..d817474d7c --- /dev/null +++ b/mods/randomizer/src/ui/rando_config.cpp @@ -0,0 +1,118 @@ +#include "rando_config.hpp" + +#include + +#include "../session.hpp" +#include "../tools.h" +#include "../paths.hpp" +#include "../../generator/seedgen/seed.hpp" +#include "../../generator/utility/string.hpp" + +#include "rando_seed_generation.hpp" +#include "config_store.hpp" + +#include +#include +#include + +namespace randomizer::ui { + +seedgen::settings::Setting* FindSetting(const std::string& key) { + if (key.empty()) { + mods::log::error("Key is empty! Unable to find setting."); + } + + // TODO: handle multi-world selection + auto& settings = GetRandomizerConfig().GetSettings(); + try { + return &settings.GetMap().at(key); + } catch (std::exception e) { + mods::log::error("Failed to get Settings Key: {}", key); + } +} + +const std::vector>& GetStartingInventoryLayoutOrder() { + static const std::vector> layoutOrder = { + // { display name , logic item name } + {"Shadow Crystal", "Shadow Crystal"}, + {"Horse Call", "Horse Call"}, + {"Fishing Rod", "Progressive Fishing Rod"}, + {"Slingshot", "Slingshot"}, + {"Lantern", "Lantern"}, + {"Gale Boomerang", "Gale Boomerang"}, + {"Iron Boots", "Iron Boots"}, + {"Bow", "Progressive Bow"}, + {"Hawkeye", "Hawkeye"}, + {"Bomb Bags", "Bomb Bag"}, + {"Giant Bomb Bags", "Giant Bomb Bag"}, + {"Clawshot", "Progressive Clawshot"}, + {"Spinner", "Spinner"}, + {"Ball and Chain", "Ball and Chain"}, + {"Dominion Rod", "Progressive Dominion Rod"}, + {"Empty Bottle", "Empty Bottle"}, + {"Auru's Memo", "Aurus Memo"}, + {"Ashei's Sketch", "Asheis Sketch"}, + {"Sky Book", "Progressive Sky Book"}, + {"Sword", "Progressive Sword"}, + {"Ordon Shield", "Ordon Shield"}, + {"Hylian Shield", "Hylian Shield"}, + {"Zora Armor", "Zora Armor"}, + {"Magic Armor", "Magic Armor"}, + {"Wallet", "Progressive Wallet"}, + {"Hidden Skills", "Progressive Hidden Skill"}, + {"Poe Souls", "Poe Soul"}, + {"Fused Shadows", "Progressive Fused Shadow"}, + {"Mirror Shards", "Progressive Mirror Shard"}, + {"Gate Keys", "Gate Keys"}, + {"Gerudo Desert Bulblin Camp Key", "Gerudo Desert Bulblin Camp Key"}, + {"Forest Temple Small Keys", "Forest Temple Small Key"}, + {"Goron Mines Small Keys", "Goron Mines Small Key"}, + {"Lakebed Temple Small Keys", "Lakebed Temple Small Key"}, + {"Arbiter's Grounds Small Keys", "Arbiters Grounds Small Key"}, + {"Snowpeak Ruins Small Keys", "Snowpeak Ruins Small Key"}, + {"Ordon Pumpkin", "Ordon Pumpkin"}, + {"Ordon Cheese", "Ordon Cheese"}, + {"Temple of Time Small Keys", "Temple of Time Small Key"}, + {"City in the Sky Small Keys", "City in the Sky Small Key"}, + {"Palace of Twilight Small Keys", "Palace of Twilight Small Key"}, + {"Hyrule Castle Small Keys", "Hyrule Castle Small Key"}, + {"Forest Temple Big Key", "Forest Temple Big Key"}, + {"Goron Mines Key Shards", "Goron Mines Key Shard"}, + {"Lakebed Temple Big Key", "Lakebed Temple Big Key"}, + {"Arbiter's Grounds Big Key", "Arbiters Grounds Big Key"}, + {"Snowpeak Ruins Bedroom Key", "Snowpeak Ruins Bedroom Key"}, + {"Temple of Time Big Key", "Temple of Time Big Key"}, + {"City in the Sky Big Key", "City in the Sky Big Key"}, + {"Palace of Twilight Big Key", "Palace of Twilight Big Key"}, + {"Hyrule Castle Big Key", "Hyrule Castle Big Key"}, + {"Gerudo Desert Portal", "Gerudo Desert Portal"}, + {"Mirror Chamber Portal", "Mirror Chamber Portal"}, + {"Snowpeak Portal", "Snowpeak Portal"}, + {"Sacred Grove Portal", "Sacred Grove Portal"}, + {"Bridge of Eldin Portal", "Bridge of Eldin Portal"}, + {"Upper Zora's River Portal", "Upper Zoras River Portal"} + }; + return layoutOrder; +} + +std::filesystem::path GetRandomizerPath() { + return paths::GetRandomizerPath() / "randomizer"; +} + +std::filesystem::path GetRandomizerSettingsPath() { + return GetRandomizerPath() / "settings.yaml"; +} + +std::filesystem::path GetRandomizerPreferencesPath() { + return GetRandomizerPath() / "preferences.yaml"; +} + +std::filesystem::path GetRandomizerPresetsPath() { + return GetRandomizerPath() / "presets"; +} + +std::filesystem::path GetRandomizerSeedsPath() { + return GetRandomizerPath() / "seeds"; +} + +} // namespace dusk::ui diff --git a/mods/randomizer/src/ui/rando_config.hpp b/mods/randomizer/src/ui/rando_config.hpp new file mode 100644 index 0000000000..1c5457e0c9 --- /dev/null +++ b/mods/randomizer/src/ui/rando_config.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +// Forward declaration +namespace randomizer::seedgen::config { +class Config; +} +class dFile_select_c; + +namespace randomizer::ui { + +void SaveNewRandomizerPreset(const std::string& presetName, bool overwriteExisting = false); +void ApplyExistingRandomizerPreset(const std::filesystem::path& presetFilePath); +void CopyPermalinkToClipboard(); +void PastePermalinkFromClipboard(); + +std::filesystem::path GetRandomizerPath(); +std::filesystem::path GetRandomizerSettingsPath(); +std::filesystem::path GetRandomizerPreferencesPath(); +std::filesystem::path GetRandomizerSeedsPath(); +std::filesystem::path GetRandomizerPresetsPath(); + +} diff --git a/mods/randomizer/src/ui/ui.cpp b/mods/randomizer/src/ui/ui.cpp new file mode 100644 index 0000000000..2914b2b2af --- /dev/null +++ b/mods/randomizer/src/ui/ui.cpp @@ -0,0 +1,81 @@ +#include "ui.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "../session.hpp" +#include "../randomizer_context.hpp" +#include "../paths.hpp" + +#include "config_store.hpp" +#include "rando_seed_generation.hpp" + +namespace randomizer::ui { +namespace { +// Seed Tab +ModResult buildSeedTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, + UiElementHandle rightPane, void*, ModError*) +{ + return MOD_OK; +} + +ModResult updateSeedTab(ModContext* ctx, void*, ModError*) { + return MOD_OK; +} + +// Settings Tab +ModResult buildSettingsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, + UiElementHandle rightPane, void*, ModError*) +{ + return MOD_OK; +} + +// Menu Tab +void OnMenuTabSelected(ModContext* ctx, void*) { + UiTabDesc tabs[2]{}; + + tabs[0].struct_size = sizeof(UiTabDesc); + tabs[0].title = "Seed"; + tabs[0].build = buildSeedTab; + tabs[0].update = updateSeedTab; + + tabs[1].struct_size = sizeof(UiTabDesc); + tabs[1].title = "Settings"; + tabs[1].build = buildSettingsTab; + + UiWindowDesc desc = UI_WINDOW_DESC_INIT; + desc.tabs = tabs; + desc.tab_count = 2; + UiWindowHandle window{}; + session::svc_mng.ui->window_push(ctx, &desc, &window); +} +} + +UiMenuTabHandle g_menu_tab{}; + +ModResult initialize() { + UiMenuTabDesc desc = UI_MENU_TAB_DESC_INIT; + desc.label = "Randomizer"; + desc.on_selected = OnMenuTabSelected; + + ModResult res = + session::svc_mng.ui->register_menu_tab(session::svc_mng.mod_ctx, &desc, &g_menu_tab); + if (res != MOD_OK) { + return res; + } + + return MOD_OK; +} + +void update() { + UpdateSeedGenerationDialog(); +} + +} \ No newline at end of file diff --git a/mods/randomizer/src/ui/ui.hpp b/mods/randomizer/src/ui/ui.hpp new file mode 100644 index 0000000000..a62ee956e0 --- /dev/null +++ b/mods/randomizer/src/ui/ui.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace randomizer::ui { +ModResult initialize(); +void update(); +} \ No newline at end of file From 2a0cd87246eb7da2a8ec9e419a2917d5fd6298ef Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Wed, 5 Aug 2026 14:27:59 -0700 Subject: [PATCH 05/15] ui: start implementing menu tabs and controls --- mods/randomizer/src/ui/rando_config.cpp | 204 ++++++++++++++++++++++++ mods/randomizer/src/ui/rando_config.hpp | 2 + mods/randomizer/src/ui/ui.cpp | 64 +------- 3 files changed, 209 insertions(+), 61 deletions(-) diff --git a/mods/randomizer/src/ui/rando_config.cpp b/mods/randomizer/src/ui/rando_config.cpp index d817474d7c..d61c05accc 100644 --- a/mods/randomizer/src/ui/rando_config.cpp +++ b/mods/randomizer/src/ui/rando_config.cpp @@ -95,6 +95,210 @@ const std::vector>& GetStartingInventoryLayo return layoutOrder; } +namespace { +// Control Helpers +void add_button(UiElementHandle pane, const char* label, const char* help_rml, + UiPressedFn on_pressed, UiElementHandle* out_handle = nullptr) +{ + UiControlDesc desc = UI_CONTROL_DESC_INIT; + desc.kind = UI_CONTROL_BUTTON; + desc.label = label; + desc.help_rml = help_rml; + desc.on_pressed = on_pressed; + session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); +} + +void add_section(UiElementHandle pane, const char* label) { + session::svc_mng.ui->pane_add_section(session::svc_mng.mod_ctx, pane, label); +} + +void add_string_input(UiElementHandle pane, const char* label, const char* help_rml, + int32_t max_length, UiControlGetFn getFn, UiControlSetFn setFn, UiElementHandle* out_handle = nullptr) +{ + UiControlDesc desc = UI_CONTROL_DESC_INIT; + desc.kind = UI_CONTROL_STRING; + desc.label = label; + desc.help_rml = help_rml; + desc.binding = UI_BINDING_CALLBACKS; + desc.get = getFn; + desc.set = setFn; + desc.max_length = max_length; + session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); +} + +void add_select_setting(UiElementHandle pane, const char* key, const char* help_rml, + UiControlGetFn getFn, UiControlSetFn setFn, UiElementHandle* out_handle = nullptr) +{ + auto setting = FindSetting(key); + auto info = setting->GetInfo(); + + std::vector optionsList; + for (size_t i = 0; i < info->GetOptions().size(); ++i) { + optionsList.push_back(info->GetOptions()[i].c_str()); + } + + UiControlDesc desc = UI_CONTROL_DESC_INIT; + desc.kind = UI_CONTROL_SELECT; + desc.label = key; + desc.help_rml = help_rml; + desc.binding = UI_BINDING_CALLBACKS; + desc.get = getFn; + desc.set = setFn; + desc.options = optionsList.data(); + desc.option_count = optionsList.size(); + session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); +} + +// Seed Management Tab +ModResult buildSeedManagementTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, + UiElementHandle rightPane, void*, ModError*) +{ + add_button(leftPane, + "Generate Seed", + "Generate a Randomizer seed using the current configuration options, and the supplied seed string.", + [](ModContext*, void*) {}); + + add_string_input(leftPane, + "Seed String", + "Current value of the seed used by the randomizer for generation. Leave blank for a random value.", + 32, + [](ModContext*, void*, UiControlValue*) {}, + [](ModContext*, void*, const UiControlValue*) {}); + + add_button(leftPane, + "Delete Seeds", + " ", + [](ModContext*, void*) {}); + + add_section(leftPane, "Permalink"); + + add_button(leftPane, + "Copy Permalink", + "Copy your current settings permalink to share with others.", + [](ModContext*, void*) {}); + + add_button(leftPane, + "Paste Permalink", + "Paste in a permalink from your clipboard. This will overwrite your current settings.", + [](ModContext*, void*) {}); + + add_section(leftPane, "Presets"); + + add_button(leftPane, + "Save Current Settings as Preset", + "Save the current settings to your list of presets.", + [](ModContext*, void*) {}); + + add_button(leftPane, + "Load Preset", + "Choose an existing preset to load from.", + [](ModContext*, void*) {}); + + return MOD_OK; +} + +ModResult updateSeedManagementTab(ModContext* ctx, void*, ModError*) { + return MOD_OK; +} + +// Seed Options Tab +ModResult buildSeedOptionsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, + UiElementHandle rightPane, void*, ModError*) +{ + add_button(leftPane, + "Reset Settings to Default", + "Reset all settings to their default values. This will also clear starting items and excluded locations.", + [](ModContext*, void*) {}); + + add_section(leftPane, "Logic Settings"); + + add_select_setting(leftPane, + "Logic Rules", + " ", + [](ModContext*, void*, UiControlValue*) {}, + [](ModContext*, void*, const UiControlValue*) {}); + + add_section(leftPane, "Access Options"); + + add_section(leftPane, "Shuffles"); + + add_section(leftPane, "Dungeon Items"); + + add_section(leftPane, "Timesavers"); + + add_section(leftPane, "Additional Settings"); + + add_section(leftPane, "Dungeon Entrance Settings"); + + add_section(leftPane, "Tricks"); + + return MOD_OK; +} + +// Hints Tab +ModResult buildHintsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, + UiElementHandle rightPane, void*, ModError*) +{ + return MOD_OK; +} + +// Starting Inventory Tab +ModResult buildStartingInventoryTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, + UiElementHandle rightPane, void*, ModError*) +{ + return MOD_OK; +} + +// Excluded Locations Tab +ModResult buildExcludedLocationsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, + UiElementHandle rightPane, void*, ModError*) +{ + return MOD_OK; +} + +// Menu Tab +void OnMenuTabSelected(ModContext* ctx, void*) { + UiTabDesc tabs[5]{}; + + tabs[0].struct_size = sizeof(UiTabDesc); + tabs[0].title = "Seed Management"; + tabs[0].build = buildSeedManagementTab; + tabs[0].update = updateSeedManagementTab; + + tabs[1].struct_size = sizeof(UiTabDesc); + tabs[1].title = "Seed Options"; + tabs[1].build = buildSeedOptionsTab; + + tabs[2].struct_size = sizeof(UiTabDesc); + tabs[2].title = "Hints"; + tabs[2].build = buildHintsTab; + + tabs[3].struct_size = sizeof(UiTabDesc); + tabs[3].title = "Starting Inventory"; + tabs[3].build = buildStartingInventoryTab; + + tabs[4].struct_size = sizeof(UiTabDesc); + tabs[4].title = "Excluded Locations"; + tabs[4].build = buildExcludedLocationsTab; + + UiWindowDesc desc = UI_WINDOW_DESC_INIT; + desc.tabs = tabs; + desc.tab_count = 5; + UiWindowHandle window{}; + session::svc_mng.ui->window_push(ctx, &desc, &window); +} +} + +UiMenuTabHandle g_menu_tab{}; + +ModResult buildMenuTab() { + UiMenuTabDesc desc = UI_MENU_TAB_DESC_INIT; + desc.label = "Randomizer"; + desc.on_selected = OnMenuTabSelected; + + return session::svc_mng.ui->register_menu_tab(session::svc_mng.mod_ctx, &desc, &g_menu_tab); +} + std::filesystem::path GetRandomizerPath() { return paths::GetRandomizerPath() / "randomizer"; } diff --git a/mods/randomizer/src/ui/rando_config.hpp b/mods/randomizer/src/ui/rando_config.hpp index 1c5457e0c9..329428531c 100644 --- a/mods/randomizer/src/ui/rando_config.hpp +++ b/mods/randomizer/src/ui/rando_config.hpp @@ -17,6 +17,8 @@ void ApplyExistingRandomizerPreset(const std::filesystem::path& presetFilePath); void CopyPermalinkToClipboard(); void PastePermalinkFromClipboard(); +ModResult buildMenuTab(); + std::filesystem::path GetRandomizerPath(); std::filesystem::path GetRandomizerSettingsPath(); std::filesystem::path GetRandomizerPreferencesPath(); diff --git a/mods/randomizer/src/ui/ui.cpp b/mods/randomizer/src/ui/ui.cpp index 2914b2b2af..0f8bfd6bd4 100644 --- a/mods/randomizer/src/ui/ui.cpp +++ b/mods/randomizer/src/ui/ui.cpp @@ -1,73 +1,15 @@ #include "ui.hpp" -#include -#include -#include -#include -#include -#include -#include - #include -#include "../session.hpp" -#include "../randomizer_context.hpp" -#include "../paths.hpp" - -#include "config_store.hpp" #include "rando_seed_generation.hpp" +#include "rando_config.hpp" namespace randomizer::ui { -namespace { -// Seed Tab -ModResult buildSeedTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, - UiElementHandle rightPane, void*, ModError*) -{ - return MOD_OK; -} - -ModResult updateSeedTab(ModContext* ctx, void*, ModError*) { - return MOD_OK; -} - -// Settings Tab -ModResult buildSettingsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, - UiElementHandle rightPane, void*, ModError*) -{ - return MOD_OK; -} - -// Menu Tab -void OnMenuTabSelected(ModContext* ctx, void*) { - UiTabDesc tabs[2]{}; - - tabs[0].struct_size = sizeof(UiTabDesc); - tabs[0].title = "Seed"; - tabs[0].build = buildSeedTab; - tabs[0].update = updateSeedTab; - - tabs[1].struct_size = sizeof(UiTabDesc); - tabs[1].title = "Settings"; - tabs[1].build = buildSettingsTab; - - UiWindowDesc desc = UI_WINDOW_DESC_INIT; - desc.tabs = tabs; - desc.tab_count = 2; - UiWindowHandle window{}; - session::svc_mng.ui->window_push(ctx, &desc, &window); -} -} - -UiMenuTabHandle g_menu_tab{}; - ModResult initialize() { - UiMenuTabDesc desc = UI_MENU_TAB_DESC_INIT; - desc.label = "Randomizer"; - desc.on_selected = OnMenuTabSelected; - - ModResult res = - session::svc_mng.ui->register_menu_tab(session::svc_mng.mod_ctx, &desc, &g_menu_tab); + ModResult res = buildMenuTab(); if (res != MOD_OK) { + mods::log::error("failed to initialize randomizer menu tab!"); return res; } From 8c4ad273b8749fbcaf76aef491b5a9a45a5107d3 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Wed, 5 Aug 2026 16:02:40 -0700 Subject: [PATCH 06/15] ui: blocking in seed options and hints tabs --- mods/randomizer/src/ui/rando_config.cpp | 112 ++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/mods/randomizer/src/ui/rando_config.cpp b/mods/randomizer/src/ui/rando_config.cpp index d61c05accc..689b1a872b 100644 --- a/mods/randomizer/src/ui/rando_config.cpp +++ b/mods/randomizer/src/ui/rando_config.cpp @@ -126,8 +126,35 @@ void add_string_input(UiElementHandle pane, const char* label, const char* help_ session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } -void add_select_setting(UiElementHandle pane, const char* key, const char* help_rml, - UiControlGetFn getFn, UiControlSetFn setFn, UiElementHandle* out_handle = nullptr) +void add_select_setting(UiElementHandle pane, const char* key, UiElementHandle* out_handle = nullptr) +{ + auto setting = FindSetting(key); + auto info = setting->GetInfo(); + + std::vector optionsList; + std::string help_rml = ""; + for (size_t i = 0; i < info->GetOptions().size(); ++i) { + optionsList.push_back(info->GetOptions()[i].c_str()); + help_rml += fmt::format("
{}: {}", info->GetOptions()[i], info->GetDescriptions()[i]); + } + + // temp dummies + auto getFn = [](ModContext*, void*, UiControlValue*) {}; + auto setFn = [](ModContext*, void*, const UiControlValue*) {}; + + UiControlDesc desc = UI_CONTROL_DESC_INIT; + desc.kind = UI_CONTROL_SELECT; + desc.label = key; + desc.help_rml = help_rml.c_str(); + desc.binding = UI_BINDING_CALLBACKS; + desc.get = getFn; + desc.set = setFn; + desc.options = optionsList.data(); + desc.option_count = optionsList.size(); + session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); +} + +void add_select_number_setting(UiElementHandle pane, const char* key, UiElementHandle* out_handle = nullptr) { auto setting = FindSetting(key); auto info = setting->GetInfo(); @@ -137,10 +164,14 @@ void add_select_setting(UiElementHandle pane, const char* key, const char* help_ optionsList.push_back(info->GetOptions()[i].c_str()); } + // temp dummies + auto getFn = [](ModContext*, void*, UiControlValue*) {}; + auto setFn = [](ModContext*, void*, const UiControlValue*) {}; + UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_SELECT; desc.label = key; - desc.help_rml = help_rml; + desc.help_rml = ""; desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; @@ -211,26 +242,68 @@ ModResult buildSeedOptionsTab(ModContext* ctx, UiWindowHandle, UiElementHandle l [](ModContext*, void*) {}); add_section(leftPane, "Logic Settings"); - - add_select_setting(leftPane, - "Logic Rules", - " ", - [](ModContext*, void*, UiControlValue*) {}, - [](ModContext*, void*, const UiControlValue*) {}); + add_select_setting(leftPane, "Logic Rules"); add_section(leftPane, "Access Options"); + add_select_setting(leftPane, "Hyrule Barrier Requirements"); + add_select_setting(leftPane, "Palace of Twilight Requirements"); + add_select_setting(leftPane, "Faron Woods Logic"); + add_select_setting(leftPane, "Mirror Chamber Access"); add_section(leftPane, "Shuffles"); + add_select_setting(leftPane, "Golden Bugs"); + add_select_setting(leftPane, "Sky Characters"); + add_select_setting(leftPane, "Gifts From NPCs"); + add_select_setting(leftPane, "Shop Items"); + add_select_setting(leftPane, "Hidden Skills"); + add_select_setting(leftPane, "Hidden Rupees"); + add_select_setting(leftPane, "Freestanding Rupees"); + add_select_setting(leftPane, "Poe Souls"); + add_select_setting(leftPane, "Ilia Memory Quest"); + add_select_setting(leftPane, "Item Scarcity"); + add_select_setting(leftPane, "Trap Item Frequency"); add_section(leftPane, "Dungeon Items"); + add_select_setting(leftPane, "Small Keys"); + add_select_setting(leftPane, "Big Keys"); + add_select_setting(leftPane, "Maps and Compasses"); + add_select_setting(leftPane, "Hyrule Castle Big Key Requirements"); + add_select_setting(leftPane, "Dungeon Rewards Can Be Anywhere"); + add_select_setting(leftPane, "No Small Keys on Bosses"); + add_select_setting(leftPane, "Unrequired Dungeons Are Barren"); add_section(leftPane, "Timesavers"); + add_select_setting(leftPane, "Skip Prologue"); + add_select_setting(leftPane, "Faron Twilight Cleared"); + add_select_setting(leftPane, "Eldin Twilight Cleared"); + add_select_setting(leftPane, "Lanayru Twilight Cleared"); + add_select_setting(leftPane, "Skip Midna's Desparate Hour"); + add_select_setting(leftPane, "Skip Minor Cutscenes"); + add_select_setting(leftPane, "Skip Major Cutscenes"); + add_select_setting(leftPane, "Unlock Map Regions"); + add_select_setting(leftPane, "Open Door of Time"); + add_select_setting(leftPane, "Active Goron Mines Magnets"); + add_select_setting(leftPane, "Lower Hyrule Castle Chandelier"); + add_select_setting(leftPane, "Skip Bridge Donation"); add_section(leftPane, "Additional Settings"); + add_select_setting(leftPane, "Starting Time of Day"); + add_select_setting(leftPane, "Logic Transform Anywhere"); + add_select_setting(leftPane, "Logic Increase Wallet Capacity"); + add_select_setting(leftPane, "Logic Damage Multiplier"); add_section(leftPane, "Dungeon Entrance Settings"); + add_select_setting(leftPane, "Lakebed Does Not Require Water Bombs"); + add_select_setting(leftPane, "Arbiters Does Not Require Bulblin Camp"); + add_select_setting(leftPane, "Snowpeak Does Not Require Reekfish Scent"); + add_select_setting(leftPane, "Sacred Grove Does Not Require Skull Kid"); + add_select_setting(leftPane, "City Does Not Require Filled Skybook"); + add_select_setting(leftPane, "Goron Mines Entrance"); + add_select_setting(leftPane, "Temple of Time Sword Requirement"); add_section(leftPane, "Tricks"); + add_select_setting(leftPane, "Back Slice as Sword"); + add_select_setting(leftPane, "Ball and Chain Webs"); return MOD_OK; } @@ -239,6 +312,27 @@ ModResult buildSeedOptionsTab(ModContext* ctx, UiWindowHandle, UiElementHandle l ModResult buildHintsTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { + add_section(leftPane, "Path Hints"); + add_select_number_setting(leftPane, "Number of Path Hints"); + add_select_setting(leftPane, "Path Hints on Midna"); + add_select_setting(leftPane, "Path Hints on Hint Signs"); + + add_section(leftPane, "Barren Hints"); + add_select_number_setting(leftPane, "Number of Barren Hints"); + add_select_setting(leftPane, "Barren Hints on Midna"); + add_select_setting(leftPane, "Barren Hints on Hint Signs"); + + add_section(leftPane, "Item Hints"); + add_select_number_setting(leftPane, "Number of Item Hints"); + add_select_setting(leftPane, "Item Hints on Midna"); + add_select_setting(leftPane, "Item Hints on Hint Signs"); + + add_section(leftPane, "Location Hints"); + add_select_number_setting(leftPane, "Number of Location Hints"); + add_select_setting(leftPane, "Location Hints on Midna"); + add_select_setting(leftPane, "Location Hints on Hint Signs"); + add_select_setting(leftPane, "Prioritize Remote Location Hints"); + return MOD_OK; } From cd2ed2843c216d8a3042dce3d9ba4c213ded8a27 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Thu, 6 Aug 2026 03:49:26 -0700 Subject: [PATCH 07/15] ui: begin hooking up some menu controls --- mods/randomizer/src/ui/rando_config.cpp | 109 +++++++++++++++++++----- 1 file changed, 90 insertions(+), 19 deletions(-) diff --git a/mods/randomizer/src/ui/rando_config.cpp b/mods/randomizer/src/ui/rando_config.cpp index 689b1a872b..fabfb28497 100644 --- a/mods/randomizer/src/ui/rando_config.cpp +++ b/mods/randomizer/src/ui/rando_config.cpp @@ -138,9 +138,30 @@ void add_select_setting(UiElementHandle pane, const char* key, UiElementHandle* help_rml += fmt::format("
{}: {}", info->GetOptions()[i], info->GetDescriptions()[i]); } - // temp dummies - auto getFn = [](ModContext*, void*, UiControlValue*) {}; - auto setFn = [](ModContext*, void*, const UiControlValue*) {}; + auto getFn = [](ModContext*, void* user_data, UiControlValue* out_value) { + auto setting = FindSetting(static_cast(user_data)); + const auto& options = setting->GetInfo()->GetOptions(); + + for (size_t i = 0; i < options.size(); ++i) { + if (options[i] == setting->GetCurrentOption()) { + out_value->int_value = i; + return; + } + } + + // default + out_value->int_value = 0; + }; + + auto setFn = [](ModContext*, void* user_data, const UiControlValue* value) { + auto setting = FindSetting(static_cast(user_data)); + const auto& options = setting->GetInfo()->GetOptions(); + + if (value->int_value >= 0 && value->int_value < options.size()) { + setting->SetCurrentOption(options[value->int_value]); + SaveRandomizerConfig(); + } + }; UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_SELECT; @@ -149,6 +170,7 @@ void add_select_setting(UiElementHandle pane, const char* key, UiElementHandle* desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; + desc.user_data = (void*)key; desc.options = optionsList.data(); desc.option_count = optionsList.size(); session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); @@ -164,9 +186,30 @@ void add_select_number_setting(UiElementHandle pane, const char* key, UiElementH optionsList.push_back(info->GetOptions()[i].c_str()); } - // temp dummies - auto getFn = [](ModContext*, void*, UiControlValue*) {}; - auto setFn = [](ModContext*, void*, const UiControlValue*) {}; + auto getFn = [](ModContext*, void* user_data, UiControlValue* out_value) { + auto setting = FindSetting(static_cast(user_data)); + const auto& options = setting->GetInfo()->GetOptions(); + + for (size_t i = 0; i < options.size(); ++i) { + if (options[i] == setting->GetCurrentOption()) { + out_value->int_value = i; + return; + } + } + + // default + out_value->int_value = 0; + }; + + auto setFn = [](ModContext*, void* user_data, const UiControlValue* value) { + auto setting = FindSetting(static_cast(user_data)); + const auto& options = setting->GetInfo()->GetOptions(); + + if (value->int_value >= 0 && value->int_value < options.size()) { + setting->SetCurrentOption(options[value->int_value]); + SaveRandomizerConfig(); + } + }; UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_SELECT; @@ -175,6 +218,7 @@ void add_select_number_setting(UiElementHandle pane, const char* key, UiElementH desc.binding = UI_BINDING_CALLBACKS; desc.get = getFn; desc.set = setFn; + desc.user_data = (void*)key; desc.options = optionsList.data(); desc.option_count = optionsList.size(); session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); @@ -187,43 +231,67 @@ ModResult buildSeedManagementTab(ModContext* ctx, UiWindowHandle, UiElementHandl add_button(leftPane, "Generate Seed", "Generate a Randomizer seed using the current configuration options, and the supplied seed string.", - [](ModContext*, void*) {}); + [](ModContext*, void*) { + if (TryCreateRandomSeed()) { + mods::log::info("Created new Seed for generator."); + } + GenerateRandomizerSeed(); + }); add_string_input(leftPane, "Seed String", "Current value of the seed used by the randomizer for generation. Leave blank for a random value.", 32, - [](ModContext*, void*, UiControlValue*) {}, - [](ModContext*, void*, const UiControlValue*) {}); + [](ModContext*, void*, UiControlValue* out_value) { + out_value->string_value = GetRandomizerConfig().GetSeed().c_str(); + }, + [](ModContext*, void*, const UiControlValue* value) { + GetRandomizerConfig().SetSeed(value->string_value); + SaveRandomizerConfig(); + }); add_button(leftPane, "Delete Seeds", - " ", - [](ModContext*, void*) {}); + "Delete any seed not currently being used.", + [](ModContext*, void*) { + // TODO + }); add_section(leftPane, "Permalink"); - add_button(leftPane, - "Copy Permalink", - "Copy your current settings permalink to share with others.", - [](ModContext*, void*) {}); + { + std::string help_rml = "Copy your current settings permalink to share with others."; + help_rml += fmt::format("
Current Permalink: {}", GetRandomizerConfig().GetPermalink()); + add_button(leftPane, + "Copy Permalink", + help_rml.c_str(), + [](ModContext*, void*) { + // TODO: need SDL clipboard access + }); + } add_button(leftPane, "Paste Permalink", "Paste in a permalink from your clipboard. This will overwrite your current settings.", - [](ModContext*, void*) {}); + [](ModContext*, void*) { + // TODO: need SDL clipboard access + }); add_section(leftPane, "Presets"); add_button(leftPane, "Save Current Settings as Preset", "Save the current settings to your list of presets.", - [](ModContext*, void*) {}); + [](ModContext*, void*) { + // TODO + }); add_button(leftPane, "Load Preset", "Choose an existing preset to load from.", - [](ModContext*, void*) {}); + [](ModContext*, void*) { + // TODO + }); return MOD_OK; } @@ -239,7 +307,10 @@ ModResult buildSeedOptionsTab(ModContext* ctx, UiWindowHandle, UiElementHandle l add_button(leftPane, "Reset Settings to Default", "Reset all settings to their default values. This will also clear starting items and excluded locations.", - [](ModContext*, void*) {}); + [](ModContext*, void*) { + GetRandomizerConfig().ResetSettingsToDefault(); + SaveRandomizerConfig(); + }); add_section(leftPane, "Logic Settings"); add_select_setting(leftPane, "Logic Rules"); From cb564f7465b6704a880f6cd5b8f4e1c77dbf7907 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Thu, 6 Aug 2026 18:00:16 -0700 Subject: [PATCH 08/15] ui: initial file select gate implementation --- mods/randomizer/CMakeLists.txt | 1 + mods/randomizer/src/hooks.cpp | 111 ++++++++++++++++++++++++ mods/randomizer/src/hooks.hpp | 7 ++ mods/randomizer/src/mod.cpp | 6 ++ mods/randomizer/src/ui/rando_config.cpp | 85 +++++++++++++++++- mods/randomizer/src/ui/rando_config.hpp | 16 ++++ 6 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 mods/randomizer/src/hooks.cpp create mode 100644 mods/randomizer/src/hooks.hpp diff --git a/mods/randomizer/CMakeLists.txt b/mods/randomizer/CMakeLists.txt index f41c9f1eac..2b55767154 100644 --- a/mods/randomizer/CMakeLists.txt +++ b/mods/randomizer/CMakeLists.txt @@ -85,6 +85,7 @@ set(RANDOMIZER_SOURCES src/verify_item_functions.cpp src/paths.cpp src/session.cpp + src/hooks.cpp src/ui/config_store.cpp src/ui/rando_seed_generation.cpp src/ui/rando_config.cpp diff --git a/mods/randomizer/src/hooks.cpp b/mods/randomizer/src/hooks.cpp new file mode 100644 index 0000000000..864a86cd8f --- /dev/null +++ b/mods/randomizer/src/hooks.cpp @@ -0,0 +1,111 @@ +#include "hooks.hpp" +#include "session.hpp" +#include "randomizer_context.hpp" +#include "ui/rando_config.hpp" + +#include +#include + +#include "d/d_file_select.h" + +DEFINE_HOOK(&dFile_select_c::selectDataNameMove, dFile_select_c__selectDataNameMove); +DEFINE_HOOK(&dFile_select_c::dataSelect, dFile_select_c__dataSelect); + +namespace randomizer::ui { +dialogSelectModeState g_dialogSelectModeState = SelectReady; +} + +namespace randomizer::hooks { +namespace { +UiDialogHandle playModeDialog{0}; + +HookAction hookPreDataSelect(ModContext*, void* args, void* retval, void* userdata) { + ui::g_dialogSelectModeState = ui::SelectReady; + ui::g_file_select_window_ctx.is_proceed = false; + return HOOK_CONTINUE; +} + +HookAction hookPreSelectDataNameMove(ModContext*, void* args, void* retval, void* userdata) { + dFile_select_c* i_this = mods::arg(args, 0); + + // if coming from "start randomizer" button, let transition occur as normal + if (ui::g_file_select_window_ctx.is_proceed) { + return HOOK_CONTINUE; + } + + bool isHeaderTxtChange = i_this->headerTxtChangeAnm(); + bool isFileRecScale = i_this->fileRecScaleAnm2(); + bool isModoruTxtDisp = i_this->modoruTxtDispAnm(); + + // if selected vanilla mode, let transition occur as normal + if (ui::g_dialogSelectModeState == ui::SelectVanilla) { + return HOOK_CONTINUE; + } + + if (ui::g_dialogSelectModeState == ui::SelectReady && isHeaderTxtChange == true && isFileRecScale == true && isModoruTxtDisp == true) { + ui::g_dialogSelectModeState = ui::SelectWait; + + auto buildDialog = [i_this]() { + UiDialogDesc desc = UI_DIALOG_DESC_INIT; + desc.title = "Play Type"; + desc.body_rml = "What mode would you like to play?"; + desc.icon = "question-mark"; + desc.variant = UI_DIALOG_NORMAL; + + UiDialogAction actions[2]; + actions[0] = { + .label = "Vanilla", + .on_pressed = [](ModContext* ctx, UiDialogHandle dialogHandle, void*) { + mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK); + randomizer_GetContext() = RandomizerContext(); + ui::g_dialogSelectModeState = ui::SelectVanilla; + session::svc_mng.ui->dialog_close(ctx, dialogHandle); + }, + .user_data = nullptr, + .keep_open = false, + }; + actions[1] = { + .label = "Randomizer", + .on_pressed = [](ModContext* ctx, UiDialogHandle dialogHandle, void* userdata) { + mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK); + ui::g_dialogSelectModeState = ui::SelectRandomizer; + ui::buildFileSelectGateMenu(static_cast(userdata)); + session::svc_mng.ui->dialog_close(ctx, dialogHandle); + }, + .user_data = i_this, + .keep_open = false, + }; + desc.actions = actions; + desc.action_count = 2; + + if (session::svc_mng.ui->dialog_push(session::svc_mng.mod_ctx, &desc, &playModeDialog) != MOD_OK) { + mods::log::error("Failed to push dialog"); + return MOD_ERROR; + } + + return MOD_OK; + }; + + if (buildDialog() != MOD_OK) { + mods::log::error("Failed to build dialog"); + return HOOK_CONTINUE; + } + } + + return HOOK_SKIP_ORIGINAL; +} +} + +ModResult initialize() { +#define ADD_HOOK_PRE(originalFn, hookFn) \ + if (mods::hook::add_pre(hookFn) != MOD_OK) { \ + mods::log::error("Failed to add pre-hook for " #originalFn); \ + return MOD_ERROR; \ + } + + ADD_HOOK_PRE(dFile_select_c__selectDataNameMove, hookPreSelectDataNameMove); + ADD_HOOK_PRE(dFile_select_c__dataSelect, hookPreDataSelect); + + return MOD_OK; +} +} \ No newline at end of file diff --git a/mods/randomizer/src/hooks.hpp b/mods/randomizer/src/hooks.hpp new file mode 100644 index 0000000000..a27ea0aaba --- /dev/null +++ b/mods/randomizer/src/hooks.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace randomizer::hooks { +ModResult initialize(); +} \ No newline at end of file diff --git a/mods/randomizer/src/mod.cpp b/mods/randomizer/src/mod.cpp index 4421193432..a770ccf44a 100644 --- a/mods/randomizer/src/mod.cpp +++ b/mods/randomizer/src/mod.cpp @@ -3,6 +3,7 @@ #include "session.hpp" #include "ui/ui.hpp" +#include "hooks.hpp" DEFINE_MOD(); IMPORT_SERVICE(HostService, svc_host); @@ -32,6 +33,11 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { return mods::set_error(error, result, "failed to initialize session"); } + result = randomizer::hooks::initialize(); + if (result != MOD_OK) { + return mods::set_error(error, result, "failed to initialize hooks"); + } + result = randomizer::ui::initialize(); if (result != MOD_OK) { return mods::set_error(error, result, "failed to initialize ui"); diff --git a/mods/randomizer/src/ui/rando_config.cpp b/mods/randomizer/src/ui/rando_config.cpp index fabfb28497..5e5ed9a3fb 100644 --- a/mods/randomizer/src/ui/rando_config.cpp +++ b/mods/randomizer/src/ui/rando_config.cpp @@ -15,6 +15,8 @@ #include #include +#include "d/d_file_select.h" + namespace randomizer::ui { seedgen::settings::Setting* FindSetting(const std::string& key) { @@ -95,16 +97,21 @@ const std::vector>& GetStartingInventoryLayo return layoutOrder; } +UiMenuTabHandle g_menu_tab{}; + +FileSelectGateWindowCtx g_file_select_window_ctx{}; + namespace { // Control Helpers void add_button(UiElementHandle pane, const char* label, const char* help_rml, - UiPressedFn on_pressed, UiElementHandle* out_handle = nullptr) + UiPressedFn on_pressed, void* userdata = nullptr, UiElementHandle* out_handle = nullptr) { UiControlDesc desc = UI_CONTROL_DESC_INIT; desc.kind = UI_CONTROL_BUTTON; desc.label = label; desc.help_rml = help_rml; desc.on_pressed = on_pressed; + desc.user_data = userdata; session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } @@ -452,9 +459,31 @@ void OnMenuTabSelected(ModContext* ctx, void*) { UiWindowHandle window{}; session::svc_mng.ui->window_push(ctx, &desc, &window); } -} -UiMenuTabHandle g_menu_tab{}; +// Play Tab +ModResult buildPlayTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, + UiElementHandle rightPane, void*, ModError*) +{ + add_button(leftPane, + "Selected Seed", + "", + [](ModContext*, void*) { + // TODO + }); + + add_button(leftPane, + "Start Randomizer", + "", + [](ModContext*, void* userdata) { + // set flag to move to name screen after window close + g_file_select_window_ctx.is_proceed = true; + session::svc_mng.ui->window_close(session::svc_mng.mod_ctx, *static_cast(userdata)); + }, + &g_file_select_window_ctx.window_handle); + + return MOD_OK; +} +} ModResult buildMenuTab() { UiMenuTabDesc desc = UI_MENU_TAB_DESC_INIT; @@ -464,6 +493,56 @@ ModResult buildMenuTab() { return session::svc_mng.ui->register_menu_tab(session::svc_mng.mod_ctx, &desc, &g_menu_tab); } +ModResult buildFileSelectGateMenu(dFile_select_c* fileSelect) { + UiTabDesc tabs[6]{}; + + tabs[0].struct_size = sizeof(UiTabDesc); + tabs[0].title = "Play"; + tabs[0].build = buildPlayTab; + + tabs[1].struct_size = sizeof(UiTabDesc); + tabs[1].title = "Seed Management"; + tabs[1].build = buildSeedManagementTab; + tabs[1].update = updateSeedManagementTab; + + tabs[2].struct_size = sizeof(UiTabDesc); + tabs[2].title = "Seed Options"; + tabs[2].build = buildSeedOptionsTab; + + tabs[3].struct_size = sizeof(UiTabDesc); + tabs[3].title = "Hints"; + tabs[3].build = buildHintsTab; + + tabs[4].struct_size = sizeof(UiTabDesc); + tabs[4].title = "Starting Inventory"; + tabs[4].build = buildStartingInventoryTab; + + tabs[5].struct_size = sizeof(UiTabDesc); + tabs[5].title = "Excluded Locations"; + tabs[5].build = buildExcludedLocationsTab; + + UiWindowDesc desc = UI_WINDOW_DESC_INIT; + desc.tabs = tabs; + desc.tab_count = 6; + desc.user_data = fileSelect; + desc.on_closed = [](ModContext*, UiWindowHandle, void* userdata) { + dFile_select_c* i_this = static_cast(userdata); + + // if closing the window through backing out, return to file select + if (!g_file_select_window_ctx.is_proceed) { + i_this->headerTxtSet(0x43, 1, 0); + i_this->fileRecScaleAnmInitSet2(0.0f, 1.0f); + i_this->nameMoveAnmInitSet(0xd29, 0xd1f); + i_this->modoruTxtDispAnmInit(0); + i_this->mDataSelProc = dFile_select_c::DATASELPROC_NAME_TO_DATA_SELECT_MOVE; + } + + g_dialogSelectModeState = SelectReady; + }; + + session::svc_mng.ui->window_push(session::svc_mng.mod_ctx, &desc, &g_file_select_window_ctx.window_handle); +} + std::filesystem::path GetRandomizerPath() { return paths::GetRandomizerPath() / "randomizer"; } diff --git a/mods/randomizer/src/ui/rando_config.hpp b/mods/randomizer/src/ui/rando_config.hpp index 329428531c..c0d5c3b608 100644 --- a/mods/randomizer/src/ui/rando_config.hpp +++ b/mods/randomizer/src/ui/rando_config.hpp @@ -3,6 +3,7 @@ #include #include #include +#include "mods/svc/ui.h" // Forward declaration namespace randomizer::seedgen::config { @@ -12,12 +13,27 @@ class dFile_select_c; namespace randomizer::ui { +enum dialogSelectModeState : uint8_t { + SelectReady, + SelectWait, + SelectVanilla, + SelectRandomizer +}; +extern dialogSelectModeState g_dialogSelectModeState; + +struct FileSelectGateWindowCtx { + UiWindowHandle window_handle{}; + bool is_proceed{false}; +}; +extern FileSelectGateWindowCtx g_file_select_window_ctx; + void SaveNewRandomizerPreset(const std::string& presetName, bool overwriteExisting = false); void ApplyExistingRandomizerPreset(const std::filesystem::path& presetFilePath); void CopyPermalinkToClipboard(); void PastePermalinkFromClipboard(); ModResult buildMenuTab(); +ModResult buildFileSelectGateMenu(dFile_select_c*); std::filesystem::path GetRandomizerPath(); std::filesystem::path GetRandomizerSettingsPath(); From 769f83db251317dcb359d8a77cb9ff8b02183cf1 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Fri, 7 Aug 2026 07:46:43 -0700 Subject: [PATCH 09/15] add in hooks --- extern/aurora | 2 +- mods/randomizer/CMakeLists.txt | 1 - mods/randomizer/src/hooks.cpp | 571 ++++++++++++++++++++++++++++++++- 3 files changed, 571 insertions(+), 3 deletions(-) diff --git a/extern/aurora b/extern/aurora index 8005d336ab..1d10fa1bc5 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 8005d336ab02f0c617fc27fabbea4af42ab04caa +Subproject commit 1d10fa1bc502910a6336fdac32f31cd0ac39710d diff --git a/mods/randomizer/CMakeLists.txt b/mods/randomizer/CMakeLists.txt index 2b55767154..3496312a59 100644 --- a/mods/randomizer/CMakeLists.txt +++ b/mods/randomizer/CMakeLists.txt @@ -101,7 +101,6 @@ file(GLOB_RECURSE RANDOMIZER_DATA RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" # to both the mod and the standalone generator add_library(randomizer_embeds OBJECT) -b_embed(randomizer_embeds "res/shadow_crystal.bti") foreach (RANDOMIZER_FILE IN LISTS RANDOMIZER_DATA) if (RANDOMIZER_FILE MATCHES "^generator/data/tests") continue() diff --git a/mods/randomizer/src/hooks.cpp b/mods/randomizer/src/hooks.cpp index 864a86cd8f..ca82a24dfc 100644 --- a/mods/randomizer/src/hooks.cpp +++ b/mods/randomizer/src/hooks.cpp @@ -2,15 +2,58 @@ #include "session.hpp" #include "randomizer_context.hpp" #include "ui/rando_config.hpp" +#include "flags.h" +#include "stages.h" +#include "tools.h" +#include "item_ids.h" #include #include +#include "d/actor/d_a_alink.h" #include "d/d_file_select.h" +#include "d/d_meter2_info.h" +#include "d/d_save.h" +#include "d/d_shop_system.h" DEFINE_HOOK(&dFile_select_c::selectDataNameMove, dFile_select_c__selectDataNameMove); DEFINE_HOOK(&dFile_select_c::dataSelect, dFile_select_c__dataSelect); +DEFINE_HOOK(&dSv_event_c::isEventBit, dSv_event_c__isEventBit); +DEFINE_HOOK(&dSv_event_c::onEventBit, dSv_event_c__onEventBit); + +DEFINE_HOOK(&dSv_memBit_c::isSwitch, dSv_memBit_c__isSwitch); +DEFINE_HOOK(&dSv_memBit_c::onSwitch, dSv_memBit_c__onSwitch); +DEFINE_HOOK(&dSv_memBit_c::onDungeonItem, dSv_memBit_c__onDungeonItem); +DEFINE_HOOK(&dSv_memBit_c::offDungeonItem, dSv_memBit_c__offDungeonItem); +DEFINE_HOOK(&dSv_memBit_c::isDungeonItem, dSv_memBit_c__isDungeonItem); + +DEFINE_HOOK(&dSv_player_status_b_c::isDarkClearLV, dSv_player_status_b_c__isDarkClearLV); + +DEFINE_HOOK(&dSv_player_item_c::checkEmptyBottle, dSv_player_item_c__checkEmptyBottle); +DEFINE_HOOK(&dSv_player_item_c::setLineUpItem, dSv_player_item_c__setLineUpItem); + +DEFINE_HOOK(&dSv_info_c::onSwitch, dSv_info_c__onSwitch); + +DEFINE_HOOK(&dMsgFlow_c::query001, dMsgFlow_c__query001); +DEFINE_HOOK(&dMsgFlow_c::query022, dMsgFlow_c__query022); +DEFINE_HOOK(&dMsgFlow_c::query025, dMsgFlow_c__query025); +DEFINE_HOOK(&dMsgFlow_c::query049, dMsgFlow_c__query049); +DEFINE_HOOK(&dMsgFlow_c::event035, dMsgFlow_c__event035); + +/*DEFINE_HOOK_SYMBOL("__Z21dComIfGp_setNextStagePKcsaafjiasii", + void(char const*, s16, s8, s8, f32, u32, int, s8, s16, int, int), setNextStage);*/ + +DEFINE_HOOK_SYMBOL("daObj_Gb_Create", int(fopAc_ac_c*), ObjGb_Create); + +DEFINE_HOOK(&dMeter2Info_readItemTexture, readItemTexture); + +DEFINE_HOOK(&dShopSystem_c::seq_decide_yes, dShopSystem_c__seq_decide_yes); + +DEFINE_HOOK(&CheckFieldItemCreateHeap, dItemData_CheckFieldItemCreateHeap); + +DEFINE_HOOK(&dEvt_control_c::talkEnd, dEvt_control_c__talkEnd); + namespace randomizer::ui { dialogSelectModeState g_dialogSelectModeState = SelectReady; } @@ -18,7 +61,6 @@ dialogSelectModeState g_dialogSelectModeState = SelectReady; namespace randomizer::hooks { namespace { UiDialogHandle playModeDialog{0}; - HookAction hookPreDataSelect(ModContext*, void* args, void* retval, void* userdata) { ui::g_dialogSelectModeState = ui::SelectReady; ui::g_file_select_window_ctx.is_proceed = false; @@ -94,6 +136,493 @@ HookAction hookPreSelectDataNameMove(ModContext*, void* args, void* retval, void return HOOK_SKIP_ORIGINAL; } + + +HookAction hookPreIsEventBit(ModContext*, void* args, void* retval, void*) { + const u16 i_no = mods::arg(args, 1); + auto& out = *static_cast(retval); + + switch (i_no) { + case BO_TALKED_TO_YOU_AFTER_OPENING_IRON_BOOTS_CHEST: { + if (daAlink_c::checkStageName(allStages[Ordon_Village_Interiors])) { + out = dComIfGs_isEventBit(HEARD_BO_TEXT_AFTER_SUMO_FIGHT) ? TRUE : FALSE; + return HOOK_SKIP_ORIGINAL; + } + break; + } + case GAVE_ILIA_HER_CHARM: // Gave Ilia the charm + case CITY_OOCCOO_CS_WATCHED: // CiTS Intro CS watched + { + if (daAlink_c::checkStageName(allStages[Hidden_Village])) { + if (!dComIfGs_isEventBit(GOT_ILIAS_CHARM)) { + // If we haven't gotten the item from Impaz then we need to return false or it + // will break her dialogue. + out = FALSE; + return HOOK_SKIP_ORIGINAL; + } + } + break; + } + case GORON_MINES_CLEARED: { + if (daAlink_c::checkStageName(allStages[Goron_Mines]) || + daAlink_c::checkStageName(allStages[Death_Mountain_Interiors])) { + out = FALSE; // The gorons will not act properly if the flag is set. + return HOOK_SKIP_ORIGINAL; + } + break; + } + case ZORA_ESCORT_CLEARED: { + if (daAlink_c::checkStageName(allStages[Castle_Town])) { + // If the flag isn't set the player will be thrown into escort when they open the door + out = TRUE; + return HOOK_SKIP_ORIGINAL; + } + if (playerIsInRoomStage(0, allStages[Kakariko_Village_Interiors])) { + out = TRUE; // Return true to prevent Renado/Ilia crash after ToT + return HOOK_SKIP_ORIGINAL; + } + break; + } + case CITY_IN_THE_SKY_CLEARED: // Would like to find where this is checked and patch it there. + { + if (!dComIfGs_isEventBit(FIXED_THE_MIRROR_OF_TWILIGHT)) { + if (randomizer_GetContext().mSettings[RandomizerContext::PALACE_OF_TWILIGHT_REQUIREMENTS] != + RandomizerContext::VANILLA) { + out = FALSE; + return HOOK_SKIP_ORIGINAL; + } + } + break; + } + case HOWLED_AT_SNOWPEAK_STONE: { + if (daAlink_c::checkStageName(allStages[Snowpeak])) { + // return false so the player can howl at the stone multiple times to remove map glitch + out = FALSE; + return HOOK_SKIP_ORIGINAL; + } + break; + } + case WATCHED_CUTSCENE_AFTER_GOATS_2: { + if (playerIsInRoomStage(1, allStages[Ordon_Village_Interiors])) { + // false -> Sera gives the milk item once they help the cat; + // true -> the shop is always usable even if the cat is not returned. + out = dComIfGs_isEventBit(SERAS_CAT_RETURNED_TO_SHOP) ? FALSE : TRUE; + return HOOK_SKIP_ORIGINAL; + } + break; + } + case FIXED_THE_MIRROR_OF_TWILIGHT: { + if (daAlink_c::checkStageName(allStages[Palace_of_Twilight])) { + out = TRUE; // If the flag is not set, the player cannot leave PoT from the inside. + return HOOK_SKIP_ORIGINAL; + } + break; + } + default: + break; + } + + return HOOK_CONTINUE; +} + +HookAction hookPreOnEventBit(ModContext*, void* args, void*, void*) { + const u16 i_no = mods:: arg(args, 1); + + switch (i_no) { + // Wolf <-> Human crash patches/bug fixes: some cutscenes/events either crash or act + // weird if Link is in the wrong form and the game no longer auto-transforms once the + // Shadow Crystal has been obtained. + case ENTERED_ORDON_SPRING_DAY_3: + if (dComIfGs_isEventBit(TRANSFORMING_UNLOCKED)) { + dComIfGs_setTransformStatus(0); + } + break; + + case WATCHED_CUTSCENE_AFTER_BEING_CAPTURED_IN_FARON_TWILIGHT: + if (dComIfGs_isEventBit(TRANSFORMING_UNLOCKED)) { + dComIfGs_setTransformStatus(1); + } + break; + + case MIDNAS_DESPERATE_HOUR_COMPLETED: + dComIfGs_onDarkClearLV(3); + break; + + case CLEARED_FARON_TWILIGHT: + // If we've already cleared Eldin Twilight, Lanayru Twilight, and MDH + if (dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED)) { + if (dComIfGs_isDarkClearLV(2) && dComIfGs_isDarkClearLV(3)) { + // Set the flag for the last transformed twilight; also puts Midna on the + // player's back + dComIfGs_onTransformLV(3); + dComIfGs_onDarkClearLV(3); + } + } + break; + + case CLEARED_ELDIN_TWILIGHT: + dComIfGs_onEventBit(MAP_WARPING_UNLOCKED); // in glitched logic, you can skip the gorge bridge + if (dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED)) { + if (dComIfGs_isDarkClearLV(1) && dComIfGs_isDarkClearLV(3)) { + dComIfGs_onTransformLV(3); + dComIfGs_onDarkClearLV(3); + } + } + // Set flag for the bridge between Castle Town and Eldin field if skip bridge + // donation is on and both Eldin and Lanayru twilight are cleared + if (dComIfGs_isEventBit(CLEARED_LANAYRU_TWILIGHT) && + randomizer_GetContext().mSettings[RandomizerContext::SKIP_BRIDGE_DONATION] == + RandomizerContext::ON) + { + dComIfGs_onEventBit(BRIDGE_REPAIR_FUNDRAISING_COMPLETED); + dComIfGs_onStageSwitch(6, 0x1B); // Bridge exists + } + break; + + case CLEARED_LANAYRU_TWILIGHT: + if (dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED)) { + if (dComIfGs_isDarkClearLV(1) && dComIfGs_isDarkClearLV(2)) { + dComIfGs_onTransformLV(3); + dComIfGs_onDarkClearLV(3); + } + } + if (dComIfGs_isEventBit(CLEARED_ELDIN_TWILIGHT) && + randomizer_GetContext().mSettings[RandomizerContext::SKIP_BRIDGE_DONATION] == + RandomizerContext::ON) + { + dComIfGs_onEventBit(BRIDGE_REPAIR_FUNDRAISING_COMPLETED); + dComIfGs_onStageSwitch(6, 0x1B); // Bridge exists + } + break; + + case REMOVE_SWORD_SHIELD_FROM_WOLF_BACK: + if (!dComIfGs_isEventBit(CLEARED_FARON_TWILIGHT)) { + dComIfGs_onTransformLV(0); // Set the last transformed twilight to include Faron + } + break; + + case GAVE_TELMA_RENADOS_LETTER: + offWarashibeItem(dItemNo_Randomizer_LETTER_e); + break; + + default: + break; + } + return HOOK_CONTINUE; +} + +HookAction hookPreMembitIsSwitch(ModContext*, void* args, void* retval, void*) { + if (getStageID() == Hidden_Village_Interiors) { + if (mods::arg(args, 1) == 0x61) { // Is Impaz in her house + *static_cast(retval) = TRUE; + return HOOK_SKIP_ORIGINAL; + } + } + return HOOK_CONTINUE; +} + +// kinda hacky check to see if this membit object is the temp memory area in save info +inline bool isTempMemBit(dSv_memBit_c* i_this) { + return i_this == &dComIfGs_getSaveInfo()->getMemory().getBit(); +} + +HookAction hookPreMembitOnSwitch(ModContext*, void* args, void*, void*) { + auto* i_this = mods::arg(args, 0); + const int i_no = mods::arg(args, 1); + + if (isTempMemBit(i_this)) { + if (getStageID() == Arbiters_Grounds) { + // Poe flame CS trigger + if (i_no == 0x26) { + i_this->offSwitch(0x45); // Open the Poe gate + return HOOK_SKIP_ORIGINAL; + } + } else if (getStageID() == Lake_Hylia) { + // Lanayru Twilight End CS trigger + if (i_no == 0xD) { + if (dComIfGs_isEventBit(TRANSFORMING_UNLOCKED)) { + // Set player to Human as the game will not do so if Shadow Crystal has + // been obtained. + dComIfGs_setTransformStatus(0); + } + } + } else if (getStageID() == Kakariko_Village) { + // Hawkeye is for sale + if (i_no == 0x3E) { + i_this->offSwitch(0xB); // Remove the coming soon sign so the hawkeye can be bought + } + } else if (getStageID() == Hyrule_Field) { + // Destroyed North Eldin rocks barrier + if (i_no == 0x11) { + // Unlock Eldin Province on the map. Done manually rather than via + // `onRegionBit`, which would see the rocks unbroken and skip the region. + dComIfGs_getSaveData()->getPlayer().getPlayerFieldLastStayInfo().mRegion |= 0x08; + } + } + } + return HOOK_CONTINUE; +} + +HookAction hookPreOnDungeonItem(ModContext*, void* args, void*, void*) { + int i_no = mods::arg(args, 1); + + // Don't use the stage life collection flag for rando + if (i_no == dSv_memBit_c::STAGE_LIFE) { + return HOOK_SKIP_ORIGINAL; + } + // Don't turn Ooccoo into the note when defeating a boss + else if (dComIfGs_isStageBossEnemy() && i_no == dSv_memBit_c::OOCCOO_NOTE) { + return HOOK_SKIP_ORIGINAL; + } + return HOOK_CONTINUE; +} + +HookAction hookPreOffDungeonItem(ModContext*, void* args, void*, void*) { + if (mods::arg(args, 1) == dSv_memBit_c::STAGE_LIFE) { + return HOOK_SKIP_ORIGINAL; + } + return HOOK_CONTINUE; +} + +HookAction hookPreIsDungeonItem(ModContext*, void* args, void* retval, void*) { + const int i_no = mods::arg(args, 1); + auto& out = *static_cast(retval); + + switch (i_no) { + case dSv_memBit_c::STAGE_LIFE: + out = FALSE; + return HOOK_SKIP_ORIGINAL; + case dSv_memBit_c::STAGE_BOSS_ENEMY: { + // If we are in a dungeon or fighting a midboss, we don't want the boss being + // defeated to affect the gameplay. + std::string stageName = dComIfGp_getStartStageName(); + if (stageName.starts_with("D_MN")) { + out = FALSE; + return HOOK_SKIP_ORIGINAL; + } + break; + } + case dSv_memBit_c::STAGE_BOSS_ENEMY_2: { + // If we are in the early rooms of FT, we don't want Ook being defeated to affect + // gameplay + if (daAlink_c::checkStageName("D_MN05") && dComIfGp_roomControl_getStayNo() < 4) { + out = FALSE; + return HOOK_SKIP_ORIGINAL; + } + break; + } + default: + break; + } + return HOOK_CONTINUE; +} + +HookAction hookPreIsDarkClearLV(ModContext*, void* args, void* retval, void*) { + if (mods::arg(args, 1) == 0 && + playerIsInRoomStage(1, allStages[Ordon_Village_Interiors])) + { + // Return false so Sera will give us the bottle if we have rescued the cat. + *static_cast(retval) = FALSE; + return HOOK_SKIP_ORIGINAL; + } + return HOOK_CONTINUE; +} + +HookAction hookPreCheckEmptyBottle(ModContext*, void*, void* retval, void*) { + if (getStageID() == Cave_of_Ordeals) { + // Return 1 to allow the player to collect the floor 50 reward, as this makes the + // game think the player has an empty bottle. + *static_cast(retval) = 1; + return HOOK_SKIP_ORIGINAL; + } + return HOOK_CONTINUE; +} + +void hookPostSetLineUpItem(ModContext*, void* args, void*, void*) { + // Allow rando to use all item slots. Checks the loaded hash rather than + // randomizer_IsActive() because this runs on file select. + if (randomizer_GetContext().mHash.empty()) { + return; + } + + auto* i_this = mods::arg(args, 0); + if (i_this->mItems[7] == dItemNo_NONE_e) { + return; + } + + // append slot 7 after the vanilla lineup, unless already present + int slot_idx = 0; + for (; slot_idx < 24; slot_idx++) { + const u8 lineup = i_this->mItemSlots[slot_idx]; + if (lineup == 7) { + return; + } + if (lineup == 0xFF) { + break; + } + } + + if (slot_idx < 24) { + i_this->mItemSlots[slot_idx] = 7; + } +} + +HookAction hookPreSaveInfoOnSwitch(ModContext*, void* args, void*, void*) { + // Set custom flag for the Temple of Time pedestal strike + if (getStageID() == Sacred_Grove && mods::arg(args, 1) == 0xEE) { + mods::arg(args, 0)->onSwitch(0x63, mods::arg(args, 2)); + } + return HOOK_CONTINUE; +} + +HookAction hookPreQuery001(ModContext*, void* args, void* retval, void*) { + auto* node = mods::arg(args, 1); + if (node->param == 0xFA) { // MDH Completed + // Return 0 to be able to turn souls into Jovani pre MDH + if (playerIsInRoomStage(5, allStages[Castle_Town_Shops])) { + *static_cast(retval) = 0; + return HOOK_SKIP_ORIGINAL; + } + } + return HOOK_CONTINUE; +} + +HookAction hookPreQuery022(ModContext*, void* args, void* retval, void*) { + if (daAlink_c::checkStageName(allStages[Ordon_Village_Interiors])) { + auto* node = mods::arg(args, 1); + if ((node->param & 0xFF) == dItemNo_Randomizer_HVY_BOOTS_e) { + // Return false so that the door in Bo's house can be opened without the Iron Boots + *static_cast(retval) = 0; + return HOOK_SKIP_ORIGINAL; + } + } + return HOOK_CONTINUE; +} + +HookAction hookPreQuery025(ModContext*, void* args, void* retval, void*) { + // 0x4461 is the key for the red potion shop item + if (playerIsInRoomStage(3, allStages[Kakariko_Village_Interiors]) && + randomizer_GetContext().mShopOverrides.contains(0x4461)) { + // Return 0 so the player can buy the red potion item from the shop. + *static_cast(retval) = 0; + return HOOK_SKIP_ORIGINAL; + } + return HOOK_CONTINUE; +} + +void hookPostQuery049(ModContext*, void*, void* retval, void*) { + // Split up getting both rewards from Jovani in randomizer + auto& out = *static_cast(retval); + if (out == 4 && !dComIfGs_isEventBit(GOT_BOTTLE_FROM_JOVANI)) { + out = 3; + } +} + +HookAction hookPreEvent035(ModContext*, void* args, void* retval, void*) { + auto* node = mods::arg(args, 1); + const u8* p = node->params; + const int prm0 = (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; + + if (prm0 == dItemNo_TOMATO_PUREE_e || prm0 == dItemNo_TASTE_e) { + dComIfGs_offItemFirstBit(prm0); + } else if (prm0 == dItemNo_RAFRELS_MEMO_e || prm0 == dItemNo_ASHS_SCRIBBLING_e) { + // rando: keep SLOT_19 (the items are randomized) + } else if (prm0 == dItemNo_LETTER_e || prm0 == dItemNo_BILL_e || + prm0 == dItemNo_WOOD_STATUE_e || prm0 == dItemNo_IRIAS_PENDANT_e) { + offWarashibeItem(prm0); + } + *static_cast(retval) = 1; + return HOOK_SKIP_ORIGINAL; +} + +// TODO: item service +/* HookAction hookPreExecItemGet(ModContext*, void* args, void*, void*) { + if (randomizer_IsActive()) { + const u8 item = mods::arg(args, 0); + item_funcs::exec_item_get(item); + dusk::mods::item_granted(item, mods::arg(args, 1), mods::arg(args, 2)); + return HOOK_SKIP_ORIGINAL; + } + return HOOK_CONTINUE; +} */ + +/* HookAction hookPreCheckItemGet(ModContext*, void* args, void* retval, void*) { + if (randomizer_IsActive()) { + *static_cast(retval) = item_funcs::check_item_get(mods::arg(args, 0), mods::arg(args, 1)); + return HOOK_SKIP_ORIGINAL; + } + return HOOK_CONTINUE; +} */ + +HookAction hookPreSetNextStage(ModContext*, void* args, void*, void*) { + randomizer_checkAndOverrideEntranceData( + mods::arg_ref(args, 0), + mods::arg_ref(args, 2), + mods::arg_ref(args, 1), + mods::arg_ref(args, 3) + ); + return HOOK_CONTINUE; +} + +HookAction hookPreObjGbCreate(ModContext*, void* args, void* retval, void*) { + if (getStageID() == StageIDs::Mirror_Chamber && !randomizer_mirrorChamberWallShouldExist()) { + *static_cast(retval) = cPhs_ERROR_e; + return HOOK_SKIP_ORIGINAL; + } + return HOOK_CONTINUE; +} + +void hookPostReadItemTexture(ModContext*, void* args, void*, void*) { + const u8 item_no = mods::arg(args, 1); + void* tex_buf1 = mods::arg(args, 2); + if (tex_buf1 == nullptr || item_no != dItemNo_Randomizer_MAGIC_LV1_e) { + return; + } + + ResourceBuffer bti = RESOURCE_BUFFER_INIT; + if (session::svc_mng.resource->load(session::svc_mng.mod_ctx, "shadow_crystal.bti", &bti) == MOD_OK) { + std::memcpy(tex_buf1, bti.data, bti.size < 0xC00 ? bti.size : 0xC00); + session::svc_mng.resource->free(session::svc_mng.mod_ctx, &bti); + } +} + +HookAction hookPreShopSeqDecideYes(ModContext*, void* args, void*, void*) { + auto* i_this = mods::arg(args, 0); + int item_no = 0; + + if (i_this->mFlow.getEventId(&item_no) == 1 && playerIsInRoomStage(3, "R_SP109")) { + const u16 key = static_cast((getStageID() << 8) | (item_no & 0xFF)); + if (randomizer_GetContext().mShopOverrides.contains(key)) { + i_this->setSoldOutFlag(); + } + } + return HOOK_CONTINUE; +} + +HookAction hookPreCheckFieldItemCreateHeap(ModContext*, void* args, void* retval, void*) { + auto* i_this = mods::arg(args, 0); + + switch (static_cast(i_this)->getItemNo()) { + case dItemNo_Randomizer_EMPTY_BOTTLE_e: + case dItemNo_Randomizer_HALF_MILK_BOTTLE_e: + case dItemNo_Randomizer_OIL_BOTTLE3_e: + case dItemNo_Randomizer_DROP_BOTTLE_e: + case dItemNo_Randomizer_LINKS_SAVINGS_e: + case dItemNo_Randomizer_POU_SPIRIT_e: + *static_cast(retval) = CheckItemCreateHeap(i_this); + return HOOK_SKIP_ORIGINAL; + default: + return HOOK_CONTINUE; + } +} + +void hookPostTalkEnd(ModContext*, void*, void*, void*) { + if (g_randomizerState.getHasPendingToDChange()) { + g_randomizerState.setHasPendingToDChange(false); + g_randomizerState.handleTimeOfDayChange(); + } +} + } ModResult initialize() { @@ -103,9 +632,49 @@ ModResult initialize() { return MOD_ERROR; \ } +#define ADD_HOOK_POST(originalFn, hookFn) \ + if (mods::hook::add_post(hookFn) != MOD_OK) { \ + mods::log::error("Failed to add post-hook for " #originalFn); \ + return MOD_ERROR; \ + } + ADD_HOOK_PRE(dFile_select_c__selectDataNameMove, hookPreSelectDataNameMove); ADD_HOOK_PRE(dFile_select_c__dataSelect, hookPreDataSelect); + ADD_HOOK_PRE(dSv_event_c__isEventBit, hookPreIsEventBit); + ADD_HOOK_PRE(dSv_event_c__onEventBit, hookPreOnEventBit); + + ADD_HOOK_PRE(dSv_memBit_c__isSwitch, hookPreMembitIsSwitch); + ADD_HOOK_PRE(dSv_memBit_c__onSwitch, hookPreMembitOnSwitch); + ADD_HOOK_PRE(dSv_memBit_c__onDungeonItem, hookPreOnDungeonItem); + ADD_HOOK_PRE(dSv_memBit_c__offDungeonItem, hookPreOffDungeonItem); + ADD_HOOK_PRE(dSv_memBit_c__isDungeonItem, hookPreIsDungeonItem); + + ADD_HOOK_PRE(dSv_player_status_b_c__isDarkClearLV, hookPreIsDarkClearLV); + + ADD_HOOK_PRE(dSv_player_item_c__checkEmptyBottle, hookPreCheckEmptyBottle); + ADD_HOOK_POST(dSv_player_item_c__setLineUpItem, hookPostSetLineUpItem); + + ADD_HOOK_PRE(dSv_info_c__onSwitch, hookPreSaveInfoOnSwitch); + + ADD_HOOK_PRE(dMsgFlow_c__query001, hookPreQuery001); + ADD_HOOK_PRE(dMsgFlow_c__query022, hookPreQuery022); + ADD_HOOK_PRE(dMsgFlow_c__query025, hookPreQuery025); + ADD_HOOK_POST(dMsgFlow_c__query049, hookPostQuery049); + ADD_HOOK_PRE(dMsgFlow_c__event035, hookPreEvent035); + + //ADD_HOOK_PRE(setNextStage, hookPreSetNextStage); + + ADD_HOOK_PRE(ObjGb_Create, hookPreObjGbCreate); + + ADD_HOOK_POST(readItemTexture, hookPostReadItemTexture); + + ADD_HOOK_PRE(dShopSystem_c__seq_decide_yes, hookPreShopSeqDecideYes); + + ADD_HOOK_PRE(dItemData_CheckFieldItemCreateHeap, hookPreCheckFieldItemCreateHeap); + + ADD_HOOK_POST(dEvt_control_c__talkEnd, hookPostTalkEnd); + return MOD_OK; } } \ No newline at end of file From 2f4d8a06d873fbbcb1386c6710944eb27196b753 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Fri, 7 Aug 2026 11:15:36 -0700 Subject: [PATCH 10/15] remove unnecessary hooks --- mods/randomizer/src/hooks.cpp | 72 ----------------------------------- 1 file changed, 72 deletions(-) diff --git a/mods/randomizer/src/hooks.cpp b/mods/randomizer/src/hooks.cpp index ca82a24dfc..f75972423b 100644 --- a/mods/randomizer/src/hooks.cpp +++ b/mods/randomizer/src/hooks.cpp @@ -35,12 +35,6 @@ DEFINE_HOOK(&dSv_player_item_c::setLineUpItem, dSv_player_item_c__setLineUpItem) DEFINE_HOOK(&dSv_info_c::onSwitch, dSv_info_c__onSwitch); -DEFINE_HOOK(&dMsgFlow_c::query001, dMsgFlow_c__query001); -DEFINE_HOOK(&dMsgFlow_c::query022, dMsgFlow_c__query022); -DEFINE_HOOK(&dMsgFlow_c::query025, dMsgFlow_c__query025); -DEFINE_HOOK(&dMsgFlow_c::query049, dMsgFlow_c__query049); -DEFINE_HOOK(&dMsgFlow_c::event035, dMsgFlow_c__event035); - /*DEFINE_HOOK_SYMBOL("__Z21dComIfGp_setNextStagePKcsaafjiasii", void(char const*, s16, s8, s8, f32, u32, int, s8, s16, int, int), setNextStage);*/ @@ -475,66 +469,6 @@ HookAction hookPreSaveInfoOnSwitch(ModContext*, void* args, void*, void*) { return HOOK_CONTINUE; } -HookAction hookPreQuery001(ModContext*, void* args, void* retval, void*) { - auto* node = mods::arg(args, 1); - if (node->param == 0xFA) { // MDH Completed - // Return 0 to be able to turn souls into Jovani pre MDH - if (playerIsInRoomStage(5, allStages[Castle_Town_Shops])) { - *static_cast(retval) = 0; - return HOOK_SKIP_ORIGINAL; - } - } - return HOOK_CONTINUE; -} - -HookAction hookPreQuery022(ModContext*, void* args, void* retval, void*) { - if (daAlink_c::checkStageName(allStages[Ordon_Village_Interiors])) { - auto* node = mods::arg(args, 1); - if ((node->param & 0xFF) == dItemNo_Randomizer_HVY_BOOTS_e) { - // Return false so that the door in Bo's house can be opened without the Iron Boots - *static_cast(retval) = 0; - return HOOK_SKIP_ORIGINAL; - } - } - return HOOK_CONTINUE; -} - -HookAction hookPreQuery025(ModContext*, void* args, void* retval, void*) { - // 0x4461 is the key for the red potion shop item - if (playerIsInRoomStage(3, allStages[Kakariko_Village_Interiors]) && - randomizer_GetContext().mShopOverrides.contains(0x4461)) { - // Return 0 so the player can buy the red potion item from the shop. - *static_cast(retval) = 0; - return HOOK_SKIP_ORIGINAL; - } - return HOOK_CONTINUE; -} - -void hookPostQuery049(ModContext*, void*, void* retval, void*) { - // Split up getting both rewards from Jovani in randomizer - auto& out = *static_cast(retval); - if (out == 4 && !dComIfGs_isEventBit(GOT_BOTTLE_FROM_JOVANI)) { - out = 3; - } -} - -HookAction hookPreEvent035(ModContext*, void* args, void* retval, void*) { - auto* node = mods::arg(args, 1); - const u8* p = node->params; - const int prm0 = (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]; - - if (prm0 == dItemNo_TOMATO_PUREE_e || prm0 == dItemNo_TASTE_e) { - dComIfGs_offItemFirstBit(prm0); - } else if (prm0 == dItemNo_RAFRELS_MEMO_e || prm0 == dItemNo_ASHS_SCRIBBLING_e) { - // rando: keep SLOT_19 (the items are randomized) - } else if (prm0 == dItemNo_LETTER_e || prm0 == dItemNo_BILL_e || - prm0 == dItemNo_WOOD_STATUE_e || prm0 == dItemNo_IRIAS_PENDANT_e) { - offWarashibeItem(prm0); - } - *static_cast(retval) = 1; - return HOOK_SKIP_ORIGINAL; -} - // TODO: item service /* HookAction hookPreExecItemGet(ModContext*, void* args, void*, void*) { if (randomizer_IsActive()) { @@ -657,12 +591,6 @@ ModResult initialize() { ADD_HOOK_PRE(dSv_info_c__onSwitch, hookPreSaveInfoOnSwitch); - ADD_HOOK_PRE(dMsgFlow_c__query001, hookPreQuery001); - ADD_HOOK_PRE(dMsgFlow_c__query022, hookPreQuery022); - ADD_HOOK_PRE(dMsgFlow_c__query025, hookPreQuery025); - ADD_HOOK_POST(dMsgFlow_c__query049, hookPostQuery049); - ADD_HOOK_PRE(dMsgFlow_c__event035, hookPreEvent035); - //ADD_HOOK_PRE(setNextStage, hookPreSetNextStage); ADD_HOOK_PRE(ObjGb_Create, hookPreObjGbCreate); From a9767ad3e8ec9884ab7897f5fb736b912c4f0978 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Fri, 7 Aug 2026 11:35:50 -0700 Subject: [PATCH 11/15] use persistent data directory --- mods/randomizer/src/paths.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mods/randomizer/src/paths.cpp b/mods/randomizer/src/paths.cpp index b418557998..388e3771a9 100644 --- a/mods/randomizer/src/paths.cpp +++ b/mods/randomizer/src/paths.cpp @@ -5,8 +5,12 @@ namespace randomizer::paths { std::filesystem::path GetRandomizerPath() { - // TODO: need a more permanent directory than this - return session::svc_mng.host->mod_dir(session::svc_mng.mod_ctx); + const char* dataDir = nullptr; + if (session::svc_mng.host->data_dir(session::svc_mng.mod_ctx, &dataDir) != MOD_OK) { + session::svc_mng.host->fail(session::svc_mng.mod_ctx, MOD_ERROR, "Failed to get data directory"); + } + + return dataDir; } std::filesystem::path GetRandomizerSettingsPath() { From e16c05211bdeb6c67a2fa44acd7188a43581a249 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Fri, 7 Aug 2026 19:42:50 -0700 Subject: [PATCH 12/15] seed selection + rando file setup working --- mods/randomizer/src/hooks.cpp | 12 +++ mods/randomizer/src/messages.cpp | 2 +- mods/randomizer/src/paths.cpp | 4 + mods/randomizer/src/paths.hpp | 3 +- mods/randomizer/src/randomizer_context.cpp | 14 +-- mods/randomizer/src/session.cpp | 87 ++++++++++++++++++ mods/randomizer/src/session.hpp | 2 + mods/randomizer/src/tools.cpp | 31 ++++++- mods/randomizer/src/tools.h | 7 ++ mods/randomizer/src/ui/rando_config.cpp | 92 ++++++++++++++----- mods/randomizer/src/ui/rando_config.hpp | 7 -- mods/randomizer/src/utilities.h | 13 --- mods/randomizer/src/verify_item_functions.cpp | 2 +- 13 files changed, 221 insertions(+), 55 deletions(-) delete mode 100644 mods/randomizer/src/utilities.h diff --git a/mods/randomizer/src/hooks.cpp b/mods/randomizer/src/hooks.cpp index f75972423b..5451c84e33 100644 --- a/mods/randomizer/src/hooks.cpp +++ b/mods/randomizer/src/hooks.cpp @@ -18,6 +18,7 @@ DEFINE_HOOK(&dFile_select_c::selectDataNameMove, dFile_select_c__selectDataNameMove); DEFINE_HOOK(&dFile_select_c::dataSelect, dFile_select_c__dataSelect); +DEFINE_HOOK(&dFile_select_c::nameInput2, dFile_select_c__nameInput2); DEFINE_HOOK(&dSv_event_c::isEventBit, dSv_event_c__isEventBit); DEFINE_HOOK(&dSv_event_c::onEventBit, dSv_event_c__onEventBit); @@ -131,6 +132,15 @@ HookAction hookPreSelectDataNameMove(ModContext*, void* args, void* retval, void return HOOK_SKIP_ORIGINAL; } +void hookPostNameInput2(ModContext*, void* args, void* retval, void* userdata) { + dFile_select_c* i_this = mods::arg(args, 0); + + if (i_this->mIsSelectEnd) { + if (!randomizer_GetContext().mHash.empty()) { + session::setupRandomizerFile(); + } + } +} HookAction hookPreIsEventBit(ModContext*, void* args, void* retval, void*) { const u16 i_no = mods::arg(args, 1); @@ -603,6 +613,8 @@ ModResult initialize() { ADD_HOOK_POST(dEvt_control_c__talkEnd, hookPostTalkEnd); + ADD_HOOK_POST(dFile_select_c__nameInput2, hookPostNameInput2); + return MOD_OK; } } \ No newline at end of file diff --git a/mods/randomizer/src/messages.cpp b/mods/randomizer/src/messages.cpp index 6e07013e19..2b635e3c2b 100644 --- a/mods/randomizer/src/messages.cpp +++ b/mods/randomizer/src/messages.cpp @@ -6,7 +6,7 @@ #include "d/d_com_inf_game.h" #include "randomizer_context.hpp" #include "custom_flow_ids.hpp" -#include "utilities.h" +#include "tools.h" #include diff --git a/mods/randomizer/src/paths.cpp b/mods/randomizer/src/paths.cpp index 388e3771a9..a0e48cfe3f 100644 --- a/mods/randomizer/src/paths.cpp +++ b/mods/randomizer/src/paths.cpp @@ -21,6 +21,10 @@ std::filesystem::path GetRandomizerPreferencesPath() { return GetRandomizerPath() / "preferences.yaml"; } +std::filesystem::path GetRandomizerPresetsPath() { + return GetRandomizerPath() / "presets"; +} + std::filesystem::path GetRandomizerSeedsPath() { return GetRandomizerPath() / "seeds"; } diff --git a/mods/randomizer/src/paths.hpp b/mods/randomizer/src/paths.hpp index 3b7e49c86d..771e9514c1 100644 --- a/mods/randomizer/src/paths.hpp +++ b/mods/randomizer/src/paths.hpp @@ -4,11 +4,10 @@ namespace randomizer::paths { -// Root of the randomizer's writable data (settings, preferences, generated seeds), -// under the host's configured data path: /randomizer/. std::filesystem::path GetRandomizerPath(); std::filesystem::path GetRandomizerSettingsPath(); std::filesystem::path GetRandomizerPreferencesPath(); +std::filesystem::path GetRandomizerPresetsPath(); std::filesystem::path GetRandomizerSeedsPath(); } // namespace randomizer::paths diff --git a/mods/randomizer/src/randomizer_context.cpp b/mods/randomizer/src/randomizer_context.cpp index 1f75849277..34f3631d78 100644 --- a/mods/randomizer/src/randomizer_context.cpp +++ b/mods/randomizer/src/randomizer_context.cpp @@ -332,12 +332,14 @@ std::optional RandomizerContext::LoadFromHash(const std::string& ha this->mReturnToPlaceOverrides[key] = override; } - // TODO: setup ui service to allow pushing toasts - /*dusk::ui::push_toast(dusk::ui::Toast{ - .title = "Randomizer", - .content = fmt::format("Loaded Randomizer Seed {}", this->mHash), - .duration = std::chrono::seconds(3), - });*/ + UiToastDesc desc = UI_TOAST_DESC_INIT; + desc.type = "success"; + desc.title_rml = "Randomizer"; + std::string body_text = fmt::format("Loaded Randomizer Seed {}", this->mHash); + desc.body_rml = body_text.c_str(); + desc.duration_ms = 3000; + randomizer::session::svc_mng.ui->push_toast(randomizer::session::svc_mng.mod_ctx, &desc); + return std::nullopt; } diff --git a/mods/randomizer/src/session.cpp b/mods/randomizer/src/session.cpp index 76d8b0c5a9..54bd41c92c 100644 --- a/mods/randomizer/src/session.cpp +++ b/mods/randomizer/src/session.cpp @@ -1,5 +1,16 @@ #include "session.hpp" +#include + +#include "randomizer_context.hpp" +#include "flags.h" +#include "item_ids.h" +#include "tools.h" + +#include "d/d_com_inf_game.h" +#include "d/d_item.h" +#include "d/d_meter2_info.h" + namespace randomizer::session { ServiceManager svc_mng; @@ -9,5 +20,81 @@ ModResult initialize(const ServiceManager& services) { return MOD_OK; } +void setupRandomizerFile() { + // Setup file based on randomizer data + auto& randoData = randomizer_GetContext(); + randoData.mCreatingSave = true; + + // Set starting flags + // Event Flags + for (const auto& flag : randoData.mStartEventFlags) { + dComIfGs_onEventBit(flag); + } + // Region Flags + for (const auto& [region, flags] : randoData.mStartRegionFlags) { + for (const auto& flag : flags) { + onRegionFlag(region, flag); + } + } + + // Map bits (fills in overworld on map) + setRegionBit(randoData.mMapBits); + + // Other flags based on starting flags + if (dComIfGs_isEventBit(CLEARED_FARON_TWILIGHT)) + { + dComIfGs_onDarkClearLV(0); + dComIfGs_setLightDropNum(0, 0x10); + execItemGet(dItemNo_Randomizer_DROP_CONTAINER_e); + execItemGet(dItemNo_Randomizer_WEAR_KOKIRI_e); + } + + if (dComIfGs_isEventBit(CLEARED_ELDIN_TWILIGHT)) + { + dComIfGs_onDarkClearLV(1); + dComIfGs_setLightDropNum(1, 0x10); + execItemGet(dItemNo_Randomizer_DROP_CONTAINER02_e); + } + + if (dComIfGs_isEventBit(CLEARED_LANAYRU_TWILIGHT)) + { + dComIfGs_onDarkClearLV(2); + dComIfGs_setLightDropNum(2, 0x10); + execItemGet(dItemNo_Randomizer_DROP_CONTAINER03_e); + } + + if (randoData.mSettings[RandomizerContext::SKIP_MINOR_CUTSCENES] == RandomizerContext::ON) + { + // Add letter data in this order to more or less reflect an order they can be obtained in game + static const int letterOrder[] = {3, 2, 4, 7, 5, 6, 13, 12, 10, 9, 8, 15, 0, 14, 11}; + int letterNum = 0; + for (int i : letterOrder) { + if (dMenu_Letter::getLetterName(i) != 0) { + dComIfGs_onLetterGetFlag(i); + dComIfGs_setGetNumber(letterNum++, i + 1); + } + } + setAllLetterRead(); + } + + // If MDH and the twilights are pre-completed + if (dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED)) + { + if ((dComIfGs_getSaveData()->getPlayer().getPlayerStatusB().mDarkClearLevelFlag & 0x7) == 0x7) + { + dComIfGs_onDarkClearLV(3); + dComIfGs_onTransformLV(3); // Puts Midna on players back + } + } + + // Set starting inventory + for (const auto& itemId: randoData.mStartingInventory) { + execItemGet(itemId); + } + + g_randomizerState = RandomizerState(); + mods::log::debug("Created Rando Save"); + randoData.mCreatingSave = false; +} } \ No newline at end of file diff --git a/mods/randomizer/src/session.hpp b/mods/randomizer/src/session.hpp index fdf295c779..266000d726 100644 --- a/mods/randomizer/src/session.hpp +++ b/mods/randomizer/src/session.hpp @@ -25,4 +25,6 @@ struct ServiceManager { extern ServiceManager svc_mng; ModResult initialize(const ServiceManager& services); + +void setupRandomizerFile(); } \ No newline at end of file diff --git a/mods/randomizer/src/tools.cpp b/mods/randomizer/src/tools.cpp index f9e794222b..ec6effdd06 100644 --- a/mods/randomizer/src/tools.cpp +++ b/mods/randomizer/src/tools.cpp @@ -10,7 +10,6 @@ #include "randomizer_context.hpp" #include "session.hpp" #include "stages.h" -#include "utilities.h" #include "verify_item_functions.h" #include @@ -746,4 +745,34 @@ bool tracker_isStageItem(int stage, int flag) { // Need to subtract 0x80 (MEMORY_ITEM constant in d_save.cpp) because the above function does it return g_dComIfG_gameInfo.info.getSavedata().getSave(stage).getBit().isItem(flag - 0x80); } +} + +// Kinda hacky, but will do for now +void onRegionFlag(int i_stageNo, int i_no) { + auto regionFlags = reinterpret_cast(&dComIfGs_getSaveData()->getSave(i_stageNo).getBit()); + const int offset = i_no / 8; + const int shift = i_no % 8; + regionFlags[offset] |= (0x80 >> shift); +} + +void setRegionBit(u8 i_region) { + dComIfGs_getSaveData()->getPlayer().getPlayerFieldLastStayInfo().mRegion |= i_region; +} + +void setAllLetterGet() { + dComIfGs_getSaveData()->getPlayer().getLetterInfo().mLetterGetFlags[0] |= 0xFFFF; +} + +void setAllLetterRead() { + dComIfGs_getSaveData()->getPlayer().getLetterInfo().mLetterReadFlags[0] |= 0xFFFF; +} + +u8 getAncientDocumentNum() { + // TODO + return 0; +} + +u8 getAreaKeyNum(int) { + // TODO + return 0; } \ No newline at end of file diff --git a/mods/randomizer/src/tools.h b/mods/randomizer/src/tools.h index bb1c8f79b6..3119c3fcac 100644 --- a/mods/randomizer/src/tools.h +++ b/mods/randomizer/src/tools.h @@ -49,6 +49,13 @@ int getLocationItem(randomizer::logic::location::Location* location); int getStageSaveId(int id); int getStageSaveId(const char* stage); +void onRegionFlag(int i_stageNo, int i_no); +void setRegionBit(u8 i_region); +void setAllLetterGet(); +void setAllLetterRead(); +u8 getAncientDocumentNum(); +u8 getAreaKeyNum(int); + bool tracker_isEventBit(u16 flag); bool tracker_isStageSwitch(int stage, int flag); bool tracker_isStageItem(int stage, int flag); \ No newline at end of file diff --git a/mods/randomizer/src/ui/rando_config.cpp b/mods/randomizer/src/ui/rando_config.cpp index 5e5ed9a3fb..76bb24b034 100644 --- a/mods/randomizer/src/ui/rando_config.cpp +++ b/mods/randomizer/src/ui/rando_config.cpp @@ -15,6 +15,7 @@ #include #include +#include "../randomizer_context.hpp" #include "d/d_file_select.h" namespace randomizer::ui { @@ -133,6 +134,21 @@ void add_string_input(UiElementHandle pane, const char* label, const char* help_ session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); } +void add_select(UiElementHandle pane, const char* label, const char* help_rml, const char** options, + size_t option_count, UiControlGetFn getFn, UiControlSetFn setFn, UiElementHandle* out_handle = nullptr) +{ + UiControlDesc desc = UI_CONTROL_DESC_INIT; + desc.kind = UI_CONTROL_SELECT; + desc.label = label; + desc.help_rml = help_rml; + desc.binding = UI_BINDING_CALLBACKS; + desc.get = getFn; + desc.set = setFn; + desc.options = options; + desc.option_count = option_count; + session::svc_mng.ui->pane_add_control(session::svc_mng.mod_ctx, pane, &desc, out_handle); +} + void add_select_setting(UiElementHandle pane, const char* key, UiElementHandle* out_handle = nullptr) { auto setting = FindSetting(key); @@ -464,11 +480,59 @@ void OnMenuTabSelected(ModContext* ctx, void*) { ModResult buildPlayTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane, UiElementHandle rightPane, void*, ModError*) { - add_button(leftPane, + std::filesystem::path seed_dir = paths::GetRandomizerSeedsPath(); + + std::string help_rml = ""; + if (std::filesystem::is_empty(seed_dir)) { + help_rml = "No seeds generated! You can generate a seed from the Seed Management Tab."; + } else { + help_rml = "Choose which seed you want to play."; + } + + std::vector seedHashes; + for (const auto& entry : std::filesystem::directory_iterator(seed_dir)) { + if (entry.is_directory()) { + seedHashes.push_back(entry.path().filename().string()); + } + } + + std::vector availableSeeds; + for (const auto& hash : seedHashes) { + availableSeeds.push_back(hash.c_str()); + } + + add_select(leftPane, "Selected Seed", - "", - [](ModContext*, void*) { - // TODO + help_rml.c_str(), + availableSeeds.data(), + availableSeeds.size(), + [](ModContext*, void*, UiControlValue* out_value) { + int idx = 0; + for (const auto& entry : std::filesystem::directory_iterator(paths::GetRandomizerSeedsPath())) { + if (entry.is_directory()) { + std::string hash = entry.path().filename().string(); + if (randomizer_GetContext().mHash == hash) { + break; + } + idx++; + } + } + + out_value->int_value = idx; + }, + [](ModContext*, void*, const UiControlValue* value) { + int idx = 0; + for (const auto& entry : std::filesystem::directory_iterator(paths::GetRandomizerSeedsPath())) { + if (entry.is_directory()) { + if (idx == value->int_value) { + std::string hash = entry.path().filename().string(); + randomizer_GetContext() = RandomizerContext(); + randomizer_GetContext().LoadFromHash(hash); + break; + } + idx++; + } + } }); add_button(leftPane, @@ -543,24 +607,4 @@ ModResult buildFileSelectGateMenu(dFile_select_c* fileSelect) { session::svc_mng.ui->window_push(session::svc_mng.mod_ctx, &desc, &g_file_select_window_ctx.window_handle); } -std::filesystem::path GetRandomizerPath() { - return paths::GetRandomizerPath() / "randomizer"; -} - -std::filesystem::path GetRandomizerSettingsPath() { - return GetRandomizerPath() / "settings.yaml"; -} - -std::filesystem::path GetRandomizerPreferencesPath() { - return GetRandomizerPath() / "preferences.yaml"; -} - -std::filesystem::path GetRandomizerPresetsPath() { - return GetRandomizerPath() / "presets"; -} - -std::filesystem::path GetRandomizerSeedsPath() { - return GetRandomizerPath() / "seeds"; -} - } // namespace dusk::ui diff --git a/mods/randomizer/src/ui/rando_config.hpp b/mods/randomizer/src/ui/rando_config.hpp index c0d5c3b608..b3c8dedcb7 100644 --- a/mods/randomizer/src/ui/rando_config.hpp +++ b/mods/randomizer/src/ui/rando_config.hpp @@ -34,11 +34,4 @@ void PastePermalinkFromClipboard(); ModResult buildMenuTab(); ModResult buildFileSelectGateMenu(dFile_select_c*); - -std::filesystem::path GetRandomizerPath(); -std::filesystem::path GetRandomizerSettingsPath(); -std::filesystem::path GetRandomizerPreferencesPath(); -std::filesystem::path GetRandomizerSeedsPath(); -std::filesystem::path GetRandomizerPresetsPath(); - } diff --git a/mods/randomizer/src/utilities.h b/mods/randomizer/src/utilities.h deleted file mode 100644 index 448ea50e24..0000000000 --- a/mods/randomizer/src/utilities.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include - -inline u8 getAncientDocumentNum() { - // TODO - return 0; -} - -inline u8 getAreaKeyNum(int) { - // TODO - return 0; -} \ No newline at end of file diff --git a/mods/randomizer/src/verify_item_functions.cpp b/mods/randomizer/src/verify_item_functions.cpp index 5a050de16b..fc1b36d653 100644 --- a/mods/randomizer/src/verify_item_functions.cpp +++ b/mods/randomizer/src/verify_item_functions.cpp @@ -6,7 +6,7 @@ #include "d/d_item.h" #include "d/d_item_data.h" #include "item_ids.h" -#include "utilities.h" +#include "tools.h" bool haveItem(u32 item) { return checkItemGet((u8)item, 1); From 1f28fd1ca538c1867f9adfd437591c6d312017c6 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 8 Aug 2026 05:38:42 -0700 Subject: [PATCH 13/15] sky character blob and stage edit registration --- mods/randomizer/src/session.cpp | 47 +++++++++++++++++++++++++ mods/randomizer/src/session.hpp | 1 + mods/randomizer/src/tools.cpp | 37 +++++++++++++++---- mods/randomizer/src/tools.h | 4 +++ mods/randomizer/src/ui/rando_config.cpp | 1 + 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/mods/randomizer/src/session.cpp b/mods/randomizer/src/session.cpp index 54bd41c92c..749fd32fad 100644 --- a/mods/randomizer/src/session.cpp +++ b/mods/randomizer/src/session.cpp @@ -6,6 +6,7 @@ #include "flags.h" #include "item_ids.h" #include "tools.h" +#include "stages.h" #include "d/d_com_inf_game.h" #include "d/d_item.h" @@ -97,4 +98,50 @@ void setupRandomizerFile() { randoData.mCreatingSave = false; } +void registerStageEdits() { + auto& ctx = randomizer_GetContext(); + auto stage_of = [](u32 key) -> const char* { + const u32 stage_id = key >> 16; + if (stage_id >= sizeof(allStages) / sizeof(allStages[0])) { + return nullptr; + } + return allStages[stage_id]; + }; + + for (const auto& [key, patches] : ctx.mObjectPatches) { + const char* stage = stage_of(key); + if (stage == nullptr) { + continue; + } + + const u8 room = (key >> 8) & 0xFF; + const s8 layer = static_cast(key & 0xFF); + for (const auto& [crc, bytes] : patches) { + StageActorHandle handle{}; + + ModResult res; + if (bytes.size() == RandomizerContext::OBJ_DELETE_SIZE) { + res = svc_mng.stage->delete_actor(mod_ctx, stage, room, layer, crc, &handle); + } else { + res = svc_mng.stage->patch_actor( + mod_ctx, stage, room, layer, crc, bytes.data(), bytes.size(), &handle); + } + } + } + + for (const auto& [key, additions] : ctx.mObjectAdditions) { + const char* stage = stage_of(key); + if (stage == nullptr) { + continue; + } + + const u8 room = (key >> 8) & 0xFF; + const s8 layer = static_cast(key & 0xFF); + for (const auto& bytes : additions) { + StageActorHandle handle{}; + svc_mng.stage->add_actor(mod_ctx, stage, room, layer, bytes.data(), bytes.size(), &handle); + } + } +} + } \ No newline at end of file diff --git a/mods/randomizer/src/session.hpp b/mods/randomizer/src/session.hpp index 266000d726..e033ebb961 100644 --- a/mods/randomizer/src/session.hpp +++ b/mods/randomizer/src/session.hpp @@ -27,4 +27,5 @@ extern ServiceManager svc_mng; ModResult initialize(const ServiceManager& services); void setupRandomizerFile(); +void registerStageEdits(); } \ No newline at end of file diff --git a/mods/randomizer/src/tools.cpp b/mods/randomizer/src/tools.cpp index ec6effdd06..3f8d575673 100644 --- a/mods/randomizer/src/tools.cpp +++ b/mods/randomizer/src/tools.cpp @@ -767,12 +767,37 @@ void setAllLetterRead() { dComIfGs_getSaveData()->getPlayer().getLetterInfo().mLetterReadFlags[0] |= 0xFFFF; } -u8 getAncientDocumentNum() { - // TODO - return 0; +constexpr const char* skyCharacterBlobName = "sky_characters"; +u8 g_skyCharacters = 0; + +void saveAncientDocumentNum() { + auto* ctx = randomizer::session::svc_mng.mod_ctx; + randomizer::session::svc_mng.save->set_blob(ctx, skyCharacterBlobName, &g_skyCharacters, sizeof(g_skyCharacters)); } -u8 getAreaKeyNum(int) { - // TODO - return 0; +void loadAncientDocumentNum() { + g_skyCharacters = 0; + size_t size = sizeof(g_skyCharacters); + auto* ctx = randomizer::session::svc_mng.mod_ctx; + randomizer::session::svc_mng.save->get_blob(ctx, skyCharacterBlobName, &g_skyCharacters, &size); +} + +u8 getAncientDocumentNum() { + return g_skyCharacters; +} + +void setAncientDocumentNum(u8 num) { + g_skyCharacters = num; + saveAncientDocumentNum(); +} + +u8 getAreaKeyNum(int i_stageNo) { + stage_stag_info_class* stagInfo = dComIfGp_getStageStagInfo(); + if (stagInfo != nullptr) { + if (i_stageNo == dStage_stagInfo_GetSaveTbl(stagInfo)) { + return dComIfGs_getKeyNum(); + } + } + + return dComIfGs_getSaveData()->getSave(i_stageNo).getBit().getKeyNum(); } \ No newline at end of file diff --git a/mods/randomizer/src/tools.h b/mods/randomizer/src/tools.h index 3119c3fcac..475fc01793 100644 --- a/mods/randomizer/src/tools.h +++ b/mods/randomizer/src/tools.h @@ -53,9 +53,13 @@ void onRegionFlag(int i_stageNo, int i_no); void setRegionBit(u8 i_region); void setAllLetterGet(); void setAllLetterRead(); +void setAncientDocumentNum(u8 num); u8 getAncientDocumentNum(); u8 getAreaKeyNum(int); +void saveAncientDocumentNum(); +void loadAncientDocumentNum(); + bool tracker_isEventBit(u16 flag); bool tracker_isStageSwitch(int stage, int flag); bool tracker_isStageItem(int stage, int flag); \ No newline at end of file diff --git a/mods/randomizer/src/ui/rando_config.cpp b/mods/randomizer/src/ui/rando_config.cpp index 76bb24b034..3ac9218e16 100644 --- a/mods/randomizer/src/ui/rando_config.cpp +++ b/mods/randomizer/src/ui/rando_config.cpp @@ -528,6 +528,7 @@ ModResult buildPlayTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane std::string hash = entry.path().filename().string(); randomizer_GetContext() = RandomizerContext(); randomizer_GetContext().LoadFromHash(hash); + session::registerStageEdits(); break; } idx++; From a8089f1204eb2c191c159751c70acb19561d4796 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 8 Aug 2026 11:25:00 -0700 Subject: [PATCH 14/15] couple small fixes --- mods/randomizer/src/tools.cpp | 1 - mods/randomizer/src/ui/rando_config.cpp | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mods/randomizer/src/tools.cpp b/mods/randomizer/src/tools.cpp index 3f8d575673..f710d9e419 100644 --- a/mods/randomizer/src/tools.cpp +++ b/mods/randomizer/src/tools.cpp @@ -788,7 +788,6 @@ u8 getAncientDocumentNum() { void setAncientDocumentNum(u8 num) { g_skyCharacters = num; - saveAncientDocumentNum(); } u8 getAreaKeyNum(int i_stageNo) { diff --git a/mods/randomizer/src/ui/rando_config.cpp b/mods/randomizer/src/ui/rando_config.cpp index 3ac9218e16..994612b5e8 100644 --- a/mods/randomizer/src/ui/rando_config.cpp +++ b/mods/randomizer/src/ui/rando_config.cpp @@ -481,6 +481,8 @@ ModResult buildPlayTab(ModContext* ctx, UiWindowHandle, UiElementHandle leftPane UiElementHandle rightPane, void*, ModError*) { std::filesystem::path seed_dir = paths::GetRandomizerSeedsPath(); + if (!std::filesystem::exists(seed_dir)) + std::filesystem::create_directory(seed_dir); std::string help_rml = ""; if (std::filesystem::is_empty(seed_dir)) { @@ -605,7 +607,7 @@ ModResult buildFileSelectGateMenu(dFile_select_c* fileSelect) { g_dialogSelectModeState = SelectReady; }; - session::svc_mng.ui->window_push(session::svc_mng.mod_ctx, &desc, &g_file_select_window_ctx.window_handle); + return session::svc_mng.ui->window_push(session::svc_mng.mod_ctx, &desc, &g_file_select_window_ctx.window_handle); } } // namespace dusk::ui From 25a736b9dc4bfe4da057c68c11fcbd8d72d6e48d Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 8 Aug 2026 13:27:12 -0700 Subject: [PATCH 15/15] getLayerNo hook --- mods/randomizer/src/hooks.cpp | 596 ++++++++++++++++++++++++++++++++++ 1 file changed, 596 insertions(+) diff --git a/mods/randomizer/src/hooks.cpp b/mods/randomizer/src/hooks.cpp index 5451c84e33..f5197d59bd 100644 --- a/mods/randomizer/src/hooks.cpp +++ b/mods/randomizer/src/hooks.cpp @@ -49,6 +49,9 @@ DEFINE_HOOK(&CheckFieldItemCreateHeap, dItemData_CheckFieldItemCreateHeap); DEFINE_HOOK(&dEvt_control_c::talkEnd, dEvt_control_c__talkEnd); +DEFINE_HOOK(&dComIfG_play_c::getLayerNo_common_common, dComIfG_play_c__getLayerNo_common_common); + + namespace randomizer::ui { dialogSelectModeState g_dialogSelectModeState = SelectReady; } @@ -567,6 +570,597 @@ void hookPostTalkEnd(ModContext*, void*, void*, void*) { } } +HookAction hookPreGetLayerNo(ModContext*, void* args, void* retval, void*) { + auto i_stageName = mods::arg(args, 0); + auto i_roomNo = mods::arg(args, 1); + auto& layer = mods::arg_ref(args, 2); + + if (strcmp(dComIfGp_getStartStageName(), "S_MV000") == 0 || + (strcmp(dComIfGp_getStartStageName(), "F_SP102") == 0 && layer == 10)) { + return HOOK_CONTINUE; + } + + int stageID = getStageID(i_stageName); + bool condition = false; + bool darkIsClear = false; + + if (layer < 0) { + layer = -1; + + // Stage is in a Twilight state + if (dKy_darkworld_stage_check(i_stageName, i_roomNo) == TRUE) { + layer = 14; + } + + if (layer < 13) { + switch(stageID) { + case Snowpeak_Ruins: { + if (dComIfGs_isEventBit(SNOWPEAK_RUINS_CLEARED)) { + layer = 3; + } + break; + } + case Snowpeak: { + if (dComIfGs_isEventBit(SNOWPEAK_RUINS_CLEARED) && (i_roomNo != 0)) { + layer = 3; + } + break; + } + case Faron_Woods: + case Faron_Woods_Interiors: { + if ((i_roomNo == 5) || (i_roomNo == 6)) { // North Faron or Mist Area + condition = dComIfGs_isEventBit(ORDON_DAY_2_OVER); // Talo Saved + if (condition) { + layer = 3; + } else { + layer = 1; + } + } + else { + condition = dComIfGs_isEventBit(ORDON_DAY_2_OVER); // Talo Saved + if (condition) { + condition = dComIfGs_isEventBit(FOREST_TEMPLE_CLEARED); // Forest Temple Completed + + if (condition) { + layer = 5; + } + } else { + layer = 1; + } + } + break; + } + + case Kakariko_Village: + { + condition = dComIfGs_isEventBit(WATCHED_CUTSCENE_AFTER_GORON_MINES); // Cutscene after GM Watched + if (condition == false) { + condition = dComIfGs_isEventBit(GORON_MINES_CLEARED); // Goron Mines Completed + if (condition == false) { + layer = 2; + + // If it is night, the layer is different. + dComIfG_get_timelayer(&layer); + } + else { + layer = 12; + } + } + else { + layer = 2; + dComIfG_get_timelayer(&layer); + } + + break; + } + case Kakariko_Graveyard: + { + condition = dComIfGs_isEventBit(GOT_ZORA_ARMOR_FROM_RUTELA); // Got Zora Armor from Rutela + if (condition == false) { + condition = dComIfGs_isEventBit(ZORA_ESCORT_CLEARED); // Zora Escort Cleared + + if (condition == false) { + layer = 2; + + // If it is night, the layer is different. + dComIfG_get_timelayer(&layer); + } + else { + layer = 4; + } + } + else { + layer = 2; + dComIfG_get_timelayer(&layer); + } + break; + } + + case Kakariko_Graveyard_Interiors: { + if (((i_roomNo == 1 && + (condition = dComIfGs_isEventBit(LAKEBED_TEMPLE_CLEARED), + condition != false)))) // Lakebed Completed + { + layer = 4; + dComIfG_get_timelayer(&layer); + } + else { + layer = 2; + dComIfG_get_timelayer(&layer); + } + break; + } + + case Kakariko_Village_Interiors: { + if (i_roomNo == 1) { // Lakebed Completed + layer = 4; + dComIfG_get_timelayer(&layer); + } + else if (i_roomNo == 3) { + layer = 2; + } + else { + layer = 2; + dComIfG_get_timelayer(&layer); + } + break; + } + + case Death_Mountain: { + condition = + dComIfGs_isEventBit(GORON_MINES_CLEARED); // Goron Mines Completed + + if (condition) { + layer = 2; + } + break; + } + + case Death_Mountain_Interiors: { + layer = 0; + break; + } + + case Lake_Hylia: { + if (i_roomNo == 1) { // Lanayru Spring + + condition = dComIfGs_isEventBit(LAKEBED_TEMPLE_CLEARED); // Lakebed Temple has been completed + if (condition) { + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_STARTED); // MDH has been started + if (condition == false) { + layer = 9; + } + else { + layer = 2; + } + } + } + else { + condition = dComIfGs_isEventBit(SKY_CANNON_REPAIRED); // Sky Cannon Repaired + if (condition == false) { + condition = dComIfGs_isEventBit(WARPED_SKY_CANNON_TO_LAKE_HYLIA); // Sky Cannon Warped to Lake Hylia + + if (condition == false) { + layer = 2; + } + else { + layer = 1; + } + } + else { + layer = 3; + } + } + break; + } + + case Castle_Town_Interiors: + { + if (condition = dComIfGs_isEventBit(LAKEBED_TEMPLE_CLEARED),condition) { // Lakebed Temple Completed + layer = 2; + if (condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED),condition) { // MDH Completed + layer = 0; + } + } + if (i_roomNo == 5) { // Telma's Bar + layer = 4; + } + break; + } + + case Castle_Town: { + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED); // MDH Completed + if (condition == false) { + condition = dComIfGs_isEventBit(LAKEBED_TEMPLE_CLEARED); // Lakebed Temple Completed + if (condition == false) { + if ((i_roomNo == 3) && + (condition = dComIfGs_isEventBit(ZORA_ESCORT_CLEARED),condition != false)) { // Zora Escort Cleared + layer = 1; + } + else if (i_roomNo == 4) { + layer = 1; + } + } + else { + layer = 2; + } + } + else { + if (((i_roomNo == 4) || (i_roomNo == 3)) || (i_roomNo == 1)) { + layer = 1; + } + else { + layer = 0; + } + } + + if (i_roomNo == 0) { + if (dComIfGs_getStartPoint() == 0xF) { + layer = 5; + } + } + break; + } + + case Zoras_Domain: { + layer = 0; + break; + } + + case Upper_Zoras_River: { + condition = dComIfGs_isEventBit(IZA_1_MINIGAME_UNLOCKED); // Iza 1 Unlocked + if (condition != false) + { + layer = 1; + } + break; + } + + case Gerudo_Desert: { + layer = 8; + + condition = dComIfGs_isEventBit(VISITED_DESERT_FOR_THE_FIRST_TIME); // Have been to desert + if (condition != false) { + layer = 0; + } + break; + } + + case Zoras_River: { + condition = dComIfGs_isEventBit(IZA_1_MINIGAME_DONE); // Iza 1 Minigame Completed + + if (condition == false) { + condition = dComIfGs_isEventBit(STARTED_IZA_1_MINIGAME); // Iza 1 Minigame Started + if (condition != false) { + layer = 2; + } + } + else { + layer = 1; + } + break; + } + + case Ordon_Village: { + if (i_roomNo == 0) { + if (!dKy_daynight_check()) { + layer = 0; + } + else { + layer = 5; + } + } + + else { + if (i_roomNo == 1) { + condition = + dComIfGs_isEventBit(ORDON_DAY_1_FINISHED); // Ordon Day 1 done + + if (condition) { + condition = dComIfGs_isEventBit(ORDON_DAY_2_OVER); // Talo Saved + if (condition) { + layer = 2; + } + else { + layer = 4; + } + } + else { + layer = 3; + } + } + } + break; + } + + case Ordon_Village_Interiors: + { + /* not used in randomizer anymore. keeping for documentation sake + if ( i_roomNo == 1 ) // Sera's Shop + { + condition = dComIfGs_isEventBit( + BOUGHT_SLINGSHOT_FROM_SERA ); // Bought slinghot from Sera + + if ( condition ) + { + layer = 2; + } + }*/ + if (i_roomNo == 2) { // Jaggle's House + + darkIsClear = dComIfGs_isDarkClearLV(0); + if (darkIsClear == false) { + condition = dComIfGs_isEventBit(FINISHED_SEWERS); // First Trip to Sewers done + if (condition != false) { + layer = 1; + } + } + else { + layer = 1; + } + } + /* not used in randomizer anymore. keeping for documentation sake + else + { + if ( i_roomNo == 5 ) // Rusl's House + { + darkIsClear = libtp::tp::d_save::isDarkClearLV( playerStatusBPtr, 0 ); + if ( darkIsClear != false ) + { + layer = 2; + } + } + }*/ + + break; + } + + case Ordon_Spring: { + condition = dComIfGs_isEventBit(ORDON_DAY_2_OVER); // Talo saved + if (condition) { + condition = + dComIfGs_isEventBit(FINISHED_SEWERS); // First trip to Sewers done + + if (condition) { + darkIsClear = dComIfGs_isDarkClearLV(0); + if (darkIsClear != false) { + layer = 2; + } + else { + layer = 4; + } + } + else { + layer = 0; + } + } + else { + condition = dComIfGs_isEventBit(TALO_CHASES_MONKEY); // Sword training done on Ordon Day 2 + if (condition) { + layer = 3; + } + else { + layer = 1; + } + } + + break; + } + + case Ordon_Ranch: { + condition = dComIfGs_isEventBit(ORDON_DAY_1_FINISHED); // Day 1 done + if (condition) { + condition = dComIfGs_isEventBit(ORDON_DAY_2_OVER); // Talo Saved + if (condition) { + condition = dComIfGs_isEventBit(WATCHED_CUTSCENE_AFTER_GOATS_2); // Saw CS after Goats 2 done + + if (condition) { + layer = 2; + dComIfG_get_timelayer(&layer); + } + else { + layer = 9; + } + } + else { + layer = 2; + } + } + else { + layer = 12; + } + break; + } + + case Hyrule_Field: { + // First 3 twilights are cleared + if ((dComIfGs_getSaveData()->getPlayer().getPlayerStatusB().mDarkClearLevelFlag & 0x7) == 0x7) { + if (dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED)) { + layer = 6; + } + else if (dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_STARTED)) { + layer = 4; + } + else { + layer = 0; + } + } + else { + layer = 0; + } + break; + } + + case Outside_Castle_Town: { + if (i_roomNo == 8) { + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED); // MDH Completed + if (condition == false) { + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_STARTED); // MDH State Activated + if (condition != false) { + layer = 4; + } + } + else { + layer = 6; + } + } + else { + if (i_roomNo == 0x10) { + condition = dComIfGs_isEventBit(GOT_WOOD_STATUE); // Wooden Statue Gotten + if (condition == false) { + condition = dComIfGs_isEventBit(TALKED_TO_LOUISE_ABOUT_THE_STOLEN_STATUE); // Talked to Louise after Medicine Scent + if (condition == false) { + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED); // MDH Completed + if (condition == false) { + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_STARTED); // MDH State Activated + if (condition != false) { + layer = 4; + } + else { + layer = 6; + } + } + else { + layer = 6; + } + } + else { + layer = 1; + } + } + else { + layer = 6; + } + } + else { + if (i_roomNo == 0x11) { + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED); // MDH Completed + if (condition == false) { + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_STARTED); // MDH State Activated + if (condition != false) { + layer = 4; + } + } + else { + layer = 0; + } + } + } + } + break; + } + + case Hidden_Village: { + condition = dComIfGs_isEventBit(GAVE_ILIA_THE_WOOD_STATUE); // Ilia shown the wooden statue + if (condition != false) { + condition = dComIfGs_isEventBit(GOT_ILIAS_CHARM); // Ilia shown Ilia's Charm + if (condition != false) { + layer = 1; + } + } + else { + layer = 1; + } + + break; + } + + case Castle_Town_Shops: { + if (i_roomNo == 5) { + layer = 0; + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_STARTED); + if (condition) { + layer = 1; + condition = dComIfGs_isEventBit(MIDNAS_DESPERATE_HOUR_COMPLETED); + if (condition) { + layer = 0; + } + } + } + else { + condition = dComIfGs_isEventBit(MALO_MART_CASTLE_TOWN_BRANCH_IS_OPEN); // CT Shop is Malo Mart + + if (condition != false) { + layer = 1; + } + } + break; + } + + case Sacred_Grove: { + layer = 2; + break; + } + + case Bulblin_Camp: { + condition = dComIfGs_isEventBit(ESCAPED_BURNING_TENT_IN_BULBLIN_CAMP); // Escaped Burning Tent in Bulblin Camp + if (condition) { + if (i_roomNo == 3) // Other states for this room are very similar, but do not have the boar + // in the dzx. + { // Setting state 1 solves for any potential softlocks regarding the boar in that area. + layer = 1; + } + else { + layer = 3; + } + } + break; + } + + case Faron_Woods_Cave: { + condition = dComIfGs_isEventBit(ORDON_DAY_2_OVER); // Talo saved + if (condition != false) { + layer = 1; + } + break; + } + + case Hyrule_Castle_Sewers: { + condition = dComIfGs_isEventBit(FINISHED_SEWERS); // Sewers Finished + if (condition) { + layer = 13; + } + else { + layer = 14; + } + break; + } + + case Hyrule_Castle: { + if (((i_roomNo != 0xb) && (i_roomNo != 0xd)) && (i_roomNo != 0xe)) { + layer = 1; + } + break; + } + + case Fishing_Pond: + case Fishing_Pond_Interiors: { + switch (g_env_light.fishing_hole_season) { + case 1: + layer = 0; + break; + case 2: + layer = 1; + break; + case 3: + layer = 2; + break; + case 4: + layer = 3; + break; + } + break; + } + default: { + break; + } + } + } + } + + return HOOK_CONTINUE; +} + } ModResult initialize() { @@ -615,6 +1209,8 @@ ModResult initialize() { ADD_HOOK_POST(dFile_select_c__nameInput2, hookPostNameInput2); + ADD_HOOK_PRE(dComIfG_play_c__getLayerNo_common_common, hookPreGetLayerNo); + return MOD_OK; } } \ No newline at end of file