From 31c5397ae348502a2e1d02d13a5834f0faa277ab Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Wed, 15 Apr 2026 14:36:20 -0400 Subject: [PATCH 01/64] Gamepad Color Implementation --- include/d/actor/d_a_alink.h | 1 + src/d/actor/d_a_alink_dusk.cpp | 107 +++++++++++++++++++++++++++++++++ src/f_ap/f_ap_game.cpp | 4 ++ 3 files changed, 112 insertions(+) diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 53228af84b..786c0a4555 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4549,6 +4549,7 @@ public: /* 0x03850 */ daAlink_procFunc mpProcFunc; #if TARGET_PC + void handleGamepadColor(); void handleWolfHowl(); void handleQuickTransform(); bool checkGyroAimItemContext(); diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index 9b937d3678..b081ff1ce8 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -4,6 +4,113 @@ #include "d/d_meter2_draw.h" #include "d/d_meter2_info.h" +cXyz currentGamepadColor = {0, 0, 0}; +cXyz finalGamepadColor = {0, 0, 0}; +float lerpSpeed = 0.0f; +const cXyz duskColor = {30, 30, -30}; + +const cXyz heartColor1 = {255, 0, 0}; +const cXyz heartColor2 = {155, 5, 5}; +const cXyz heartColor3 = {55, 5, 5}; + +float lerp(float a, float b, float t) { + return a + t * (b - a); +} + +cXyz LerpColor(cXyz a, cXyz b, float t) { + return {lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t)}; +} + +void FadeLED(cXyz newColor, float speed) { + finalGamepadColor = newColor; + lerpSpeed = speed / 30.0f; +} + +void SetLED(cXyz newColor) { + currentGamepadColor = newColor; + finalGamepadColor = newColor; +} + +void AddGamepadCurrentColor(cXyz addColor) { + finalGamepadColor.x += addColor.x; + finalGamepadColor.y += addColor.y; + finalGamepadColor.z += addColor.z; +} + +void daAlink_c::handleGamepadColor() { + bool setColor = false; + + fopAc_ac_c* zhint = dComIfGp_att_getZHint(); + if (zhint != NULL) { + FadeLED({50, 50, 175}, 2.0f); + setColor = true; + } + + u8 linkHp = Z2GetLink()->getLinkHp(); + if (linkHp <= 2) { + FadeLED(heartColor1, 2.0f); + setColor = true; + } else if (linkHp <= 4) { + FadeLED(heartColor2, 2.0f); + setColor = true; + } else if (linkHp <= 6) { + FadeLED(heartColor3, 2.0f); + setColor = true; + } + + if (!setColor) { + if (checkWolf()) { + FadeLED({115, 115, 75}, 5.0f); + setColor = true; + } else { + switch (dComIfGs_getSelectEquipClothes()) { + case dItemNo_WEAR_KOKIRI_e: + FadeLED({0, 100, 0}, 5.0f); + setColor = true; + break; + case dItemNo_WEAR_ZORA_e: + FadeLED({0, 0, 100}, 5.0f); + setColor = true; + break; + case dItemNo_ARMOR_e: + if (checkMagicArmorHeavy()) { + FadeLED({5, 100, 100}, 5.0f); + } else { + FadeLED({100, 0, 5}, 5.0f); + } + setColor = true; + break; + default: + FadeLED({235, 230, 115}, 5.0f); + setColor = true; + break; + } + } + } + + if (dKy_darkworld_check()) { + AddGamepadCurrentColor(duskColor); + } + + if (finalGamepadColor.x > 255) + finalGamepadColor.x = 255; + if (finalGamepadColor.x < 0) + finalGamepadColor.x = 0; + + if (finalGamepadColor.y > 255) + finalGamepadColor.y = 255; + if (finalGamepadColor.y < 0) + finalGamepadColor.y = 0; + + if (finalGamepadColor.z > 255) + finalGamepadColor.z = 255; + if (finalGamepadColor.z < 0) + finalGamepadColor.z = 0; + + currentGamepadColor = LerpColor(currentGamepadColor, finalGamepadColor, lerpSpeed); + PADSetColor(PAD_1, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, (u8)currentGamepadColor.z); +} + void daAlink_c::handleWolfHowl() { if (checkWolf()) { if (!dusk::getSettings().game.sunsSong) { diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 84d2ab8e97..9150d764c7 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -748,6 +748,10 @@ void fapGm_Execute() { #endif #if TARGET_PC + if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { + dynamic_cast(link)->handleGamepadColor(); + } + if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) { if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { dynamic_cast(link)->handleWolfHowl(); From c42a33154c0e5225b6bc61f421b7d18e9cf38265 Mon Sep 17 00:00:00 2001 From: Irastris Date: Mon, 20 Apr 2026 18:11:27 -0400 Subject: [PATCH 02/64] Frame Interp: Fix flickering refraction in cutscenes --- src/dusk/frame_interpolation.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index c72923da7d..e0057790df 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -408,10 +408,8 @@ void begin_presentation_camera() { } mDoLib_clipper::setup(view->fovy, view->aspect, view->near_, far_); - -#if WIDESCREEN_SUPPORT - mDoGph_gInf_c::offWideZoom(); -#endif + + // FRAME INTERP NOTE: Removed the call to offWideZoom that was here, it causes problems with presentation during cutscenes. s_presentation_depth = 1; From 89acf923e0fc6b929bd09af6c657140df187a86f Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Tue, 21 Apr 2026 01:11:49 +0200 Subject: [PATCH 03/64] Fix THP shutdown crash Cleanly shut down movie player threads on exit. I'm not 100% fond of having to manually insert a call like this into the shutdown logic, but the other solution is shutting down all processes and that's likely to result in causing more crashes. --- include/d/actor/d_a_movie_player.h | 6 ++++++ src/d/actor/d_a_movie_player.cpp | 9 +++++++++ src/m_Do/m_Do_main.cpp | 6 +++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/include/d/actor/d_a_movie_player.h b/include/d/actor/d_a_movie_player.h index 0ddc675c24..0e666ae470 100644 --- a/include/d/actor/d_a_movie_player.h +++ b/include/d/actor/d_a_movie_player.h @@ -94,6 +94,12 @@ static void __THPAudioInitialize(THPAudioDecodeInfo* info, u8* ptr); #define THP_TEXTURE_SET_COUNT 3 #endif +#if TARGET_PC +namespace dusk { + void MoviePlayerShutdown(); +} +#endif + struct daMP_THPPlayer { /* 0x000 */ DVDFileInfo fileInfo; /* 0x03C */ THPHeader header; diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index d972bddd4d..079584f714 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -4580,3 +4580,12 @@ actor_process_profile_definition g_profile_MOVIE_PLAYER = { }; AUDIO_INSTANCES; + +#if TARGET_PC +void dusk::MoviePlayerShutdown() { + // We need to cleanly shut down the threads to avoid crashes on shutdown. + if (daMP_c::m_myObj) { + daMP_c::m_myObj->daMP_c_Finish(); + } +} +#endif \ No newline at end of file diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index c597ad2804..e41bd1f24c 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -66,13 +66,15 @@ #include "SDL3/SDL_filesystem.h" #include "cxxopts.hpp" +#include "d/actor/d_a_movie_player.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" -#include "dusk/settings.h" #include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/settings.h" #include "dusk/discord_presence.hpp" #include "tracy/Tracy.hpp" #include "f_pc/f_pc_draw.h" +#include "tracy/Tracy.hpp" // --- GLOBALS --- s8 mDoMain::developmentMode = -1; @@ -614,6 +616,8 @@ int game_main(int argc, char* argv[]) { main01(); + dusk::MoviePlayerShutdown(); + dusk::ShutdownCrashReporting(); dusk::ShutdownFileLogging(); fflush(stdout); From faa86181247951a5a9560066a8f6e84a4371ba24 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Mon, 20 Apr 2026 19:37:38 -0600 Subject: [PATCH 04/64] Fix frame interpolation on save and world map menus --- src/d/d_menu_fmap2D.cpp | 19 +++++++++----- src/d/d_menu_save.cpp | 58 ++++++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index a3afaa1641..8692156daf 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -1769,14 +1769,19 @@ void dMenu_Fmap2DBack_c::calcBlink() { t * (g_fmapHIO.mMapBlink[i + 1].mUnselectedRegion.mBlinkSpeed - g_fmapHIO.mMapBlink[i].mUnselectedRegion.mBlinkSpeed); - field_0x1218++; - if (field_0x1218 >= selected_blink_speed) { - field_0x1218 = 0; - } +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x1218++; + if (field_0x1218 >= selected_blink_speed) { + field_0x1218 = 0; + } - field_0x121a++; - if (field_0x121a >= unselected_blink_speed) { - field_0x121a = 0; + field_0x121a++; + if (field_0x121a >= unselected_blink_speed) { + field_0x121a = 0; + } } f32 t_selected = 0.0f; diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index 8dd6283dfd..53dbe37ca4 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -18,6 +18,7 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "d/d_msg_scrn_explain.h" +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "JSystem/J2DGraph/J2DAnmLoader.h" #include "f_op/f_op_msg_mng.h" @@ -715,7 +716,9 @@ void dMenu_save_c::_move() { } (this->*MenuSaveProc[mMenuProc])(); +#if !TARGET_PC saveSelAnm(); +#endif if (mWarning != NULL) { mWarning->_move(); @@ -732,36 +735,46 @@ void dMenu_save_c::saveSelAnm() { } void dMenu_save_c::selFileWakuAnm() { - mFileWakuAnmFrame += 2; - if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { - mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + mFileWakuAnmFrame += 2; + if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { + mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); + } + + mFileWakuRotAnmFrame += 2; + if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { + mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); + } } mpFileWakuAnm->setFrame(mFileWakuAnmFrame); - - mFileWakuRotAnmFrame += 2; - if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { - mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); - } mpFileWakuRotAnm->setFrame(mFileWakuRotAnmFrame); } void dMenu_save_c::bookIconAnm() { - field_0x154 += 2; - if (field_0x154 >= field_0x150->getFrameMax()) { - field_0x154 -= field_0x150->getFrameMax(); +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x154 += 2; + if (field_0x154 >= field_0x150->getFrameMax()) { + field_0x154 -= field_0x150->getFrameMax(); + } + + field_0x15c += 2; + if (field_0x15c >= field_0x158->getFrameMax()) { + field_0x15c -= field_0x158->getFrameMax(); + } + + field_0x164 += 2; + if (field_0x164 >= field_0x160->getFrameMax()) { + field_0x164 -= field_0x160->getFrameMax(); + } } field_0x150->setFrame(field_0x154); - - field_0x15c += 2; - if (field_0x15c >= field_0x158->getFrameMax()) { - field_0x15c -= field_0x158->getFrameMax(); - } field_0x158->setFrame(field_0x15c); - - field_0x164 += 2; - if (field_0x164 >= field_0x160->getFrameMax()) { - field_0x164 -= field_0x160->getFrameMax(); - } field_0x160->setFrame(field_0x164); } @@ -2812,6 +2825,9 @@ void dMenu_save_c::menuSaveWide() { void dMenu_save_c::_draw2() { if (field_0x21a1 == 0) { +#if TARGET_PC + saveSelAnm(); +#endif if (mpScrnExplain != NULL) { dComIfGd_set2DOpa(&mMenuSaveExplain); } From 30a99c22f1daac3e19a95198af0d68e5545b9edc Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 20 Apr 2026 20:45:16 -0600 Subject: [PATCH 05/64] Reorganize ImGui menus (#456) * Reorganize ImGui menus * Fix crash_reporting.cpp * Update aurora --- extern/aurora | 2 +- files.cmake | 2 - include/dusk/dusk.h | 1 - include/dusk/logging.h | 4 +- include/dusk/main.h | 3 + libs/JSystem/src/JFramework/JFWDisplay.cpp | 1 + src/dusk/config.cpp | 4 +- src/dusk/crash_reporting.cpp | 3 +- src/dusk/imgui/ImGuiConsole.cpp | 5 +- src/dusk/imgui/ImGuiConsole.hpp | 2 - src/dusk/imgui/ImGuiEngine.cpp | 4 +- src/dusk/imgui/ImGuiMenuEnhancements.cpp | 248 --------- src/dusk/imgui/ImGuiMenuEnhancements.hpp | 18 - src/dusk/imgui/ImGuiMenuGame.cpp | 556 ++++++++++++++------- src/dusk/imgui/ImGuiMenuGame.hpp | 7 + src/dusk/imgui/ImGuiMenuTools.cpp | 55 +- src/dusk/logging.cpp | 6 +- src/m_Do/m_Do_main.cpp | 64 +-- 18 files changed, 496 insertions(+), 489 deletions(-) delete mode 100644 src/dusk/imgui/ImGuiMenuEnhancements.cpp delete mode 100644 src/dusk/imgui/ImGuiMenuEnhancements.hpp diff --git a/extern/aurora b/extern/aurora index b1957f10cf..5d420c9f73 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit b1957f10cf9e7ea1e0e012c1968014bd11299297 +Subproject commit 5d420c9f73c93ab9a5dcd052ac92d47362764e80 diff --git a/files.cmake b/files.cmake index e929ec85b0..c8ecb9be7a 100644 --- a/files.cmake +++ b/files.cmake @@ -1366,8 +1366,6 @@ set(DUSK_FILES src/dusk/imgui/ImGuiBloomWindow.hpp src/dusk/imgui/ImGuiMenuTools.cpp src/dusk/imgui/ImGuiMenuTools.hpp - src/dusk/imgui/ImGuiMenuEnhancements.cpp - src/dusk/imgui/ImGuiMenuEnhancements.hpp src/dusk/imgui/ImGuiPreLaunchWindow.cpp src/dusk/imgui/ImGuiPreLaunchWindow.hpp src/dusk/imgui/ImGuiFirstRunPreset.hpp diff --git a/include/dusk/dusk.h b/include/dusk/dusk.h index b751990d9c..911ddbb535 100644 --- a/include/dusk/dusk.h +++ b/include/dusk/dusk.h @@ -6,7 +6,6 @@ #include "aurora/gfx.h" extern AuroraInfo auroraInfo; -extern const char* configPath; namespace dusk { extern AuroraStats lastFrameAuroraStats; diff --git a/include/dusk/logging.h b/include/dusk/logging.h index 0a9cbf238d..9b31b96bf2 100644 --- a/include/dusk/logging.h +++ b/include/dusk/logging.h @@ -4,10 +4,12 @@ #include #include +#include + void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len); namespace dusk { - void InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel); + void InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel); void ShutdownFileLogging(); const char* GetLogFilePath(); void SendToStubLog(AuroraLogLevel level, const char* module, const char* message); diff --git a/include/dusk/main.h b/include/dusk/main.h index 2152a6d564..065f507d36 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -1,11 +1,14 @@ #ifndef DUSK_MAIN_H #define DUSK_MAIN_H +#include + namespace dusk { extern bool IsRunning; extern bool IsShuttingDown; extern bool IsGameLaunched; extern bool IsFocusPaused; + extern std::filesystem::path ConfigPath; } #endif // DUSK_MAIN_H diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 64b0fedcf2..9ea826feee 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -379,6 +379,7 @@ static void waitPrecise(Limiter& limiter, Uint64 targetNs) { static void waitForTick(u32 p1, u16 p2) { #if TARGET_PC if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) { + dusk::frameUsagePct = 0.f; return; } if (dusk::getTransientSettings().skipFrameRateLimit) { diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index c377d1ba54..176967359d 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -10,7 +10,7 @@ #include #include -#include "dusk/dusk.h" +#include "dusk/main.h" using namespace dusk::config; @@ -24,7 +24,7 @@ static absl::flat_hash_map RegisteredConfigVar static bool RegistrationDone = false; static std::string GetConfigJsonPath() { - return fmt::format("{}{}", configPath, ConfigFileName); + return (dusk::ConfigPath / ConfigFileName).string(); } ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) { diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 73f432e418..0499f0d6a1 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -3,6 +3,7 @@ #include "dusk/app_info.hpp" #include "dusk/dusk.h" #include "dusk/logging.h" +#include "dusk/main.h" #include "dusk/settings.h" #include "version.h" @@ -66,7 +67,7 @@ std::string GetReleaseName() { } std::filesystem::path GetSentryDatabasePath() { - return std::filesystem::path(configPath) / "sentry"; + return dusk::ConfigPath / "sentry"; } std::filesystem::path GetLogAttachmentPath() { diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index f5e25277c7..0b32fd5078 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -319,9 +319,11 @@ namespace dusk { } } + // The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg, + // so make the window bg fully transparent temporarily + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); if (showMenu && ImGui::BeginMainMenuBar()) { m_menuGame.draw(); - m_menuEnhancements.draw(); m_menuTools.draw(); const auto fpsLabel = @@ -336,6 +338,7 @@ namespace dusk { ImGui::EndMainMenuBar(); } + ImGui::PopStyleColor(); if (!getSettings().backend.wasPresetChosen) { m_firstRunPreset.draw(); diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 70c5184d0d..de4e66c7d6 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -9,7 +9,6 @@ #include #include "ImGuiFirstRunPreset.hpp" -#include "ImGuiMenuEnhancements.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiMenuTools.hpp" #include "ImGuiPreLaunchWindow.hpp" @@ -52,7 +51,6 @@ private: ImGuiFirstRunPreset m_firstRunPreset; ImGuiMenuGame m_menuGame; - ImGuiMenuEnhancements m_menuEnhancements; ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last diff --git a/src/dusk/imgui/ImGuiEngine.cpp b/src/dusk/imgui/ImGuiEngine.cpp index 46bfa8f3ab..4b6a7fb531 100644 --- a/src/dusk/imgui/ImGuiEngine.cpp +++ b/src/dusk/imgui/ImGuiEngine.cpp @@ -126,7 +126,7 @@ void ImGuiEngine_Initialize(float scale) { auto* colors = style.Colors; colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f); colors[ImGuiCol_TextDisabled] = ImVec4(0.36f, 0.42f, 0.47f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 0.98f); colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); colors[ImGuiCol_Border] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); @@ -137,7 +137,7 @@ void ImGuiEngine_Initialize(float scale) { colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.12f, 0.14f, 0.65f); colors[ImGuiCol_TitleBgActive] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 0.80f); colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.39f); colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.18f, 0.22f, 0.25f, 1.00f); diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp deleted file mode 100644 index 7d179aa578..0000000000 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ /dev/null @@ -1,248 +0,0 @@ -#include "imgui.h" - -#include "ImGuiMenuEnhancements.hpp" -#include "ImGuiConfig.hpp" -#include "dusk/settings.h" - -namespace dusk { - ImGuiMenuEnhancements::ImGuiMenuEnhancements() {} - - void ImGuiMenuEnhancements::draw() { - if (ImGui::BeginMenu("Enhancements")) { - if (ImGui::BeginMenu("Gameplay")) { - ImGui::SeparatorText("Preferences"); - - config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game."); - } - - config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disables the main HUD of the game.\n" - "Useful for recording or a more immersive experience!"); - } - - ImGui::SeparatorText("Difficulty"); - - config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); - - config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Any hit will instantly kill you."); - } - - config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hearts will never drop from enemies,\n" - "pots and various other places."); - } - - ImGui::SeparatorText("Quality of Life"); - - config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)"); - } - - config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time."); - } - - config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version."); - } - - config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version."); - } - - config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the delay when writing to the Memory Card."); - } - - config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Make text scroll immediately by holding B."); - } - - config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents Link from playing a struggle animation\n" - "when grabbing ledges or climbing on vines."); - } - - config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full."); - } - - config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Link won't recoil when his sword hits walls."); - } - - config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); - } - - config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the warning screen shown when starting the game."); - } - - config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day."); - } - - config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Graphics")) { - config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Improves the shadow resolution, making them higher quality."); - } - - config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation); - const bool frameInterpolationHovered = ImGui::IsItemHovered(); - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); - ImGui::TextUnformatted("[EXPERIMENTAL]"); - ImGui::PopStyleColor(); - if (frameInterpolationHovered || ImGui::IsItemHovered()) { - ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Audio")) { - config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disable the beeping sound when having low health."); - } - - config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Input")) { - config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); - - ImGui::SeparatorText("Gyro"); - - config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers\n" - "while in look mode (C-Up) and while aiming the\n" - "Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n" - "Ball and Chain, and Dominion Rod."); - } - - config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n" - "tilt the Rollgoal table in Hena's Cabin."); - } - - if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f"); - config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f"); - - if (getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Additional multiplier for scaling how strongly\n" - "the gyroscope affects the Rollgoal table."); - } - } - - config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n" - "reducing drift and jitter when the controller is still."); - } - - config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Low values track raw gyro input more closely,\n" - "while higher values smooth out input over time."); - } - - config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch); - config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw); - } - - ImGui::SeparatorText("Tools"); - - config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); - } - - ImGui::EndMenu(); - } - - ImGui::Separator(); - - if (ImGui::BeginMenu("Cheats")) { - config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); - config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); - config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); - config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); - config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); - config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); - config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); - config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); - config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); - - config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); - - config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows you to transform even if NPCs are looking."); - } - - config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Speeds up Spinner movement when holding R."); - } - - config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Makes the magic armor work without rupees."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Technical")) { - config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" - "the first released version."); - } - - ImGui::EndMenu(); - } - - ImGui::EndMenu(); - } - } -} diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.hpp b/src/dusk/imgui/ImGuiMenuEnhancements.hpp deleted file mode 100644 index f40baaad65..0000000000 --- a/src/dusk/imgui/ImGuiMenuEnhancements.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef DUSK_IMGUI_MENUENHANCEMENTS_HPP -#define DUSK_IMGUI_MENUENHANCEMENTS_HPP - -#include -#include -#include - -#include "imgui.h" - -namespace dusk { - class ImGuiMenuEnhancements { - public: - ImGuiMenuEnhancements(); - void draw(); - }; -} - -#endif // DUSK_IMGUI_MENUENHANCEMENTS_HPP diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index c6676df774..691434427c 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -5,45 +5,18 @@ #include "ImGuiConsole.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiConfig.hpp" -#include #include "JSystem/JUtility/JUTGamePad.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" -#include "dusk/dusk.h" +#include "dusk/main.h" #include "dusk/hotkeys.h" #include "dusk/settings.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include -#include #include -#include - -#include "dusk/main.h" - -#if defined(__APPLE__) -#include -#endif - -#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__)) -#define DUSK_CAN_OPEN_DATA_FOLDER 1 - -namespace fs = std::filesystem; - -static void OpenDataFolder() { - const std::string path = fs::absolute(fs::path(aurora::g_config.configPath)).generic_string(); -#if defined(_WIN32) - const std::string url = std::string("file:///") + path; -#else - const std::string url = std::string("file://") + path; -#endif - (void)SDL_OpenURL(url.c_str()); -} -#else -#define DUSK_CAN_OPEN_DATA_FOLDER 0 -#endif namespace { constexpr int kInternalResolutionScaleMax = 12; @@ -63,149 +36,13 @@ namespace dusk { ImGuiMenuGame::ImGuiMenuGame() {} void ImGuiMenuGame::draw() { - if (ImGui::BeginMenu("Game")) { - if (ImGui::BeginMenu("Graphics")) { - if (!IsMobile) { - if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { - ToggleFullscreen(); - } - - if (ImGui::MenuItem("Default Window Size")) { - getSettings().video.enableFullscreen.setValue(false); - VISetWindowFullscreen(false); - VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); - VICenterWindow(); - } - } - - bool vsync = getSettings().video.enableVsync; - if (ImGui::Checkbox("Enable Vsync", &vsync)) { - getSettings().video.enableVsync.setValue(vsync); - aurora_enable_vsync(vsync); - config::Save(); - } - - bool lockAspect = getSettings().video.lockAspectRatio; - if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) { - getSettings().video.lockAspectRatio.setValue(lockAspect); - - if (lockAspect) { - AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); - } else { - AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); - } - - config::Save(); - } - - u32 internalResolutionWidth = 0; - u32 internalResolutionHeight = 0; - AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); - ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, - internalResolutionHeight); - - int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, - kInternalResolutionScaleMax); - if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, - scale == 0 ? "Auto" : "%dx")) - { - getSettings().game.internalResolutionScale.setValue(scale); - VISetFrameBufferScale(static_cast(scale)); - config::Save(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Auto renders at the native window resolution.\n" - "Higher values scale the game's internal framebuffer."); - } - - constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; - int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); - if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { - for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) { - const bool selected = bloomMode == i; - if (ImGui::Selectable(bloomModeNames[i], selected)) { - getSettings().game.bloomMode.setValue(static_cast(i)); - config::Save(); - } - if (selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - bool bloomOff = bloomMode == static_cast(BloomMode::Off); - if (bloomOff) ImGui::BeginDisabled(); - float mult = getSettings().game.bloomMultiplier.getValue(); - if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) { - getSettings().game.bloomMultiplier.setValue(mult); - config::Save(); - } - if (bloomOff) ImGui::EndDisabled(); - - ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Audio")) { - ImGui::Text("Master Volume"); - if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { - dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); - } - - if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { - dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); - } - /* - // TODO: Implement additional settings - ImGui::Text("Main Music Volume"); - ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100); - - ImGui::Text("Sub Music Volume"); - ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100); - - ImGui::Text("Sound Effects Volume"); - ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100); - - ImGui::Text("Fanfare Volume"); - ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100); - - Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); - if (audioMgr != nullptr) { - } - */ - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Controller")) { - ImGui::MenuItem("Configure Controller", nullptr, &m_showControllerConfig); - ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Interface")) { - config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); - config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); -#if DUSK_ENABLE_SENTRY_NATIVE - config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); -#endif - if (!IsMobile) { - config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); - } - - ImGui::EndMenu(); - } - - ImGui::Separator(); - -#if DUSK_CAN_OPEN_DATA_FOLDER - if (ImGui::MenuItem("Open Data Folder")) { - OpenDataFolder(); - } -#endif + if (ImGui::BeginMenu("Settings")) { + drawAudioMenu(); + drawCheatsMenu(); + drawGameplayMenu(); + drawGraphicsMenu(); + drawInputMenu(); + drawInterfaceMenu(); ImGui::Separator(); @@ -221,6 +58,383 @@ namespace dusk { } } + void ImGuiMenuGame::drawGraphicsMenu() { + if (ImGui::BeginMenu("Graphics")) { + ImGui::SeparatorText("Display"); + + if (!IsMobile) { + if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { + ToggleFullscreen(); + } + + if (ImGui::MenuItem("Restore Default Window Size")) { + getSettings().video.enableFullscreen.setValue(false); + VISetWindowFullscreen(false); + VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); + VICenterWindow(); + } + } + + bool vsync = getSettings().video.enableVsync; + if (ImGui::Checkbox("Enable VSync", &vsync)) { + getSettings().video.enableVsync.setValue(vsync); + aurora_enable_vsync(vsync); + config::Save(); + } + + bool lockAspect = getSettings().video.lockAspectRatio; + if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) { + getSettings().video.lockAspectRatio.setValue(lockAspect); + + if (lockAspect) { + AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); + } else { + AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); + } + + config::Save(); + } + + ImGui::SeparatorText("Resolution"); + + u32 internalResolutionWidth = 0; + u32 internalResolutionHeight = 0; + AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); + ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, + internalResolutionHeight); + + int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, + kInternalResolutionScaleMax); + if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, + scale == 0 ? "Auto" : "%dx")) + { + getSettings().game.internalResolutionScale.setValue(scale); + VISetFrameBufferScale(static_cast(scale)); + config::Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Auto renders at the native window resolution.\n" + "Higher values scale the game's internal framebuffer."); + } + + config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Improves the shadow resolution, making them higher quality."); + } + + ImGui::SeparatorText("Post-Processing"); + + constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; + int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); + if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { + for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) { + const bool selected = bloomMode == i; + if (ImGui::Selectable(bloomModeNames[i], selected)) { + getSettings().game.bloomMode.setValue(static_cast(i)); + config::Save(); + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + bool bloomOff = bloomMode == static_cast(BloomMode::Off); + if (bloomOff) ImGui::BeginDisabled(); + float mult = getSettings().game.bloomMultiplier.getValue(); + if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) { + getSettings().game.bloomMultiplier.setValue(mult); + config::Save(); + } + if (bloomOff) ImGui::EndDisabled(); + + ImGui::SeparatorText("Rendering"); + + config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation); + const bool frameInterpolationHovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); + ImGui::TextUnformatted("[EXPERIMENTAL]"); + ImGui::PopStyleColor(); + if (frameInterpolationHovered || ImGui::IsItemHovered()) { + ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur."); + } + + ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawGameplayMenu() { + if (ImGui::BeginMenu("Gameplay")) { + ImGui::SeparatorText("General"); + + config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game."); + } + + config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Disables the main HUD of the game.\n" + "Useful for recording or a more immersive experience!"); + } + + config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" + "the first released version."); + } + + ImGui::SeparatorText("Difficulty"); + + config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); + + config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Any hit will instantly kill you."); + } + + config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Hearts will never drop from enemies,\n" + "pots and various other places."); + } + + ImGui::SeparatorText("Quality of Life"); + + config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)"); + } + + config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time."); + } + + config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version."); + } + + config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version."); + } + + config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the delay when writing to the Memory Card."); + } + + config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Make text scroll immediately by holding B."); + } + + config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Prevents Link from playing a struggle animation\n" + "when grabbing ledges or climbing on vines."); + } + + config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full."); + } + + config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Link won't recoil when his sword hits walls."); + } + + config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); + } + + config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the warning screen shown when starting the game."); + } + + config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day."); + } + + config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawCheatsMenu() { + if (ImGui::BeginMenu("Cheats")) { + ImGui::SeparatorText("Resources"); + config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); + config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); + config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); + config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); + config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); + config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); + + ImGui::SeparatorText("Abilities"); + config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); + config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); + config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); + config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); + + config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Allows you to transform even if NPCs are looking."); + } + + config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Speeds up Spinner movement when holding R."); + } + + config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Makes the magic armor work without rupees."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawAudioMenu() { + if (ImGui::BeginMenu("Audio")) { + ImGui::Text("Master Volume"); + if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { + dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); + } + + if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { + dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); + } + /* + // TODO: Implement additional settings + ImGui::Text("Main Music Volume"); + ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100); + + ImGui::Text("Sub Music Volume"); + ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100); + + ImGui::Text("Sound Effects Volume"); + ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100); + + ImGui::Text("Fanfare Volume"); + ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100); + + Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); + if (audioMgr != nullptr) { + } + */ + + ImGui::SeparatorText("Tweaks"); + + config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Disable the beeping sound when having low health."); + } + + config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawInputMenu() { + if (ImGui::BeginMenu("Input")) { + ImGui::SeparatorText("Controller"); + + ImGui::MenuItem("Configure Controller", nullptr, &m_showControllerConfig); + + config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); + + ImGui::SeparatorText("Gyro"); + + config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables the gyroscope on supported controllers\n" + "while in look mode (C-Up) and while aiming the\n" + "Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n" + "Ball and Chain, and Dominion Rod."); + } + + config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n" + "tilt the Rollgoal table in Hena's Cabin."); + } + + if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) { + config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f"); + config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f"); + + if (getSettings().game.enableGyroRollgoal) { + config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Additional multiplier for scaling how strongly\n" + "the gyroscope affects the Rollgoal table."); + } + } + + config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n" + "reducing drift and jitter when the controller is still."); + } + + config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Low values track raw gyro input more closely,\n" + "while higher values smooth out input over time."); + } + + config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch); + config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw); + } + + ImGui::SeparatorText("Tools"); + + config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); + } + + ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawInterfaceMenu() { + if (ImGui::BeginMenu("Interface")) { + config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); + config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); +#if DUSK_ENABLE_SENTRY_NATIVE + config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); +#endif + if (!IsMobile) { + config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); + } + + ImGui::EndMenu(); + } + } + static void drawVirtualStick(const char* id, const ImVec2& stick) { float scale = ImGuiScale(); ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 45 * scale, ImGui::GetCursorPos().y + 10)); diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index 4d51cbc865..e21374c8f4 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -19,6 +19,13 @@ namespace dusk { static void ToggleFullscreen(); private: + void drawAudioMenu(); + void drawInputMenu(); + void drawGraphicsMenu(); + void drawGameplayMenu(); + void drawCheatsMenu(); + void drawInterfaceMenu(); + struct { int m_selectedPort = 0; bool m_isReading = false; diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 1f9d42fbc9..97f5b5a8d3 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -16,10 +16,58 @@ #include "dusk/main.h" #include "m_Do/m_Do_main.h" +#include +#include + +#if defined(__APPLE__) +#include +#endif + +#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__)) +#define DUSK_CAN_OPEN_DATA_FOLDER 1 + +namespace fs = std::filesystem; + +static void OpenDataFolder() { + const std::string path = fs::absolute(dusk::ConfigPath).generic_string(); +#if defined(_WIN32) + const std::string url = std::string("file:///") + path; +#else + const std::string url = std::string("file://") + path; +#endif + (void)SDL_OpenURL(url.c_str()); +} +#else +#define DUSK_CAN_OPEN_DATA_FOLDER 0 +#endif + namespace dusk { ImGuiMenuTools::ImGuiMenuTools() {} void ImGuiMenuTools::draw() { + if (ImGui::BeginMenu("Tools")) { + if (!dusk::IsGameLaunched) { + ImGui::BeginDisabled(); + } + + ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); + ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); + ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); + + if (!dusk::IsGameLaunched) { + ImGui::EndDisabled(); + } + +#if DUSK_CAN_OPEN_DATA_FOLDER + ImGui::Separator(); + if (ImGui::MenuItem("Open Data Folder")) { + OpenDataFolder(); + } +#endif + + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Debug")) { bool developmentMode = mDoMain::developmentMode == 1; if (ImGui::Checkbox("Development Mode", &developmentMode)) { @@ -59,9 +107,6 @@ namespace dusk { ImGui::MenuItem("Debug Overlay", hotkeys::SHOW_DEBUG_OVERLAY, &m_showDebugOverlay); ImGui::MenuItem("Heap Viewer", hotkeys::SHOW_HEAP_VIEWER, &m_showHeapOverlay); ImGui::MenuItem("Player Info", hotkeys::SHOW_PLAYER_INFO, &m_showPlayerInfo); - ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); - ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); - ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); ImGui::MenuItem("Debug Camera", hotkeys::SHOW_DEBUG_CAMERA, &m_showCameraOverlay); ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug); ImGui::MenuItem("Bloom", nullptr, &m_showBloomWindow); @@ -96,7 +141,9 @@ namespace dusk { ImGui::SetNextWindowBgAlpha(0.65f); if (ImGui::Begin("Debug Overlay", nullptr, windowFlags)) { ImGuiStringViewText(fmt::format(FMT_STRING("FPS: {:.2f}\n"), io.Framerate)); - ImGuiStringViewText(fmt::format(FMT_STRING("Frame usage: {:.1f}%\n"), frameUsagePct)); + if (frameUsagePct > 0.f) { + ImGuiStringViewText(fmt::format(FMT_STRING("Frame usage: {:.1f}%\n"), frameUsagePct)); + } ImGui::Separator(); diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 3fdd6cdf45..172059aea4 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -168,14 +168,14 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m aurora::Module DuskLog("dusk"); -void dusk::InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel) { +void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel) { std::lock_guard lock(g_logMutex); - if (g_logFile != nullptr || configDir == nullptr) { + if (g_logFile != nullptr || configDir.empty()) { return; } std::error_code ec; - const std::filesystem::path logsDir = std::filesystem::path(configDir) / "logs"; + const std::filesystem::path logsDir = configDir / "logs"; std::filesystem::create_directories(logsDir, ec); if (ec) { std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n", diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index e41bd1f24c..2164089e9f 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -98,6 +98,7 @@ bool dusk::IsRunning = true; bool dusk::IsShuttingDown = false; bool dusk::IsGameLaunched = false; bool dusk::IsFocusPaused = false; +std::filesystem::path dusk::ConfigPath; #endif s32 LOAD_COPYDATE(void*) { @@ -130,7 +131,6 @@ s32 LOAD_COPYDATE(void*) { AuroraInfo auroraInfo; AuroraStats dusk::lastFrameAuroraStats; float dusk::frameUsagePct = 0.0f; -const char* configPath; bool launchUILoop() { while (dusk::IsRunning && !dusk::IsGameLaunched) { @@ -379,7 +379,7 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) { } } -static const char* CalculateConfigPath() { +static std::filesystem::path CalculateConfigPath() { const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName); if (!result) { DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError()); @@ -388,13 +388,12 @@ static const char* CalculateConfigPath() { return result; } -static void EnsureInitialPipelineCache(const char* configDir) { - if (configDir == nullptr) { +static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) { + if (configDir.empty()) { return; } - const std::filesystem::path configPathFs(configDir); - const std::filesystem::path pipelineCachePath = configPathFs / "pipeline_cache.db"; + const std::filesystem::path pipelineCachePath = configDir / "pipeline_cache.db"; if (std::filesystem::exists(pipelineCachePath)) { return; } @@ -413,10 +412,10 @@ static void EnsureInitialPipelineCache(const char* configDir) { } std::error_code ec; - std::filesystem::create_directories(configPathFs, ec); + std::filesystem::create_directories(configDir, ec); if (ec) { DuskLog.warn("Failed to create config directory '{}' for pipeline cache: {}", - configPathFs.string(), ec.message()); + configDir.string(), ec.message()); return; } @@ -507,37 +506,38 @@ int game_main(int argc, char* argv[]) { exit(1); } - configPath = CalculateConfigPath(); + dusk::ConfigPath = CalculateConfigPath(); const auto startupLogLevel = static_cast(parsed_arg_options["log-level"].as()); - dusk::InitializeFileLogging(configPath, startupLogLevel); + dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel); dusk::config::LoadFromUserPreferences(); ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::InitializeCrashReporting(); - EnsureInitialPipelineCache(configPath); - - AuroraConfig config{}; - config.appName = dusk::AppName; - config.configPath = configPath; - config.vsync = dusk::getSettings().video.enableVsync; - config.startFullscreen = dusk::getSettings().video.enableFullscreen; - config.windowPosX = -1; - config.windowPosY = -1; - config.windowWidth = defaultWindowWidth * 2; - config.windowHeight = defaultWindowHeight * 2; - config.desiredBackend = ResolveDesiredBackend(parsed_arg_options); - config.logCallback = &aurora_log_callback; - config.logLevel = startupLogLevel; - config.mem1Size = 256 * 1024 * 1024; - config.mem2Size = 24 * 1024 * 1024; - config.allowJoystickBackgroundEvents = true; - config.imGuiInitCallback = &aurora_imgui_init_callback; - config.allowTextureReplacements = true; - config.allowTextureDumps = false; - + EnsureInitialPipelineCache(dusk::ConfigPath); PADSetDefaultMapping(&defaultPadMapping); - auroraInfo = aurora_initialize(argc, argv, &config); + { + const auto configPathString = dusk::ConfigPath.string(); + AuroraConfig config{}; + config.appName = dusk::AppName; + config.configPath = configPathString.c_str(); + config.vsync = dusk::getSettings().video.enableVsync; + config.startFullscreen = dusk::getSettings().video.enableFullscreen; + config.windowPosX = -1; + config.windowPosY = -1; + config.windowWidth = defaultWindowWidth * 2; + config.windowHeight = defaultWindowHeight * 2; + config.desiredBackend = ResolveDesiredBackend(parsed_arg_options); + config.logCallback = &aurora_log_callback; + config.logLevel = startupLogLevel; + config.mem1Size = 256 * 1024 * 1024; + config.mem2Size = 24 * 1024 * 1024; + config.allowJoystickBackgroundEvents = true; + config.imGuiInitCallback = &aurora_imgui_init_callback; + config.allowTextureReplacements = true; + config.allowTextureDumps = false; + auroraInfo = aurora_initialize(argc, argv, &config); + } #ifdef DUSK_DISCORD_RPC dusk::discord::Initialize(); From 4b3c559df30bb59a073d7bab7d9b522e14655dc1 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 20 Apr 2026 20:47:03 -0600 Subject: [PATCH 06/64] CMake: Don't build a game static lib --- CMakeLists.txt | 8 +------- files.cmake | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e8132a98fd..77cd0b9694 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -399,12 +399,6 @@ target_include_directories(game_base PRIVATE ${GAME_INCLUDE_DIRS}) target_link_libraries(game_debug PRIVATE ${GAME_LIBS}) target_link_libraries(game_base PRIVATE ${GAME_LIBS}) -# Combined game library -add_library(game STATIC - $ - $) -target_link_libraries(game PUBLIC ${GAME_LIBS}) - if(ANDROID) add_library(dusk SHARED src/dusk/main.cpp) set_target_properties(dusk PROPERTIES OUTPUT_NAME main) @@ -414,7 +408,7 @@ endif () target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0) target_include_directories(dusk PRIVATE include) -target_link_libraries(dusk PRIVATE game aurora::main) +target_link_libraries(dusk PRIVATE game_base game_debug aurora::main) if (TARGET crashpad_handler) add_dependencies(dusk crashpad_handler) endif () diff --git a/files.cmake b/files.cmake index c8ecb9be7a..c50cac2d8c 100644 --- a/files.cmake +++ b/files.cmake @@ -15,7 +15,6 @@ set(DOLZEL_FILES src/m_Do/m_Do_DVDError.cpp src/m_Do/m_Do_MemCard.cpp src/m_Do/m_Do_MemCardRWmng.cpp - src/m_Do/m_Do_machine_exception.cpp src/m_Do/m_Do_hostIO.cpp src/c/c_damagereaction.cpp src/c/c_dylink.cpp From 62f3d09076c117685b73c2f0bec18f6b7aa5b9ad Mon Sep 17 00:00:00 2001 From: madeline Date: Mon, 20 Apr 2026 20:22:06 -0700 Subject: [PATCH 07/64] fix incorrect eldin field name on map --- include/d/d_menu_fmap.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/d/d_menu_fmap.h b/include/d/d_menu_fmap.h index 026e98e056..48db37fef3 100644 --- a/include/d/d_menu_fmap.h +++ b/include/d/d_menu_fmap.h @@ -75,7 +75,9 @@ public: /* 0x8 */ BE(u16) mAreaName; /* 0xA */ u8 mCount; #ifdef _MSVC_LANG - u8* __get_mRoomNos() const { return (u8*)(this + 1); } + // Room numbers start at offset 0xB (right after mCount), NOT at sizeof(data)=12. + // (u8*)(this+1) would give offset 12 because MSVC sizeof=12; use &mCount+1 instead. + u8* __get_mRoomNos() const { return (u8*)&mCount + 1; } __declspec(property(get = __get_mRoomNos)) u8* mRoomNos; #else /* 0xB */ u8 mRoomNos[0]; From 46f6dc67c1a47f38731602cbef04e906e34c67b4 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Tue, 21 Apr 2026 00:46:22 -0700 Subject: [PATCH 08/64] make file select card wait times obey instantSaves setting --- src/d/d_file_select.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 23ceb96f00..17d5f2c4e4 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -70,11 +70,7 @@ dFs_HIO_c::dFs_HIO_c() { select_icon_appear_frames = 5; appear_display_wait_frames = 15; field_0x000d = 15; - #if TARGET_PC - card_wait_frames = 0; - #else card_wait_frames = 90; - #endif test_frame_counts[0] = 1.11f; test_frame_counts[1] = 1.11f; test_frame_counts[2] = 1.11f; @@ -2367,7 +2363,7 @@ void dFile_select_c::CommandExec() { break; } - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; } void dFile_select_c::DataEraseWait() { @@ -4759,7 +4755,7 @@ void dFile_select_c::MemCardFormatYesSel2Disp() { bool isErrorTxtChange = errorTxtChangeAnm(); bool isYnMenuMove = yesnoMenuMoveAnm(); if (isErrorTxtChange == true && isYnMenuMove == true) { - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; mDoMemCd_Format(); mCardCheckProc = MEMCARDCHECKPROC_FORMAT; } @@ -4830,7 +4826,7 @@ void dFile_select_c::MemCardMakeGameFileSelDisp() { if (isErrorTxtChange == true && isYnMenuMove == true && isKetteiTxtDisp == true) { if (field_0x0268 != 0) { - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; setInitSaveData(); dataSave(); mCardCheckProc = MEMCARDCHECKPROC_MAKE_GAMEFILE; From 18d70df188c451d0845514424a9aedafba66fed8 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Tue, 21 Apr 2026 12:17:56 -0400 Subject: [PATCH 09/64] Small Imgui changes for better visibility by end user (#473) Co-authored-by: MelonSpeedruns --- src/dusk/imgui/ImGuiMenuGame.cpp | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 691434427c..f41da74b7f 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -67,7 +67,7 @@ namespace dusk { ToggleFullscreen(); } - if (ImGui::MenuItem("Restore Default Window Size")) { + if (ImGui::Button("Restore Default Window Size")) { getSettings().video.enableFullscreen.setValue(false); VISetWindowFullscreen(false); VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); @@ -75,6 +75,8 @@ namespace dusk { } } + ImGui::Separator(); + bool vsync = getSettings().video.enableVsync; if (ImGui::Checkbox("Enable VSync", &vsync)) { getSettings().video.enableVsync.setValue(vsync); @@ -312,14 +314,14 @@ namespace dusk { void ImGuiMenuGame::drawAudioMenu() { if (ImGui::BeginMenu("Audio")) { + + ImGui::SeparatorText("Volume"); + ImGui::Text("Master Volume"); if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); } - if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { - dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); - } /* // TODO: Implement additional settings ImGui::Text("Main Music Volume"); @@ -339,6 +341,13 @@ namespace dusk { } */ + ImGui::SeparatorText("Effects"); + + if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { + dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); + } + + ImGui::SeparatorText("Tweaks"); config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); @@ -359,7 +368,11 @@ namespace dusk { if (ImGui::BeginMenu("Input")) { ImGui::SeparatorText("Controller"); - ImGui::MenuItem("Configure Controller", nullptr, &m_showControllerConfig); + if (ImGui::Button("Configure Controller")){ + m_showControllerConfig = !m_showControllerConfig; + } + + ImGui::SeparatorText("Camera"); config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); From 595a6f1c9efc6fd7a92a7c2da0dd7e1743a60d9a Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 21 Apr 2026 14:52:16 -0600 Subject: [PATCH 10/64] Enable DoF (+ setting) & fix texture cache leak --- extern/aurora | 2 +- include/dusk/settings.h | 1 + src/dusk/imgui/ImGuiMenuGame.cpp | 2 ++ src/dusk/settings.cpp | 2 ++ src/m_Do/m_Do_graphic.cpp | 3 +++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 5d420c9f73..ccb9dc1ad7 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 5d420c9f73c93ab9a5dcd052ac92d47362764e80 +Subproject commit ccb9dc1ad78f7e278af19d308a4f3e870ca04889 diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 03749967c9..2cec252006 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -77,6 +77,7 @@ struct UserSettings { ConfigVar enableFrameInterpolation; ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; + ConfigVar enableDepthOfField; // Audio ConfigVar noLowHpSound; diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index f41da74b7f..448bd531ff 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -165,6 +165,8 @@ namespace dusk { ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); + config::ImGuiCheckbox("Enable Depth of Field", getSettings().game.enableDepthOfField); + ImGui::EndMenu(); } } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 2bcdbe2185..aacca0dbc9 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -51,6 +51,7 @@ UserSettings g_userSettings = { .enableFrameInterpolation = {"game.enableFrameInterpolation", false}, .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, + .enableDepthOfField {"game.enableDepthOfField", true}, // Audio .noLowHpSound {"game.noLowHpSound", false}, @@ -143,6 +144,7 @@ void registerSettings() { Register(g_userSettings.game.disableWaterRefraction); Register(g_userSettings.game.internalResolutionScale); Register(g_userSettings.game.shadowResolutionMultiplier); + Register(g_userSettings.game.enableDepthOfField); Register(g_userSettings.game.enableFastIronBoots); Register(g_userSettings.game.canTransformAnywhere); Register(g_userSettings.game.freeMagicArmor); diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index d949d10ff0..4dc505ac03 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -1155,6 +1155,9 @@ static void drawDepth2(view_class* param_0, view_port_class* param_1, int param_ GXSetProjection(ortho, GX_ORTHOGRAPHIC); GXSetCurrentMtx(0); +#ifdef TARGET_PC + if (dusk::getSettings().game.enableDepthOfField) +#endif if (l_tevColor0.a > -255 && sp8 == 1) { GXBegin(GX_QUADS, GX_VTXFMT0, 4); GXPosition3s16(x_orig, y_orig_pos, -5); From dd3a61d84cb3e80c0424b21278299b87eefc4797 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Tue, 21 Apr 2026 15:13:55 -0700 Subject: [PATCH 11/64] fix dmap background --- src/d/d_menu_dmap.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index 66533dd12c..17d9193241 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -984,7 +984,36 @@ void dMenu_DmapBg_c::update() { JUT_ASSERT(2323, mpBackTexture != NULL); void* spec = mpArchive->getResource("spec/spec.dat"); + #if TARGET_PC + struct dmap_spec { + /* 0x00 */ BE(f32) field_0x0; + /* 0x04 */ BE(f32) field_0x4; + /* 0x08 */ BE(f32) field_0x8; + /* 0x0C */ u8 field_0xc; + /* 0x0D */ u8 field_0xd; + /* 0x0E */ u8 field_0xe; + /* 0x0F */ u8 field_0xf; + /* 0x10 */ u8 field_0x10; + /* 0x11 */ u8 field_0x11; + /* 0x12 */ u8 field_0x12; + /* 0x13 */ u8 field_0x13; + }; + dmap_spec* dspec = (dmap_spec*)spec; + + field_0xd80 = dspec->field_0x0; + field_0xd84 = dspec->field_0x4; + field_0xd88 = dspec->field_0x8; + field_0xd8c = dspec->field_0xc; + field_0xd8d = dspec->field_0xd; + field_0xd8e = dspec->field_0xe; + field_0xd8f = dspec->field_0xf; + field_0xd90 = dspec->field_0x10; + field_0xd91 = dspec->field_0x11; + field_0xd92 = dspec->field_0x12; + field_0xd93 = dspec->field_0x13; + #else memcpy(&field_0xd80, spec, 20); + #endif } } From d99205ecc616ff855f250b497eb42ce6e326b250 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 21 Apr 2026 17:12:21 -0600 Subject: [PATCH 12/64] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index ccb9dc1ad7..b524038d75 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit ccb9dc1ad78f7e278af19d308a4f3e870ca04889 +Subproject commit b524038d75444519f5ab685ef37da12300eab4ed From a15d0af139d7dd8ce5fdd110d28c2f4637245ae3 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 21 Apr 2026 17:39:53 -0600 Subject: [PATCH 13/64] Better fix for mirror crashes --- extern/aurora | 2 +- src/d/actor/d_a_mirror.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index b524038d75..26da4c6bb0 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit b524038d75444519f5ab685ef37da12300eab4ed +Subproject commit 26da4c6bb08937ab07b5bd8bbf779b143c672a88 diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index 4711dee681..fb4142c0bb 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -30,6 +30,10 @@ static char* l_arcName = "Mirror"; static char* l_arcName2 = "MR-Table"; dMirror_packet_c::dMirror_packet_c() { +#ifdef TARGET_PC + GXInitTexObj(&mTexObj, nullptr, 0, 0, static_cast(-1), GX_MAX_TEXWRAPMODE, + GX_MAX_TEXWRAPMODE, GX_FALSE); +#endif reset(); } From cf080523cb83c2987d2b238a369418f8b408495d Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 21 Apr 2026 18:59:11 -0700 Subject: [PATCH 14/64] dsp oscillator channels fixes #471 fixes #131 --- src/dusk/audio/DuskDsp.cpp | 143 ++++++++++++++++++++++++++++++++++--- src/dusk/audio/DuskDsp.hpp | 3 + 2 files changed, 136 insertions(+), 10 deletions(-) diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index bcad272a47..e074981844 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -5,14 +5,16 @@ #include #include +#include #include +#include #include #include "Adpcm.hpp" #include "freeverb/revmodel.hpp" -#include "JSystem/JAudio2/JASDriverIF.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/endian.h" +#include "dusk/logging.h" #include "global.h" #include "tracy/Tracy.hpp" @@ -95,6 +97,13 @@ static void RenderChannel( ChannelAuxData& channelAux, OutputSubframe& subframe); +static void RenderOutputChannel( + const JASDsp::TChannel& sourceChannel, + ChannelAuxData& aux, + OutputChannel outputChannel, + const std::span inputSamples, + OutputSubframe& fullOutputSubframe); + /** * Converts a pitch value on a DSP channel to a sample rate. */ @@ -117,6 +126,8 @@ static void ResetChannel(JASDsp::TChannel& channel, ChannelAuxData& aux) { aux.resamplePos = 0.0; aux.resamplePrev = 0; + aux.oscPhase = 0; + aux.prev_lp_out = 0.0f; aux.prev_lp_in = 0.0f; @@ -141,6 +152,119 @@ static void MixSubframe(DspSubframe& dst, const DspSubframe& src) { } } +enum class OscType : u16 { + SQUARE_WAVE_PW_50 = 0, + SAW_WAVE = 1, + SQUARE_WAVE_PW_25 = 3, + TRIANGLE_WAVE = 4, + // idk what 5 and 6 are + SINE_WAVE = 7, + // idk what 8 and 9 are + SINE_WAVE_VAR_STEP = 10, + EVOLVING_HARMONIC = 11, + EVOLVING_RAMP = 12, +}; + +static s16 gEvolvingHarmonic[64]; + +static void GenerateEvolvingHarmonic() { + static bool initialized = false; + if (!initialized) { + gEvolvingHarmonic[62] = 8191; + gEvolvingHarmonic[63] = 16383; + initialized = true; + } + + u32 prev2 = (u32)gEvolvingHarmonic[62]; + u32 prev1 = (u32)gEvolvingHarmonic[63]; + + for (int i = 0; i < 64; i += 2) { + u32 cur = (u32)gEvolvingHarmonic[i]; + gEvolvingHarmonic[i] = (s16)((s32)(prev2 * prev1 - (cur << 16)) >> 16); + prev2 = prev1; + prev1 = cur; + + cur = (u32)gEvolvingHarmonic[i + 1]; + gEvolvingHarmonic[i + 1] = (s16)((s32)(2u * (prev2 * prev1 + (cur << 16))) >> 16); + prev2 = prev1; + prev1 = cur; + } +} + + +static void RenderOscChannel( + JASDsp::TChannel& channel, + ChannelAuxData& channelAux, + OutputSubframe& subframe) { + if (channel.mResetFlag) + ResetChannel(channel, channelAux); + + const u32 pitch = channel.mPitch; + DspSubframe buf = {}; + const auto oscType = static_cast(channel.mBytesPerBlock); + + switch (oscType) { + case OscType::SQUARE_WAVE_PW_50: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = channelAux.oscPhase < 0x8000u ? 0.5f : -0.5f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::SQUARE_WAVE_PW_25: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = channelAux.oscPhase < 0x4000u ? 0.5f : -0.5f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::SAW_WAVE: + case OscType::EVOLVING_RAMP: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = (f32)(s16)channelAux.oscPhase / 32768.0f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::SINE_WAVE: + case OscType::SINE_WAVE_VAR_STEP: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = sinf((f32)channelAux.oscPhase * (2.0f * M_PI / 65536.0f)) * 0.5f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::TRIANGLE_WAVE: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = 0.5f - fabsf((f32)(s16)channelAux.oscPhase / 32768.0f); + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::EVOLVING_HARMONIC: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = gEvolvingHarmonic[channelAux.oscPhase >> 10] / 32768.0f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + default: + DuskLog.error("RenderOscChannel: unimplemented oscillator type {}", channel.mBytesPerBlock); + break; + } + + auto samples = std::span(buf).subspan(0, DSP_SUBFRAME_SIZE); + RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, samples, subframe); + RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, samples, subframe); +} + + void dusk::audio::DspRender(OutputSubframe& subframe) { ZoneScoped; if (DumpAudio != sDumpWasActive) { @@ -152,6 +276,8 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { } } + GenerateEvolvingHarmonic(); + std::span channels(JASDsp::CH_BUF, DSP_CHANNELS); DspSubframe reverbInputL = {}; @@ -174,17 +300,14 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { channel.mIsFinished = true; continue; } - else if (channel.mWaveAramAddress == 0) { - // I think these are oscillator channels? Not backed by audio. - // No idea how to implement these yet, so skip them. - channel.mIsFinished = true; - continue; - } - - ValidateChannel(channel); OutputSubframe channelSubframe = {}; - RenderChannel(channel, channelAux, channelSubframe); + if (channel.mWaveAramAddress == 0) { + RenderOscChannel(channel, channelAux, channelSubframe); + } else { + ValidateChannel(channel); + RenderChannel(channel, channelAux, channelSubframe); + } if (EnableReverb) { // scale the input to the reverb rather than using wet/dry on the output. diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index 3ca90d6311..8000e627a1 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -53,6 +53,9 @@ namespace dusk::audio { // last consumed sample from decodeBuf s16 resamplePrev; + // phase of oscillator channels + u16 oscPhase; + // low pass previous state f32 prev_lp_out; // out[n-1] f32 prev_lp_in; // in[n-1] From 366e47245eb08bea71d208d8a3b553219e43217b Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 21 Apr 2026 19:23:53 -0700 Subject: [PATCH 15/64] make sun song play the correct notes --- src/Z2AudioLib/Z2WolfHowlMgr.cpp | 10 +++++----- src/dusk/audio/DuskDsp.cpp | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Z2AudioLib/Z2WolfHowlMgr.cpp b/src/Z2AudioLib/Z2WolfHowlMgr.cpp index cb4f387a21..4866af7a5d 100644 --- a/src/Z2AudioLib/Z2WolfHowlMgr.cpp +++ b/src/Z2AudioLib/Z2WolfHowlMgr.cpp @@ -117,8 +117,8 @@ static Z2WolfHowlLine sNewSong3[9] = { #if TARGET_PC static Z2WolfHowlLine sHowlTimeSong[6] = { - {HOWL_LINE_MID, 20}, {HOWL_LINE_LOW, 20}, {HOWL_LINE_HIGH, 40}, - {HOWL_LINE_MID, 20}, {HOWL_LINE_LOW, 20}, {HOWL_LINE_HIGH, 40}, + {HOWL_LINE_MID, 15}, {HOWL_LINE_LOW, 15}, {HOWL_LINE_HIGH, 30}, + {HOWL_LINE_MID, 15}, {HOWL_LINE_LOW, 15}, {HOWL_LINE_HIGH, 30}, }; #endif @@ -368,9 +368,9 @@ void Z2WolfHowlMgr::setCorrectData(s8 curveID, Z2WolfHowlData* data) { break; #if TARGET_PC case Z2WOLFHOWL_TIMESONG: - cPitchUp = 1.259906f; - cPitchCenter = 0.94387f; - cPitchDown = 0.840885f; + cPitchUp = 1.3348f; + cPitchCenter = 1.0f; + cPitchDown = 0.7937f; break; #endif default: diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index e074981844..f576a5d0f1 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include "Adpcm.hpp" From 1e93657ab591b7295e71f95eb25ad915d71429c7 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Tue, 21 Apr 2026 19:25:28 -0700 Subject: [PATCH 16/64] Update aurora for fix --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 26da4c6bb0..6d69b7822e 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 26da4c6bb08937ab07b5bd8bbf779b143c672a88 +Subproject commit 6d69b7822e95ad9e82537848c968058be4fbbca5 From d78c46a628980ac8729711eaa444ddd5a7d20b6b Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 21 Apr 2026 22:45:18 -0600 Subject: [PATCH 17/64] Rework interpolation pacing; interpolate every frame --- include/dusk/frame_interpolation.h | 1 + include/dusk/game_clock.h | 19 ++++------ src/dusk/frame_interpolation.cpp | 14 +++++--- src/dusk/game_clock.cpp | 57 ++++++++++++++++++------------ src/m_Do/m_Do_main.cpp | 33 +++++++++-------- 5 files changed, 70 insertions(+), 54 deletions(-) diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index bec9b600cf..8c19e7e59b 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -16,6 +16,7 @@ void ensure_initialized(); void begin_record(); void end_record(); +void begin_sim_tick(); void begin_frame(bool enabled, bool is_sim_frame, float step); void interpolate(); float get_interpolation_step(); diff --git a/include/dusk/game_clock.h b/include/dusk/game_clock.h index 4a394b5c2e..8bb277e070 100644 --- a/include/dusk/game_clock.h +++ b/include/dusk/game_clock.h @@ -1,13 +1,8 @@ -#ifndef DUSK_GAME_CLOCK_H -#define DUSK_GAME_CLOCK_H +#pragma once -#include - -namespace dusk { -namespace game_clock { +namespace dusk::game_clock { void ensure_initialized(); -void reset_accumulator(); void reset_frame_timer(); constexpr float sim_pace() { return 1.0f / 30.0f; } @@ -18,16 +13,14 @@ constexpr float ui_initial_dt() { return 1.0f / 60.0f; } struct MainLoopPacer { float presentation_dt_seconds; bool is_interpolating; - bool do_sim_tick; - float interpolation_step; + int sim_ticks_to_run; float sim_pace; }; MainLoopPacer advance_main_loop(); +void commit_sim_tick(); +float sample_interpolation_step(); float consume_interval(const void* consumer); -} // namespace game_clock -} // namespace dusk - -#endif // DUSK_GAME_CLOCK_H +} // namespace dusk::game_clock diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index e0057790df..f3f8a842e1 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -127,14 +127,20 @@ void ensure_initialized() { s_initialized = true; } +void begin_sim_tick() { + ensure_initialized(); + if (!g_enabled) { + return; + } + + s_interpolationCallBackWork.clear(); + s_cam_prev = std::move(s_cam_curr); +} + void begin_frame(bool enabled, bool is_sim_frame, float step) { g_enabled = enabled; g_is_sim_frame = is_sim_frame; g_step = std::clamp(step, 0.0f, 1.0f); - if (is_sim_frame) { - s_interpolationCallBackWork.clear(); - s_cam_prev = std::move(s_cam_curr); - } } bool is_enabled() { diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index a262d0283c..29bd699c7d 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -5,34 +5,32 @@ #include #include -namespace dusk { -namespace game_clock { +namespace dusk::game_clock { using clock = std::chrono::steady_clock; bool s_initialized = false; clock::time_point s_previous_sample{}; -float s_sim_accumulator = 0.0f; +clock::time_point s_current_snapshot_time{}; std::unordered_map s_interval_last_sample; +constexpr clock::duration kSimPeriodDuration = + std::chrono::duration_cast(std::chrono::duration(sim_pace())); +constexpr int kMaxSimTicksPerFrame = 2; + void ensure_initialized() { if (s_initialized) { return; } s_previous_sample = clock::now(); - s_sim_accumulator = sim_pace(); + s_current_snapshot_time = s_previous_sample; s_initialized = true; } -void reset_accumulator() { - ensure_initialized(); - s_sim_accumulator = fmodf(s_sim_accumulator, sim_pace()); -} - void reset_frame_timer() { s_previous_sample = clock::now(); - s_sim_accumulator = 0.0f; + s_current_snapshot_time = s_previous_sample; } MainLoopPacer advance_main_loop() { @@ -42,25 +40,41 @@ MainLoopPacer advance_main_loop() { const float presentation_dt = std::chrono::duration(now - s_previous_sample).count(); s_previous_sample = now; - s_sim_accumulator += presentation_dt; - MainLoopPacer out{}; out.presentation_dt_seconds = presentation_dt; - const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit; + const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation && + !dusk::getTransientSettings().skipFrameRateLimit; out.is_interpolating = should_interpolate; out.sim_pace = sim_pace(); if (!should_interpolate) { - s_sim_accumulator = 0.0f; - out.do_sim_tick = true; - out.interpolation_step = 0.0f; - return out; - } else { - out.do_sim_tick = s_sim_accumulator >= sim_pace(); - out.interpolation_step = out.do_sim_tick ? 0.0f : s_sim_accumulator / sim_pace(); + s_current_snapshot_time = now; + out.sim_ticks_to_run = 1; return out; } + + int sim_ticks_to_run = 0; + clock::time_point projected_snapshot_time = s_current_snapshot_time; + const clock::time_point render_time = now - kSimPeriodDuration; + while (sim_ticks_to_run < kMaxSimTicksPerFrame && projected_snapshot_time < render_time) { + projected_snapshot_time += kSimPeriodDuration; + sim_ticks_to_run++; + } + out.sim_ticks_to_run = sim_ticks_to_run; + return out; +} + +void commit_sim_tick() { + ensure_initialized(); + s_current_snapshot_time += kSimPeriodDuration; +} + +float sample_interpolation_step() { + ensure_initialized(); + const float step = + std::chrono::duration(clock::now() - s_current_snapshot_time).count() / sim_pace(); + return std::clamp(step, 0.0f, 1.0f); } float consume_interval(const void* consumer) { @@ -78,5 +92,4 @@ float consume_interval(const void* consumer) { return dt; } -} // namespace game_clock -} // namespace dusk +} // namespace dusk::game_clock diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 2164089e9f..579cbd8c8b 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -242,8 +242,6 @@ void main01(void) { continue; } - const dusk::game_clock::MainLoopPacer pacing = dusk::game_clock::advance_main_loop(); - VIWaitForRetrace(); dusk::lastFrameAuroraStats = *aurora_get_stats(); @@ -254,28 +252,33 @@ void main01(void) { mDoGph_gInf_c::updateRenderSize(); - dusk::frame_interp::begin_frame(pacing.is_interpolating, pacing.do_sim_tick, pacing.interpolation_step); + const auto pacing = dusk::game_clock::advance_main_loop(); if (pacing.is_interpolating) { - if (pacing.do_sim_tick) { + if (pacing.sim_ticks_to_run > 0) { + dusk::frame_interp::begin_frame(true, true, 0.0f); dusk::frame_interp::set_ui_tick_pending(true); - mDoCPd_c::read(); - DuskDebugPad(); - dusk::gyro::read(pacing.sim_pace); - fapGm_Execute(); - mDoAud_Execute(); - dusk::game_clock::reset_accumulator(); + for (int sim_tick = 0; sim_tick < pacing.sim_ticks_to_run; ++sim_tick) { + dusk::frame_interp::begin_sim_tick(); + mDoCPd_c::read(); + DuskDebugPad(); + dusk::gyro::read(pacing.sim_pace); + fapGm_Execute(); + mDoAud_Execute(); + dusk::game_clock::commit_sim_tick(); + } } + + dusk::frame_interp::begin_frame(true, false, + dusk::game_clock::sample_interpolation_step()); dusk::frame_interp::interpolate(); dusk::frame_interp::begin_presentation_camera(); - if (!pacing.do_sim_tick) { - // run draw functions for anything specially marked to handle interp on non-sim - // ticks - fpcM_DrawIterater((fpcM_DrawIteraterFunc)fpcM_Draw); - } + // run draw functions for anything specially marked to handle interp + fpcM_DrawIterater((fpcM_DrawIteraterFunc)fpcM_Draw); cAPIGph_Painter(); dusk::frame_interp::end_presentation_camera(); dusk::frame_interp::set_ui_tick_pending(false); } else { + dusk::frame_interp::begin_frame(false, true, 0.0f); dusk::frame_interp::set_ui_tick_pending(true); // Game Inputs From 396ea02fe5440d182c5424025c251870d355c501 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 21 Apr 2026 23:33:03 -0600 Subject: [PATCH 18/64] Fix flowers with interpolation on (#486) --- src/d/actor/d_flower.inc | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 702337d9e5..58daa354a1 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -699,8 +699,8 @@ void dFlower_packet_c::draw() { if (!cLib_checkBit(sp44->m_state, 4) && !cLib_checkBit(sp44->m_state, 0x40)) { #ifdef TARGET_PC Mtx flower_mtx; - if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&sp44->m_modelMtx), flower_mtx)) { - + if (dusk::frame_interp::lookup_replacement(&sp44->m_modelMtx, flower_mtx)) { + cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); GXLoadPosMtxImm(flower_mtx, 0); } else #endif @@ -854,21 +854,18 @@ void dFlower_packet_c::draw() { if (!cLib_checkBit(sp34->m_state, 4) && cLib_checkBit(sp34->m_state, 0x40)) { #ifdef TARGET_PC Mtx flower_mtx; - if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&sp34->m_modelMtx), flower_mtx)) { + if (dusk::frame_interp::lookup_replacement(&sp34->m_modelMtx, flower_mtx)) { cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); GXLoadPosMtxImm(flower_mtx, 0); - } else { + } else #endif + { GXLoadPosMtxImm(sp34->m_modelMtx, 0); -#ifdef TARGET_PC } -#endif GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); - #if TARGET_PC GXLoadTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, GX_TEXMAP0); #endif - if (!cLib_checkBit(sp34->m_state, 8)) { if (!cLib_checkBit(sp34->m_state, 0x10)) { GXCallDisplayList(mp_Jhana01DL, m_Jhana01DL_size); @@ -995,7 +992,7 @@ void dFlower_packet_c::update() { mDoMtx_stack_c::scaleM(temp_f31, temp_f31, temp_f31); cMtx_concat(j3dSys.getViewMtx(), temp_r28, data_p->m_modelMtx); #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), data_p->m_modelMtx); + dusk::frame_interp::record_final_mtx(temp_r28, data_p->m_modelMtx); #endif } } From 58f2679deffb2cf001fcb88f030e004092305f2a Mon Sep 17 00:00:00 2001 From: Irastris Date: Wed, 22 Apr 2026 01:41:12 -0400 Subject: [PATCH 19/64] Rollgoal: Gyro & Mirror Mode Fixes --- src/d/actor/d_a_mg_fshop.cpp | 20 ++++++++++---------- src/dusk/gyro.cpp | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/d/actor/d_a_mg_fshop.cpp b/src/d/actor/d_a_mg_fshop.cpp index 1c93648eac..99f39e410e 100644 --- a/src/d/actor/d_a_mg_fshop.cpp +++ b/src/d/actor/d_a_mg_fshop.cpp @@ -761,6 +761,11 @@ static void koro2_game(fshop_class* i_this) { sp5C.x = mDoCPd_c::getStickX3D(PAD_1); sp5C.y = 0.0f; sp5C.z = mDoCPd_c::getStickY(PAD_1); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + sp5C.x = -sp5C.x; + } +#endif MtxPosition(&sp5C, &sp68); f32 reg_f31 = sp68.x; @@ -782,20 +787,15 @@ static void koro2_game(fshop_class* i_this) { reg_f30 = 0.0f; } + s16 gyro_ax = 0; + s16 gyro_az = 0; #if TARGET_PC if (dusk::getSettings().game.enableGyroRollgoal) { - s16 rg_add_x; - s16 rg_add_z; - dusk::gyro::rollgoalTableOffset(rg_add_x, rg_add_z); - s16 tgt_x = static_cast(reg_f30 * (-6000.0f + JREG_F(7))) + rg_add_x; - s16 tgt_z = static_cast(reg_f31 * (-6000.0f + JREG_F(8))) + rg_add_z; - cLib_addCalcAngleS2(&i_this->field_0x4020.x, tgt_x, 4, 0x200); - cLib_addCalcAngleS2(&i_this->field_0x4020.z, tgt_z, 4, 0x200); + dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az); } -#else - cLib_addCalcAngleS2(&i_this->field_0x4020.x, reg_f30 * (-6000.0f + JREG_F(7)), 4, 0x200); - cLib_addCalcAngleS2(&i_this->field_0x4020.z, reg_f31 * (-6000.0f + JREG_F(8)), 4, 0x200); #endif + cLib_addCalcAngleS2(&i_this->field_0x4020.x, reg_f30 * (-6000.0f + JREG_F(7)) + gyro_ax, 4, 0x200); + cLib_addCalcAngleS2(&i_this->field_0x4020.z, reg_f31 * (-6000.0f + JREG_F(8)) + gyro_az, 4, 0x200); } #if TARGET_PC if (i_this->field_0x4010 != 2) { diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index d390c9c8b4..5b6e880a1f 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -3,7 +3,7 @@ namespace dusk::gyro { namespace { -constexpr s32 kRollgoalTableMaxOffset = 12000; +constexpr s32 kRollgoalTableMaxOffset = 6500; constexpr float kGyroEmaAlphaMin = 0.05f; constexpr float kGyroEmaAlphaMax = 1.0f; From a2a56122e27062cd0c37f17b8ad20be44f33f8e2 Mon Sep 17 00:00:00 2001 From: Irastris Date: Wed, 22 Apr 2026 01:46:10 -0400 Subject: [PATCH 20/64] Gyro: Hawk Aiming --- src/d/actor/d_a_alink_dusk.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index 045a9655bd..0f442aff94 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -154,6 +154,7 @@ bool daAlink_c::checkGyroAimContext() { case PROC_BOW_SUBJECT: case PROC_BOOMERANG_SUBJECT: case PROC_COPY_ROD_SUBJECT: + case PROC_HAWK_SUBJECT: case PROC_HOOKSHOT_SUBJECT: case PROC_SWIM_HOOKSHOT_SUBJECT: case PROC_HORSE_BOW_SUBJECT: From 6f34bb050ab1d636177ef08df487dcd5fcfc5a17 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 22 Apr 2026 00:14:41 -0600 Subject: [PATCH 21/64] Call J3DModel::diff in mDoExt_modelEntryDL when interpolating Fixes #355 --- src/m_Do/m_Do_ext.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/m_Do/m_Do_ext.cpp b/src/m_Do/m_Do_ext.cpp index 84bf51ecf5..fc8952e91d 100644 --- a/src/m_Do/m_Do_ext.cpp +++ b/src/m_Do/m_Do_ext.cpp @@ -351,8 +351,13 @@ void mDoExt_modelUpdateDL(J3DModel* i_model) { void mDoExt_modelEntryDL(J3DModel* i_model) { #if TARGET_PC - if (!dusk::frame_interp::is_sim_frame()) + if (!dusk::frame_interp::is_sim_frame()) { + // FRAME INTERP NOTE: This fixes issue #355 where some lights would flicker. + // This is likely better solved by updating J3DMaterial::needsInterpCallBack, + // but it's unclear what exactly needs to be added. + i_model->diff(); return; + } #endif modelMtxErrorCheck(i_model); From 319efbe66296a697cb2ef9959a708f5a64af8d39 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 22 Apr 2026 00:30:11 -0600 Subject: [PATCH 22/64] Reset game clock with over 250ms frame gap --- src/dusk/game_clock.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index 29bd699c7d..8b887f610c 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -17,6 +17,7 @@ std::unordered_map s_interval_last_sample; constexpr clock::duration kSimPeriodDuration = std::chrono::duration_cast(std::chrono::duration(sim_pace())); +constexpr clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250); constexpr int kMaxSimTicksPerFrame = 2; void ensure_initialized() { @@ -30,14 +31,15 @@ void ensure_initialized() { void reset_frame_timer() { s_previous_sample = clock::now(); - s_current_snapshot_time = s_previous_sample; + s_current_snapshot_time = s_previous_sample - kSimPeriodDuration; } MainLoopPacer advance_main_loop() { ensure_initialized(); const clock::time_point now = clock::now(); - const float presentation_dt = std::chrono::duration(now - s_previous_sample).count(); + const clock::duration frame_gap = now - s_previous_sample; + const float presentation_dt = std::chrono::duration(frame_gap).count(); s_previous_sample = now; MainLoopPacer out{}; @@ -54,6 +56,12 @@ MainLoopPacer advance_main_loop() { return out; } + if (frame_gap > kAbnormalGapResetThreshold) { + s_current_snapshot_time = now - kSimPeriodDuration; + out.sim_ticks_to_run = 0; + return out; + } + int sim_ticks_to_run = 0; clock::time_point projected_snapshot_time = s_current_snapshot_time; const clock::time_point render_time = now - kSimPeriodDuration; From 832e567620aa6a2ab7cb5a181b9347b5ffbe1fc7 Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 22 Apr 2026 01:50:17 -0700 Subject: [PATCH 23/64] better share states --- src/dusk/imgui/ImGuiStateShare.cpp | 224 ++++++++++++++++++++++++++--- src/dusk/imgui/ImGuiStateShare.hpp | 35 +++-- 2 files changed, 226 insertions(+), 33 deletions(-) diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index 172aad17a5..8d3af0bedb 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -5,14 +5,18 @@ #include "imgui.h" #include "fmt/format.h" #include "absl/strings/escaping.h" +#include "nlohmann/json.hpp" #include "d/d_com_inf_game.h" #include "dusk/main.h" +#include "dusk/io.hpp" #include namespace dusk { +using json = nlohmann::json; + #pragma pack(push, 1) struct StateSharePacket { char stageName[8]; @@ -24,8 +28,52 @@ struct StateSharePacket { #pragma pack(pop) static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c); +static constexpr auto STATES_FILENAME = "states.json"; -void ImGuiStateShare::copyState() { +static std::string GetStatesFilePath() { + return (dusk::ConfigPath / STATES_FILENAME).string(); +} + +void ImGuiStateShare::loadStatesFile() { + m_loaded = true; + const std::filesystem::path filePath = dusk::ConfigPath / STATES_FILENAME; + if (!std::filesystem::exists(filePath)) { + return; + } + try { + const std::string pathStr = filePath.string(); + auto data = io::FileStream::ReadAllBytes(pathStr.c_str()); + auto j = json::parse(data); + if (!j.is_array()) { + return; + } + for (const auto& entry : j) { + if (!entry.contains("name") || !entry.contains("data")) { + continue; + } + SavedStateEntry s; + s.name = entry["name"].get(); + s.encoded = entry["data"].get(); + m_states.push_back(std::move(s)); + } + } catch (const std::exception& e) { + m_statusMsg = fmt::format("Failed to load states: {}", e.what()); + } +} + +void ImGuiStateShare::saveStatesFile() { + json j = json::array(); + for (const auto& s : m_states) { + j.push_back({{"name", s.name}, {"data", s.encoded}}); + } + try { + io::FileStream::WriteAllText(GetStatesFilePath().c_str(), j.dump(2)); + } catch (const std::exception& e) { + m_statusMsg = fmt::format("Failed to save states: {}", e.what()); + } +} + +std::string ImGuiStateShare::encodeCurrentState() { StateSharePacket pkt = {}; strncpy(pkt.stageName, dComIfGp_getStartStageName(), 7); pkt.roomNo = dComIfGp_getStartStageRoomNo(); @@ -40,20 +88,12 @@ void ImGuiStateShare::copyState() { std::string compressed(bound, '\0'); compressed.resize(ZSTD_compress(compressed.data(), bound, raw.data(), raw.size(), 1)); - std::string encoded = absl::Base64Escape(compressed); - ImGui::SetClipboardText(encoded.c_str()); - m_statusMsg = "Copied to clipboard."; + return absl::Base64Escape(compressed); } -bool ImGuiStateShare::pasteState() { - const char* clip = ImGui::GetClipboardText(); - if (!clip || clip[0] == '\0') { - m_statusMsg = "Clipboard is empty."; - return false; - } - +bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::string& name) { std::string decoded; - if (!absl::Base64Unescape(clip, &decoded)) { + if (!absl::Base64Unescape(encoded, &decoded)) { m_statusMsg = "Invalid base64."; return false; } @@ -78,7 +118,6 @@ bool ImGuiStateShare::pasteState() { memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c)); s16 spawnPoint = pkt.startPoint == -4 ? -1 : pkt.startPoint; - if (spawnPoint == -1) { dComIfGs_setRestartRoomParam(pkt.roomNo & 0x3F); } @@ -86,34 +125,174 @@ bool ImGuiStateShare::pasteState() { dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer); m_pendingInfo = g_dComIfG_gameInfo.info; - m_statusMsg = fmt::format("Warping to {} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); + if (name.empty()) { + m_statusMsg = fmt::format("{} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); + } else { + m_statusMsg = fmt::format("{}: {} room {} layer {}.", name, pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); + } return true; } void ImGuiStateShare::tickPendingApply() { - if (!m_pendingInfo.has_value() || dComIfGp_isEnableNextStage()) + if (!m_pendingInfo.has_value() || dComIfGp_isEnableNextStage()) { return; + } g_dComIfG_gameInfo.info = *m_pendingInfo; m_pendingInfo.reset(); } +static bool ValidateEncodedState(const std::string& encoded) { + std::string decoded; + if (!absl::Base64Unescape(encoded, &decoded)) { + return false; + } + unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size()); + return dSize != ZSTD_CONTENTSIZE_ERROR && dSize != ZSTD_CONTENTSIZE_UNKNOWN && dSize >= PACKET_TOTAL; +} + void ImGuiStateShare::draw(bool& open) { - if (dusk::IsGameLaunched) + if (dusk::IsGameLaunched) { tickPendingApply(); + } - if (!open) + if (!m_loaded) { + loadStatesFile(); + } + + if (!open) { return; + } + ImGui::SetNextWindowSizeConstraints(ImVec2(400, 0), ImVec2(FLT_MAX, FLT_MAX)); if (!ImGui::Begin("State Share", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { ImGui::End(); return; } - if (!dusk::IsGameLaunched) ImGui::BeginDisabled(); - if (ImGui::Button("Copy State")) copyState(); + const bool gameRunning = dusk::IsGameLaunched; + + const float rowH = ImGui::GetTextLineHeightWithSpacing(); + const float listH = rowH * 8 + ImGui::GetStyle().FramePadding.y * 2; + ImGui::BeginChild("##states", ImVec2(0, listH), true); + + if (m_states.empty()) { + ImGui::TextDisabled("No saved states. Save or import one below."); + } + + int toDelete = -1; + for (int i = 0; i < (int)m_states.size(); ++i) { + ImGui::PushID(i); + + if (m_renamingIndex == i) { + ImGui::SetNextItemWidth(150); + bool done = ImGui::InputText("##rename", m_renameBuffer, sizeof(m_renameBuffer), + ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll); + if (done) { + if (m_renameBuffer[0] != '\0') { + m_states[i].name = m_renameBuffer; + } + m_renamingIndex = -1; + saveStatesFile(); + } else if (ImGui::IsItemDeactivated()) { + m_renamingIndex = -1; + } + } else { + ImGui::Selectable(m_states[i].name.c_str(), false, ImGuiSelectableFlags_None, ImVec2(150, 0)); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Double-click to rename"); + if (ImGui::IsMouseDoubleClicked(0)) { + m_renamingIndex = i; + strncpy(m_renameBuffer, m_states[i].name.c_str(), sizeof(m_renameBuffer) - 1); + m_renameBuffer[sizeof(m_renameBuffer) - 1] = '\0'; + ImGui::SetKeyboardFocusHere(-1); + } + } + } + + ImGui::SameLine(); + if (!gameRunning) { ImGui::BeginDisabled(); } + if (ImGui::Button("Load")) { + applyEncodedState(m_states[i].encoded, m_states[i].name); + } + if (!gameRunning) { ImGui::EndDisabled(); } + + ImGui::SameLine(); + if (ImGui::Button("Copy")) { + ImGui::SetClipboardText(m_states[i].encoded.c_str()); + m_statusMsg = fmt::format("'{}' copied to clipboard.", m_states[i].name); + } + + ImGui::SameLine(); + if (ImGui::Button("Del")) { + toDelete = i; + } + + ImGui::PopID(); + } + + if (toDelete >= 0) { + if (m_renamingIndex == toDelete) { m_renamingIndex = -1; } + m_states.erase(m_states.begin() + toDelete); + saveStatesFile(); + } + + ImGui::EndChild(); + + // Toolbar + if (!gameRunning) { ImGui::BeginDisabled(); } + if (ImGui::Button("Save Current")) { + SavedStateEntry entry; + entry.name = fmt::format("State {}", m_states.size() + 1); + entry.encoded = encodeCurrentState(); + m_states.push_back(std::move(entry)); + saveStatesFile(); + m_statusMsg = fmt::format("Saved as '{}'.", m_states.back().name); + } + if (!gameRunning) { ImGui::EndDisabled(); } + ImGui::SameLine(); - if (ImGui::Button("Import State")) pasteState(); - if (!dusk::IsGameLaunched) ImGui::EndDisabled(); + if (ImGui::Button("Import Clipboard")) { + const char* clip = ImGui::GetClipboardText(); + if (!clip || clip[0] == '\0') { + m_statusMsg = "Clipboard is empty."; + } else { + std::string clipStr = clip; + if (!ValidateEncodedState(clipStr)) { + m_statusMsg = "Clipboard does not contain a valid state."; + } else { + SavedStateEntry entry; + entry.name = fmt::format("Imported {}", m_states.size() + 1); + entry.encoded = std::move(clipStr); + m_states.push_back(std::move(entry)); + saveStatesFile(); + m_statusMsg = fmt::format("Imported as '{}'.", m_states.back().name); + } + } + } + + if (!m_states.empty()) { + ImGui::SameLine(); + if (ImGui::Button("Clear All")) { + ImGui::OpenPopup("##clearall"); + } + + if (ImGui::BeginPopup("##clearall")) { + ImGui::Text("Delete all saved states?"); + ImGui::Spacing(); + if (ImGui::Button("Yes, clear all")) { + m_states.clear(); + m_renamingIndex = -1; + saveStatesFile(); + m_statusMsg = "All states cleared."; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } if (!m_statusMsg.empty()) { ImGui::Spacing(); @@ -125,8 +304,9 @@ void ImGuiStateShare::draw(bool& open) { } void ImGuiMenuTools::ShowStateShare() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) + if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) { return; + } m_stateShare.draw(m_showStateShare); } diff --git a/src/dusk/imgui/ImGuiStateShare.hpp b/src/dusk/imgui/ImGuiStateShare.hpp index a09cfd5963..6d319889b8 100644 --- a/src/dusk/imgui/ImGuiStateShare.hpp +++ b/src/dusk/imgui/ImGuiStateShare.hpp @@ -4,21 +4,34 @@ #include "d/d_save.h" #include #include +#include namespace dusk { - class ImGuiStateShare { - public: - void draw(bool& open); - private: - void copyState(); - bool pasteState(); - void tickPendingApply(); +struct SavedStateEntry { + std::string name; + std::string encoded; +}; + +class ImGuiStateShare { +public: + void draw(bool& open); + +private: + std::string encodeCurrentState(); + bool applyEncodedState(const std::string& encoded, const std::string& name = {}); + void tickPendingApply(); + void loadStatesFile(); + void saveStatesFile(); + + std::vector m_states; + std::string m_statusMsg; + std::optional m_pendingInfo; + int m_renamingIndex = -1; + char m_renameBuffer[128] = {}; + bool m_loaded = false; +}; - std::string m_statusMsg; - std::optional m_pendingInfo; - }; } #endif - \ No newline at end of file From 1787de517c8397c2c14312f5cd98c3aedb78cef9 Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 22 Apr 2026 01:55:23 -0700 Subject: [PATCH 24/64] properly set oxygen in share states --- src/dusk/imgui/ImGuiStateShare.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index 8d3af0bedb..f24b209b1d 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -139,6 +139,9 @@ void ImGuiStateShare::tickPendingApply() { } g_dComIfG_gameInfo.info = *m_pendingInfo; m_pendingInfo.reset(); + dComIfGp_offOxygenShowFlag(); + dComIfGp_setMaxOxygen(600); + dComIfGp_setOxygen(600); } static bool ValidateEncodedState(const std::string& encoded) { From 9c562ff740cd33fb246228caa27e820313cf124d Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 22 Apr 2026 02:44:58 -0700 Subject: [PATCH 25/64] state packs and partial states --- src/dusk/imgui/ImGuiStateShare.cpp | 124 ++++++++++++++++++++++++++--- src/dusk/imgui/ImGuiStateShare.hpp | 6 +- tools/saves_to_states_json.py | 58 ++++++++++++++ 3 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 tools/saves_to_states_json.py diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index f24b209b1d..cb3d3a561c 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -10,7 +10,11 @@ #include "d/d_com_inf_game.h" #include "dusk/main.h" #include "dusk/io.hpp" +#include "dusk/logging.h" +#include "../file_select.hpp" +#include "aurora/lib/window.hpp" +#include #include namespace dusk { @@ -27,9 +31,21 @@ struct StateSharePacket { }; #pragma pack(pop) -static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c); +static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c); +static constexpr size_t PACKET_SAVE_ONLY = sizeof(StateSharePacket) + sizeof(dSv_save_c); static constexpr auto STATES_FILENAME = "states.json"; +static bool ValidateEncodedState(const std::string&); + +void ImGuiStateShare::onMergeFileSelected(void* userdata, const char* path, const char* /*error*/) { + auto* self = static_cast(userdata); + if (path != nullptr) { + self->m_pendingMergePath = path; + } +} + + + static std::string GetStatesFilePath() { return (dusk::ConfigPath / STATES_FILENAME).string(); } @@ -64,7 +80,7 @@ void ImGuiStateShare::loadStatesFile() { void ImGuiStateShare::saveStatesFile() { json j = json::array(); for (const auto& s : m_states) { - j.push_back({{"name", s.name}, {"data", s.encoded}}); + j.push_back(json{{"name", s.name}, {"data", s.encoded}}); } try { io::FileStream::WriteAllText(GetStatesFilePath().c_str(), j.dump(2)); @@ -99,7 +115,14 @@ bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::s } unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size()); - if (dSize == ZSTD_CONTENTSIZE_ERROR || dSize == ZSTD_CONTENTSIZE_UNKNOWN || dSize < PACKET_TOTAL) { + if (dSize == ZSTD_CONTENTSIZE_ERROR || dSize == ZSTD_CONTENTSIZE_UNKNOWN) { + m_statusMsg = "Not a valid state string."; + return false; + } + + const bool isFull = (dSize == PACKET_TOTAL); + const bool isPartial = (dSize == PACKET_SAVE_ONLY); + if (!isFull && !isPartial) { m_statusMsg = "Not a valid state string."; return false; } @@ -115,15 +138,27 @@ bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::s memcpy(&pkt, raw.data(), sizeof(pkt)); pkt.stageName[7] = '\0'; - memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c)); + if (isFull) { + memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c)); + m_pendingInfo = g_dComIfG_gameInfo.info; + m_pendingSavedata.reset(); + } else { + memcpy(&g_dComIfG_gameInfo.info.mSavedata, raw.data() + sizeof(pkt), sizeof(dSv_save_c)); + m_pendingSavedata = g_dComIfG_gameInfo.info.mSavedata; + m_pendingInfo.reset(); + } s16 spawnPoint = pkt.startPoint == -4 ? -1 : pkt.startPoint; if (spawnPoint == -1) { dComIfGs_setRestartRoomParam(pkt.roomNo & 0x3F); } + DuskLog.info("StateShare: applying {} state - stage={} room={} layer={} point={} lastSceneMode={}", + isFull ? "full" : "partial", + pkt.stageName, (int)pkt.roomNo, (int)pkt.layer, (int)spawnPoint, + dComIfGs_getLastSceneMode()); + dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer); - m_pendingInfo = g_dComIfG_gameInfo.info; if (name.empty()) { m_statusMsg = fmt::format("{} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); @@ -134,11 +169,21 @@ bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::s } void ImGuiStateShare::tickPendingApply() { - if (!m_pendingInfo.has_value() || dComIfGp_isEnableNextStage()) { + if (!m_pendingInfo.has_value() && !m_pendingSavedata.has_value()) { return; } - g_dComIfG_gameInfo.info = *m_pendingInfo; - m_pendingInfo.reset(); + if (dComIfGp_isEnableNextStage()) { + return; + } + if (m_pendingInfo.has_value()) { + DuskLog.info("StateShare: tickPendingApply full - lastSceneMode={}", dComIfGs_getLastSceneMode()); + g_dComIfG_gameInfo.info = *m_pendingInfo; + m_pendingInfo.reset(); + } else { + DuskLog.info("StateShare: tickPendingApply partial - lastSceneMode={}", dComIfGs_getLastSceneMode()); + g_dComIfG_gameInfo.info.mSavedata = *m_pendingSavedata; + m_pendingSavedata.reset(); + } dComIfGp_offOxygenShowFlag(); dComIfGp_setMaxOxygen(600); dComIfGp_setOxygen(600); @@ -150,7 +195,55 @@ static bool ValidateEncodedState(const std::string& encoded) { return false; } unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size()); - return dSize != ZSTD_CONTENTSIZE_ERROR && dSize != ZSTD_CONTENTSIZE_UNKNOWN && dSize >= PACKET_TOTAL; + return dSize == PACKET_TOTAL || dSize == PACKET_SAVE_ONLY; +} + +void ImGuiStateShare::mergeFromFile(const std::string& path) { + try { + auto data = io::FileStream::ReadAllBytes(path.c_str()); + auto j = json::parse(data); + if (!j.is_array()) { + m_statusMsg = "File does not contain a JSON array."; + return; + } + + std::unordered_set existingNames; + for (const auto& s : m_states) { + existingNames.insert(s.name); + } + + int added = 0; + int skipped = 0; + for (const auto& entry : j) { + if (!entry.contains("name") || !entry.contains("data")) { + ++skipped; + continue; + } + const std::string name = entry["name"].get(); + const std::string encoded = entry["data"].get(); + if (!ValidateEncodedState(encoded)) { + ++skipped; + continue; + } + if (existingNames.count(name)) { + ++skipped; + continue; + } + SavedStateEntry s; + s.name = name; + s.encoded = encoded; + existingNames.insert(s.name); + m_states.push_back(std::move(s)); + ++added; + } + + if (added > 0) { + saveStatesFile(); + } + m_statusMsg = fmt::format("Merged: {} added, {} skipped.", added, skipped); + } catch (const std::exception& e) { + m_statusMsg = fmt::format("Failed to load file: {}", e.what()); + } } void ImGuiStateShare::draw(bool& open) { @@ -162,6 +255,11 @@ void ImGuiStateShare::draw(bool& open) { loadStatesFile(); } + if (!m_pendingMergePath.empty()) { + mergeFromFile(m_pendingMergePath); + m_pendingMergePath.clear(); + } + if (!open) { return; } @@ -243,7 +341,7 @@ void ImGuiStateShare::draw(bool& open) { // Toolbar if (!gameRunning) { ImGui::BeginDisabled(); } - if (ImGui::Button("Save Current")) { + if (ImGui::Button("Current")) { SavedStateEntry entry; entry.name = fmt::format("State {}", m_states.size() + 1); entry.encoded = encodeCurrentState(); @@ -273,6 +371,12 @@ void ImGuiStateShare::draw(bool& open) { } } + ImGui::SameLine(); + if (ImGui::Button("Load Pack")) { + static constexpr SDL_DialogFileFilter filter = {"State pack", "json"}; + ShowFileSelect(&onMergeFileSelected, this, aurora::window::get_sdl_window(), &filter, 1, nullptr, false); + } + if (!m_states.empty()) { ImGui::SameLine(); if (ImGui::Button("Clear All")) { diff --git a/src/dusk/imgui/ImGuiStateShare.hpp b/src/dusk/imgui/ImGuiStateShare.hpp index 6d319889b8..7739e3db3b 100644 --- a/src/dusk/imgui/ImGuiStateShare.hpp +++ b/src/dusk/imgui/ImGuiStateShare.hpp @@ -23,13 +23,17 @@ private: void tickPendingApply(); void loadStatesFile(); void saveStatesFile(); + void mergeFromFile(const std::string& path); + static void onMergeFileSelected(void* userdata, const char* path, const char* error); std::vector m_states; std::string m_statusMsg; - std::optional m_pendingInfo; + std::optional m_pendingInfo; + std::optional m_pendingSavedata; int m_renamingIndex = -1; char m_renameBuffer[128] = {}; bool m_loaded = false; + std::string m_pendingMergePath; }; } diff --git a/tools/saves_to_states_json.py b/tools/saves_to_states_json.py new file mode 100644 index 0000000000..90032cf9d6 --- /dev/null +++ b/tools/saves_to_states_json.py @@ -0,0 +1,58 @@ +""" +Convert a folder of TPGZ saves to a states.json + +Usage: + python saves_to_states_json.py path/to/saves [prefix] + +Requirements: + pip install zstandard +""" + +import base64 +import json +import struct +import sys +import zstandard +from pathlib import Path + +SAVE_C_SIZE = 0x958 + +PACKET_FORMAT = "<8sbbh" + +RETURN_PLACE_OFF = 0x058 +NAME_OFF = RETURN_PLACE_OFF + 0x00 +ROOM_OFF = RETURN_PLACE_OFF + 0x09 +SPAWN_POINT_OFF = RETURN_PLACE_OFF + 0x08 + +folder = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent +out_path = folder / "states.json" + +if len(sys.argv) > 2: + prefix = sys.argv[2] +else: + prefix = None + +cctx = zstandard.ZstdCompressor(level=1) +states = [] + +for bin_path in sorted(folder.glob("*.bin")): + raw = bin_path.read_bytes() + save_c = raw[:SAVE_C_SIZE] + if len(save_c) < SAVE_C_SIZE: + print(f" skip {bin_path.name}: too small ({len(save_c)} bytes)") + continue + + stage_name = save_c[NAME_OFF:NAME_OFF + 8] + room_no = struct.unpack_from("b", save_c, ROOM_OFF)[0] + spawn_point = struct.unpack_from("B", save_c, SPAWN_POINT_OFF)[0] + + pkt = struct.pack(PACKET_FORMAT, stage_name, room_no, -1, spawn_point) + payload = pkt + save_c + encoded = base64.b64encode(cctx.compress(payload)).decode("ascii") + + stage_str = stage_name.rstrip(b"\x00").decode("ascii", errors="replace") + print(f" {bin_path.stem:30s} stage={stage_str!r} room={room_no} point={spawn_point}") + states.append({"name": f"({prefix}) {bin_path.stem}" if prefix else bin_path.stem, "data": encoded}) + +out_path.write_text(json.dumps(states, indent=2)) +print(f"\nWrote {len(states)} states to {out_path}") From 42e8d9ab9d2d721a374f01134202f154d1396838 Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 22 Apr 2026 02:50:49 -0700 Subject: [PATCH 26/64] name fix --- src/dusk/imgui/ImGuiStateShare.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index cb3d3a561c..d2cb2deaaa 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -341,7 +341,7 @@ void ImGuiStateShare::draw(bool& open) { // Toolbar if (!gameRunning) { ImGui::BeginDisabled(); } - if (ImGui::Button("Current")) { + if (ImGui::Button("Save")) { SavedStateEntry entry; entry.name = fmt::format("State {}", m_states.size() + 1); entry.encoded = encodeCurrentState(); From c4d01b82a6d77be93e830a75909e5150971a73f9 Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 22 Apr 2026 03:16:13 -0700 Subject: [PATCH 27/64] get rid of old logs --- src/dusk/imgui/ImGuiStateShare.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index d2cb2deaaa..9d88181b10 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -153,11 +153,6 @@ bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::s dComIfGs_setRestartRoomParam(pkt.roomNo & 0x3F); } - DuskLog.info("StateShare: applying {} state - stage={} room={} layer={} point={} lastSceneMode={}", - isFull ? "full" : "partial", - pkt.stageName, (int)pkt.roomNo, (int)pkt.layer, (int)spawnPoint, - dComIfGs_getLastSceneMode()); - dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer); if (name.empty()) { @@ -176,11 +171,9 @@ void ImGuiStateShare::tickPendingApply() { return; } if (m_pendingInfo.has_value()) { - DuskLog.info("StateShare: tickPendingApply full - lastSceneMode={}", dComIfGs_getLastSceneMode()); g_dComIfG_gameInfo.info = *m_pendingInfo; m_pendingInfo.reset(); } else { - DuskLog.info("StateShare: tickPendingApply partial - lastSceneMode={}", dComIfGs_getLastSceneMode()); g_dComIfG_gameInfo.info.mSavedata = *m_pendingSavedata; m_pendingSavedata.reset(); } From 4466bf4e123569685b7c110306b4db89af645829 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Wed, 22 Apr 2026 13:00:35 -0400 Subject: [PATCH 28/64] wip --- src/d/actor/d_a_alink_dusk.cpp | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index b081ff1ce8..b35daf445f 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -5,9 +5,10 @@ #include "d/d_meter2_info.h" cXyz currentGamepadColor = {0, 0, 0}; +cXyz currentGamepadFlashColor = {0, 0, 0}; cXyz finalGamepadColor = {0, 0, 0}; float lerpSpeed = 0.0f; -const cXyz duskColor = {30, 30, -30}; +const cXyz duskColor = {45, 45, -45}; const cXyz heartColor1 = {255, 0, 0}; const cXyz heartColor2 = {155, 5, 5}; @@ -46,18 +47,21 @@ void daAlink_c::handleGamepadColor() { setColor = true; } - u8 linkHp = Z2GetLink()->getLinkHp(); - if (linkHp <= 2) { - FadeLED(heartColor1, 2.0f); - setColor = true; - } else if (linkHp <= 4) { - FadeLED(heartColor2, 2.0f); - setColor = true; - } else if (linkHp <= 6) { - FadeLED(heartColor3, 2.0f); - setColor = true; + if (!checkEventRun()) { + u8 linkHp = Z2GetLink()->getLinkHp(); + if (linkHp <= 2) { + FadeLED(heartColor1, 2.0f); + setColor = true; + } else if (linkHp <= 4) { + FadeLED(heartColor2, 2.0f); + setColor = true; + } else if (linkHp <= 6) { + FadeLED(heartColor3, 2.0f); + setColor = true; + } } + if (!setColor) { if (checkWolf()) { FadeLED({115, 115, 75}, 5.0f); @@ -92,6 +96,8 @@ void daAlink_c::handleGamepadColor() { AddGamepadCurrentColor(duskColor); } + AddGamepadCurrentColor(currentGamepadFlashColor); + if (finalGamepadColor.x > 255) finalGamepadColor.x = 255; if (finalGamepadColor.x < 0) @@ -108,7 +114,8 @@ void daAlink_c::handleGamepadColor() { finalGamepadColor.z = 0; currentGamepadColor = LerpColor(currentGamepadColor, finalGamepadColor, lerpSpeed); - PADSetColor(PAD_1, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, (u8)currentGamepadColor.z); + PADSetColor(PAD_CHAN0, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, + (u8)currentGamepadColor.z); } void daAlink_c::handleWolfHowl() { From f916a48db0d2d143daa3f55183856bb7950609d3 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Wed, 22 Apr 2026 13:36:06 -0400 Subject: [PATCH 29/64] optimized code and removed bugs --- include/d/actor/d_a_alink.h | 1 - src/d/actor/d_a_alink_dusk.cpp | 114 --------------------------------- src/f_ap/f_ap_game.cpp | 108 ++++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 118 deletions(-) diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index ed4f7f59bc..9980ab776f 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4549,7 +4549,6 @@ public: /* 0x03850 */ daAlink_procFunc mpProcFunc; #if TARGET_PC - void handleGamepadColor(); void handleWolfHowl(); void handleQuickTransform(); bool checkGyroAimContext(); diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index 85bc12b21c..0f442aff94 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -4,120 +4,6 @@ #include "d/d_meter2_draw.h" #include "d/d_meter2_info.h" -cXyz currentGamepadColor = {0, 0, 0}; -cXyz currentGamepadFlashColor = {0, 0, 0}; -cXyz finalGamepadColor = {0, 0, 0}; -float lerpSpeed = 0.0f; -const cXyz duskColor = {45, 45, -45}; - -const cXyz heartColor1 = {255, 0, 0}; -const cXyz heartColor2 = {155, 5, 5}; -const cXyz heartColor3 = {55, 5, 5}; - -float lerp(float a, float b, float t) { - return a + t * (b - a); -} - -cXyz LerpColor(cXyz a, cXyz b, float t) { - return {lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t)}; -} - -void FadeLED(cXyz newColor, float speed) { - finalGamepadColor = newColor; - lerpSpeed = speed / 30.0f; -} - -void SetLED(cXyz newColor) { - currentGamepadColor = newColor; - finalGamepadColor = newColor; -} - -void AddGamepadCurrentColor(cXyz addColor) { - finalGamepadColor.x += addColor.x; - finalGamepadColor.y += addColor.y; - finalGamepadColor.z += addColor.z; -} - -void daAlink_c::handleGamepadColor() { - bool setColor = false; - - fopAc_ac_c* zhint = dComIfGp_att_getZHint(); - if (zhint != NULL) { - FadeLED({50, 50, 175}, 2.0f); - setColor = true; - } - - if (!checkEventRun()) { - u8 linkHp = Z2GetLink()->getLinkHp(); - if (linkHp <= 2) { - FadeLED(heartColor1, 2.0f); - setColor = true; - } else if (linkHp <= 4) { - FadeLED(heartColor2, 2.0f); - setColor = true; - } else if (linkHp <= 6) { - FadeLED(heartColor3, 2.0f); - setColor = true; - } - } - - - if (!setColor) { - if (checkWolf()) { - FadeLED({115, 115, 75}, 5.0f); - setColor = true; - } else { - switch (dComIfGs_getSelectEquipClothes()) { - case dItemNo_WEAR_KOKIRI_e: - FadeLED({0, 100, 0}, 5.0f); - setColor = true; - break; - case dItemNo_WEAR_ZORA_e: - FadeLED({0, 0, 100}, 5.0f); - setColor = true; - break; - case dItemNo_ARMOR_e: - if (checkMagicArmorHeavy()) { - FadeLED({5, 100, 100}, 5.0f); - } else { - FadeLED({100, 0, 5}, 5.0f); - } - setColor = true; - break; - default: - FadeLED({235, 230, 115}, 5.0f); - setColor = true; - break; - } - } - } - - if (dKy_darkworld_check()) { - AddGamepadCurrentColor(duskColor); - } - - AddGamepadCurrentColor(currentGamepadFlashColor); - - if (finalGamepadColor.x > 255) - finalGamepadColor.x = 255; - if (finalGamepadColor.x < 0) - finalGamepadColor.x = 0; - - if (finalGamepadColor.y > 255) - finalGamepadColor.y = 255; - if (finalGamepadColor.y < 0) - finalGamepadColor.y = 0; - - if (finalGamepadColor.z > 255) - finalGamepadColor.z = 255; - if (finalGamepadColor.z < 0) - finalGamepadColor.z = 0; - - currentGamepadColor = LerpColor(currentGamepadColor, finalGamepadColor, lerpSpeed); - PADSetColor(PAD_CHAN0, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, - (u8)currentGamepadColor.z); -} - void daAlink_c::handleWolfHowl() { if (checkWolf()) { if (!dusk::getSettings().game.sunsSong) { diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 3610f270a3..9550cee06e 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -733,10 +733,112 @@ static void fapGm_AfterRecord() { fapGm_After(); } -static void duskExecute() { - if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { - dynamic_cast(link)->handleGamepadColor(); +cXyz currentGamepadColor = {0, 0, 0}; +cXyz finalGamepadColor = {0, 0, 0}; +cXyz additionalGamepadColor = {0, 0, 0}; + +float lerpSpeed = 0.0f; + +const cXyz duskColor = {50, 50, -50}; +const cXyz noColor = {0, 0, 0}; + +float lerp(float a, float b, float t) { + return a + t * (b - a); +} + +cXyz LerpColor(cXyz a, cXyz b, float t) { + return {lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t)}; +} + +void FadeLED(cXyz newColor, float speed) { + finalGamepadColor = newColor; + lerpSpeed = speed / 30.0f; +} + +void SetLED(cXyz newColor) { + currentGamepadColor = newColor; + finalGamepadColor = newColor; +} + +void SetGamepadAdditionalColor(cXyz addColor) { + additionalGamepadColor.x = addColor.x; + additionalGamepadColor.y = addColor.y; + additionalGamepadColor.z = addColor.z; +} + +void handleGamepadColor() { + bool setColor = false; + + fopAc_ac_c* zhint = dComIfGp_att_getZHint(); + if (zhint != NULL) { + FadeLED({50, 50, 175}, 2.0f); + setColor = true; } + + daPy_py_c* player = daPy_getPlayerActorClass(); + daAlink_c* link = daAlink_getAlinkActorClass(); + + if (link != nullptr && !setColor) { + if (link->checkWolf()) { + FadeLED({115, 115, 75}, 5.0f); + setColor = true; + } else { + switch (dComIfGs_getSelectEquipClothes()) { + case dItemNo_WEAR_KOKIRI_e: + FadeLED({0, 100, 0}, 5.0f); + setColor = true; + break; + case dItemNo_WEAR_ZORA_e: + FadeLED({0, 0, 100}, 5.0f); + setColor = true; + break; + case dItemNo_ARMOR_e: + if (link->checkMagicArmorHeavy()) { + FadeLED({5, 100, 100}, 5.0f); + } else { + FadeLED({100, 0, 5}, 5.0f); + } + setColor = true; + break; + case dItemNo_WEAR_CASUAL_e: + FadeLED({235, 230, 115}, 5.0f); + setColor = true; + break; + } + } + } + + if (dKy_darkworld_check()) { + SetGamepadAdditionalColor(duskColor); + } else { + SetGamepadAdditionalColor(noColor); + } + + f32 finalRed = finalGamepadColor.x + additionalGamepadColor.x; + f32 finalGreen = finalGamepadColor.y + additionalGamepadColor.y; + f32 finalBlue = finalGamepadColor.z + additionalGamepadColor.z; + + if (finalRed > 255) + finalRed = 255; + if (finalRed < 0) + finalRed = 0; + + if (finalGreen > 255) + finalGreen = 255; + if (finalGreen < 0) + finalGreen = 0; + + if (finalBlue > 255) + finalBlue = 255; + if (finalBlue < 0) + finalBlue = 0; + + currentGamepadColor = LerpColor(currentGamepadColor, cXyz{finalRed, finalGreen, finalBlue}, lerpSpeed); + PADSetColor(PAD_CHAN0, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, (u8)currentGamepadColor.z); +} + +static void duskExecute() { + handleGamepadColor(); if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) { if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { From ac3d3314c42497f43a7ec06c02e3012b20685335 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 22 Apr 2026 11:47:40 -0600 Subject: [PATCH 30/64] Incorporate roll into gyro horizontal aiming --- src/dusk/gyro.cpp | 52 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index 5b6e880a1f..e64fb6d09a 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -1,16 +1,25 @@ #include "dusk/gyro.h" #include "d/actor/d_a_alink.h" +#include namespace dusk::gyro { namespace { constexpr s32 kRollgoalTableMaxOffset = 6500; constexpr float kGyroEmaAlphaMin = 0.05f; constexpr float kGyroEmaAlphaMax = 1.0f; +// Smooth gravity separately so the yaw/roll blend doesn't twitch with raw accel noise. +constexpr float kGravityEmaAlpha = 0.1f; +constexpr float kMinGravityProjection = 0.2f; +// Let roll contribute more strongly as the pad approaches an upright posture. +constexpr float kRollAimBoostMax = 2.0f; bool s_sensor_enabled = false; +bool s_accel_enabled = false; float s_smooth_gx = 0.0f; float s_smooth_gy = 0.0f; float s_smooth_gz = 0.0f; +float s_gravity_y = 0.0f; +float s_gravity_z = 0.0f; float s_yaw_rad = 0.0f; float s_pitch_rad = 0.0f; float s_roll_rad = 0.0f; @@ -19,6 +28,7 @@ s32 s_rollgoal_az = 0; void reset_filter_state() { s_smooth_gx = s_smooth_gy = s_smooth_gz = 0.0f; + s_gravity_y = s_gravity_z = 0.0f; s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f; s_rollgoal_ax = s_rollgoal_az = 0; } @@ -54,6 +64,10 @@ void read(float dt) { PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); s_sensor_enabled = false; } + if (s_accel_enabled) { + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE); + s_accel_enabled = false; + } reset_filter_state(); return; } @@ -68,6 +82,13 @@ void read(float dt) { s_sensor_enabled = true; } + if (!s_accel_enabled && PADHasSensor(PAD_CHAN0, PAD_SENSOR_ACCEL) && + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, TRUE)) + { + // We only need accel for the gravity-aware yaw/roll mix. + s_accel_enabled = true; + } + f32 gyro[3]; if (!PADGetSensorData(PAD_CHAN0, PAD_SENSOR_GYRO, gyro, 3)) { return; @@ -80,9 +101,34 @@ void read(float dt) { s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy); s_smooth_gz += smooth_alpha * (gyro[2] - s_smooth_gz); - s_pitch_rad = -apply_deadband(s_smooth_gx, deadband) * dt * dusk::getSettings().game.gyroSensitivityX; - s_yaw_rad = apply_deadband(s_smooth_gy, deadband) * dt * dusk::getSettings().game.gyroSensitivityY; - s_roll_rad = apply_deadband(s_smooth_gz, deadband) * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X + const float pitch_rate = apply_deadband(s_smooth_gx, deadband); + const float yaw_rate = apply_deadband(s_smooth_gy, deadband); + const float roll_rate = apply_deadband(s_smooth_gz, deadband); + + s_pitch_rad = -pitch_rate * dt * dusk::getSettings().game.gyroSensitivityX; + s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X + + float horizontal_rate = yaw_rate; + if (s_accel_enabled) { + f32 accel[3]; + if (PADGetSensorData(PAD_CHAN0, PAD_SENSOR_ACCEL, accel, 3)) { + s_gravity_y += kGravityEmaAlpha * (accel[1] - s_gravity_y); + s_gravity_z += kGravityEmaAlpha * (accel[2] - s_gravity_z); + + // Project gravity onto the controller's yaw/roll plane to infer which axis + // should dominate horizontal aim at the current pitch. + const float gravity_yz_len = std::sqrt((s_gravity_y * s_gravity_y) + (s_gravity_z * s_gravity_z)); + if (gravity_yz_len >= kMinGravityProjection) { + const float yaw_weight = s_gravity_y / gravity_yz_len; + const float roll_mix = std::fabs(s_gravity_z) / gravity_yz_len; + const float roll_weight = s_gravity_z / gravity_yz_len; + const float roll_boost = 1.0f + (roll_mix * (kRollAimBoostMax - 1.0f)); + horizontal_rate = (yaw_rate * yaw_weight) + (roll_rate * roll_weight * roll_boost); + } + } + } + + s_yaw_rad = horizontal_rate * dt * dusk::getSettings().game.gyroSensitivityY; s_pitch_rad = dusk::getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; s_yaw_rad = dusk::getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; From c350b7b8ed4eb7f8abee044d3d23a857bf64b462 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Wed, 22 Apr 2026 13:52:14 -0400 Subject: [PATCH 31/64] fix building on linux --- src/f_ap/f_ap_game.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 9550cee06e..9e87acccc7 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -742,12 +742,8 @@ float lerpSpeed = 0.0f; const cXyz duskColor = {50, 50, -50}; const cXyz noColor = {0, 0, 0}; -float lerp(float a, float b, float t) { - return a + t * (b - a); -} - cXyz LerpColor(cXyz a, cXyz b, float t) { - return {lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t)}; + return {std::lerp(a.x, b.x, t), std::lerp(a.y, b.y, t), std::lerp(a.z, b.z, t)}; } void FadeLED(cXyz newColor, float speed) { From ca247095dae47c500cc0afeb65979d6e2d5a4cb9 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 22 Apr 2026 12:20:44 -0600 Subject: [PATCH 32/64] Reset gravity baseline on aim start --- src/dusk/gyro.cpp | 52 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index e64fb6d09a..abe22909c1 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -15,11 +15,15 @@ constexpr float kRollAimBoostMax = 2.0f; bool s_sensor_enabled = false; bool s_accel_enabled = false; +bool s_was_aiming = false; +bool s_have_gravity_baseline = false; float s_smooth_gx = 0.0f; float s_smooth_gy = 0.0f; float s_smooth_gz = 0.0f; float s_gravity_y = 0.0f; float s_gravity_z = 0.0f; +float s_baseline_gravity_y = 0.0f; +float s_baseline_gravity_z = 0.0f; float s_yaw_rad = 0.0f; float s_pitch_rad = 0.0f; float s_roll_rad = 0.0f; @@ -29,6 +33,9 @@ s32 s_rollgoal_az = 0; void reset_filter_state() { s_smooth_gx = s_smooth_gy = s_smooth_gz = 0.0f; s_gravity_y = s_gravity_z = 0.0f; + s_baseline_gravity_y = s_baseline_gravity_z = 0.0f; + s_was_aiming = false; + s_have_gravity_baseline = false; s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f; s_rollgoal_ax = s_rollgoal_az = 0; } @@ -59,7 +66,12 @@ bool queryGyroAimContext() { } void read(float dt) { - if (!s_sensor_keep_alive && !queryGyroAimContext()) { + const bool aim_active = queryGyroAimContext(); + const bool aim_just_started = aim_active && !s_was_aiming; + const bool aim_just_ended = !aim_active && s_was_aiming; + s_was_aiming = aim_active; + + if (!s_sensor_keep_alive && !aim_active) { if (s_sensor_enabled) { PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); s_sensor_enabled = false; @@ -72,6 +84,12 @@ void read(float dt) { return; } + if (aim_just_started || aim_just_ended) { + s_gravity_y = s_gravity_z = 0.0f; + s_baseline_gravity_y = s_baseline_gravity_z = 0.0f; + s_have_gravity_baseline = false; + } + if (!s_sensor_enabled) { if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) { return; @@ -109,19 +127,35 @@ void read(float dt) { s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X float horizontal_rate = yaw_rate; - if (s_accel_enabled) { + if (aim_active && s_accel_enabled) { f32 accel[3]; if (PADGetSensorData(PAD_CHAN0, PAD_SENSOR_ACCEL, accel, 3)) { - s_gravity_y += kGravityEmaAlpha * (accel[1] - s_gravity_y); - s_gravity_z += kGravityEmaAlpha * (accel[2] - s_gravity_z); + if (!s_have_gravity_baseline) { + s_gravity_y = accel[1]; + s_gravity_z = accel[2]; + } else { + s_gravity_y += kGravityEmaAlpha * (accel[1] - s_gravity_y); + s_gravity_z += kGravityEmaAlpha * (accel[2] - s_gravity_z); + } - // Project gravity onto the controller's yaw/roll plane to infer which axis - // should dominate horizontal aim at the current pitch. + // Compare the current gravity projection against the gravity vector from + // aim start so the user's resting hold angle becomes the neutral baseline. const float gravity_yz_len = std::sqrt((s_gravity_y * s_gravity_y) + (s_gravity_z * s_gravity_z)); if (gravity_yz_len >= kMinGravityProjection) { - const float yaw_weight = s_gravity_y / gravity_yz_len; - const float roll_mix = std::fabs(s_gravity_z) / gravity_yz_len; - const float roll_weight = s_gravity_z / gravity_yz_len; + const float current_gravity_y = s_gravity_y / gravity_yz_len; + const float current_gravity_z = s_gravity_z / gravity_yz_len; + + if (!s_have_gravity_baseline) { + s_baseline_gravity_y = current_gravity_y; + s_baseline_gravity_z = current_gravity_z; + s_have_gravity_baseline = true; + } + + const float yaw_weight = + (s_baseline_gravity_y * current_gravity_y) + (s_baseline_gravity_z * current_gravity_z); + const float roll_weight = + (s_baseline_gravity_y * current_gravity_z) - (s_baseline_gravity_z * current_gravity_y); + const float roll_mix = std::fabs(roll_weight); const float roll_boost = 1.0f + (roll_mix * (kRollAimBoostMax - 1.0f)); horizontal_rate = (yaw_rate * yaw_weight) + (roll_rate * roll_weight * roll_boost); } From 19c86b1b73157caa7c5114fd7c6098da3a769100 Mon Sep 17 00:00:00 2001 From: Captain Kitty Cat <68467449+Captainkittyca2@users.noreply.github.com> Date: Wed, 22 Apr 2026 21:27:54 +0300 Subject: [PATCH 33/64] Items Don't Despawn (#488) * Indefinite Item Drops * Preserve Disappear Effect when Disabled * Changed to "Items Don't Despawn". Under "Cheats" * SetItemTooltip for description --- include/dusk/settings.h | 1 + src/d/actor/d_a_obj_item.cpp | 15 ++++++++++++++- src/dusk/imgui/ImGuiMenuGame.cpp | 2 ++ src/dusk/settings.cpp | 2 ++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 2cec252006..4dbfaa73ef 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -101,6 +101,7 @@ struct UserSettings { ConfigVar infiniteOil; ConfigVar infiniteOxygen; ConfigVar infiniteRupees; + ConfigVar enableIndefiniteItemDrops; ConfigVar moonJump; ConfigVar superClawshot; ConfigVar alwaysGreatspin; diff --git a/src/d/actor/d_a_obj_item.cpp b/src/d/actor/d_a_obj_item.cpp index 0db3ac3a55..6c7e82ab3e 100644 --- a/src/d/actor/d_a_obj_item.cpp +++ b/src/d/actor/d_a_obj_item.cpp @@ -390,6 +390,9 @@ void daItem_c::procMainNormal() { cLib_chaseF(&scale.z, mItemScale.z, step_z); } + #if TARGET_PC + if (!dusk::getSettings().game.enableIndefiniteItemDrops) { + #endif if (mWaitTimer == 0) { if (mDisappearTimer == 0) { deleteItem(); @@ -399,6 +402,9 @@ void daItem_c::procMainNormal() { changeDraw(); } } + #if TARGET_PC + } + #endif mCcCyl.SetC(current.pos); dComIfG_Ccsp()->Set(&mCcCyl); @@ -1058,9 +1064,16 @@ int daItem_c::CountTimer() { if (checkCountTimer()) { if (mWaitTimer > 0) { mWaitTimer--; - } else if (mDisappearTimer > 0) { + } + #if TARGET_PC + else if (!dusk::getSettings().game.enableIndefiniteItemDrops && mDisappearTimer > 0) { mDisappearTimer--; } + #else + else if (mDisappearTimer > 0) { + mDisappearTimer--; + } + #endif } cLib_calcTimer(&mBoomWindTgTimer); diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 448bd531ff..c55836c68d 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -288,6 +288,8 @@ namespace dusk { config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); + config::ImGuiCheckbox("Items Don't Despawn", getSettings().game.enableIndefiniteItemDrops); + ImGui::SetItemTooltip("Items Don't Despawn Unless You Load A Different Room In Which Case They Do But Even Under Some Circumstances They Don't, It Is Quite Rare Though"); ImGui::SeparatorText("Abilities"); config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index aacca0dbc9..28397f80f2 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -75,6 +75,7 @@ UserSettings g_userSettings = { .infiniteOil{"game.infiniteOil", false}, .infiniteOxygen{"game.infiniteOxygen", false}, .infiniteRupees{"game.infiniteRupees", false}, + .enableIndefiniteItemDrops {"game.enableIndefiniteItemDrops", false}, .moonJump{"game.moonJump", false}, .superClawshot{"game.superClawshot", false}, .alwaysGreatspin{"game.alwaysGreatspin", false}, @@ -160,6 +161,7 @@ void registerSettings() { Register(g_userSettings.game.infiniteOil); Register(g_userSettings.game.infiniteOxygen); Register(g_userSettings.game.infiniteRupees); + Register(g_userSettings.game.enableIndefiniteItemDrops); Register(g_userSettings.game.moonJump); Register(g_userSettings.game.superClawshot); Register(g_userSettings.game.alwaysGreatspin); From 9e9d11ae89cb53a9eebd7c6a16e664c9e63f9dc8 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Wed, 22 Apr 2026 18:47:51 -0400 Subject: [PATCH 34/64] Widescreenified Fused Shadow Animations for all first 3 bosses --- src/d/actor/d_a_b_bq.cpp | 10 +++++++++- src/d/actor/d_a_b_ob.cpp | 11 ++++++++++- src/d/actor/d_a_e_fm.cpp | 11 ++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/d/actor/d_a_b_bq.cpp b/src/d/actor/d_a_b_bq.cpp index 70ab84705b..637e82360e 100644 --- a/src/d/actor/d_a_b_bq.cpp +++ b/src/d/actor/d_a_b_bq.cpp @@ -2059,7 +2059,15 @@ static void demo_camera(b_bq_class* i_this) { for (int i = 0; i < 5; i++) { static u16 g_e_i[] = {0x83EB, 0x83EC, 0x83ED, 0x83EE, 0x83EF}; - dComIfGp_particle_set(g_e_i[i], &pos, NULL, NULL); + #if TARGET_PC + if (i == 0) { + static const cXyz effWideScale = {mDoGph_gInf_c::getAspect(), 1.0f, 1.0f}; + dComIfGp_particle_set(g_e_i[i], &pos, NULL, &effWideScale); + } else + #endif + { + dComIfGp_particle_set(g_e_i[i], &pos, NULL, NULL); + } } i_this->mSound.startCreatureSound(Z2SE_EN_BOSS_CONVERGE, 0, 0); diff --git a/src/d/actor/d_a_b_ob.cpp b/src/d/actor/d_a_b_ob.cpp index 5622375fec..f7aa9b1f0b 100644 --- a/src/d/actor/d_a_b_ob.cpp +++ b/src/d/actor/d_a_b_ob.cpp @@ -2725,7 +2725,16 @@ static void demo_camera(b_ob_class* i_this) { for (int i = 0; i < 5; i++) { static u16 ex_eff[] = {dPa_RM(ID_ZI_S_OI_CONVERGE_FILTER), dPa_RM(ID_ZI_S_OI_CONVERGE_FILTEROUT), dPa_RM(ID_ZI_S_OI_CONVERGE_HIDE), dPa_RM(ID_ZI_S_OI_CONVERGE_POLYGON_A), dPa_RM(ID_ZI_S_OI_CONVERGE_POLYGON_B)}; - dComIfGp_particle_set(ex_eff[i], &room_pos, NULL, &sc); + + #if TARGET_PC + if (i == 0) { + static const cXyz effWideScale = {mDoGph_gInf_c::getAspect() * 10.0f, 10.0f, 10.0f}; + dComIfGp_particle_set(ex_eff[i], &room_pos, NULL, &effWideScale); + } else + #endif + { + dComIfGp_particle_set(ex_eff[i], &room_pos, NULL, &sc); + } } i_this->mDemoCamEye.set(-4820.0f, -18600.0f, -510.0f); diff --git a/src/d/actor/d_a_e_fm.cpp b/src/d/actor/d_a_e_fm.cpp index ca01f73c29..81a1b404f0 100644 --- a/src/d/actor/d_a_e_fm.cpp +++ b/src/d/actor/d_a_e_fm.cpp @@ -1677,7 +1677,16 @@ static void demo_camera(e_fm_class* i_this) { cXyz spBC(0.0f, 0.0f, 0.0f); for (int i = 0; i < 4; i++) { static u16 g_e_i[] = {0x847B, 0x847C, 0x847D, 0x847E}; - dComIfGp_particle_set(g_e_i[i], &spBC, NULL, NULL); + + #if TARGET_PC + if (i == 0) { + static const cXyz effWideScale = {mDoGph_gInf_c::getAspect(), 1.0f, 1.0f}; + dComIfGp_particle_set(g_e_i[i], &spBC, NULL, &effWideScale); + } else + #endif + { + dComIfGp_particle_set(g_e_i[i], &spBC, NULL, NULL); + } } i_this->mDemoCamFovy = 55.0f + NREG_F(10); From 5fcffa0b4f0a4a8f4f9df067142543c76a2fc76a Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 22 Apr 2026 17:18:18 -0600 Subject: [PATCH 35/64] Use SDL_GetTicksNS instead of std::chrono --- include/dusk/time.h | 50 +++++++++++----------- libs/JSystem/src/JFramework/JFWDisplay.cpp | 8 ++-- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/include/dusk/time.h b/include/dusk/time.h index 948a2fc171..c43437f639 100644 --- a/include/dusk/time.h +++ b/include/dusk/time.h @@ -1,9 +1,10 @@ #ifndef DUSK_TIME_H #define DUSK_TIME_H -#include -#include #include +#include + +#include "SDL3/SDL_timer.h" #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -15,28 +16,26 @@ #include #include #include -#else -#include "SDL3/SDL_timer.h" #endif class Limiter { - using delta_clock = std::chrono::high_resolution_clock; - using duration_t = std::chrono::nanoseconds; - public: - void Reset() { m_oldTime = delta_clock::now(); } + using duration_t = Uint64; + + void Reset() { m_oldTime = SDL_GetTicksNS(); } void Sleep(duration_t targetFrameTime) { - if (targetFrameTime.count() == 0) { + if (targetFrameTime == 0) { return; } - auto start = delta_clock::now(); + const Uint64 start = SDL_GetTicksNS(); duration_t adjustedSleepTime = SleepTime(targetFrameTime); - if (adjustedSleepTime.count() > 0) { + if (adjustedSleepTime > 0) { NanoSleep(adjustedSleepTime); - duration_t overslept = TimeSince(start) - adjustedSleepTime; - if (overslept < duration_t{targetFrameTime}) { + const duration_t elapsed = TimeSince(start); + const duration_t overslept = elapsed > adjustedSleepTime ? elapsed - adjustedSleepTime : 0; + if (overslept < targetFrameTime) { m_overheadTimes[m_overheadTimeIdx] = overslept; m_overheadTimeIdx = (m_overheadTimeIdx + 1) % m_overheadTimes.size(); } @@ -45,23 +44,23 @@ public: } duration_t SleepTime(duration_t targetFrameTime) { - const auto sleepTime = duration_t{targetFrameTime} - TimeSince(m_oldTime); - m_overhead = std::accumulate(m_overheadTimes.begin(), m_overheadTimes.end(), duration_t{}) / m_overheadTimes.size(); + const duration_t elapsed = TimeSince(m_oldTime); + const duration_t sleepTime = elapsed < targetFrameTime ? targetFrameTime - elapsed : 0; + m_overhead = std::accumulate(m_overheadTimes.begin(), m_overheadTimes.end(), duration_t{0}) / + m_overheadTimes.size(); if (sleepTime > m_overhead) { return sleepTime - m_overhead; } - return duration_t{0}; + return 0; } private: - delta_clock::time_point m_oldTime; + Uint64 m_oldTime = 0; std::array m_overheadTimes{}; size_t m_overheadTimeIdx = 0; - duration_t m_overhead = duration_t{0}; + duration_t m_overhead = 0; - duration_t TimeSince(delta_clock::time_point start) { - return std::chrono::duration_cast(delta_clock::now() - start); - } + duration_t TimeSince(Uint64 start) const { return SDL_GetTicksNS() - start; } #if _WIN32 void NanoSleep(const duration_t duration) { @@ -85,9 +84,10 @@ private: LARGE_INTEGER start, current; QueryPerformanceCounter(&start); - LONGLONG ticksToWait = static_cast(duration.count() * countPerNs); - if (DWORD ms = std::chrono::duration_cast(duration).count(); ms > 1) { - ::Sleep(ms - 1); + const LONGLONG ticksToWait = static_cast(duration * countPerNs); + const Uint64 ms = duration / 1'000'000ULL; + if (ms > 1) { + ::Sleep(static_cast(ms - 1)); } do { QueryPerformanceCounter(¤t); @@ -99,7 +99,7 @@ private: } while (current.QuadPart - start.QuadPart < ticksToWait); } #else - void NanoSleep(const duration_t duration) { SDL_DelayPrecise(duration.count()); } + void NanoSleep(const duration_t duration) { SDL_DelayPrecise(duration); } #endif }; diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 9ea826feee..abec596247 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -368,11 +368,11 @@ constexpr auto FRAME_PERIOD = std::chrono::duration_cast(1001.0 / 30000.0)); constexpr auto RETRACE_PERIOD = FRAME_PERIOD / 2; -static void waitPrecise(Limiter& limiter, Uint64 targetNs) { - const auto sleepTime = limiter.SleepTime(std::chrono::nanoseconds(targetNs)); +static void waitPrecise(Limiter& limiter, Limiter::duration_t targetNs) { + const auto sleepTime = limiter.SleepTime(targetNs); dusk::frameUsagePct = - 100.0f * (1.0f - static_cast(sleepTime.count()) / static_cast(targetNs)); - limiter.Sleep(std::chrono::nanoseconds(targetNs)); + 100.0f * (1.0f - static_cast(sleepTime) / static_cast(targetNs)); + limiter.Sleep(targetNs); } #endif From 0038afa39219abcf1671a4fee09299733c00597a Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Wed, 22 Apr 2026 17:51:16 -0600 Subject: [PATCH 36/64] Press esc to exit full screen --- src/dusk/imgui/ImGuiConsole.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 0b32fd5078..085b34dc2d 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -305,6 +305,10 @@ namespace dusk { ImGuiMenuGame::ToggleFullscreen(); } + if (ImGui::IsKeyPressed(ImGuiKey_Escape) && getSettings().video.enableFullscreen) { + ImGuiMenuGame::ToggleFullscreen(); + } + if (!dusk::IsGameLaunched) { m_preLaunchWindow.draw(); } From 6a8f3516f9b538092c0c32e8d67d2b75ad8fab46 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Wed, 22 Apr 2026 18:35:07 -0600 Subject: [PATCH 37/64] Frame interp: Fix dmap scroll arrows --- include/d/d_menu_dmap.h | 4 ++++ src/d/d_menu_dmap.cpp | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/include/d/d_menu_dmap.h b/include/d/d_menu_dmap.h index 78c659e2cf..e50c533654 100644 --- a/include/d/d_menu_dmap.h +++ b/include/d/d_menu_dmap.h @@ -103,6 +103,10 @@ public: field_0xd98 = param_1; } +#if TARGET_PC + void resetScrollArrowMask() { field_0xdda = 0; } +#endif + /* 0xC98 */ JKRExpHeap* mpHeap; /* 0xC9C */ JKRExpHeap* mpTalkHeap; /* 0xCA0 */ STControl* mpStick; diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index 17d9193241..bb557efdac 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -21,6 +21,7 @@ #include "d/d_msg_string.h" #include "d/d_meter_haihai.h" #include "d/d_menu_window.h" +#include "dusk/settings.h" #include "f_op/f_op_msg_mng.h" #include "m_Do/m_Do_graphic.h" #include @@ -945,9 +946,15 @@ void dMenu_DmapBg_c::draw() { mpMeterHaihai->drawHaihai(field_0xdda, x1 + (local_224.x + local_218.x) / 2, y1 + (local_224.y + local_218.y) / 2, - -35.0f + (local_224.x - local_218.x), + -35.0f + (local_224.x - local_218.x), -35.0f + (local_224.y - local_218.y)); +#if TARGET_PC + if (!dusk::getSettings().game.enableFrameInterpolation) { + field_0xdda = 0; + } +#else field_0xdda = 0; +#endif } dMenu_Dmap_c::myclass->drawFloorScreenTop(mFloorScreen, field_0xd94, field_0xd98, grafContext); @@ -2574,6 +2581,11 @@ void dMenu_Dmap_c::zoomIn_proc() { } void dMenu_Dmap_c::zoomOut_init_proc() { +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + mpDrawBg->resetScrollArrowMask(); + } +#endif Z2GetAudioMgr()->seStart(Z2SE_SY_MAP_ZOOMOUT, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); mMapCtrl->initZoomOut(10); mpDrawBg->iconScaleAnmInit(1.0f, 0.0f, 10); From ae54f024cd6e8c134103b37297f11032c8d26eb0 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 22 Apr 2026 21:48:58 -0600 Subject: [PATCH 38/64] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 6d69b7822e..63550a8375 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 6d69b7822e95ad9e82537848c968058be4fbbca5 +Subproject commit 63550a83759974dd18bc13cd420888188be9caf9 From 53e8662335d91730ce913aab328d1a3b4932ac5c Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Wed, 22 Apr 2026 21:57:36 -0600 Subject: [PATCH 39/64] frame interp: dselect_cursor_c in d_menu_ring --- include/d/d_menu_ring.h | 5 +++++ include/d/d_select_cursor.h | 3 +++ src/d/d_menu_ring.cpp | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/include/d/d_menu_ring.h b/include/d/d_menu_ring.h index 39d29a35e8..acb7747b88 100644 --- a/include/d/d_menu_ring.h +++ b/include/d/d_menu_ring.h @@ -206,6 +206,11 @@ private: /* 0x6D3 */ u8 field_0x6d3; #if TARGET_PC f32 mSelectItemSlideElapsed[4]; + f32 mCursorInterpPrevX; + f32 mCursorInterpPrevY; + f32 mCursorInterpCurrX; + f32 mCursorInterpCurrY; + bool mCursorInterpInit; #endif }; diff --git a/include/d/d_select_cursor.h b/include/d/d_select_cursor.h index 1bd5991d16..1ca226611b 100644 --- a/include/d/d_select_cursor.h +++ b/include/d/d_select_cursor.h @@ -48,6 +48,9 @@ public: } #ifdef TARGET_PC + f32 getPositionX() const { return mPositionX; } + f32 getPositionY() const { return mPositionY; } + void refreshAspectScale(); #endif diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index 529597248a..dff9c79091 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -185,6 +185,13 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i field_0x682 = 0xc000; break; } +#if TARGET_PC + mCursorInterpPrevX = 0.0f; + mCursorInterpPrevY = 0.0f; + mCursorInterpCurrX = 0.0f; + mCursorInterpCurrY = 0.0f; + mCursorInterpInit = false; +#endif for (int i = 0; i < 4; i++) { field_0x674[i] = 0; #if TARGET_PC @@ -631,6 +638,34 @@ void dMenu_Ring_c::_draw() { } else { drawSelectItem(); drawItem2(); +#if TARGET_PC + if (dusk::frame_interp::is_enabled() && mAlphaRate >= 1.0f) { + f32 cursorX = mpDrawCursor->getPositionX(); + f32 cursorY = mpDrawCursor->getPositionY(); + + if (dusk::frame_interp::get_ui_tick_pending()) { + mCursorInterpPrevX = mCursorInterpCurrX; + mCursorInterpPrevY = mCursorInterpCurrY; + mCursorInterpCurrX = cursorX; + mCursorInterpCurrY = cursorY; + + if (!mCursorInterpInit) { + mCursorInterpPrevX = mCursorInterpCurrX; + mCursorInterpPrevY = mCursorInterpCurrY; + mCursorInterpInit = true; + } + } + if (mCursorInterpInit) { + const f32 step = dusk::frame_interp::get_interpolation_step(); + mpDrawCursor->setPos( + mCursorInterpPrevX + (mCursorInterpCurrX - mCursorInterpPrevX) * step, + mCursorInterpPrevY + (mCursorInterpCurrY - mCursorInterpPrevY) * step + ); + } + } else { + mCursorInterpInit = false; + } +#endif mpDrawCursor->draw(); mpItemExplain->trans(mCenterPosX, mCenterPosY); mpItemExplain->draw((J2DOrthoGraph*)grafPort); From bfd8b9f453a1458123e4aa8a4f7cf8cdf43dc918 Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 22 Apr 2026 22:27:14 -0700 Subject: [PATCH 40/64] make state share loads basically instant --- include/dusk/settings.h | 1 + libs/JSystem/src/JFramework/JFWDisplay.cpp | 8 ++++++++ src/dusk/imgui/ImGuiStateShare.cpp | 7 +++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 2cec252006..5eb333c6fd 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -148,6 +148,7 @@ struct TransientSettings { CollisionViewSettings collisionView; bool skipFrameRateLimit; bool moveLinkActive; + bool stateShareLoadActive; }; TransientSettings& getTransientSettings(); diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 9ea826feee..889fe0ebea 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -18,6 +18,7 @@ #include "dusk/logging.h" #include "dusk/settings.h" #include "dusk/time.h" +#include "f_op/f_op_overlap_mng.h" #include "SDL3/SDL_timer.h" #include "tracy/Tracy.hpp" @@ -385,6 +386,13 @@ static void waitForTick(u32 p1, u16 p2) { if (dusk::getTransientSettings().skipFrameRateLimit) { p1 = OS_TIMER_CLOCK / 120; } + + #if TARGET_PC + if (fopOvlpM_IsPeek() && dusk::getTransientSettings().stateShareLoadActive) { + return; + } + #endif + ZoneScopedC(tracy::Color::DimGray); #endif diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index 9d88181b10..c7a1c9bf94 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -11,6 +11,7 @@ #include "dusk/main.h" #include "dusk/io.hpp" #include "dusk/logging.h" +#include "dusk/settings.h" #include "../file_select.hpp" #include "aurora/lib/window.hpp" @@ -153,7 +154,8 @@ bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::s dComIfGs_setRestartRoomParam(pkt.roomNo & 0x3F); } - dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer); + dusk::getTransientSettings().stateShareLoadActive = true; + dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer, 0.0f, 0, 1, 0, 0, 1, 3); if (name.empty()) { m_statusMsg = fmt::format("{} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); @@ -180,6 +182,7 @@ void ImGuiStateShare::tickPendingApply() { dComIfGp_offOxygenShowFlag(); dComIfGp_setMaxOxygen(600); dComIfGp_setOxygen(600); + dusk::getTransientSettings().stateShareLoadActive = false; } static bool ValidateEncodedState(const std::string& encoded) { @@ -258,7 +261,7 @@ void ImGuiStateShare::draw(bool& open) { } ImGui::SetNextWindowSizeConstraints(ImVec2(400, 0), ImVec2(FLT_MAX, FLT_MAX)); - if (!ImGui::Begin("State Share", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { + if (!ImGui::Begin("State Manager", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { ImGui::End(); return; } From 7c9e99220a86c4c44ae4d5af5b257d7e7f0410ea Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 22 Apr 2026 22:33:55 -0700 Subject: [PATCH 41/64] better impl of insta state shares --- src/dusk/imgui/ImGuiStateShare.cpp | 11 ++++++++++- src/dusk/imgui/ImGuiStateShare.hpp | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index c7a1c9bf94..c4bd04842d 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -12,6 +12,7 @@ #include "dusk/io.hpp" #include "dusk/logging.h" #include "dusk/settings.h" +#include "f_op/f_op_overlap_mng.h" #include "../file_select.hpp" #include "aurora/lib/window.hpp" @@ -155,6 +156,7 @@ bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::s } dusk::getTransientSettings().stateShareLoadActive = true; + m_stateSharePeekSeen = false; dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer, 0.0f, 0, 1, 0, 0, 1, 3); if (name.empty()) { @@ -182,7 +184,6 @@ void ImGuiStateShare::tickPendingApply() { dComIfGp_offOxygenShowFlag(); dComIfGp_setMaxOxygen(600); dComIfGp_setOxygen(600); - dusk::getTransientSettings().stateShareLoadActive = false; } static bool ValidateEncodedState(const std::string& encoded) { @@ -245,6 +246,14 @@ void ImGuiStateShare::mergeFromFile(const std::string& path) { void ImGuiStateShare::draw(bool& open) { if (dusk::IsGameLaunched) { tickPendingApply(); + if (dusk::getTransientSettings().stateShareLoadActive) { + if (fopOvlpM_IsPeek()) { + m_stateSharePeekSeen = true; + } else if (m_stateSharePeekSeen) { + dusk::getTransientSettings().stateShareLoadActive = false; + m_stateSharePeekSeen = false; + } + } } if (!m_loaded) { diff --git a/src/dusk/imgui/ImGuiStateShare.hpp b/src/dusk/imgui/ImGuiStateShare.hpp index 7739e3db3b..a2dc681833 100644 --- a/src/dusk/imgui/ImGuiStateShare.hpp +++ b/src/dusk/imgui/ImGuiStateShare.hpp @@ -33,6 +33,7 @@ private: int m_renamingIndex = -1; char m_renameBuffer[128] = {}; bool m_loaded = false; + bool m_stateSharePeekSeen = false; std::string m_pendingMergePath; }; From 6c252c6d2657d838c132c332bbf6f6f5a73da2da Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 22 Apr 2026 22:37:21 -0700 Subject: [PATCH 42/64] disallow breaking share state by overlapping loads --- src/dusk/imgui/ImGuiStateShare.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index c4bd04842d..adbe5b6c67 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -276,6 +276,7 @@ void ImGuiStateShare::draw(bool& open) { } const bool gameRunning = dusk::IsGameLaunched; + const bool loadInProgress = dusk::getTransientSettings().stateShareLoadActive; const float rowH = ImGui::GetTextLineHeightWithSpacing(); const float listH = rowH * 8 + ImGui::GetStyle().FramePadding.y * 2; @@ -316,11 +317,11 @@ void ImGuiStateShare::draw(bool& open) { } ImGui::SameLine(); - if (!gameRunning) { ImGui::BeginDisabled(); } + if (!gameRunning || loadInProgress) { ImGui::BeginDisabled(); } if (ImGui::Button("Load")) { applyEncodedState(m_states[i].encoded, m_states[i].name); } - if (!gameRunning) { ImGui::EndDisabled(); } + if (!gameRunning || loadInProgress) { ImGui::EndDisabled(); } ImGui::SameLine(); if (ImGui::Button("Copy")) { From cccddee106c7e7f72b93c50a3f77278699c9ada4 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Wed, 22 Apr 2026 22:58:38 -0700 Subject: [PATCH 43/64] Add ability to rotate link on the collection screen --- include/dusk/settings.h | 2 ++ src/d/d_menu_collect.cpp | 7 +++++++ src/dusk/imgui/ImGuiMenuGame.cpp | 5 +++++ src/dusk/settings.cpp | 2 ++ 4 files changed, 16 insertions(+) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 4dbfaa73ef..1b16b00ea8 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -69,6 +69,8 @@ struct UserSettings { ConfigVar invertCameraXAxis; ConfigVar disableMainHUD; ConfigVar pauseOnFocusLost; + ConfigVar enableLinkDollRotation; + // Graphics ConfigVar bloomMode; diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index 46c9c1a9d9..612806d372 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -2399,6 +2399,13 @@ void dMenu_Collect3D_c::_move(u8 param_0, u8 param_1) { posZ = 550.0f; } toItem3Dpos(linkPos.x, posY, posZ, &itemPos); + +#if TARGET_PC + if (dusk::getSettings().game.enableLinkDollRotation) { + const f32 angle = mDoCPd_c::getSubStickX3D(PAD_1) * 2048; + ANGLE_ADD(mLinkAngle, angle); + } else +#endif if (param_0 == 0 && param_1 == 0) { f32 temp = 450.0f; ANGLE_ADD(mLinkAngle, temp); diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index c55836c68d..84de7f6575 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -191,6 +191,11 @@ namespace dusk { ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" "the first released version."); } + + config::ImGuiCheckbox("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables Rotating Link in the collection menu"); + } ImGui::SeparatorText("Difficulty"); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 28397f80f2..bdaa654cc0 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -43,6 +43,7 @@ UserSettings g_userSettings = { .invertCameraXAxis {"game.invertCameraXAxis", false}, .disableMainHUD {"game.disableMainHUD", false}, .pauseOnFocusLost {"game.pauseOnFocusLost", false}, + .enableLinkDollRotation = {"game.enableLinkDollRotation", false }, // Graphics .bloomMode {"game.bloomMode", BloomMode::Classic}, @@ -150,6 +151,7 @@ void registerSettings() { Register(g_userSettings.game.canTransformAnywhere); Register(g_userSettings.game.freeMagicArmor); Register(g_userSettings.game.restoreWiiGlitches); + Register(g_userSettings.game.enableLinkDollRotation); Register(g_userSettings.game.noMissClimbing); Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); From d8a792760260e46e277b9dcee8b765107921f9f7 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Wed, 22 Apr 2026 22:59:56 -0700 Subject: [PATCH 44/64] Clarify tooltip --- src/dusk/imgui/ImGuiMenuGame.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 84de7f6575..1f29039d60 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -194,7 +194,7 @@ namespace dusk { config::ImGuiCheckbox("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables Rotating Link in the collection menu"); + ImGui::SetTooltip("Enables Rotating Link in the collection menu with the C-Stick"); } ImGui::SeparatorText("Difficulty"); From 5a109313cbefc7b683706b4c3efd2c6d3c8f9efd Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Wed, 22 Apr 2026 23:02:11 -0700 Subject: [PATCH 45/64] Minor tooltip cleanup --- src/dusk/imgui/ImGuiMenuGame.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 1f29039d60..7e54f2ffa3 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -194,7 +194,7 @@ namespace dusk { config::ImGuiCheckbox("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables Rotating Link in the collection menu with the C-Stick"); + ImGui::SetTooltip("Enables rotating Link in the collection menu with the C-Stick"); } ImGui::SeparatorText("Difficulty"); From a83b4186af5a2e8b4c74dfcc3ce0d9334ddf4977 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Thu, 23 Apr 2026 03:25:19 -0700 Subject: [PATCH 46/64] fix holding B in the middle of text --- src/d/d_msg_class.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 37085a6bb9..7af18f0732 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -1987,6 +1987,13 @@ bool jmessage_tSequenceProcessor::do_isReady() { } #endif +#if TARGET_PC + if (dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)) { + field_0xb2 = 1; + pReference->setSendTimer(0); + } +#endif + if (dComIfGp_checkMesgBgm()) { bool isItemMusicPlaying = true; if (mDoAud_checkPlayingSubBgmFlag() != Z2BGM_ITEM_GET && From 06e6b0d47ef1f224f84e8c81597d27d5e222d3b6 Mon Sep 17 00:00:00 2001 From: madeline Date: Thu, 23 Apr 2026 03:58:35 -0700 Subject: [PATCH 47/64] fix safety bit on interp fixes #507 --- libs/JSystem/include/JSystem/JUtility/JUTGamePad.h | 3 +++ libs/JSystem/src/JUtility/JUTGamePad.cpp | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/libs/JSystem/include/JSystem/JUtility/JUTGamePad.h b/libs/JSystem/include/JSystem/JUtility/JUTGamePad.h index 30bf685700..45e7227fe3 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTGamePad.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTGamePad.h @@ -263,6 +263,9 @@ public: /* 0x9C */ u8 field_0x9c[4]; /* 0xA0 */ OSTime mResetHoldStartTime; /* 0xA8 */ u8 field_0xa8; +#if TARGET_PC + u32 mResetHoldFrameCount; +#endif }; /** diff --git a/libs/JSystem/src/JUtility/JUTGamePad.cpp b/libs/JSystem/src/JUtility/JUTGamePad.cpp index 091cc382d1..2ddf4397dc 100644 --- a/libs/JSystem/src/JUtility/JUTGamePad.cpp +++ b/libs/JSystem/src/JUtility/JUTGamePad.cpp @@ -64,6 +64,9 @@ BOOL JUTGamePad::init() { void JUTGamePad::clear() { mButtonReset.mReset = false; field_0xa8 = 1; +#if TARGET_PC + mResetHoldFrameCount = 0; +#endif } PADStatus JUTGamePad::mPadStatus[4]; @@ -219,11 +222,19 @@ void JUTGamePad::update() { mButtonReset.mReset = false; } else if (!JUTGamePad::C3ButtonReset::sResetOccurred) { if (mButtonReset.mReset == true) { +#if TARGET_PC + checkResetCallback(++mResetHoldFrameCount * (OS_TIMER_CLOCK / 30)); +#else OSTime hold_time = OSGetTime() - mResetHoldStartTime; checkResetCallback(hold_time); +#endif } else { mButtonReset.mReset = true; +#if TARGET_PC + mResetHoldFrameCount = 0; +#else mResetHoldStartTime = OSGetTime(); +#endif } } From 251c6e7aecb26433a19b49ab87e56f2f0e9b352f Mon Sep 17 00:00:00 2001 From: madeline Date: Thu, 23 Apr 2026 06:01:28 -0700 Subject: [PATCH 48/64] more precise link debug info --- src/dusk/imgui/ImGuiMenuTools.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 97f5b5a8d3..00a03e635b 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -210,7 +210,7 @@ namespace dusk { ImGui::Text("Link"); ImGuiStringViewText( player != nullptr - ? fmt::format("Position: {: .2f}, {: .2f}, {: .2f}\n", player->current.pos.x, player->current.pos.y, player->current.pos.z) + ? fmt::format("Position: {: .4f}, {: .4f}, {: .4f}\n", player->current.pos.x, player->current.pos.y, player->current.pos.z) : "Position: ?, ?, ?\n" ); @@ -222,7 +222,7 @@ namespace dusk { ImGuiStringViewText( player != nullptr - ? fmt::format("Speed: {0}\n", player->speedF) + ? fmt::format("Speed: {: .4f}\n", player->speedF) : "Speed: ?\n" ); @@ -230,7 +230,7 @@ namespace dusk { ImGui::Text("Epona"); ImGuiStringViewText( horse != nullptr - ? fmt::format("Position: {: .2f}, {: .2f}, {: .2f}\n", horse->current.pos.x, horse->current.pos.y, horse->current.pos.z) + ? fmt::format("Position: {: .4f}, {: .4f}, {: .4f}\n", horse->current.pos.x, horse->current.pos.y, horse->current.pos.z) : "Position: ?, ?, ?\n" ); @@ -242,7 +242,7 @@ namespace dusk { ImGuiStringViewText( horse != nullptr - ? fmt::format("Speed: {0}\n", horse->speedF) + ? fmt::format("Speed: {: .4f}\n", horse->speedF) : "Speed: ?\n" ); From c991c7c407ff5b443624b7ba4960c91695a53eaf Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Thu, 23 Apr 2026 09:42:02 -0400 Subject: [PATCH 49/64] Fixes sun songing underwater --- src/d/actor/d_a_alink_dusk.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index 0f442aff94..d53476aa91 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -41,7 +41,7 @@ void daAlink_c::handleWolfHowl() { return; } - bool canTransform = false; + bool canHowl = false; if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY) && !checkMagneBootsOn()) { if (!checkForestOldCentury()) { @@ -52,12 +52,17 @@ void daAlink_c::handleWolfHowl() { (checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) && (checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10)))) { - canTransform = true; + canHowl = true; } } } } + if (!canHowl) { + Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); + return; + } + getWolfHowlMgrP()->setCorrectCurve(9); procWolfHowlDemoInit(); } From 14aeefb8137d2d6ebd42a29493f5e35a1e1a34ae Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Thu, 23 Apr 2026 08:38:47 -0600 Subject: [PATCH 50/64] Frame interp: interpolate ring menu cursor along arc --- include/d/d_menu_ring.h | 4 +++ src/d/d_menu_ring.cpp | 58 ++++++++++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/include/d/d_menu_ring.h b/include/d/d_menu_ring.h index acb7747b88..74624eac80 100644 --- a/include/d/d_menu_ring.h +++ b/include/d/d_menu_ring.h @@ -210,6 +210,10 @@ private: f32 mCursorInterpPrevY; f32 mCursorInterpCurrX; f32 mCursorInterpCurrY; + s16 mCursorInterpPrevAngle; + s16 mCursorInterpCurrAngle; + bool mCursorInterpPrevAngular; + bool mCursorInterpCurrAngular; bool mCursorInterpInit; #endif }; diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index dff9c79091..ba2b86b760 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -190,6 +190,10 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i mCursorInterpPrevY = 0.0f; mCursorInterpCurrX = 0.0f; mCursorInterpCurrY = 0.0f; + mCursorInterpPrevAngle = 0; + mCursorInterpCurrAngle = 0; + mCursorInterpPrevAngular = false; + mCursorInterpCurrAngular = false; mCursorInterpInit = false; #endif for (int i = 0; i < 4; i++) { @@ -639,34 +643,70 @@ void dMenu_Ring_c::_draw() { drawSelectItem(); drawItem2(); #if TARGET_PC + f32 simX = 0.0f; + f32 simY = 0.0f; + bool restoreSimPos = false; if (dusk::frame_interp::is_enabled() && mAlphaRate >= 1.0f) { - f32 cursorX = mpDrawCursor->getPositionX(); - f32 cursorY = mpDrawCursor->getPositionY(); + simX = mpDrawCursor->getPositionX(); + simY = mpDrawCursor->getPositionY(); + + const bool isAngular = (mStatus == STATUS_MOVE) && !mDirectSelectActive; if (dusk::frame_interp::get_ui_tick_pending()) { mCursorInterpPrevX = mCursorInterpCurrX; mCursorInterpPrevY = mCursorInterpCurrY; - mCursorInterpCurrX = cursorX; - mCursorInterpCurrY = cursorY; + mCursorInterpPrevAngle = mCursorInterpCurrAngle; + mCursorInterpPrevAngular = mCursorInterpCurrAngular; - if (!mCursorInterpInit) { + mCursorInterpCurrX = simX; + mCursorInterpCurrY = simY; + mCursorInterpCurrAngle = field_0x66e; + mCursorInterpCurrAngular = isAngular; + + // reset prev = curr for first render pass or + // when angle modes prev/curr differ + // to prevent arrival jitter + if (!mCursorInterpInit || + mCursorInterpPrevAngular != mCursorInterpCurrAngular) { mCursorInterpPrevX = mCursorInterpCurrX; mCursorInterpPrevY = mCursorInterpCurrY; + mCursorInterpPrevAngle = mCursorInterpCurrAngle; + mCursorInterpPrevAngular = mCursorInterpCurrAngular; mCursorInterpInit = true; } } if (mCursorInterpInit) { const f32 step = dusk::frame_interp::get_interpolation_step(); - mpDrawCursor->setPos( - mCursorInterpPrevX + (mCursorInterpCurrX - mCursorInterpPrevX) * step, - mCursorInterpPrevY + (mCursorInterpCurrY - mCursorInterpPrevY) * step - ); + if (mCursorInterpPrevAngular && mCursorInterpCurrAngular) { + const s16 delta = mCursorInterpCurrAngle - mCursorInterpPrevAngle; + const s16 lerpedAngle = mCursorInterpPrevAngle + (s16)(delta * step); + + // yoinked from stick_move_proc() + const f32 x = g_ringHIO.mItemRingPosX + FB_WIDTH_BASE / 2 + + mRingRadiusH * cM_ssin(lerpedAngle); + const f32 y = g_ringHIO.mItemRingPosY + FB_HEIGHT_BASE / 2 + + mRingRadiusV * cM_scos(lerpedAngle); + mpDrawCursor->setPos(x, y); + } else { + mpDrawCursor->setPos( + mCursorInterpPrevX + (mCursorInterpCurrX - mCursorInterpPrevX) * step, + mCursorInterpPrevY + (mCursorInterpCurrY - mCursorInterpPrevY) * step + ); + } + restoreSimPos = true; } } else { mCursorInterpInit = false; } #endif mpDrawCursor->draw(); +#if TARGET_PC + // prevents offsetting at destination on the next frame + // since stick_wait_proc doesn't call setPos and we clobbered mPositionX/Y + if (restoreSimPos) { + mpDrawCursor->setPos(simX, simY); + } +#endif mpItemExplain->trans(mCenterPosX, mCenterPosY); mpItemExplain->draw((J2DOrthoGraph*)grafPort); drawFlag0(); From 4453316bb036423f772cf11491fb3fd277a7068d Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Thu, 23 Apr 2026 14:25:50 -0400 Subject: [PATCH 51/64] Separated gamepad color into its own files --- files.cmake | 1 + include/dusk/gamepad_color.h | 8 +++ src/dusk/gamepad_color.cpp | 106 +++++++++++++++++++++++++++++++++++ src/f_ap/f_ap_game.cpp | 101 +-------------------------------- 4 files changed, 116 insertions(+), 100 deletions(-) create mode 100644 include/dusk/gamepad_color.h create mode 100644 src/dusk/gamepad_color.cpp diff --git a/files.cmake b/files.cmake index c50cac2d8c..32a0996227 100644 --- a/files.cmake +++ b/files.cmake @@ -1348,6 +1348,7 @@ set(DUSK_FILES src/dusk/game_clock.cpp src/dusk/globals.cpp src/dusk/gyro.cpp + src/dusk/gamepad_color.cpp src/dusk/io.cpp src/dusk/layout.cpp src/dusk/logging.cpp diff --git a/include/dusk/gamepad_color.h b/include/dusk/gamepad_color.h new file mode 100644 index 0000000000..c7cfc1e716 --- /dev/null +++ b/include/dusk/gamepad_color.h @@ -0,0 +1,8 @@ +#pragma once + +#ifndef GAMEPAD_COLOR_H +#define GAMEPAD_COLOR_H + +void handleGamepadColor(); + +#endif \ No newline at end of file diff --git a/src/dusk/gamepad_color.cpp b/src/dusk/gamepad_color.cpp new file mode 100644 index 0000000000..cba32da632 --- /dev/null +++ b/src/dusk/gamepad_color.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include +#include + +cXyz currentGamepadColor = {0, 0, 0}; +cXyz finalGamepadColor = {0, 0, 0}; +cXyz additionalGamepadColor = {0, 0, 0}; + +float lerpSpeed = 0.0f; + +const cXyz duskColor = {50, 50, -50}; +const cXyz noColor = {0, 0, 0}; + +cXyz LerpColor(cXyz a, cXyz b, float t) { + return {std::lerp(a.x, b.x, t), std::lerp(a.y, b.y, t), std::lerp(a.z, b.z, t)}; +} + +void FadeLED(cXyz newColor, float speed) { + finalGamepadColor = newColor; + lerpSpeed = speed / 30.0f; +} + +void SetLED(cXyz newColor) { + currentGamepadColor = newColor; + finalGamepadColor = newColor; +} + +void SetGamepadAdditionalColor(cXyz addColor) { + additionalGamepadColor.x = addColor.x; + additionalGamepadColor.y = addColor.y; + additionalGamepadColor.z = addColor.z; +} + +void handleGamepadColor() { + bool setColor = false; + + fopAc_ac_c* zhint = dComIfGp_att_getZHint(); + if (zhint != NULL) { + FadeLED({50, 50, 175}, 2.0f); + setColor = true; + } + + daPy_py_c* player = daPy_getPlayerActorClass(); + daAlink_c* link = daAlink_getAlinkActorClass(); + + if (link != nullptr && !setColor) { + if (link->checkWolf()) { + FadeLED({115, 115, 75}, 5.0f); + setColor = true; + } else { + switch (dComIfGs_getSelectEquipClothes()) { + case dItemNo_WEAR_KOKIRI_e: + FadeLED({0, 100, 0}, 5.0f); + setColor = true; + break; + case dItemNo_WEAR_ZORA_e: + FadeLED({0, 0, 100}, 5.0f); + setColor = true; + break; + case dItemNo_ARMOR_e: + if (link->checkMagicArmorHeavy()) { + FadeLED({5, 100, 100}, 5.0f); + } else { + FadeLED({100, 0, 5}, 5.0f); + } + setColor = true; + break; + case dItemNo_WEAR_CASUAL_e: + FadeLED({235, 230, 115}, 5.0f); + setColor = true; + break; + } + } + } + + if (dKy_darkworld_check()) { + SetGamepadAdditionalColor(duskColor); + } else { + SetGamepadAdditionalColor(noColor); + } + + f32 finalRed = finalGamepadColor.x + additionalGamepadColor.x; + f32 finalGreen = finalGamepadColor.y + additionalGamepadColor.y; + f32 finalBlue = finalGamepadColor.z + additionalGamepadColor.z; + + if (finalRed > 255) + finalRed = 255; + if (finalRed < 0) + finalRed = 0; + + if (finalGreen > 255) + finalGreen = 255; + if (finalGreen < 0) + finalGreen = 0; + + if (finalBlue > 255) + finalBlue = 255; + if (finalBlue < 0) + finalBlue = 0; + + currentGamepadColor = LerpColor(currentGamepadColor, cXyz{finalRed, finalGreen, finalBlue}, lerpSpeed); + PADSetColor(PAD_CHAN0, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, (u8)currentGamepadColor.z); +} \ No newline at end of file diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 9e87acccc7..78fc240db4 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -23,6 +23,7 @@ #include "m_Do/m_Do_graphic.h" #include "m_Do/m_Do_main.h" #include "tracy/Tracy.hpp" +#include fapGm_HIO_c::fapGm_HIO_c() { mUsingHostIO = true; @@ -733,106 +734,6 @@ static void fapGm_AfterRecord() { fapGm_After(); } -cXyz currentGamepadColor = {0, 0, 0}; -cXyz finalGamepadColor = {0, 0, 0}; -cXyz additionalGamepadColor = {0, 0, 0}; - -float lerpSpeed = 0.0f; - -const cXyz duskColor = {50, 50, -50}; -const cXyz noColor = {0, 0, 0}; - -cXyz LerpColor(cXyz a, cXyz b, float t) { - return {std::lerp(a.x, b.x, t), std::lerp(a.y, b.y, t), std::lerp(a.z, b.z, t)}; -} - -void FadeLED(cXyz newColor, float speed) { - finalGamepadColor = newColor; - lerpSpeed = speed / 30.0f; -} - -void SetLED(cXyz newColor) { - currentGamepadColor = newColor; - finalGamepadColor = newColor; -} - -void SetGamepadAdditionalColor(cXyz addColor) { - additionalGamepadColor.x = addColor.x; - additionalGamepadColor.y = addColor.y; - additionalGamepadColor.z = addColor.z; -} - -void handleGamepadColor() { - bool setColor = false; - - fopAc_ac_c* zhint = dComIfGp_att_getZHint(); - if (zhint != NULL) { - FadeLED({50, 50, 175}, 2.0f); - setColor = true; - } - - daPy_py_c* player = daPy_getPlayerActorClass(); - daAlink_c* link = daAlink_getAlinkActorClass(); - - if (link != nullptr && !setColor) { - if (link->checkWolf()) { - FadeLED({115, 115, 75}, 5.0f); - setColor = true; - } else { - switch (dComIfGs_getSelectEquipClothes()) { - case dItemNo_WEAR_KOKIRI_e: - FadeLED({0, 100, 0}, 5.0f); - setColor = true; - break; - case dItemNo_WEAR_ZORA_e: - FadeLED({0, 0, 100}, 5.0f); - setColor = true; - break; - case dItemNo_ARMOR_e: - if (link->checkMagicArmorHeavy()) { - FadeLED({5, 100, 100}, 5.0f); - } else { - FadeLED({100, 0, 5}, 5.0f); - } - setColor = true; - break; - case dItemNo_WEAR_CASUAL_e: - FadeLED({235, 230, 115}, 5.0f); - setColor = true; - break; - } - } - } - - if (dKy_darkworld_check()) { - SetGamepadAdditionalColor(duskColor); - } else { - SetGamepadAdditionalColor(noColor); - } - - f32 finalRed = finalGamepadColor.x + additionalGamepadColor.x; - f32 finalGreen = finalGamepadColor.y + additionalGamepadColor.y; - f32 finalBlue = finalGamepadColor.z + additionalGamepadColor.z; - - if (finalRed > 255) - finalRed = 255; - if (finalRed < 0) - finalRed = 0; - - if (finalGreen > 255) - finalGreen = 255; - if (finalGreen < 0) - finalGreen = 0; - - if (finalBlue > 255) - finalBlue = 255; - if (finalBlue < 0) - finalBlue = 0; - - currentGamepadColor = LerpColor(currentGamepadColor, cXyz{finalRed, finalGreen, finalBlue}, lerpSpeed); - PADSetColor(PAD_CHAN0, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, (u8)currentGamepadColor.z); -} - static void duskExecute() { handleGamepadColor(); From 1e6e1976e343124fbe85b34c6c2602aacc760d31 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 23 Apr 2026 13:07:42 -0600 Subject: [PATCH 52/64] Clamp max LOD for Wolf Link and Midna eyes --- src/d/actor/d_a_alink_wolf.inc | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/d/actor/d_a_alink_wolf.inc b/src/d/actor/d_a_alink_wolf.inc index 0a6da841c5..063c5253f1 100644 --- a/src/d/actor/d_a_alink_wolf.inc +++ b/src/d/actor/d_a_alink_wolf.inc @@ -149,6 +149,23 @@ void daAlink_c::changeWolf() { mpLinkModel = initModel(static_cast(dComIfG_getObjectRes(l_wArcName, 14)), 0x20200); +#ifdef TARGET_PC + // Update Wolf Link's eye maxLOD to prevent the eyes from disappearing + { + J3DTexture* tex = mpLinkModel->getModelData()->getTexture(); + JUTNameTab* nametable = mpLinkModel->getModelData()->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != NULL && strcmp(tex_name, "wl_eyeball") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } + } +#endif + J3DModelData* chainModelData = static_cast(dComIfG_getObjectRes(l_wArcName, 15)); for (u16 i = 0; i < 4; i++) { mpWlChainModels[i] = initModel(chainModelData, 0); @@ -162,6 +179,23 @@ void daAlink_c::changeWolf() { mpWlMidnaHairModel = initModelEnv(static_cast(dComIfG_getObjectRes(l_wArcName, 11)), 0x1000000); +#ifdef TARGET_PC + // Update Midna's eye maxLOD to prevent the eyes from disappearing + { + J3DTexture* tex = mpWlMidnaModel->getModelData()->getTexture(); + JUTNameTab* nametable = mpWlMidnaModel->getModelData()->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != NULL && strcmp(tex_name, "midona_eyeball") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } + } +#endif + mpDMidnaBrk = static_cast(dComIfG_getObjectRes(l_wArcName, 18)); mpDMidnaBrk->searchUpdateMaterialID(mpWlMidnaModel->getModelData()); mpWlMidnaModel->getModelData()->entryTevRegAnimator(mpDMidnaBrk); From 23130d5a57fd6dc2f792927bd7539d0d0cb9d01c Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Thu, 23 Apr 2026 15:08:34 -0400 Subject: [PATCH 53/64] Thinner map lines at higher resolution Affects both the Map Menu and the Mini-Map. --- src/d/d_map_path.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index 916bd2c223..8288a2ef9f 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -237,6 +237,13 @@ void dDrawPath_c::rendering(dDrawPath_c::line_class const* p_line) { if (isDrawType(p_line->field_0x0)) { int width = getLineWidth(p_line->field_0x1); + #if TARGET_PC + f32 height = JUTVideo::getManager()->getRenderHeight() / 448.0f; + if (height > 1.0f) { + width /= 2; + } + #endif + if (width > 0 && p_line->mDataNum >= 2) { GXSetLineWidth(width, GX_TO_ZERO); GXSetTevColor(GX_TEVREG0, *getLineColor(p_line->field_0x0 & 0x3F, p_line->field_0x1)); From dfdac1c1cd2ba4fde1fde47ec373edfebf4dd693 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Thu, 23 Apr 2026 15:11:51 -0700 Subject: [PATCH 54/64] update setup instructions --- README.md | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 7322441b74..c2fe53576d 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,19 @@ First make sure your dump of the game is clean and supported by Dusk. You can do this by checking the sha1 hash of your dump against this list of supported versions. | Version | sha1 hash | -| ------------ | ---------------------------------------- | +|--------------| ---------------------------------------- | | GameCube USA | 75edd3ddff41f125d1b4ce1a40378f1b565519e7 | +| GameCube PAL | 2601822a488eeb86fb89db16ca8f29c2c953e1ca | ### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases) ### 3. Setup the game -#### Windows - Extract the zip folder -- Place your dump of the game into the same folder where you extracted to -- Launch `dusk.exe` +- Launch Dusk +- Select Options, then set the ISO Path to your supported game dump +- Press Start Game to play! -#### macOS -- TODO - -#### Linux -- TODO - -#### iOS -- TODO - -#### android -- TODO +![DuskOptions](assets/dusk_options.png) # Building If you'd like to build Dusk from source, please read the [build instructions](docs/building.md). From f04a0ffcf1293e57f87398ee201adaa76c252d07 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Thu, 23 Apr 2026 15:12:40 -0700 Subject: [PATCH 55/64] forgot pic oops --- assets/dusk_options.png | Bin 0 -> 83757 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/dusk_options.png diff --git a/assets/dusk_options.png b/assets/dusk_options.png new file mode 100644 index 0000000000000000000000000000000000000000..4ed4ad7563d0325f41ab82dd605f9236d546f320 GIT binary patch literal 83757 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

