From 370068a7b04c98b0d7f6c9e898052f3715b9c3d6 Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:34:34 -0500 Subject: [PATCH] recreate original launcher structure via recompui + prep multigame ui --- CMakeLists.txt | 1 + include/zelda_game.h | 15 ++ include/zelda_launcher.h | 2 + src/game/rom_decompression.cpp | 24 ++- src/main/launcher.cpp | 270 +++++++++++++++++++++++++++++++++ src/main/main.cpp | 49 +++--- 6 files changed, 338 insertions(+), 23 deletions(-) create mode 100644 src/main/launcher.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 51e19b0..be997ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -141,6 +141,7 @@ add_executable(Zelda64Recompiled) set (SOURCES ${CMAKE_SOURCE_DIR}/src/main/launcher_animation.cpp + ${CMAKE_SOURCE_DIR}/src/main/launcher.cpp ${CMAKE_SOURCE_DIR}/src/main/main.cpp ${CMAKE_SOURCE_DIR}/src/main/support.cpp ${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp diff --git a/include/zelda_game.h b/include/zelda_game.h index 739eaa0..e251422 100644 --- a/include/zelda_game.h +++ b/include/zelda_game.h @@ -5,10 +5,25 @@ #include #include +#include "librecomp/game.hpp" + namespace zelda64 { + enum Game { + OoT, + MM + }; + + constexpr zelda64::Game default_game = Game::MM; + + zelda64::Game get_current_game(); + zelda64::Game swap_current_game(); + + const recomp::GameEntry &get_game_entry(Game game); + void quicksave_save(); void quicksave_load(); std::vector decompress_mm(std::span compressed_rom); + std::vector decompress_oot(std::span compressed_rom); }; #endif diff --git a/include/zelda_launcher.h b/include/zelda_launcher.h index f88f7ee..6bc7539 100644 --- a/include/zelda_launcher.h +++ b/include/zelda_launcher.h @@ -4,6 +4,8 @@ #include "recompui/recompui.h" namespace zelda64 { + void on_launcher_init(recompui::LauncherMenu *menu); + void launcher_animation_setup(recompui::LauncherMenu *menu); void launcher_animation_update(recompui::LauncherMenu *menu); } diff --git a/src/game/rom_decompression.cpp b/src/game/rom_decompression.cpp index 2576fb2..f676882 100644 --- a/src/game/rom_decompression.cpp +++ b/src/game/rom_decompression.cpp @@ -64,11 +64,17 @@ constexpr uint32_t byteswap(uint32_t val) { } #endif -// Produces a decompressed MM rom. This is only needed because the game has compressed code. + +// Produces a decompressed rom. This is only needed because the game has compressed code. // For other recomps using this repo as an example, you can omit the decompression routine and // set the corresponding fields in the GameEntry if the game doesn't have compressed code, // even if it does have compressed data. -std::vector zelda64::decompress_mm(std::span compressed_rom) { +static std::vector decompress( + std::span compressed_rom, + const std::u8string &header_code, + const size_t dma_data_rom_addr, + const size_t decompressed_size +) { // Sanity check the rom size and header. These should already be correct from the runtime's check, // but it should prevent this file from accidentally being copied to another recomp. if (compressed_rom.size() != 0x2000000) { @@ -76,7 +82,7 @@ std::vector zelda64::decompress_mm(std::span compressed_ return {}; } - if (compressed_rom[0x3B] != 'N' || compressed_rom[0x3C] != 'Z' || compressed_rom[0x3D] != 'S' || compressed_rom[0x3E] != 'E') { + if (compressed_rom[0x3B] != header_code[0] || compressed_rom[0x3C] != header_code[1] || compressed_rom[0x3D] != header_code[2] || compressed_rom[0x3E] != header_code[3]) { assert(false); return {}; } @@ -98,10 +104,8 @@ std::vector zelda64::decompress_mm(std::span compressed_ DmaDataEntry cur_entry{}; size_t cur_entry_index = 0; - constexpr size_t dma_data_rom_addr = 0x1A500; - std::vector ret{}; - ret.resize(0x2F00000); + ret.resize(decompressed_size); size_t content_end = 0; @@ -166,3 +170,11 @@ std::vector zelda64::decompress_mm(std::span compressed_ return ret; } + +static std::vector zelda64::decompress_mm(std::span compressed_rom) { + return decompress(compressed_rom, u8"NZSE", 0x1A500, 0x2F00000); +} + +static std::vector zelda64::decompress_oot(std::span compressed_rom) { + return decompress(compressed_rom, u8"CZLE", 0x7430, 0x347F000); +} diff --git a/src/main/launcher.cpp b/src/main/launcher.cpp new file mode 100644 index 0000000..c9592b1 --- /dev/null +++ b/src/main/launcher.cpp @@ -0,0 +1,270 @@ +#include "recompui/recompui.h" +#include "zelda_game.h" +#include "zelda_launcher.h" + +// UI Components +namespace { + using namespace recompui; + + class SubtitleTitleButton : public Element { + protected: + Style selected_style; + Style hover_style; + Style focus_style; + std::function pressed_callback; + bool selected = false; + bool right = false; + bool soon = false; + Element *exposed_contents = nullptr; + + Label *title = nullptr; + Label *subtitle = nullptr; + Element *soon_label_wrap = nullptr; + Label *soon_label = nullptr; + Label *soon_label_tm = nullptr; + int soon_opacity = 0; + int soon_opacity_goal = 0; + + // Element overrides. + virtual void process_event(const Event &e) override { + switch (e.type) { + case EventType::Click: + if (!soon && !selected && pressed_callback) { + pressed_callback(); + } + break; + case EventType::Hover: + if (soon) { + soon_opacity_goal = std::get(e.variant).active ? 255 : 0; + queue_update(); + } else { + set_style_enabled(hover_state, std::get(e.variant).active && !selected); + } + + break; + case EventType::Focus: + if (!soon) { + set_style_enabled(focus_state, std::get(e.variant).active); + } + break; + case EventType::Update: + if (soon && soon_opacity_goal != soon_opacity) { + int rate = soon_opacity_goal < soon_opacity ? -1 : 1; + soon_opacity += rate; + soon_label_tm->set_color(theme::color::TextDim, soon_opacity); + queue_update(); + } + break; + default: + assert(false && "Unknown event type."); + break; + } + }; + std::string_view get_type_name() override { return "SubtitleTitleButton"; } + public: + SubtitleTitleButton(ResourceId rid, Element *parent, const std::string &text, bool right_align = false, bool soon = false) : Element(rid, parent, Events(EventType::Click, EventType::Hover, EventType::Enable, EventType::Focus, EventType::Update), "button", false) { + auto context = recompui::get_current_context(); + this->soon = soon; + right = right_align; + set_display(Display::Block); + set_position(Position::Relative); + set_width_auto(); + set_height_auto(); + set_padding(0); + set_background_color(theme::color::Transparent); + set_color(theme::color::TextDim); + set_text_align(right ? TextAlign::Right : TextAlign::Left); + + set_opacity(0.5); + + selected_style.set_color(theme::color::Text); + selected_style.set_cursor(Cursor::None); + selected_style.set_opacity(1); + hover_style.set_color(theme::color::Primary); + hover_style.set_opacity(1); + focus_style.set_color(theme::color::Primary); + focus_style.set_opacity(1); + + if (!soon) { + enable_focus(); + set_cursor(Cursor::Pointer); + } + add_style(&selected_style, checked_state); + add_style(&hover_style, hover_state); + add_style(&focus_style, focus_state); + title = context.create_element