MQ6fww0xU&2G(=Sp+OSruCRGqy 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 Date: Thu, 23 Apr 2026 21:05:38 -0600 Subject: [PATCH 56/64] Fix a couple issues for Xcode projects (#513) * Link separate JSystem libraries due to control.cpp This allows the Xcode generator to work, otherwise it breaks on the duplicate control.o files within the game_debug library. * Fix __memcpy define on GCC * Try LINK_GROUP:RESCAN for GNU ld * Combine dusk/game_base/game_debug targets * Fix compile defs syntax --- CMakeLists.txt | 79 ++++++++++++++++++++++++++++------------- extern/aurora | 2 +- files.cmake | 86 +++++++++++++++++++++++++++++++++++++++++++-- include/global.h | 12 ++++++- src/dusk/extras.c | 26 +++++++++++--- src/dusk/extras.cpp | 27 -------------- 6 files changed, 170 insertions(+), 62 deletions(-) delete mode 100644 src/dusk/extras.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 77cd0b9694..19fd5d673f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,11 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +# Folder-based instead of target-based organization +# in Visual Studio and Xcode generators +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "_cmake") + if (CMAKE_SYSTEM_NAME STREQUAL Linux) set(DAWN_USE_WAYLAND ON CACHE BOOL "Enable support for Wayland surface" FORCE) endif () @@ -281,7 +286,7 @@ set(DUSK_TP_VERSION ${DUSK_GAME_NAME}${DUSK_GAME_VERSION}) message(STATUS "dusk: Game Version: ${DUSK_TP_VERSION}") -source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${JSYSTEM_FILES} ${JSYSTEM_DEBUG_FILES} ${REL_FILES}) +source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES}) source_group("dusk" FILES ${DUSK_FILES}) set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 @@ -374,41 +379,65 @@ endif () # game_debug is for game code files that we know work when compiled with DEBUG=1 # Of course, if building a release build, this distinction is irrelevant -add_library(game_debug OBJECT ${JSYSTEM_DEBUG_FILES} ${SSYSTEM_FILES} +set(GAME_DEBUG_FILES + ${SSYSTEM_FILES} src/dusk/audio/DuskAudioSystem.cpp src/dusk/audio/JASCriticalSection.cpp src/dusk/audio/DuskDsp.cpp src/dusk/audio/Adpcm.cpp src/dusk/audio/DspStub.cpp - src/dusk/imgui/ImGuiAudio.cpp) + src/dusk/imgui/ImGuiAudio.cpp +) +set_source_files_properties( + ${GAME_DEBUG_FILES} + PROPERTIES + COMPILE_DEFINITIONS "$<$:DEBUG=1>;$<$:PARTIAL_DEBUG=1>" +) # game_base is for all other game code files -add_library(game_base OBJECT ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${JSYSTEM_FILES} ${REL_FILES} ${DUSK_FILES} ${DOLPHIN_FILES}) +set(GAME_BASE_FILES + ${DOLZEL_FILES} + ${Z2AUDIOLIB_FILES} + ${REL_FILES} + ${DUSK_FILES} + ${DOLPHIN_FILES} +) +set_source_files_properties( + ${GAME_BASE_FILES} + PROPERTIES + COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0;$<$:PARTIAL_DEBUG=1>" +) -target_compile_definitions(game_debug PRIVATE ${GAME_COMPILE_DEFS} $<$:DEBUG=1> $<$:PARTIAL_DEBUG=1>) -target_compile_definitions(game_base PRIVATE ${GAME_COMPILE_DEFS} NDEBUG=1 NDEBUG_DEFINED=1 DEBUG_DEFINED=0 $<$:PARTIAL_DEBUG=1>) +foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES) + target_compile_definitions(${jsystem_lib} PRIVATE + ${GAME_COMPILE_DEFS} + $<$:DEBUG=1> + $<$:PARTIAL_DEBUG=1> + ) + target_include_directories(${jsystem_lib} PRIVATE ${GAME_INCLUDE_DIRS}) + target_link_libraries(${jsystem_lib} PRIVATE ${GAME_LIBS}) + set_target_properties(${jsystem_lib} PROPERTIES FOLDER "JSystem") +endforeach() -# only apply PCH to game_base since not all headers are necessarily validated with DEBUG=1 -target_precompile_headers(game_base PRIVATE "$<$:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>") - -target_include_directories(game_debug PRIVATE ${GAME_INCLUDE_DIRS}) -target_include_directories(game_base PRIVATE ${GAME_INCLUDE_DIRS}) - -# This implicitly pulls in the library include directories even though no -# linking actually takes place for object libraries -target_link_libraries(game_debug PRIVATE ${GAME_LIBS}) -target_link_libraries(game_base PRIVATE ${GAME_LIBS}) - -if(ANDROID) - add_library(dusk SHARED src/dusk/main.cpp) - set_target_properties(dusk PROPERTIES OUTPUT_NAME main) -else () - add_executable(dusk src/dusk/main.cpp) +set(JSYSTEM_LINK_LIBRARIES ${JSYSTEM_LIBRARIES}) +if (CMAKE_CXX_LINK_GROUP_USING_RESCAN_SUPPORTED OR CMAKE_LINK_GROUP_USING_RESCAN_SUPPORTED) + # GNU ld resolves static archives in a single left-to-right pass. The split + # JSystem libraries reference each other, so they need a RESCAN group there. + set(JSYSTEM_LINK_LIBRARIES "$") endif () -target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0) -target_include_directories(dusk PRIVATE include) -target_link_libraries(dusk PRIVATE game_base game_debug aurora::main) +set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES}) +if(ANDROID) + add_library(dusk SHARED ${DUSK_FILES}) + set_target_properties(dusk PROPERTIES OUTPUT_NAME main) +else () + add_executable(dusk ${DUSK_FILES}) +endif () + +target_compile_definitions(dusk PRIVATE ${GAME_COMPILE_DEFS}) +target_include_directories(dusk PRIVATE ${GAME_INCLUDE_DIRS}) +target_link_libraries(dusk PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES}) +target_precompile_headers(dusk PRIVATE "$<$:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>") if (TARGET crashpad_handler) add_dependencies(dusk crashpad_handler) endif () diff --git a/extern/aurora b/extern/aurora index 63550a8375..524b683fe0 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 63550a83759974dd18bc13cd420888188be9caf9 +Subproject commit 524b683fe07710350358846fe31155bbc8b60fc5 diff --git a/files.cmake b/files.cmake index 32a0996227..af101b6c47 100644 --- a/files.cmake +++ b/files.cmake @@ -314,7 +314,7 @@ set(SSYSTEM_FILES src/SSystem/SStandard/s_basic.cpp ) -set(JSYSTEM_DEBUG_FILES +add_library(JSystem_JParticle STATIC libs/JSystem/src/JParticle/JPAResourceManager.cpp libs/JSystem/src/JParticle/JPAResource.cpp libs/JSystem/src/JParticle/JPABaseShape.cpp @@ -330,10 +330,19 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JParticle/JPAEmitter.cpp libs/JSystem/src/JParticle/JPAParticle.cpp libs/JSystem/src/JParticle/JPAMath.cpp +) + +add_library(JSystem_JFramework STATIC libs/JSystem/src/JFramework/JFWSystem.cpp libs/JSystem/src/JFramework/JFWDisplay.cpp +) + +add_library(JSystem_J3DU STATIC libs/JSystem/src/J3DU/J3DUClipper.cpp libs/JSystem/src/J3DU/J3DUDL.cpp +) + +add_library(JSystem_JKernel STATIC libs/JSystem/src/JKernel/JKRHeap.cpp libs/JSystem/src/JKernel/JKRExpHeap.cpp libs/JSystem/src/JKernel/JKRSolidHeap.cpp @@ -359,14 +368,23 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JKernel/JKRDvdRipper.cpp libs/JSystem/src/JKernel/JKRDvdAramRipper.cpp libs/JSystem/src/JKernel/JKRDecomp.cpp +) + +add_library(JSystem_JMath STATIC libs/JSystem/src/JMath/JMath.cpp libs/JSystem/src/JMath/random.cpp libs/JSystem/src/JMath/JMATrigonometric.cpp +) + +add_library(JSystem_JSupport STATIC libs/JSystem/src/JSupport/JSUList.cpp libs/JSystem/src/JSupport/JSUInputStream.cpp libs/JSystem/src/JSupport/JSUOutputStream.cpp libs/JSystem/src/JSupport/JSUMemoryStream.cpp libs/JSystem/src/JSupport/JSUFileStream.cpp +) + +add_library(JSystem_JUtility STATIC libs/JSystem/src/JUtility/JUTCacheFont.cpp libs/JSystem/src/JUtility/JUTResource.cpp libs/JSystem/src/JUtility/JUTTexture.cpp @@ -387,6 +405,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JUtility/JUTConsole.cpp libs/JSystem/src/JUtility/JUTDirectFile.cpp libs/JSystem/src/JUtility/JUTFontData_Ascfont_fix12.cpp +) + +add_library(JSystem_JStage STATIC libs/JSystem/src/JStage/JSGActor.cpp libs/JSystem/src/JStage/JSGAmbientLight.cpp libs/JSystem/src/JStage/JSGCamera.cpp @@ -394,6 +415,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JStage/JSGLight.cpp libs/JSystem/src/JStage/JSGObject.cpp libs/JSystem/src/JStage/JSGSystem.cpp +) + +add_library(JSystem_J2DGraph STATIC libs/JSystem/src/J2DGraph/J2DGrafContext.cpp libs/JSystem/src/J2DGraph/J2DOrthoGraph.cpp libs/JSystem/src/J2DGraph/J2DTevs.cpp @@ -412,6 +436,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/J2DGraph/J2DAnmLoader.cpp libs/JSystem/src/J2DGraph/J2DAnimation.cpp libs/JSystem/src/J2DGraph/J2DManage.cpp +) + +add_library(JSystem_J3DGraphBase STATIC libs/JSystem/src/J3DGraphBase/J3DGD.cpp libs/JSystem/src/J3DGraphBase/J3DSys.cpp libs/JSystem/src/J3DGraphBase/J3DVertex.cpp @@ -426,6 +453,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/J3DGraphBase/J3DTevs.cpp libs/JSystem/src/J3DGraphBase/J3DDrawBuffer.cpp libs/JSystem/src/J3DGraphBase/J3DStruct.cpp +) + +add_library(JSystem_J3DGraphAnimator STATIC libs/JSystem/src/J3DGraphAnimator/J3DShapeTable.cpp libs/JSystem/src/J3DGraphAnimator/J3DJointTree.cpp libs/JSystem/src/J3DGraphAnimator/J3DModelData.cpp @@ -437,6 +467,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/J3DGraphAnimator/J3DCluster.cpp libs/JSystem/src/J3DGraphAnimator/J3DJoint.cpp libs/JSystem/src/J3DGraphAnimator/J3DMaterialAttach.cpp +) + +add_library(JSystem_J3DGraphLoader STATIC libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory.cpp libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp libs/JSystem/src/J3DGraphLoader/J3DClusterLoader.cpp @@ -445,6 +478,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/J3DGraphLoader/J3DJointFactory.cpp libs/JSystem/src/J3DGraphLoader/J3DShapeFactory.cpp libs/JSystem/src/J3DGraphLoader/J3DAnmLoader.cpp +) + +add_library(JSystem_JStudio STATIC libs/JSystem/src/JStudio/JStudio/ctb.cpp libs/JSystem/src/JStudio/JStudio/ctb-data.cpp libs/JSystem/src/JStudio/JStudio/functionvalue.cpp @@ -459,6 +495,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JStudio/JStudio/stb.cpp libs/JSystem/src/JStudio/JStudio/stb-data-parse.cpp libs/JSystem/src/JStudio/JStudio/stb-data.cpp +) + +add_library(JSystem_JStudio_JStage STATIC libs/JSystem/src/JStudio/JStudio_JStage/control.cpp libs/JSystem/src/JStudio/JStudio_JStage/object.cpp libs/JSystem/src/JStudio/JStudio_JStage/object-actor.cpp @@ -466,10 +505,19 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JStudio/JStudio_JStage/object-camera.cpp libs/JSystem/src/JStudio/JStudio_JStage/object-fog.cpp libs/JSystem/src/JStudio/JStudio_JStage/object-light.cpp +) + +add_library(JSystem_JStudio_JAudio2 STATIC libs/JSystem/src/JStudio/JStudio_JAudio2/control.cpp libs/JSystem/src/JStudio/JStudio_JAudio2/object-sound.cpp +) + +add_library(JSystem_JStudio_JParticle STATIC libs/JSystem/src/JStudio/JStudio_JParticle/control.cpp libs/JSystem/src/JStudio/JStudio_JParticle/object-particle.cpp +) + +add_library(JSystem_JAudio2 STATIC libs/JSystem/src/JAudio2/JASCalc.cpp libs/JSystem/src/JAudio2/JASTaskThread.cpp libs/JSystem/src/JAudio2/JASDvdThread.cpp @@ -534,22 +582,34 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JAudio2/JAUSoundAnimator.cpp libs/JSystem/src/JAudio2/JAUSoundTable.cpp libs/JSystem/src/JAudio2/JAUStreamFileTable.cpp +) + +add_library(JSystem_JMessage STATIC libs/JSystem/src/JMessage/control.cpp libs/JSystem/src/JMessage/data.cpp libs/JSystem/src/JMessage/processor.cpp libs/JSystem/src/JMessage/resource.cpp libs/JSystem/src/JMessage/locale.cpp +) + +add_library(JSystem_JGadget STATIC libs/JSystem/src/JGadget/binary.cpp libs/JSystem/src/JGadget/define.cpp libs/JSystem/src/JGadget/linklist.cpp libs/JSystem/src/JGadget/search.cpp libs/JSystem/src/JGadget/std-vector.cpp +) + +add_library(JSystem_JAHostIO STATIC libs/JSystem/src/JAHostIO/JAHFrameNode.cpp libs/JSystem/src/JAHostIO/JAHioMessage.cpp libs/JSystem/src/JAHostIO/JAHioMgr.cpp libs/JSystem/src/JAHostIO/JAHioNode.cpp libs/JSystem/src/JAHostIO/JAHioUtil.cpp libs/JSystem/src/JAHostIO/JAHVirtualNode.cpp +) + +add_library(JSystem_JHostIO STATIC libs/JSystem/src/JHostIO/JORFile.cpp libs/JSystem/src/JHostIO/JORHostInfo.cpp libs/JSystem/src/JHostIO/JORMessageBox.cpp @@ -559,7 +619,28 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JHostIO/JHIMccBuf.cpp ) -set(JSYSTEM_FILES +set(JSYSTEM_LIBRARIES + JSystem_JParticle + JSystem_JFramework + JSystem_J3DU + JSystem_JKernel + JSystem_JMath + JSystem_JSupport + JSystem_JUtility + JSystem_JStage + JSystem_J2DGraph + JSystem_J3DGraphBase + JSystem_J3DGraphAnimator + JSystem_J3DGraphLoader + JSystem_JStudio + JSystem_JStudio_JStage + JSystem_JStudio_JAudio2 + JSystem_JStudio_JParticle + JSystem_JAudio2 + JSystem_JMessage + JSystem_JGadget + JSystem_JAHostIO + JSystem_JHostIO ) set(REL_FILES @@ -1341,7 +1422,6 @@ set(DUSK_FILES src/dusk/crash_reporting.cpp src/dusk/endian.cpp src/dusk/extras.c - src/dusk/extras.cpp src/dusk/file_select.cpp src/dusk/file_select.hpp src/dusk/frame_interpolation.cpp diff --git a/include/global.h b/include/global.h index eda2a610ec..9301d12e11 100644 --- a/include/global.h +++ b/include/global.h @@ -73,6 +73,9 @@ #endif #ifndef __MWERKS__ +#ifdef __cplusplus +extern "C" { +#endif // Silence clangd errors about MWCC PPC intrinsics by declaring them here. extern int __cntlzw(unsigned int); extern int __rlwimi(int, int, int, int, int); @@ -80,7 +83,14 @@ extern void __dcbf(void*, int); extern void __dcbz(void*, int); extern void __sync(); extern int __abs(int); -void* __memcpy(void*, const void*, int); +#if defined(__has_builtin) && __has_builtin(__builtin_memcpy) +#define __memcpy __builtin_memcpy +#else +#define __memcpy memcpy +#endif +#ifdef __cplusplus +} +#endif #endif #ifndef M_PI diff --git a/src/dusk/extras.c b/src/dusk/extras.c index e693985479..a1fb26e998 100644 --- a/src/dusk/extras.c +++ b/src/dusk/extras.c @@ -3,6 +3,27 @@ #include #include +#ifdef _MSC_VER +#include +#endif + +void __dcbz(void* addr, int offset) { + // Gekko cache lines are 32 bytes. + // dcbz usually requires addr to be 32-byte aligned. + memset((char*)addr + offset, 0, 32); +} + +int __cntlzw(unsigned int val) { + if (val == 0) return 32; // PowerPC returns 32 if the input is 0 +#ifdef _MSC_VER + unsigned long idx; + _BitScanReverse(&idx, val); + return 31 - (int)idx; +#else + return __builtin_clz(val); +#endif +} + #ifndef _MSC_VER int stricmp(const char* str1, const char* str2) { char a_var; @@ -48,11 +69,6 @@ int strnicmp(const char* str1, const char* str2, int n) { } #endif - -void *_memcpy(void* dest, void const* src, int n) { - return memcpy(dest, src, n); -} - void DCZeroRange(void* addr, uint32_t nBytes) { #if defined(_MSC_VER) || TARGET_ANDROID memset(addr, 0, nBytes); diff --git a/src/dusk/extras.cpp b/src/dusk/extras.cpp deleted file mode 100644 index de402f028b..0000000000 --- a/src/dusk/extras.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// C++ Mangled version of extras.c -#include -#include -#ifdef _MSC_VER -#include -#endif - -void *__memcpy(void* dest, void const* src, int n) { - return memcpy(dest, src, n); -} - -void __dcbz(void* addr, int offset) { - // Gekko cache lines are 32 bytes. - // dcbz usually requires addr to be 32-byte aligned. - memset((char*)addr + offset, 0, 32); -} - -int __cntlzw(unsigned int val) { - if (val == 0) return 32; // PowerPC returns 32 if the input is 0 -#ifdef _MSC_VER - unsigned long idx; - _BitScanReverse(&idx, val); - return 31 - (int)idx; -#else - return __builtin_clz(val); -#endif -} From d625c7ab0ce686c8723241d6d902808531d2b696 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Thu, 23 Apr 2026 20:26:50 -0700 Subject: [PATCH 57/64] fix save editor clothes change crash --- src/dusk/imgui/ImGuiSaveEditor.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/dusk/imgui/ImGuiSaveEditor.cpp b/src/dusk/imgui/ImGuiSaveEditor.cpp index 5802c07b47..e91b718895 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.cpp +++ b/src/dusk/imgui/ImGuiSaveEditor.cpp @@ -10,6 +10,7 @@ #include "d/d_item_data.h" #include "d/d_meter2_info.h" #include "d/d_save.h" +#include "d/actor/d_a_player.h" #include @@ -579,20 +580,21 @@ namespace dusk { if (ImGui::BeginCombo("Clothes", itemMap.find(statusA.mSelectEquip[0])->second.m_name.c_str())) { - if (ImGui::Selectable("None")) { - statusA.mSelectEquip[0] = dItemNo_NONE_e; - } if (ImGui::Selectable("Ordon Clothes")) { - statusA.mSelectEquip[0] = dItemNo_WEAR_CASUAL_e; + dMeter2Info_setCloth(dItemNo_WEAR_CASUAL_e, false); + daPy_getPlayerActorClass()->setClothesChange(0); } if (ImGui::Selectable("Hero's Clothes")) { - statusA.mSelectEquip[0] = dItemNo_WEAR_KOKIRI_e; + dMeter2Info_setCloth(dItemNo_WEAR_KOKIRI_e, false); + daPy_getPlayerActorClass()->setClothesChange(0); } if (ImGui::Selectable("Zora Armor")) { - statusA.mSelectEquip[0] = dItemNo_WEAR_ZORA_e; + dMeter2Info_setCloth(dItemNo_WEAR_ZORA_e, false); + daPy_getPlayerActorClass()->setClothesChange(0); } if (ImGui::Selectable("Magic Armor")) { - statusA.mSelectEquip[0] = dItemNo_ARMOR_e; + dMeter2Info_setCloth(dItemNo_ARMOR_e, false); + daPy_getPlayerActorClass()->setClothesChange(0); } ImGui::EndCombo(); } From 3cb5e5172b5d5c5dbf68df8b73e6327a1eb7fe23 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 23 Apr 2026 23:42:15 -0600 Subject: [PATCH 58/64] Add Adult Link eye LOD fix --- src/d/actor/d_a_alink_wolf.inc | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/d/actor/d_a_alink_wolf.inc b/src/d/actor/d_a_alink_wolf.inc index 0a6da841c5..0e4e8f99c8 100644 --- a/src/d/actor/d_a_alink_wolf.inc +++ b/src/d/actor/d_a_alink_wolf.inc @@ -342,6 +342,26 @@ void daAlink_c::changeLink(int param_0) { initModel(static_cast(dComIfG_getObjectRes(mArcName, "zl_face.bmd")), 0x20200); } +#ifdef TARGET_PC + // Update Adult Link's eye maxLOD to prevent the eyes from disappearing + { + J3DTexture* tex = mpLinkFaceModel->getModelData()->getTexture(); + JUTNameTab* nametable = mpLinkFaceModel->getModelData()->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != nullptr && + (strcmp(tex_name, "al_eyeball") == 0 || strcmp(tex_name, "highlight02") == 0 || + strcmp(tex_name, "eye_kage01") == 0)) + { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } + } +#endif + modelData = static_cast(dComIfG_getObjectRes(mArcName, "al_bootsH.bmd")); u16 i; for (i = 0; i < 2; i++) { From daf4b1dfeba637a8c6962a4f326fadfbb7bbddbc Mon Sep 17 00:00:00 2001 From: CraftyBoss Date: Thu, 23 Apr 2026 23:31:07 -0700 Subject: [PATCH 59/64] update aurora, add pre launch option to determine save file type (gci or raw) --- extern/aurora | 2 +- include/dusk/settings.h | 1 + src/dusk/imgui/ImGuiPreLaunchWindow.cpp | 28 +++++++++++++++++++++++++ src/dusk/settings.cpp | 4 +++- src/m_Do/m_Do_MemCard.cpp | 2 ++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/extern/aurora b/extern/aurora index 524b683fe0..be1395c134 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 524b683fe07710350358846fe31155bbc8b60fc5 +Subproject commit be1395c1343a5587cceea25754daa5cacce34f76 diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 6596adf92d..039e51af7d 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -127,6 +127,7 @@ struct UserSettings { ConfigVar wasPresetChosen; ConfigVar enableCrashReporting; ConfigVar duskMenuOpen; + ConfigVar cardFileType; } backend; }; diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp index ecbd41e158..8eff1be8d4 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -46,6 +46,17 @@ static std::string ShowIsoInvalidError(const iso::ValidationError code) { } } +static std::string_view card_type_name(CARDFileType type) { + switch (type) { + case CARD_GCIFOLDER: + return "GCI Folder"sv; + case CARD_RAWIMAGE: + return "Card Image"sv; + default: + return ""sv; + } +} + void fileDialogCallback(void* userdata, const char* path, const char* error) { auto* self = static_cast(userdata); if (error != nullptr) { @@ -216,6 +227,23 @@ void ImGuiPreLaunchWindow::drawOptions() { if (configuredBackendId != m_initialGraphicsBackend) { ImGui::TextDisabled("Restart Required"); } + auto curFileType = (CARDFileType)getSettings().backend.cardFileType.getValue(); + + if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) {\ + + if (ImGui::Selectable("GCI Folder", curFileType == CARD_GCIFOLDER)) { + getSettings().backend.cardFileType.setValue(CARD_GCIFOLDER); + config::Save(); + } + + if (ImGui::Selectable("Card Image", curFileType == CARD_RAWIMAGE)) { + getSettings().backend.cardFileType.setValue(CARD_RAWIMAGE); + config::Save(); + } + + ImGui::EndCombo(); + } + ImGui::EndChild(); } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index bdaa654cc0..3e6ee733fe 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -99,7 +99,8 @@ UserSettings g_userSettings = { .showPipelineCompilation {"backend.showPipelineCompilation", false}, .wasPresetChosen {"backend.wasPresetChosen", false}, .enableCrashReporting {"backend.enableCrashReporting", true}, - .duskMenuOpen {"backend.duskMenuOpen", false} + .duskMenuOpen {"backend.duskMenuOpen", false}, + .cardFileType {"backend.cardFileType", static_cast(CARD_GCIFOLDER)} } }; @@ -185,6 +186,7 @@ void registerSettings() { Register(g_userSettings.backend.wasPresetChosen); Register(g_userSettings.backend.enableCrashReporting); Register(g_userSettings.backend.duskMenuOpen); + Register(g_userSettings.backend.cardFileType); } // Transient settings diff --git a/src/m_Do/m_Do_MemCard.cpp b/src/m_Do/m_Do_MemCard.cpp index a0c0302b09..5cb612f128 100644 --- a/src/m_Do/m_Do_MemCard.cpp +++ b/src/m_Do/m_Do_MemCard.cpp @@ -77,6 +77,8 @@ static OSThread MemCardThread; void mDoMemCd_Ctrl_c::ThdInit() { #if !PLATFORM_SHIELD + CARDSetLoadType((CARDFileType)dusk::getSettings().backend.cardFileType.getValue()); + CARDInit(DUSK_GAME_NAME, DUSK_GAME_VERSION); #endif From 78301a8a83addfb5ac4f9771a163c7ff3533254c Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 24 Apr 2026 00:39:03 -0600 Subject: [PATCH 60/64] Restore half-size in drawDepth2 --- src/m_Do/m_Do_graphic.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index 4dc505ac03..425d1e25fe 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -1029,15 +1029,8 @@ static void drawDepth2(view_class* param_0, view_port_class* param_1, int param_ GX_FALSE, 0); } - #if TARGET_PC - // use full size for pc for higher quality background elements - u16 halfWidth = width; - u16 halfHeight = height; - #else u16 halfWidth = width >> 1; u16 halfHeight = height >> 1; - #endif - GXRenderModeObj* sp24 = JUTGetVideoManager()->getRenderMode(); GXSetCopyFilter(GX_FALSE, NULL, GX_TRUE, sp24->vfilter); GXSetTexCopySrc(x_orig, y_orig_pos, width, height); From 1c00e2cdde9e1f22db6171350482f25dc6add9fc Mon Sep 17 00:00:00 2001 From: CraftyBoss Date: Thu, 23 Apr 2026 23:40:20 -0700 Subject: [PATCH 61/64] fix unsafe cstr usage in flag editor, remove stray backslash --- src/dusk/imgui/ImGuiPreLaunchWindow.cpp | 2 +- src/dusk/imgui/ImGuiSaveEditor.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp index 8eff1be8d4..2727e78718 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -229,7 +229,7 @@ void ImGuiPreLaunchWindow::drawOptions() { } auto curFileType = (CARDFileType)getSettings().backend.cardFileType.getValue(); - if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) {\ + if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) { if (ImGui::Selectable("GCI Folder", curFileType == CARD_GCIFOLDER)) { getSettings().backend.cardFileType.setValue(CARD_GCIFOLDER); diff --git a/src/dusk/imgui/ImGuiSaveEditor.cpp b/src/dusk/imgui/ImGuiSaveEditor.cpp index e91b718895..eced44c7f7 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.cpp +++ b/src/dusk/imgui/ImGuiSaveEditor.cpp @@ -1491,11 +1491,11 @@ namespace dusk { } ImGui::TableNextColumn(); - ImGui::Text(e.flagName.c_str()); + ImGuiStringViewText(e.flagName); ImGui::TableNextColumn(); - ImGui::Text(e.location.c_str()); + ImGuiStringViewText(e.location); ImGui::TableNextColumn(); - ImGui::Text(e.description.c_str()); + ImGuiStringViewText(e.description); } ImGui::EndTable(); } From 746910c59f466ae1a0614d2ee8879af886986ca8 Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 24 Apr 2026 05:13:31 -0400 Subject: [PATCH 62/64] Register interp callback for d_a_obj_item Fixes rupee color changes --- src/d/actor/d_a_obj_item.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/d/actor/d_a_obj_item.cpp b/src/d/actor/d_a_obj_item.cpp index 6c7e82ab3e..e45de1933d 100644 --- a/src/d/actor/d_a_obj_item.cpp +++ b/src/d/actor/d_a_obj_item.cpp @@ -16,6 +16,10 @@ #include "f_op/f_op_camera_mng.h" #include "m_Do/m_Do_mtx.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + static f32 Reflect(cXyz* i_vec, cBgS_PolyInfo const& i_polyinfo, f32 i_scale) { cM3dGPla plane; @@ -31,6 +35,27 @@ static f32 Reflect(cXyz* i_vec, cBgS_PolyInfo const& i_polyinfo, f32 i_scale) { return 0.0f; } +#if TARGET_PC +static void d_a_obj_item_interp_callback(bool isSimFrame, void* pUserWork) { + daItem_c* item = static_cast(pUserWork); + if (item == NULL || item->mpModel == NULL || !item->chkDraw()) { + return; + } + item->setTevStr(); + if (item->mpBrkAnm != NULL) { + s8 tevFrm = item->getTevFrm(); + if (tevFrm != -1) { + item->mpBrkAnm->entry(item->mpModel->getModelData(), tevFrm); + } else { + item->mpBrkAnm->entry(item->mpModel->getModelData()); + } + } + if (item->chkFlag(4)) { + fopAcM_setEffectMtx(item, item->mpModel->getModelData()); + } +} +#endif + const daItemBase_data& daItemBase_c::getData() { return m_data; } @@ -353,6 +378,10 @@ int daItem_c::_daItem_draw() { return 1; } +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&d_a_obj_item_interp_callback, this); +#endif + if (chkDraw()) { return DrawBase(); } From 109f0a50e524bd165749077bcd5694eebe9efae6 Mon Sep 17 00:00:00 2001 From: madeline Date: Fri, 24 Apr 2026 03:37:35 -0700 Subject: [PATCH 63/64] fix sun song again --- src/Z2AudioLib/Z2WolfHowlMgr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Z2AudioLib/Z2WolfHowlMgr.cpp b/src/Z2AudioLib/Z2WolfHowlMgr.cpp index 4866af7a5d..f12d453fbe 100644 --- a/src/Z2AudioLib/Z2WolfHowlMgr.cpp +++ b/src/Z2AudioLib/Z2WolfHowlMgr.cpp @@ -369,7 +369,7 @@ void Z2WolfHowlMgr::setCorrectData(s8 curveID, Z2WolfHowlData* data) { #if TARGET_PC case Z2WOLFHOWL_TIMESONG: cPitchUp = 1.3348f; - cPitchCenter = 1.0f; + cPitchCenter = 0.8909f; cPitchDown = 0.7937f; break; #endif From 5fab665f21baabfc96025918cab29bc08806b0eb Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 24 Apr 2026 15:25:14 +0200 Subject: [PATCH 64/64] Don't allocate giant unused stacks for movie player How to save 800 KiB of commit charge easy --- src/d/actor/d_a_movie_player.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index 079584f714..1f8bdeecd7 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -2855,7 +2855,7 @@ void* daMP_Reader(void*) { #endif } -static u8 daMP_ReadThreadStack[0x2000]; +static u8 daMP_ReadThreadStack[DUSK_IF_ELSE(8, 0x2000)]; #if TARGET_PC static BOOL VideoThreadCancelled; @@ -2880,7 +2880,7 @@ static BOOL daMP_CreateReadThread(s32 param_0) { static OSThread daMP_VideoDecodeThread; -static u8 daMP_VideoDecodeThreadStack[0x64000]; +static u8 daMP_VideoDecodeThreadStack[DUSK_IF_ELSE(8, 0x64000)]; static OSMessageQueue daMP_FreeTextureSetQueue; @@ -3132,7 +3132,7 @@ static BOOL AudioThreadCancelled; static OSThread daMP_AudioDecodeThread; -static u8 daMP_AudioDecodeThreadStack[0x64000]; +static u8 daMP_AudioDecodeThreadStack[DUSK_IF_ELSE(8, 0x64000)]; static OSMessageQueue daMP_FreeAudioBufferQueue;