From a6690c2052552a7339664f7ec7965da1ac747ee3 Mon Sep 17 00:00:00 2001 From: madeline Date: Sun, 12 Apr 2026 23:22:25 -0700 Subject: [PATCH 01/70] speedrun timer --- CMakeLists.txt | 4 + files.cmake | 3 + include/dusk/livesplit.h | 16 ++ include/dusk/settings.h | 5 + src/d/d_bright_check.cpp | 4 + src/dusk/imgui/ImGuiConsole.cpp | 27 +++- src/dusk/imgui/ImGuiConsole.hpp | 4 + src/dusk/imgui/ImGuiMenuEnhancements.cpp | 18 +++ src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp | 96 ++++++++++++ src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp | 13 ++ src/dusk/livesplit.cpp | 183 ++++++++++++++++++++++ src/dusk/settings.cpp | 10 +- src/f_ap/f_ap_game.cpp | 4 + 13 files changed, 385 insertions(+), 2 deletions(-) create mode 100644 include/dusk/livesplit.h create mode 100644 src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp create mode 100644 src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp create mode 100644 src/dusk/livesplit.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 775606ca8e..f1b4e4e26c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,6 +216,10 @@ set(GAME_INCLUDE_DIRS set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient) +if (WIN32) + list(APPEND GAME_LIBS Ws2_32) +endif () + if (DUSK_MOVIE_SUPPORT) if (TARGET libjpeg-turbo::turbojpeg-static) list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static) diff --git a/files.cmake b/files.cmake index 7507fb6742..cfc6fcbdea 100644 --- a/files.cmake +++ b/files.cmake @@ -1361,6 +1361,8 @@ set(DUSK_FILES src/dusk/imgui/ImGuiMenuTools.hpp src/dusk/imgui/ImGuiMenuEnhancements.cpp src/dusk/imgui/ImGuiMenuEnhancements.hpp + src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp + src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp src/dusk/imgui/ImGuiPreLaunchWindow.cpp src/dusk/imgui/ImGuiPreLaunchWindow.hpp src/dusk/imgui/ImGuiFirstRunPreset.hpp @@ -1373,6 +1375,7 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStubLog.cpp src/dusk/imgui/ImGuiMapLoader.cpp src/dusk/imgui/ImGuiSaveEditor.cpp + src/dusk/livesplit.cpp src/dusk/offset_ptr.cpp src/dusk/OSContext.cpp src/dusk/OSThread.cpp diff --git a/include/dusk/livesplit.h b/include/dusk/livesplit.h new file mode 100644 index 0000000000..b283a29af4 --- /dev/null +++ b/include/dusk/livesplit.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace dusk::speedrun { +void onGameFrame(); +uint64_t getFrameCount(); +void start(); +void reset(); +void connectLiveSplit(const char* host = "127.0.0.1", int port = 16834); +void disconnectLiveSplit(); +bool consumeConnectedEvent(); +bool consumeDisconnectedEvent(); +void updateLiveSplit(); +void shutdown(); +} diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 2ab587d3ce..1a5772e310 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -79,6 +79,11 @@ struct UserSettings { // Controls ConfigVar enableTurboKeybind; + + // Tools + ConfigVar speedrunTimer; + ConfigVar speedrunTimerOverlay; + ConfigVar liveSplitEnabled; } game; struct { diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index ac28e46838..ae4849cbe9 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -9,6 +9,7 @@ #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" #include "d/d_msg_string.h" +#include "dusk/livesplit.h" #include "m_Do/m_Do_controller_pad.h" dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) { @@ -138,6 +139,9 @@ void dBrightCheck_c::modeWait() {} void dBrightCheck_c::modeMove() { if (mDoCPd_c::getTrigA(PAD_1) || mDoCPd_c::getTrigStart(PAD_1)) { mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0); +#ifdef TARGET_PC + dusk::speedrun::start(); +#endif mCompleteCheck = true; mMode = MODE_WAIT_e; } diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 1bff5da458..dc27f17c39 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -16,6 +16,7 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_main.h" #include "dusk/config.hpp" +#include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/settings.h" #include "dusk/audio/DuskAudioSystem.h" @@ -38,6 +39,10 @@ namespace dusk { ImGui::TextUnformatted(text.data(), text.data() + text.size()); } + void DuskToast(std::string_view message, float duration) { + g_imguiConsole.AddToast(message, duration); + } + void ImGuiTextCenter(std::string_view text) { ImGui::NewLine(); float fontSize = ImGui::CalcTextSize(text.data(), text.data() + text.size()).x; @@ -235,6 +240,8 @@ namespace dusk { m_menuEnhancements.draw(); m_menuTools.draw(); + m_menuSpeedrunTimer.draw(); + ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 80.0f * ImGuiScale()); ImGuiIO& io = ImGui::GetIO(); ImGuiStringViewText(fmt::format(FMT_STRING("FPS: {:.2f}\n"), io.Framerate)); @@ -252,12 +259,26 @@ namespace dusk { } if (dusk::IsGameLaunched && !m_isLaunchInitialized) { - m_toasts.emplace_back("Press F1 to toggle menu"s, 5.f); + DuskToast("Press F1 to toggle menu"s); m_isLaunchInitialized = true; + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::connectLiveSplit(); + } } + m_menuSpeedrunTimer.drawOverlay(); + m_menuGame.windowControllerConfig(); m_menuGame.windowInputViewer(); + + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::updateLiveSplit(); + if (dusk::speedrun::consumeConnectedEvent()) + AddToast("LiveSplit connected"); + else if (dusk::speedrun::consumeDisconnectedEvent()) + AddToast("LiveSplit disconnected"); + } + if (dusk::IsGameLaunched) { m_menuTools.ShowDebugOverlay(); m_menuTools.ShowCameraOverlay(); @@ -383,6 +404,10 @@ namespace dusk { return false; } + void ImGuiConsole::AddToast(std::string_view message, float duration) { + m_toasts.emplace_back(std::string(message), duration); + } + void ImGuiConsole::ShowToasts() { if (m_toasts.empty()) { return; diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 0296dc24cc..bec998eb63 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -9,6 +9,7 @@ #include "ImGuiFirstRunPreset.hpp" #include "ImGuiMenuEnhancements.hpp" #include "ImGuiMenuGame.hpp" +#include "ImGuiMenuSpeedrunTimer.hpp" #include "ImGuiMenuTools.hpp" #include "ImGuiPreLaunchWindow.hpp" #include "imgui.h" @@ -22,6 +23,7 @@ public: void PostDraw(); static bool CheckMenuViewToggle(ImGuiKey key, bool& active); + void AddToast(std::string_view message, float duration = 3.f); private: struct Toast { @@ -39,6 +41,7 @@ private: ImGuiFirstRunPreset m_firstRunPreset; ImGuiMenuGame m_menuGame; ImGuiMenuEnhancements m_menuEnhancements; + ImGuiMenuSpeedrunTimer m_menuSpeedrunTimer; ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last @@ -57,6 +60,7 @@ std::string BytesToString(size_t bytes); void SetOverlayWindowLocation(int corner); bool ShowCornerContextMenu(int& corner, int avoidCorner); void ImGuiStringViewText(std::string_view text); +void DuskToast(std::string_view message, float duration = 3.f); void ImGuiBeginGroupPanel(const char* name, const ImVec2& size); void ImGuiEndGroupPanel(); void ImGuiTextCenter(std::string_view text); diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index 50e44f5c60..dfe8f5282b 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -2,6 +2,8 @@ #include "ImGuiMenuEnhancements.hpp" #include "ImGuiConfig.hpp" +#include "ImGuiConsole.hpp" +#include "dusk/livesplit.h" #include "dusk/settings.h" namespace dusk { @@ -181,6 +183,22 @@ namespace dusk { "This will not work with the \"Unlock Framerate\" enhancement."); } + bool prevSpeedrunTimer = getSettings().game.speedrunTimer; + config::ImGuiCheckbox("Speedrun Timer", getSettings().game.speedrunTimer); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Shows a speedrun timer in the menu bar."); + } + if ((bool)getSettings().game.speedrunTimer != prevSpeedrunTimer) { + if (!getSettings().game.speedrunTimer) { + getSettings().game.speedrunTimerOverlay.setValue(false); + if (getSettings().game.liveSplitEnabled) { + getSettings().game.liveSplitEnabled.setValue(false); + dusk::speedrun::disconnectLiveSplit(); + DuskToast("LiveSplit disconnected", 3.f); + } + } + } + ImGui::EndMenu(); } diff --git a/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp new file mode 100644 index 0000000000..6edbb7e084 --- /dev/null +++ b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp @@ -0,0 +1,96 @@ +#include "fmt/format.h" +#include "imgui.h" + +#include "ImGuiMenuSpeedrunTimer.hpp" +#include "ImGuiConfig.hpp" +#include "ImGuiConsole.hpp" +#include "dusk/livesplit.h" +#include "dusk/settings.h" + +namespace dusk { + static auto formatTime(uint64_t frames) { + const uint64_t totalSec = frames / 30; + + return fmt::format( + FMT_STRING("{:d}:{:02d}:{:02d}.{:02d}"), + totalSec / 3600, + (totalSec / 60) % 60, + totalSec % 60, + (int)(((f32)(frames % 30) / 30.0f) * 100.0f) + ); + } + + void ImGuiMenuSpeedrunTimer::draw() { + if (!getSettings().game.speedrunTimer) return; + + const uint64_t frames = dusk::speedrun::getFrameCount(); + + if (ImGui::BeginMenu("Timer##speedrun_timer")) { + ImGui::TextUnformatted(formatTime(frames).c_str()); + + ImGui::Separator(); + + if (ImGui::MenuItem("Reset")) { + dusk::speedrun::reset(); + } + + config::ImGuiCheckbox("Show Overlay", getSettings().game.speedrunTimerOverlay); + + bool prevLiveSplit = getSettings().game.liveSplitEnabled; + config::ImGuiCheckbox("LiveSplit Connection", getSettings().game.liveSplitEnabled); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Connect to LiveSplit server on localhost:16834."); + } + + if ((bool)getSettings().game.liveSplitEnabled != prevLiveSplit) { + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::connectLiveSplit(); + } else { + dusk::speedrun::disconnectLiveSplit(); + DuskToast("LiveSplit disconnected", 3.f); + } + } + + ImGui::EndMenu(); + } + + } + + void ImGuiMenuSpeedrunTimer::drawOverlay() { + if (!getSettings().game.speedrunTimer || !getSettings().game.speedrunTimerOverlay) { + return; + } + + const uint64_t frames = dusk::speedrun::getFrameCount(); + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + + const float padding = 10.f; + + ImGui::SetNextWindowPos( + ImVec2( + viewport->WorkPos.x + viewport->WorkSize.x - padding, + viewport->WorkPos.y + viewport->WorkSize.y - padding + ), + ImGuiCond_Always, ImVec2(1.f, 1.f) + ); + + ImGui::SetNextWindowBgAlpha(0.65f); + + const float fixedWidth = ImGui::CalcTextSize("9:59:59.99").x; + + ImGui::SetNextWindowContentSize(ImVec2(fixedWidth, 0.f)); + + if ( + ImGui::Begin("##speedrun_overlay", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav + ) + ) { + ImGui::TextUnformatted(formatTime(frames).c_str()); + } + ImGui::End(); + } +} diff --git a/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp new file mode 100644 index 0000000000..8cb524469f --- /dev/null +++ b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp @@ -0,0 +1,13 @@ +#ifndef DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP +#define DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP + +namespace dusk { + class ImGuiMenuSpeedrunTimer { + public: + void draw(); + void drawOverlay(); + private: + }; +} + +#endif // DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP diff --git a/src/dusk/livesplit.cpp b/src/dusk/livesplit.cpp new file mode 100644 index 0000000000..cec765f223 --- /dev/null +++ b/src/dusk/livesplit.cpp @@ -0,0 +1,183 @@ +#if _WIN32 + #include + #include + using socket_t = SOCKET; + static void closeSocket(socket_t s) { closesocket(s); } +#else + #include + #include + #include + #include + #include + #include + using socket_t = int; + static void closeSocket(socket_t s) { close(s); } + #ifndef INVALID_SOCKET + #define INVALID_SOCKET -1 + #endif +#endif + +#include +#include "dusk/livesplit.h" +#include "f_op/f_op_overlap_mng.h" + +namespace dusk::speedrun { + +static bool running = false; +static uint64_t frameCount = 0; +static socket_t sock = INVALID_SOCKET; +static bool wasLoading = false; +static bool connected = false; +static bool connectPending = false; +static bool disconnectPending = false; + +static void sendCmd(const char* cmd) { + if (sock == INVALID_SOCKET) { + return; + } + + char msg[64]; + int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd); + + if (send(sock, msg, len, 0) >= 0) { + if (!connected) { + connected = connectPending = true; + } + + return; + } + +#if _WIN32 + int err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK || err == WSAENOTCONN) { + return; + } +#else + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOTCONN) { + return; + } +#endif + + if (connected) disconnectPending = true; + closeSocket(sock); + sock = INVALID_SOCKET; + connected = connectPending = false; +} + +uint64_t getFrameCount() { + return frameCount; +} + +void onGameFrame() { + if (!running) { + return; + } + + bool loading = fopOvlpM_IsDoingReq() != 0; + + if (loading != wasLoading) { + sendCmd(loading ? "pausegametime" : "unpausegametime"); + wasLoading = loading; + } + + if (!loading) { + ++frameCount; + } +} + +void start() { + if (running) { + return; + } + + running = true; + frameCount = 0; + wasLoading = false; + sendCmd("initgametime"); + sendCmd("reset"); + sendCmd("starttimer"); +} + +void reset() { + running = false; + frameCount = 0; + wasLoading = false; + sendCmd("reset"); +} + +void connectLiveSplit(const char* host, int port) { +#if _WIN32 + WSADATA wd{}; WSAStartup(MAKEWORD(2, 2), &wd); +#endif + + if (sock != INVALID_SOCKET) { + closeSocket(sock); sock = INVALID_SOCKET; + } + + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (sock == INVALID_SOCKET) { + return; + } + +#if _WIN32 + u_long nb = 1; + ioctlsocket(sock, FIONBIO, &nb); +#else + fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK); +#endif + + sockaddr_in addr{}; addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + inet_pton(AF_INET, host, &addr.sin_addr); + connect(sock, (sockaddr*)&addr, sizeof(addr)); + sendCmd("initgametime"); +} + +void disconnectLiveSplit() { + if (sock != INVALID_SOCKET) { + closeSocket(sock); + sock = INVALID_SOCKET; + connected = false; + } +} + +bool consumeConnectedEvent() { bool v = connectPending; connectPending = false; return v; } +bool consumeDisconnectedEvent() { bool v = disconnectPending; disconnectPending = false; return v; } + +void updateLiveSplit() { + if (sock == INVALID_SOCKET) { + return; + } + + if (!connected) { + sendCmd("initgametime"); + return; + } + + if (!running) { + return; + } + + const uint64_t totalMs = frameCount * 1000 / 30; + const uint64_t totalSec = totalMs / 1000; + char cmd[32]; + + snprintf(cmd, sizeof(cmd), "setgametime %u:%02u:%02u.%03u", + (uint32_t)(totalSec / 3600), + (uint32_t)((totalSec / 60) % 60), + (uint32_t)(totalSec % 60), + (uint32_t)(totalMs % 1000) + ); + + sendCmd(cmd); +} + +void shutdown() { + disconnectLiveSplit(); +#if _WIN32 + WSACleanup(); +#endif +} + +} diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 0e39e55854..064bd8a86d 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -66,7 +66,12 @@ UserSettings g_userSettings = { .restoreWiiGlitches {"game.restoreWiiGlitches", false}, // Controls - .enableTurboKeybind {"game.enableTurboKeybind", false} + .enableTurboKeybind {"game.enableTurboKeybind", false}, + + // Tools + .speedrunTimer {"game.speedrunTimer", false}, + .speedrunTimerOverlay {"game.speedrunTimerOverlay", false}, + .liveSplitEnabled {"game.liveSplitEnabled", false} }, .backend = { @@ -123,6 +128,9 @@ void registerSettings() { Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.enableTurboKeybind); + Register(g_userSettings.game.speedrunTimer); + Register(g_userSettings.game.speedrunTimerOverlay); + Register(g_userSettings.game.liveSplitEnabled); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.enableFrameInterpolation); Register(g_userSettings.game.enableGyroAim); diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index f54ca1edad..48cf4fa16d 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -15,6 +15,7 @@ #include "d/d_model.h" #include "d/d_tresure.h" #include "dusk/frame_interpolation.h" +#include "dusk/livesplit.h" #include "dusk/logging.h" #include "f_op/f_op_camera_mng.h" #include "f_op/f_op_draw_tag.h" @@ -771,6 +772,9 @@ void fapGm_Execute() { fpcM_ManagementFunc(NULL, fapGm_After); #endif cCt_Counter(0); +#ifdef TARGET_PC + dusk::speedrun::onGameFrame(); +#endif } fapGm_HIO_c g_HIO; From 35ea13c53a4ef4263ec712df599708649773e540 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 24 Apr 2026 12:25:15 -0600 Subject: [PATCH 02/70] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index be1395c134..9ff83bcb97 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit be1395c1343a5587cceea25754daa5cacce34f76 +Subproject commit 9ff83bcb9718391a25c3c5ffa1faf470effb1c86 From 1fea4f02ed556b798eb7fcbfced02b7c3a2ec423 Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 24 Apr 2026 14:26:52 -0400 Subject: [PATCH 03/70] Fix camera bouncing with interp enabled --- src/dusk/frame_interpolation.cpp | 38 ++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index f3f8a842e1..941c9ed318 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -292,12 +292,20 @@ void interp_view(::view_class* view) { return; const f32 step = get_interpolation_step(); + const bool is_cam_curr_authoritative = g_is_sim_frame && step <= 0.0f; + cXyz eye; cXyz center; cXyz up; - lerp_xyz(&eye, s_cam_prev.eye, s_cam_curr.eye, step); - lerp_xyz(¢er, s_cam_prev.center, s_cam_curr.center, step); - lerp_xyz(&up, s_cam_prev.up, s_cam_curr.up, step); + if (is_cam_curr_authoritative) { + eye = s_cam_curr.eye; + center = s_cam_curr.center; + up = s_cam_curr.up; + } else { + lerp_xyz(&eye, s_cam_prev.eye, s_cam_curr.eye, step); + lerp_xyz(¢er, s_cam_prev.center, s_cam_curr.center, step); + lerp_xyz(&up, s_cam_prev.up, s_cam_curr.up, step); + } if (!up.normalizeRS()) { up = s_cam_curr.up; up.normalizeRS(); @@ -306,19 +314,25 @@ void interp_view(::view_class* view) { view->lookat.eye = eye; view->lookat.center = center; view->lookat.up = up; - view->bank = lerp_bank(s_cam_prev.bank, s_cam_curr.bank, step); - view->fovy = s_cam_prev.fovy + (s_cam_curr.fovy - s_cam_prev.fovy) * step; - view->aspect = s_cam_prev.aspect + (s_cam_curr.aspect - s_cam_prev.aspect) * step; - view->near_ = s_cam_prev.near_ + (s_cam_curr.near_ - s_cam_prev.near_) * step; - view->far_ = s_cam_prev.far_ + (s_cam_curr.far_ - s_cam_prev.far_) * step; + if (is_cam_curr_authoritative) { + view->bank = s_cam_curr.bank; + view->fovy = s_cam_curr.fovy; + view->aspect = s_cam_curr.aspect; + view->near_ = s_cam_curr.near_; + view->far_ = s_cam_curr.far_; + } else { + view->bank = lerp_bank(s_cam_prev.bank, s_cam_curr.bank, step); + view->fovy = s_cam_prev.fovy + (s_cam_curr.fovy - s_cam_prev.fovy) * step; + view->aspect = s_cam_prev.aspect + (s_cam_curr.aspect - s_cam_prev.aspect) * step; + view->near_ = s_cam_prev.near_ + (s_cam_curr.near_ - s_cam_prev.near_) * step; + view->far_ = s_cam_prev.far_ + (s_cam_curr.far_ - s_cam_prev.far_) * step; + } // FRAME INTERP TODO: It might be better if I rewired the game to not clear this flag until the // next sim frame, but I don't care enough to right now #if WIDESCREEN_SUPPORT - if (mDoGph_gInf_c::isWide() && !mDoGph_gInf_c::isWideZoom() && step >= 0.5f ? - s_cam_curr.wideZoom : - s_cam_prev.wideZoom) - { + const f32 wide_step = is_cam_curr_authoritative ? 1.0f : step; + if (mDoGph_gInf_c::isWide() && !mDoGph_gInf_c::isWideZoom() && wide_step >= 0.5f ? s_cam_curr.wideZoom : s_cam_prev.wideZoom) { mDoGph_gInf_c::onWideZoom(); } #endif From f0e8379fa52cf9ccd608a21582408f0e4bc55a5a Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 24 Apr 2026 13:19:09 -0600 Subject: [PATCH 04/70] Fix UaF in d_a_mirror Resolves #505 --- src/d/actor/d_a_mirror.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index fb4142c0bb..746f32a9f6 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -599,6 +599,13 @@ int daMirror_c::execute() { return 1; } +#if TARGET_PC + if (mPacket.mbReset) { + mPacket.mModelCount = 0; + mPacket.mbReset = false; + } +#endif + daPy_py_c* player = daPy_getLinkPlayerActorClass(); JUT_ASSERT(0, player != NULL); From b2c09d56d6551d74f6b6654098f24b1afd03d1a7 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 24 Apr 2026 13:34:51 -0600 Subject: [PATCH 05/70] Remove unnecessary logic from dMirror_packet_c::entryModel --- src/d/actor/d_a_mirror.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index 746f32a9f6..86ef4314f3 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -84,13 +84,6 @@ void dMirror_packet_c::calcMinMax() { } int dMirror_packet_c::entryModel(J3DModel* i_model) { -#if TARGET_PC - if (mbReset) { - mModelCount = 0; - mbReset = false; - } -#endif - if (mModelCount >= 0x40) { return 0; } From 171dbdd0a14b46b5baebfd3f6ad929547a0cce92 Mon Sep 17 00:00:00 2001 From: roeming Date: Fri, 24 Apr 2026 16:50:31 -0400 Subject: [PATCH 06/70] Implement custom sorting when sorting event flags by flag on/off --- src/dusk/imgui/ImGuiSaveEditor.cpp | 96 +++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 20 deletions(-) diff --git a/src/dusk/imgui/ImGuiSaveEditor.cpp b/src/dusk/imgui/ImGuiSaveEditor.cpp index eced44c7f7..cc89d37275 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.cpp +++ b/src/dusk/imgui/ImGuiSaveEditor.cpp @@ -1347,6 +1347,51 @@ namespace dusk { } } + template + concept FlagIter = requires(T t) { + ++t; + --t; + t + 1; + t < t; + { t->flagID } -> std::convertible_to; + }; + + template + concept FlagTester = requires(T t, u16 flagID) { + { t(flagID) } -> std::convertible_to; + }; + + static void sortByFlags(FlagIter auto begin, FlagIter auto end, FlagTester auto&& flagTester) { + if (begin == end) return; + + FlagIter auto fullEnd = end; + + // We want to find the location of where we can swap our `On` flags to. + // We're gonna put the `Off` bits first, and the `On` bits last. 0 < 1 + // We can achieve this by skipping all the `On` bits at the end. + + // backtrack until we find a bit that is off + while (begin < --end && flagTester(end->flagID)) { + // move the end pointer back while we find on bits + } + + // end should now be pointing to a bit that is off + while (begin < end) { + // if there's a flag that's on + if (flagTester(begin->flagID)) { + // move it to the end + std::rotate(begin, begin + 1, fullEnd); + // move back the end of where we're checking + --end; + // begin will now point to the next piece of data + // because we've rotated the data >= begin to the left + } else { + // not on, check next flag + ++begin; + } + } + } + void ImGuiSaveEditor::drawFlagsTab() { if (ImGui::TreeNode("Current Region Flags")) { dSv_memBit_c& membit = g_dComIfG_gameInfo.info.mMemory.mBit; @@ -1446,30 +1491,41 @@ namespace dusk { sort != nullptr && sort->SpecsCount > 0 && (sort->SpecsDirty || sort->Specs[0].ColumnIndex == COLUMN_FLAG)) { - auto column = sort->Specs->ColumnIndex; - const auto cmp = [&](const duskImguiEventFlagEntry& l, - const duskImguiEventFlagEntry& r) -> bool { - switch (column) { - case COLUMN_FLAG: - return (bool)event.isEventBit(l.flagID) < - (bool)event.isEventBit(r.flagID); - case COLUMN_NAME: - return l.flagName < r.flagName; - case COLUMN_LOC: - return l.location < r.location; - case COLUMN_DESC: - return l.description < r.description; - } - return false; - }; - + const auto column = sort->Specs[0].ColumnIndex; const auto direction = sort->Specs[0].SortDirection; - if (direction == ImGuiSortDirection_Ascending) { - std::sort(std::begin(duskImguiEventFlags), std::end(duskImguiEventFlags), cmp); + // if we're sorting by flags, do special sort, regular sort is bad for sorting bools + // it can swap values that are the same, and that causes constant reordering + if (column == COLUMN_FLAG) { + const auto testEventFunc = [&event](u16 flag) -> bool { return event.isEventBit(flag); }; + + if (direction == ImGuiSortDirection_Ascending) { + sortByFlags(std::begin(duskImguiEventFlags), + std::end(duskImguiEventFlags), testEventFunc); + } else { + sortByFlags(std::rbegin(duskImguiEventFlags), + std::rend(duskImguiEventFlags), testEventFunc); + } } else { - std::sort(std::rbegin(duskImguiEventFlags), std::rend(duskImguiEventFlags), cmp); + const auto cmp = [column](const duskImguiEventFlagEntry& l, + const duskImguiEventFlagEntry& r) -> bool { + switch (column) { + case COLUMN_NAME: return l.flagName < r.flagName; + case COLUMN_LOC: return l.location < r.location; + case COLUMN_DESC: return l.description < r.description; + default: return false; + } + }; + + if (direction == ImGuiSortDirection_Ascending) { + std::sort(std::begin(duskImguiEventFlags), + std::end(duskImguiEventFlags), cmp); + } else { + std::sort(std::rbegin(duskImguiEventFlags), + std::rend(duskImguiEventFlags), cmp); + } } + sort->SpecsDirty = false; } From d5aabe9f9ed2a13d275413cca114bf4b99c929b1 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Fri, 24 Apr 2026 18:06:02 -0400 Subject: [PATCH 07/70] Even faster tears to match TPHD --- src/d/actor/d_a_obj_drop.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/d/actor/d_a_obj_drop.cpp b/src/d/actor/d_a_obj_drop.cpp index 275a3d6e12..2b373c1e64 100644 --- a/src/d/actor/d_a_obj_drop.cpp +++ b/src/d/actor/d_a_obj_drop.cpp @@ -265,7 +265,7 @@ int daObjDrop_c::modeParentWait() { mModeAction = 1; #if TARGET_PC - mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 20 : 40; + mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 0 : 40; if (dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0) { current.pos.y += 100.0f; } else { @@ -285,7 +285,7 @@ int daObjDrop_c::modeParentWait() { case 2: createBodyEffect(); #if TARGET_PC - mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 5 : 45; + mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 0 : 45; #else mModeTimer = 45; #endif From 274bf61b3ea216ec4f3e0db0610d8858e9d9420e Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 24 Apr 2026 22:56:47 -0400 Subject: [PATCH 08/70] Fix Link's hands detaching when interp is enabled --- src/d/actor/d_a_alink.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index e6943baf96..4fea05cd53 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -18936,11 +18936,20 @@ void daAlink_c::setDrawHand() { mpLinkHandModel->setBaseTRMtx(mpLinkModel->getBaseTRMtx()); mpLinkHandModel->calc(); +#if TARGET_PC + // FRAME INTERP NOTE: Always set these, otherwise the hands occasionally zip to origin. + // Doing it regardless of interpolation being active seems harmless. + mpLinkHandModel->setAnmMtx(1, mpLinkModel->getAnmMtx(9)); + mpLinkHandModel->setAnmMtx(2, mpLinkModel->getAnmMtx(0xE)); +#endif + if (var_r30 == 0xFE || var_r30 == 0xFB) { field_0x06d0 = field_0x06d8; } else { field_0x06d0 = mpLinkHandModel->getModelData()->getMaterialNodePointer(var_r30)->getShape(); +#if !TARGET_PC mpLinkHandModel->setAnmMtx(1, mpLinkModel->getAnmMtx(9)); +#endif } if (var_r30 == 0xFB) { @@ -18959,7 +18968,9 @@ void daAlink_c::setDrawHand() { field_0x06d4 = field_0x06dc; } else { field_0x06d4 = mpLinkHandModel->getModelData()->getMaterialNodePointer(var_r29)->getShape(); +#if !TARGET_PC mpLinkHandModel->setAnmMtx(2, mpLinkModel->getAnmMtx(0xE)); +#endif } if (var_r29 == 0xFB) { From c61e32cd4fd52af7495fc9366fae7c2f86c81384 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 24 Apr 2026 22:10:56 -0600 Subject: [PATCH 09/70] Update aurora for GXPeekZ Resolves #281 Resolves #468 --- extern/aurora | 2 +- src/dusk/stubs.cpp | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/extern/aurora b/extern/aurora index 9ff83bcb97..8d9618dc4b 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 9ff83bcb9718391a25c3c5ffa1faf470effb1c86 +Subproject commit 8d9618dc4bd82e476754c5b841b85fba6ee6ddab diff --git a/src/dusk/stubs.cpp b/src/dusk/stubs.cpp index 07e420445c..c6832d2cac 100644 --- a/src/dusk/stubs.cpp +++ b/src/dusk/stubs.cpp @@ -1020,10 +1020,6 @@ void GXInitTexCacheRegion(GXTexRegion* region, GXBool is_32b_mipmap, u32 tmem_ev // XXX, this should be some struct? // GXRenderModeObj GXNtsc480IntDf; //GXRenderModeObj GXNtsc480Int; -void GXPeekZ(u16 x, u16 y, u32* z) { - STUB_LOG(); - *z = 0; -} void GXReadXfRasMetric(u32* xf_wait_in, u32* xf_wait_out, u32* ras_busy, u32* clocks) { STUB_LOG(); *xf_wait_in = 0; From c8e89a0f998b88bf464aeabfc3913c4af6675a46 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sat, 25 Apr 2026 00:31:37 -0400 Subject: [PATCH 10/70] Fix grass cutting when interp is enabled --- src/d/d_particle.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/d/d_particle.cpp b/src/d/d_particle.cpp index 248424e9a2..9b89afc7b1 100644 --- a/src/d/d_particle.cpp +++ b/src/d/d_particle.cpp @@ -1972,7 +1972,12 @@ void dPa_light8PcallBack::draw(JPABaseEmitter* param_1, JPABaseParticle* param_2 JGeometry::TVec3 local_154; JGeometry::TVec3 local_160; JGeometry::TVec3 local_16c; - dPa_setWindPower(param_2); +#if TARGET_PC + if (dusk::frame_interp::is_sim_frame()) +#endif + { + dPa_setWindPower(param_2); + } MTXIdentity(local_60); MTXIdentity(auStack_90); param_2->getBaseAxis(&local_10c); From 4e264d6a22d98835d62b3c29cb48376e9a88adaa Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 24 Apr 2026 22:53:41 -0600 Subject: [PATCH 11/70] Fix Zbuffer texture binding Resolves #511 --- src/d/d_resorce.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/d/d_resorce.cpp b/src/d/d_resorce.cpp index 73f031beae..bb982c4afb 100644 --- a/src/d/d_resorce.cpp +++ b/src/d/d_resorce.cpp @@ -102,11 +102,7 @@ static void setIndirectTex(J3DModelData* i_modelData) { texture->setResTIMG(i, *mDoGph_gInf_c::getFrameBufferTimg()); } if (memcmp(textureName, "Zbuffer", 8) == 0) { -#if !TARGET_PC texture->setResTIMG(i, *mDoGph_gInf_c::getZbufferTimg()); -#else - DuskLog.warn("Zbuffer texture binding not yet supported"); -#endif } } } From e755148f1673b23d41016fa58def537b2e3a9ddf Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Sat, 25 Apr 2026 16:59:04 -0400 Subject: [PATCH 12/70] Fix some UBs (#544) Co-authored-by: MelonSpeedruns --- src/d/actor/d_a_alink.cpp | 5 +++++ src/d/actor/d_flower.inc | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 4fea05cd53..28c979bab4 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -5877,6 +5877,11 @@ void daAlink_c::setItemMatrix(int param_0) { modelCalc(mSheathModel); int var_r26; + + #if AVOID_UB + var_r26 = 0; + #endif + if (!checkNoResetFlg3(FLG3_UNK_4000000)) { if (mEquipItem == 0x103 || param_0 != 0) { mSwordModel->setBaseTRMtx(mpLinkModel->getAnmMtx(mLeftItemJntNo)); diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 58daa354a1..1850e1e1ad 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -789,6 +789,10 @@ void dFlower_packet_c::draw() { GXColor sp28; + #if AVOID_UB + sp28 = {1, 1, 1, 1}; + #endif + //u8 sp26, sp25, sp24; GXColor sp24; sp24.r = -0.4f * temp_r29->AmbCol.r * var_f29; From 60e8836968685bc7fc05460967ca87e07db0b390 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Sat, 25 Apr 2026 16:59:30 -0400 Subject: [PATCH 13/70] Tears Collect during cutscenes (#539) TPHD has tears collectable just as fast during cutscenes, this changes our implementation to reflect that. Co-authored-by: MelonSpeedruns --- src/d/actor/d_a_e_ym.cpp | 6 +++--- src/d/actor/d_a_obj_drop.cpp | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/d/actor/d_a_e_ym.cpp b/src/d/actor/d_a_e_ym.cpp index 758bec6269..3f6bef6788 100644 --- a/src/d/actor/d_a_e_ym.cpp +++ b/src/d/actor/d_a_e_ym.cpp @@ -1069,7 +1069,7 @@ void daE_YM_c::executeDown() { if (mAcch.ChkGroundHit()) { if (mFlyType != 1) { #if TARGET_PC - bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears ? 2.0f : 1.0f); #else bckSet(6, 0, 0.0f, 1.0f); #endif @@ -1093,7 +1093,7 @@ void daE_YM_c::executeDown() { mSound.startCreatureSound(Z2SE_CM_BODYFALL_WATER_M, 0, -1); mSound.startCreatureSound(Z2SE_EN_YM_MOGAKU, 0, -1); #if TARGET_PC - bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears ? 2.0f : 1.0f); #else bckSet(6, 0, 0.0f, 1.0f); #endif @@ -1115,7 +1115,7 @@ void daE_YM_c::executeDown() { || dComIfG_Bgsp().GetGroundCode(gnd_chk) == 5) { #if TARGET_PC - bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 2.0f : 1.0f); + bckSet(6, 0, 0.0f, dusk::getSettings().game.fastTears ? 2.0f : 1.0f); #else bckSet(6, 0, 0.0f, 1.0f); #endif diff --git a/src/d/actor/d_a_obj_drop.cpp b/src/d/actor/d_a_obj_drop.cpp index 2b373c1e64..18d27840f7 100644 --- a/src/d/actor/d_a_obj_drop.cpp +++ b/src/d/actor/d_a_obj_drop.cpp @@ -265,8 +265,8 @@ int daObjDrop_c::modeParentWait() { mModeAction = 1; #if TARGET_PC - mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 0 : 40; - if (dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0) { + mModeTimer = dusk::getSettings().game.fastTears ? 0 : 40; + if (dusk::getSettings().game.fastTears) { current.pos.y += 100.0f; } else { current.pos.y += 300.0f; @@ -285,7 +285,7 @@ int daObjDrop_c::modeParentWait() { case 2: createBodyEffect(); #if TARGET_PC - mModeTimer = dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0 ? 0 : 45; + mModeTimer = dusk::getSettings().game.fastTears ? 0 : 45; #else mModeTimer = 45; #endif @@ -331,7 +331,7 @@ int daObjDrop_c::modeWait() { case 2: case 50: #if TARGET_PC - if (dusk::getSettings().game.fastTears && dComIfGp_event_getMode() == 0) { + if (dusk::getSettings().game.fastTears) { f32 player_dist = current.pos.abs(daPy_getPlayerActorClass()->current.pos); f32 home_dist = current.pos.abs(home.pos); From 5ec5f8864a06586339a4ad36ed8b3b8d2e745f1f Mon Sep 17 00:00:00 2001 From: Irastris Date: Sat, 25 Apr 2026 16:59:40 -0400 Subject: [PATCH 14/70] Register interp callback for d_a_e_sm2 Fixes chu color changes --- src/d/actor/d_a_e_sm2.cpp | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/d/actor/d_a_e_sm2.cpp b/src/d/actor/d_a_e_sm2.cpp index fdbc338670..81633aa009 100644 --- a/src/d/actor/d_a_e_sm2.cpp +++ b/src/d/actor/d_a_e_sm2.cpp @@ -14,6 +14,10 @@ #include "f_op/f_op_camera_mng.h" #include +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class daE_SM2_HIO_c : public fOpAcm_HIO_entry_c { public: daE_SM2_HIO_c(); @@ -76,6 +80,55 @@ static int nodeCallBack(J3DJoint* i_joint, int param_1) { return 1; } +#if TARGET_PC +static void daE_SM2_interp_callback(bool isSimFrame, void* pUserWork) { + e_sm2_class* i_this = static_cast(pUserWork); + if (i_this == NULL) { + return; + } + + fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->enemy; + g_env_light.settingTevStruct(0, &actor->current.pos, &actor->tevStr); + + if (!i_this->isPiece) { + if (i_this->modelMorf == NULL) { + return; + } + J3DModel* model = i_this->modelMorf->getModel(); + if (model == NULL) { + return; + } + g_env_light.setLightTevColorType_MAJI(model, &actor->tevStr); + + J3DMaterial* material = model->getModelData()->getMaterialNodePointer(0); + material->getTevKColor(1)->r = i_this->color_R; + material->getTevKColor(1)->g = i_this->color_G; + material->getTevKColor(1)->b = i_this->color_B; + material->getTevKColor(1)->a = 217.0f * i_this->color_alpha; + + if (i_this->pbtk != NULL) { + i_this->pbtk->entry(model->getModelData()); + } + } else { + if (i_this->pieceModelMorf == NULL) { + return; + } + J3DModel* model = i_this->pieceModelMorf->getModel(); + if (model == NULL) { + return; + } + + J3DMaterial* material = model->getModelData()->getMaterialNodePointer(0); + material->getTevKColor(1)->r = i_this->color_R; + material->getTevKColor(1)->g = i_this->color_G; + material->getTevKColor(1)->b = i_this->color_B; + material->getTevKColor(1)->a = 217.0f * i_this->color_alpha; + + g_env_light.setLightTevColorType_MAJI(model, &actor->tevStr); + } +} +#endif + static int daE_SM2_Draw(e_sm2_class* i_this) { fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->enemy; g_env_light.settingTevStruct(0, &actor->current.pos, &actor->tevStr); @@ -122,6 +175,10 @@ static int daE_SM2_Draw(e_sm2_class* i_this) { i_this->pieceInvisModel.entryDL(NULL); } +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&daE_SM2_interp_callback, i_this); +#endif + return 1; } From 41420bc71cdd27d805a6c1f8906063fb0ed75608 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sat, 25 Apr 2026 17:07:49 -0400 Subject: [PATCH 15/70] Chu Fix 2: Electric Boogaloo --- src/d/actor/d_a_e_sm2.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/d/actor/d_a_e_sm2.cpp b/src/d/actor/d_a_e_sm2.cpp index 81633aa009..f55c4411cb 100644 --- a/src/d/actor/d_a_e_sm2.cpp +++ b/src/d/actor/d_a_e_sm2.cpp @@ -131,6 +131,11 @@ static void daE_SM2_interp_callback(bool isSimFrame, void* pUserWork) { static int daE_SM2_Draw(e_sm2_class* i_this) { fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->enemy; + +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&daE_SM2_interp_callback, i_this); +#endif + g_env_light.settingTevStruct(0, &actor->current.pos, &actor->tevStr); J3DModel* model; @@ -175,10 +180,6 @@ static int daE_SM2_Draw(e_sm2_class* i_this) { i_this->pieceInvisModel.entryDL(NULL); } -#if TARGET_PC - dusk::frame_interp::add_interpolation_callback(&daE_SM2_interp_callback, i_this); -#endif - return 1; } From 977ad16806128059036d964849dc1f76d81ac6e3 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 25 Apr 2026 23:46:12 +0200 Subject: [PATCH 16/70] GCN PAL support (#533) * Basic PAL ISO & language support Probably still needs much more work * Add language selector to pre-launch * Store DVDDiskID in a global Can use this later for things * More version system API improvements * d_name mostly region switching fully JPN doesn't work yet cuz it'll be a nightmare, probably. * More version switching support * Mark GCN PAL as supported ROM * Fix remaining REL assets to have PAL offsets * d_a_mg_fish PAL * d_a_mg_fshop PAL * isRegionUsa helper * d_menu_fishing PAL * d_msg_class PAL * m_Do_MemCardRWmng PAL * Update CARDInit call & remove DUSK_TP_VERSION * Fix Ganon cape Missed this one. * Remove tp_version from Sentry --------- Co-authored-by: Luke Street --- CMakeLists.txt | 10 +- cmake-variants.yaml | 10 - files.cmake | 1 + include/d/d_name.h | 2 +- include/d/d_s_logo.h | 4 +- include/dusk/dvd_asset.hpp | 15 +- include/dusk/settings.h | 16 + include/dusk/version.hpp | 67 ++++ include/global.h | 4 + src/d/actor/d_a_mant.cpp | 13 +- src/d/actor/d_a_mg_fish.cpp | 27 +- src/d/actor/d_a_mg_fshop.cpp | 22 +- src/d/actor/d_a_mg_rod.cpp | 30 +- src/d/actor/d_a_player.cpp | 13 +- src/d/actor/d_a_title.cpp | 30 +- src/d/actor/d_flower.inc | 28 +- src/d/actor/d_grass.inc | 19 +- src/d/d_file_sel_info.cpp | 15 +- src/d/d_menu_fishing.cpp | 8 +- src/d/d_msg_class.cpp | 14 +- src/d/d_msg_object.cpp | 29 +- src/d/d_msg_unit.cpp | 4 +- src/d/d_name.cpp | 399 ++++++++++++++++++------ src/d/d_s_logo.cpp | 182 ++++++++++- src/d/d_save.cpp | 8 +- src/dusk/config.cpp | 1 + src/dusk/crash_reporting.cpp | 1 - src/dusk/dvd_asset.cpp | 28 +- src/dusk/imgui/ImGuiPreLaunchWindow.cpp | 18 ++ src/dusk/iso_validate.cpp | 1 + src/dusk/settings.cpp | 3 + src/dusk/version.cpp | 82 +++++ src/m_Do/m_Do_MemCard.cpp | 7 +- src/m_Do/m_Do_MemCardRWmng.cpp | 36 ++- src/m_Do/m_Do_main.cpp | 21 ++ 35 files changed, 957 insertions(+), 211 deletions(-) create mode 100644 include/dusk/version.hpp create mode 100644 src/dusk/version.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 19fd5d673f..b326931e79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -280,22 +280,16 @@ set(DUSK_COMPANY_NAME "Twilit Realm") set(DUSK_FILE_DESCRIPTION "Dusk") set(DUSK_PRODUCT_NAME "Dusk") set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors") -set(DUSK_GAME_NAME "GZ2E") -set(DUSK_GAME_VERSION "01") -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} ${REL_FILES}) source_group("dusk" FILES ${DUSK_FILES}) -set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 - DUSK_TP_VERSION="${DUSK_TP_VERSION}" DUSK_GAME_NAME="${DUSK_GAME_NAME}" DUSK_GAME_VERSION="${DUSK_GAME_VERSION}") +set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0) set(GAME_INCLUDE_DIRS include src - assets/${DUSK_TP_VERSION} + assets/GZ2E01 # TODO: make this dynamic if needed? libs/JSystem/include libs extern/aurora/include/dolphin diff --git a/cmake-variants.yaml b/cmake-variants.yaml index 62d2642311..37edcede95 100644 --- a/cmake-variants.yaml +++ b/cmake-variants.yaml @@ -13,13 +13,3 @@ buildType: short: RelWithDebInfo long: Optimized, with debug symbols buildType: RelWithDebInfo - -tp_version: - default: GZ2E01 - description: TP Version - choices: - GZ2E01: - short: GZ2E01 - long: GZ2E01 - settings: - DUSK_TP_VERSION: GZ2E01 diff --git a/files.cmake b/files.cmake index af101b6c47..d45dd7dc7c 100644 --- a/files.cmake +++ b/files.cmake @@ -1466,4 +1466,5 @@ set(DUSK_FILES src/dusk/OSThread.cpp src/dusk/OSMutex.cpp src/dusk/discord_presence.cpp + src/dusk/version.cpp ) diff --git a/include/d/d_name.h b/include/d/d_name.h index e685871738..3ac6592457 100644 --- a/include/d/d_name.h +++ b/include/d/d_name.h @@ -89,7 +89,7 @@ public: void MojiSelectAnm3(); int mojiChange(u8); void selectMojiSet(); - #if REGION_JPN + #if TARGET_PC || REGION_JPN int checkDakuon(int, u8); int setDakuon(int, u8); #endif diff --git a/include/d/d_s_logo.h b/include/d/d_s_logo.h index 56d10eef82..ab714a17ae 100644 --- a/include/d/d_s_logo.h +++ b/include/d/d_s_logo.h @@ -79,7 +79,7 @@ public: bool isProgressiveMode(); void setRenderMode(); - #if VERSION == VERSION_GCN_PAL || PLATFORM_WII || PLATFORM_SHIELD + #if TARGET_PC || VERSION == VERSION_GCN_PAL || PLATFORM_WII || PLATFORM_SHIELD u8 getPalLanguage(); #endif @@ -149,7 +149,7 @@ public: /* 0x200 */ dDlst_2D_c* mNvLogo; /* 0x204 */ dDlst_2D_c* mMocImg; #endif -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC || VERSION == VERSION_GCN_PAL /* 0x1FC */ mDoDvdThd_mountArchive_c* mpPalLogoResCommand; #endif /* 0x1FC */ request_of_phase_process_class* m_preLoad_dylPhase; diff --git a/include/dusk/dvd_asset.hpp b/include/dusk/dvd_asset.hpp index 014c683d44..8594d4631c 100644 --- a/include/dusk/dvd_asset.hpp +++ b/include/dusk/dvd_asset.hpp @@ -1,22 +1,31 @@ #pragma once #include "dolphin/types.h" +#include "version.hpp" namespace dusk { +struct OffsetVersion { + version::GameVersion mGameVersion; + u32 mOffset; + + constexpr OffsetVersion(const version::GameVersion gameVersion, const u32 offset) + : mGameVersion(gameVersion), mOffset(offset) {} +}; + /** * Load bytes from the main DOL by GameCube virtual address */ -bool LoadDolAsset(void* dst, u32 virtualAddress, s32 size); +bool LoadDolAsset(void* dst, std::initializer_list virtualAddress, s32 size); /** * Load bytes from a REL file in the ISO filesystem, dst must be 32-byte aligned */ -bool LoadRelAsset(void* dst, const char* dvdPath, s32 offset, s32 size); +bool LoadRelAsset(void* dst, const char* dvdPath, std::initializer_list offset, s32 size); /** * Load bytes from a REL inside RELS.arc */ -bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, s32 offset, s32 size); +bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, std::initializer_list offset, s32 size); } // namespace dusk diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 039e51af7d..9721b93614 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -13,12 +13,26 @@ enum class BloomMode : int { Dusk = 2, }; +enum class GameLanguage : u8 { + English = OS_LANGUAGE_ENGLISH, + German = OS_LANGUAGE_GERMAN, + French = OS_LANGUAGE_FRENCH, + Spanish = OS_LANGUAGE_SPANISH, + Italian = OS_LANGUAGE_ITALIAN, +}; + namespace config { template <> struct ConfigEnumRange { static constexpr auto min = BloomMode::Off; static constexpr auto max = BloomMode::Dusk; }; + +template <> +struct ConfigEnumRange { + static constexpr auto min = GameLanguage::English; + static constexpr auto max = GameLanguage::Italian; +}; } // Persistent user settings @@ -46,6 +60,8 @@ struct UserSettings { // Game settings struct { + ConfigVar language; + // QoL ConfigVar enableQuickTransform; ConfigVar hideTvSettingsScreen; diff --git a/include/dusk/version.hpp b/include/dusk/version.hpp new file mode 100644 index 0000000000..fe393b049b --- /dev/null +++ b/include/dusk/version.hpp @@ -0,0 +1,67 @@ +#ifndef DUSK_VERSION_HPP +#define DUSK_VERSION_HPP + +/** + * Functionality for switching game behavior based on the loaded game version (e.g. PAL/JPN, GC/Wii) + */ +namespace dusk::version { + enum class GameVersion : u8 { + GcnUsa = VERSION_GCN_USA, + GcnPal = VERSION_GCN_PAL, + GcnJpn = VERSION_GCN_JPN, + WiiUsaRev0 = VERSION_WII_USA_R0, + WiiUsa = VERSION_WII_USA_R2, + WiiPal = VERSION_WII_PAL, + WiiJpn = VERSION_WII_JPN, + WiiKor = VERSION_WII_KOR, + }; + + bool isGcn(); + bool isWii(); + bool isPalOrAtLeastWiiR2(); + + bool isRegionPal(); + bool isRegionJpn(); + bool isRegionUsa(); + + GameVersion getGameVersion(); + + const DVDDiskID& getDiskID(); + + void init(); + + template + struct VersionOption { + GameVersion mVersion; + T mValue; + + constexpr VersionOption(GameVersion version, T value) : mVersion(version), mValue(value) {} + }; + + template + const T& versionSelect(const std::initializer_list> options) { + const auto version = getGameVersion(); + for (const auto& opt : options) { + if (opt.mVersion == version) { + return opt.mValue; + } + } + + // Unable to find value. + abort(); + } + + template + const T& versionSelect(const std::initializer_list> options, const T& defaultValue) { + const auto version = getGameVersion(); + for (const auto& opt : options) { + if (opt.mVersion == version) { + return opt.mValue; + } + } + + return defaultValue; + } +} // namespace dusk::version + +#endif // DUSK_VERSION_HPP diff --git a/include/global.h b/include/global.h index 9301d12e11..556c5f6207 100644 --- a/include/global.h +++ b/include/global.h @@ -230,12 +230,16 @@ using std::isnan; // Some basic macros that are more convenient than putting down #if blocks for one-line changes. #if TARGET_PC #define IF_DUSK(statement) statement +#define IF_DUSK_BLOCK(cond) if (cond) { +#define IF_DUSK_BLOCK_END } #define IF_DUSK_ARG(expr) , expr #define IF_NOT_DUSK(statement) #define DUSK_IF_ELSE(dusk, orig) dusk #else #define IF_DUSK(statement) #define IF_DUSK_ARG(expr) +#define IF_DUSK_BLOCK(cond) +#define IF_DUSK_BLOCK_END #define IF_NOT_DUSK(statement) statement #define DUSK_IF_ELSE(dusk, orig) orig #endif diff --git a/src/d/actor/d_a_mant.cpp b/src/d/actor/d_a_mant.cpp index 282502419f..6a559c0e1c 100644 --- a/src/d/actor/d_a_mant.cpp +++ b/src/d/actor/d_a_mant.cpp @@ -13,9 +13,12 @@ #if TARGET_PC #include "dusk/dvd_asset.hpp" #include "dusk/frame_interpolation.h" -static u8* l_Egnd_mantTEX_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", 0x1C00, 0x4000), true); return buf; } -static u8* l_Egnd_mantTEX_U_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", 0x5C00, 0x4000), true); return buf; } -static u8* l_Egnd_mantPAL_get() { alignas(32) static u8 buf[0x60]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", 0x9C00, 0x60), true); return buf; } + +using GameVersion = dusk::version::GameVersion; + +static u8* l_Egnd_mantTEX_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x1C00}, {GameVersion::GcnPal, 0x1C00}}, 0x4000), true); return buf; } +static u8* l_Egnd_mantTEX_U_get() { alignas(32) static u8 buf[0x4000]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x5C00}, {GameVersion::GcnPal, 0x5C00}}, 0x4000), true); return buf; } +static u8* l_Egnd_mantPAL_get() { alignas(32) static u8 buf[0x60]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0x9C00}, {GameVersion::GcnPal, 0x9C00}}, 0x60), true); return buf; } #define l_Egnd_mantTEX (l_Egnd_mantTEX_get()) #define l_Egnd_mantTEX_U (l_Egnd_mantTEX_U_get()) #define l_Egnd_mantPAL (l_Egnd_mantPAL_get()) @@ -251,7 +254,9 @@ static u32 l_texCoord[338] = { }; #if TARGET_PC -static u8* l_Egnd_mantDL_get() { alignas(32) static u8 buf[0x3EC]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", 0xA9A0, 0x3EC), true); return buf; } +using GameVersion = dusk::version::GameVersion; + +static u8* l_Egnd_mantDL_get() { alignas(32) static u8 buf[0x3EC]; static bool _ = (dusk::LoadRelAsset(buf, "/rel/Final/Release/d_a_mant.rel", {{GameVersion::GcnUsa, 0xA9A0}, {GameVersion::GcnPal, 0xA9A0}}, 0x3EC), true); return buf; } #define l_Egnd_mantDL (l_Egnd_mantDL_get()) #else #include "assets/l_Egnd_mantDL.h" diff --git a/src/d/actor/d_a_mg_fish.cpp b/src/d/actor/d_a_mg_fish.cpp index 90781345ad..6791bcb61c 100644 --- a/src/d/actor/d_a_mg_fish.cpp +++ b/src/d/actor/d_a_mg_fish.cpp @@ -22,6 +22,7 @@ #include "d/d_s_play.h" #include "d/d_vibration.h" #include "f_op/f_op_kankyo_mng.h" +#include "dusk/version.hpp" #include #define ANM_MG_FISH_MOUTH_CLOSE 4 @@ -3230,7 +3231,17 @@ static int daMg_Fish_Execute(mg_fish_class* i_this) { daPy_py_c* player = daPy_getPlayerActorClass(); -#if VERSION == VERSION_GCN_JPN +#if TARGET_PC + if (dusk::version::isRegionJpn()) { + lit_1008 = 0; + } else if (dusk::version::isRegionPal()) { + if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { + lit_1008 = 2; + } else { + lit_1008 = 0; + } + } +#elif VERSION == VERSION_GCN_JPN lit_1008 = 0; #elif VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { @@ -3843,7 +3854,19 @@ static int daMg_Fish_Create(fopAc_ac_c* i_this) { a_this->mResName = "O_gD_bott"; } -#if VERSION == VERSION_GCN_JPN +#if TARGET_PC + if (dusk::version::isRegionJpn()) { + lit_1008 = 0; + } else if (dusk::version::isRegionPal()) { + if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { + lit_1008 = 2; + } else { + lit_1008 = 0; + } + } else { + lit_1008 = 1; + } +#elif VERSION == VERSION_GCN_JPN lit_1008 = 0; #elif VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { diff --git a/src/d/actor/d_a_mg_fshop.cpp b/src/d/actor/d_a_mg_fshop.cpp index 99f39e410e..f0905efd5d 100644 --- a/src/d/actor/d_a_mg_fshop.cpp +++ b/src/d/actor/d_a_mg_fshop.cpp @@ -5,14 +5,15 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep +#include "Z2AudioLib/Z2Instances.h" +#include "d/actor/d_a_mg_fish.h" #include "d/actor/d_a_mg_fshop.h" #include "d/actor/d_a_npc_henna.h" -#include "d/actor/d_a_mg_fish.h" #include "d/actor/d_a_player.h" -#include "f_op/f_op_camera_mng.h" -#include "d/d_timer.h" #include "d/d_s_play.h" -#include "Z2AudioLib/Z2Instances.h" +#include "d/d_timer.h" +#include "dusk/version.hpp" +#include "f_op/f_op_camera_mng.h" #if TARGET_PC #include "dusk/gyro.h" @@ -1742,7 +1743,18 @@ static int daFshop_Create(fopAc_ac_c* actor) { fopAcM_createChild(fpcNm_FSHOP_e, fopAcM_GetID(actor), 0xFFFFFF23, &actor->current.pos, fopAcM_GetRoomNo(actor), NULL, NULL, -1, NULL); u8 sp10; -#if VERSION == VERSION_GCN_PAL || VERSION == VERSION_WII_PAL +#if TARGET_PC + if (dusk::version::isRegionPal()) { + if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { + sp10 = 2; + } else { + sp10 = 0; + } + } else { + sp10 = 1; + } + +#elif VERSION == VERSION_GCN_PAL || VERSION == VERSION_WII_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { sp10 = 2; } else { diff --git a/src/d/actor/d_a_mg_rod.cpp b/src/d/actor/d_a_mg_rod.cpp index 96ad7aa148..956a9e3cff 100644 --- a/src/d/actor/d_a_mg_rod.cpp +++ b/src/d/actor/d_a_mg_rod.cpp @@ -25,6 +25,8 @@ #include #include +#include "dusk/version.hpp" + class dmg_rod_HIO_c : public JORReflexible { public: dmg_rod_HIO_c(); @@ -5734,10 +5736,22 @@ static void play_camera_u(dmg_rod_class* i_this) { static int dmg_rod_Execute(dmg_rod_class* i_this) { fopAc_ac_c* actor = &i_this->actor; + #if TARGET_PC + if (dusk::version::isPalOrAtLeastWiiR2()) { + if (dComIfGs_getPalLanguage() == 0) { + data_804BBBD4 = 2; + } else { + data_804BBBD4 = 0; + } + } else if (dusk::version::isRegionJpn()) { + data_804BBBD4 = 0; + } else { + data_804BBBD4 = 1; + } //TODO: It seems possible that dComIfGs_getPalLanguage returns a constant value for non-PAL // versions (causing the first block to be elided), and it's also possible that the value // being compared against is an enum value with per-version definitions. - #if VERSION == VERSION_SHIELD_DEBUG + #elif VERSION == VERSION_SHIELD_DEBUG if (dComIfGs_getPalLanguage() == 1) { data_804BBBD4 = 2; } else { @@ -6303,7 +6317,19 @@ static int dmg_rod_Create(fopAc_ac_c* i_this) { heap_size = 0xC9A0; } - #if VERSION == VERSION_SHIELD_DEBUG + #if TARGET_PC + if (dusk::version::isPalOrAtLeastWiiR2()) { + if (dComIfGs_getPalLanguage() == 0) { + data_804BBBD4 = 2; + } else { + data_804BBBD4 = 0; + } + } else if (dusk::version::isRegionJpn()) { + data_804BBBD4 = 0; + } else { + data_804BBBD4 = 1; + } + #elif VERSION == VERSION_SHIELD_DEBUG if (dComIfGs_getPalLanguage() == 1) { data_804BBBD4 = 2; } else { diff --git a/src/d/actor/d_a_player.cpp b/src/d/actor/d_a_player.cpp index 8ceb0ef237..6148d23e3a 100644 --- a/src/d/actor/d_a_player.cpp +++ b/src/d/actor/d_a_player.cpp @@ -369,18 +369,17 @@ JKRHeap* daPy_anmHeap_c::setAnimeHeap() { #if !PLATFORM_WII #if TARGET_PC #include "dusk/dvd_asset.hpp" +using GameVersion = dusk::version::GameVersion; static const u8* l_sightDL_get() { static u8 buf[0x89]; static bool _ = ( dusk::LoadDolAsset( buf, - #if VERSION == VERSION_GCN_PAL - 0x803BBDA0, - #elif VERSION == VERSION_GCN_JPN - 0x803B4220, - #elif VERSION == VERSION_GCN_USA - 0x803BA0C0, - #endif +{ + {GameVersion::GcnUsa, 0x803BA0C0}, + {GameVersion::GcnPal, 0x803BBDA0}, + {GameVersion::GcnJpn, 0x803B4220} + }, 0x89 ), true diff --git a/src/d/actor/d_a_title.cpp b/src/d/actor/d_a_title.cpp index 6df52abc5b..a75a96cb2e 100644 --- a/src/d/actor/d_a_title.cpp +++ b/src/d/actor/d_a_title.cpp @@ -1,21 +1,22 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep +#include "JSystem/J2DGraph/J2DScreen.h" +#include "JSystem/J2DGraph/J2DTextBox.h" +#include "JSystem/JKernel/JKRExpHeap.h" +#include "JSystem/JKernel/JKRMemArchive.h" #include "d/actor/d_a_title.h" +#include "d/d_com_inf_game.h" +#include "d/d_demo.h" +#include "d/d_menu_collect.h" +#include "d/d_pane_class_alpha.h" #include "d/d_s_logo.h" #include "d/d_s_play.h" -#include "d/d_demo.h" -#include "d/d_pane_class_alpha.h" -#include "d/d_menu_collect.h" +#include "dusk/version.hpp" +#include "f_op/f_op_msg_mng.h" +#include "f_op/f_op_overlap_mng.h" +#include "f_op/f_op_scene_mng.h" #include "m_Do/m_Do_Reset.h" #include "m_Do/m_Do_controller_pad.h" -#include "d/d_com_inf_game.h" -#include "JSystem/JKernel/JKRExpHeap.h" -#include "f_op/f_op_overlap_mng.h" -#include "f_op/f_op_msg_mng.h" -#include "f_op/f_op_scene_mng.h" -#include "JSystem/J2DGraph/J2DScreen.h" -#include "JSystem/JKernel/JKRMemArchive.h" -#include "JSystem/J2DGraph/J2DTextBox.h" #include "m_Do/m_Do_graphic.h" #ifdef TARGET_PC @@ -49,7 +50,10 @@ static u8 const lit_3772[12] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC +using namespace dusk::version; +#define l_arcName versionSelect({{GameVersion::GcnPal, "TitlePal"}}, "Title") +#elif VERSION == VERSION_GCN_PAL static char const l_arcName[] = "TitlePal"; #else static char const l_arcName[] = "Title"; @@ -59,7 +63,7 @@ daTit_HIO_c::daTit_HIO_c() { mPSScaleX = 1.0f; mPSScaleY = 1.0f; - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL switch (OSGetLanguage()) { case OS_LANGUAGE_ENGLISH: case OS_LANGUAGE_GERMAN: diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 1850e1e1ad..5f54ea4bda 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -17,7 +17,8 @@ const u16 l_J_Ohana00_64TEX__height = 63; #if TARGET_PC #include "dusk/dvd_asset.hpp" -static u8* l_J_Ohana00_64TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x9060, 0x800), true); return buf; } +using GameVersion = dusk::version::GameVersion; +static u8* l_J_Ohana00_64TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9060}, {GameVersion::GcnPal, 0x9060}}, 0x800), true); return buf; } #define l_J_Ohana00_64TEX (l_J_Ohana00_64TEX_get()) #else #include "assets/l_J_Ohana00_64TEX.h" @@ -113,10 +114,12 @@ static u8 l_flowerTexCoord[] = { 0x3E, 0xA7, 0x72, 0xD6, 0xBD, 0x2F, 0x46, 0xAA}; #if TARGET_PC -static u8* l_J_hana00DL_get() { static u8 buf[0x150]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x9D20, 0x150), true); return buf; } -static u8* l_J_hana00_cDL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x9E80, 0xDE), true); return buf; } -static u8* l_matDL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x9F60, 0x99), true); return buf; } -static u8* l_matLight4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xA000, 0x99), true); return buf; } +using GameVersion = dusk::version::GameVersion; + +static u8* l_J_hana00DL_get() { static u8 buf[0x150]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9D20}, {GameVersion::GcnPal, 0x9D20}}, 0x150), true); return buf; } +static u8* l_J_hana00_cDL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9E80}, {GameVersion::GcnPal, 0x9E80}}, 0xDE), true); return buf; } +static u8* l_matDL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9F60}, {GameVersion::GcnPal, 0x9F60}}, 0x99), true); return buf; } +static u8* l_matLight4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xA000}, {GameVersion::GcnPal, 0xA000}}, 0x99), true); return buf; } #define l_J_hana00DL (l_J_hana00DL_get()) #define l_J_hana00_cDL (l_J_hana00_cDL_get()) #define l_matDL (l_matDL_get()) @@ -142,7 +145,8 @@ const u16 l_J_Ohana01_64128_0419TEX__height = 127; #endif #if TARGET_PC -static u8* l_J_Ohana01_64128_0419TEX_get() { static u8 buf[0x1000]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xA0A0, 0x1000), true); return buf; } +using GameVersion = dusk::version::GameVersion; +static u8* l_J_Ohana01_64128_0419TEX_get() { static u8 buf[0x1000]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xA0A0}, {GameVersion::GcnPal, 0xA0A0}}, 0x1000), true); return buf; } #define l_J_Ohana01_64128_0419TEX (l_J_Ohana01_64128_0419TEX_get()) #else #include "assets/l_J_Ohana01_64128_0419TEX.h" @@ -274,11 +278,13 @@ static u8 l_flowerTexCoord2[] = { 0x40, 0x1B, 0x7D, 0x52, 0x3F, 0x80, 0x3F, 0x79, 0x40, 0x1B, 0x7D, 0x52, 0x3F, 0x51, 0x10, 0x6F}; #if TARGET_PC -static u8* l_J_hana01DL_get() { static u8 buf[0x138]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xB7C0, 0x138), true); return buf; } -static u8* l_J_hana01_c_00DL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xB900, 0xDE), true); return buf; } -static u8* l_J_hana01_c_01DL_get() { static u8 buf[0x128]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xB9E0, 0x128), true); return buf; } -static u8* l_mat2DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xBB20, 0x99), true); return buf; } -static u8* l_mat2Light4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0xBBC0, 0x99), true); return buf; } +using GameVersion = dusk::version::GameVersion; + +static u8* l_J_hana01DL_get() { static u8 buf[0x138]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB7C0}, {GameVersion::GcnPal, 0xB7C0}}, 0x138), true); return buf; } +static u8* l_J_hana01_c_00DL_get() { static u8 buf[0xDE]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB900}, {GameVersion::GcnPal, 0xB900}}, 0xDE), true); return buf; } +static u8* l_J_hana01_c_01DL_get() { static u8 buf[0x128]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xB9E0}, {GameVersion::GcnPal, 0xB9E0}}, 0x128), true); return buf; } +static u8* l_mat2DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xBB20}, {GameVersion::GcnPal, 0xBB20}}, 0x99), true); return buf; } +static u8* l_mat2Light4DL_get() { static u8 buf[0x99]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0xBBC0}, {GameVersion::GcnPal, 0xBBC0}}, 0x99), true); return buf; } #define l_J_hana01DL (l_J_hana01DL_get()) #define l_J_hana01_c_00DL (l_J_hana01_c_00DL_get()) #define l_J_hana01_c_01DL (l_J_hana01_c_01DL_get()) diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index ab2724060f..9bc8815753 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -20,8 +20,9 @@ const u16 l_M_kusa05_RGBATEX__height = 31; #if TARGET_PC #include "dusk/dvd_asset.hpp" -static u8* l_M_kusa05_RGBATEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x7680, 0x800), true); return buf; } -static u8* l_M_Hijiki00TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x7E80, 0x800), true); return buf; } +using GameVersion = dusk::version::GameVersion; +static u8* l_M_kusa05_RGBATEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x7680}, {GameVersion::GcnPal, 0x7680}}, 0x800), true); return buf; } +static u8* l_M_Hijiki00TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x7E80}, {GameVersion::GcnPal, 0x7E80}}, 0x800), true); return buf; } #define l_M_kusa05_RGBATEX (l_M_kusa05_RGBATEX_get()) #define l_M_Hijiki00TEX (l_M_Hijiki00TEX_get()) #else @@ -113,12 +114,14 @@ static u8 l_texCoord[160] = { }; #if TARGET_PC -static u8* l_M_Kusa_9qDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8B00, 0xCB), true); return buf; } -static u8* l_M_Kusa_9q_cDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8BE0, 0xCB), true); return buf; } -static u8* l_M_TenGusaDL_get() { static u8 buf[0xD4]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8CC0, 0xD4), true); return buf; } -static u8* l_Tengusa_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8DA0, 0xA8), true); return buf; } -static u8* l_kusa9q_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8E60, 0xA8), true); return buf; } -static u8* l_kusa9q_l4_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", 0x8F20, 0xA8), true); return buf; } +using GameVersion = dusk::version::GameVersion; + +static u8* l_M_Kusa_9qDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8B00}, {GameVersion::GcnPal, 0x8B00}}, 0xCB), true); return buf; } +static u8* l_M_Kusa_9q_cDL_get() { static u8 buf[0xCB]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8BE0}, {GameVersion::GcnPal, 0x8BE0}}, 0xCB), true); return buf; } +static u8* l_M_TenGusaDL_get() { static u8 buf[0xD4]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8CC0}, {GameVersion::GcnPal, 0x8CC0}}, 0xD4), true); return buf; } +static u8* l_Tengusa_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8DA0}, {GameVersion::GcnPal, 0x8DA0}}, 0xA8), true); return buf; } +static u8* l_kusa9q_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8E60}, {GameVersion::GcnPal, 0x8E60}}, 0xA8), true); return buf; } +static u8* l_kusa9q_l4_matDL_get() { static u8 buf[0xA8]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x8F20}, {GameVersion::GcnPal, 0x8F20}}, 0xA8), true); return buf; } #define l_M_Kusa_9qDL (l_M_Kusa_9qDL_get()) #define l_M_Kusa_9q_cDL (l_M_Kusa_9q_cDL_get()) #define l_M_TenGusaDL (l_M_TenGusaDL_get()) diff --git a/src/d/d_file_sel_info.cpp b/src/d/d_file_sel_info.cpp index f305f59a9e..0c987e6d63 100644 --- a/src/d/d_file_sel_info.cpp +++ b/src/d/d_file_sel_info.cpp @@ -14,6 +14,8 @@ #include #include +#include "dusk/version.hpp" + dFile_info_c::dFile_info_c(JKRArchive* i_archive, u8 param_1) { mArchive = i_archive; field_0x22 = param_1; @@ -169,7 +171,18 @@ void dFile_info_c::setSaveDate(dSv_save_c* i_savedata) { OSCalendarTime time; OSTicksToCalendarTime(i_savedata->getPlayer().getPlayerStatusB().getDateIpl(), &time); - #if (VERSION == VERSION_GCN_JPN) || (VERSION == VERSION_WII_JPN) + #if TARGET_PC + if (dusk::version::isRegionJpn()) { + sprintf(mSaveDate, "%d.%02d.%02d %02d:%02d", time.year, time.mon + 1, time.mday, + time.hour, time.min); + } else if (dusk::version::isRegionPal() && dComIfGs_getPalLanguage() != dSv_player_config_c::LANGUAGE_ENGLISH) { + sprintf(mSaveDate, "%02d/%02d/%d %02d:%02d", time.mday, time.mon + 1, time.year, time.hour, + time.min); + } else { + sprintf(mSaveDate, "%02d/%02d/%d %02d:%02d", time.mon + 1, time.mday, time.year, time.hour, + time.min); + } + #elif (VERSION == VERSION_GCN_JPN) || (VERSION == VERSION_WII_JPN) sprintf(mSaveDate, "%d.%02d.%02d %02d:%02d", time.year, time.mon + 1, time.mday, time.hour, time.min); #elif VERSION == VERSION_GCN_PAL diff --git a/src/d/d_menu_fishing.cpp b/src/d/d_menu_fishing.cpp index 615323767b..b76002d540 100644 --- a/src/d/d_menu_fishing.cpp +++ b/src/d/d_menu_fishing.cpp @@ -16,6 +16,8 @@ #include "m_Do/m_Do_graphic.h" #include +#include "dusk/version.hpp" + typedef void (dMenu_Fishing_c::*initFunc)(); initFunc map_init_process[] = { &dMenu_Fishing_c::wait_init, @@ -135,9 +137,9 @@ bool dMenu_Fishing_c::isSync() { } void dMenu_Fishing_c::init() { - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL BOOL isEnglish = FALSE; - if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) { + if (dusk::version::isRegionUsa() || (dusk::version::isRegionPal() && dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH)) { isEnglish = TRUE; } #endif @@ -145,7 +147,7 @@ void dMenu_Fishing_c::init() { for (int i = 0; i < MAX_FINDABLE_FISHES; i++) { if (dComIfGs_getFishNum(i) != 0) { // Fish has been caught once, display it along with it's params - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (isEnglish) { setFishParam(i, dComIfGs_getFishNum(i), dComIfGs_getFishSize(i) / 2.54f); } else { diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 7af18f0732..4040eb0d7f 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -307,7 +307,7 @@ static u8 getOutFontNumberType(int param_0) { } } -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC || VERSION == VERSION_GCN_PAL static void setPlayerName(char* i_player_name, u8 param_2) { if (param_2 != 0) { strcpy(i_player_name, dComIfGs_getPlayerName()); @@ -1485,7 +1485,7 @@ bool jmessage_tMeasureProcessor::do_tag(u32 i_tag, void const* i_data, u32 i_siz char buffer[40]; switch (i_tag & 0xFF00FFFF) { case MSGTAG_PLAYER_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setPlayerName(buffer, 1); } else { @@ -1495,7 +1495,7 @@ bool jmessage_tMeasureProcessor::do_tag(u32 i_tag, void const* i_data, u32 i_siz push_word(buffer); return true; case MSGTAG_HORSE_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setHorseName(buffer, 1); } else { @@ -4270,7 +4270,7 @@ bool jmessage_string_tMeasureProcessor::do_tag(u32 i_tag, void const* i_data, u3 char buffer[40]; switch (i_tag & 0xFF00FFFF) { case MSGTAG_PLAYER_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setPlayerName(buffer, 1); } else { @@ -4281,7 +4281,7 @@ bool jmessage_string_tMeasureProcessor::do_tag(u32 i_tag, void const* i_data, u3 stack_pushCurrent(buffer); break; case MSGTAG_HORSE_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setHorseName(buffer, 1); } else { @@ -4878,7 +4878,7 @@ bool jmessage_string_tRenderingProcessor::do_tag(u32 i_tag, void const* i_data, char buffer[40]; switch (i_tag & 0xFF00FFFF) { case MSGTAG_PLAYER_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setPlayerName(buffer, 1); } else { @@ -4889,7 +4889,7 @@ bool jmessage_string_tRenderingProcessor::do_tag(u32 i_tag, void const* i_data, push_word(buffer); break; case MSGTAG_HORSE_GENITIV: - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL if (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_GERMAN) { setHorseName(buffer, 1); } else { diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index 9b7b64ebda..ae0e3d8427 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -24,9 +24,11 @@ #include "f_op/f_op_msg_mng.h" #include #include + +#include "JSystem/JKernel/JKRExpHeap.h" +#include "dusk/version.hpp" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_lib.h" -#include "JSystem/JKernel/JKRExpHeap.h" #if TARGET_PC #include "dusk/settings.h" @@ -1643,7 +1645,30 @@ void dMsgObject_c::readMessageGroupLocal(mDoDvdThd_mountXArchive_c** p_arcMount) #else #if TARGET_PC // Original game UB - snprintf(arcName, sizeof(arcName), "/res/Msgus/bmgres%d.arc", msgGroup); + + if (dusk::version::isRegionPal()) { + switch (dComIfGs_getPalLanguage()) { + case dSv_player_config_c::LANGUAGE_GERMAN: + snprintf(arcName, sizeof(arcName), "/res/Msgde/bmgres%d.arc", msgGroup); + break; + case dSv_player_config_c::LANGUAGE_FRENCH: + snprintf(arcName, sizeof(arcName), "/res/Msgfr/bmgres%d.arc", msgGroup); + break; + case dSv_player_config_c::LANGUAGE_SPANISH: + snprintf(arcName, sizeof(arcName), "/res/Msgsp/bmgres%d.arc", msgGroup); + break; + case dSv_player_config_c::LANGUAGE_ITALIAN: + snprintf(arcName, sizeof(arcName), "/res/Msgit/bmgres%d.arc", msgGroup); + break; + default: + snprintf(arcName, sizeof(arcName), "/res/Msguk/bmgres%d.arc", msgGroup); + } + } else if (dusk::version::isRegionJpn()) { + snprintf(arcName, sizeof(arcName), "/res/Msgjp/bmgres%d.arc", msgGroup); + } else { + snprintf(arcName, sizeof(arcName), "/res/Msgus/bmgres%d.arc", msgGroup); + } + #else sprintf(arcName, "/res/Msgus/bmgres%d.arc", msgGroup); #endif diff --git a/src/d/d_msg_unit.cpp b/src/d/d_msg_unit.cpp index f773abd373..7bbe8b3d70 100644 --- a/src/d/d_msg_unit.cpp +++ b/src/d/d_msg_unit.cpp @@ -334,11 +334,11 @@ void dMsgUnit_c::setTag(int i_type, int i_value, char* o_buffer, bool param_4) { vals[1] = ((dMsgUnit_inf1_section_t*)pInfoBlock)->entries[i_type].endFrame; #endif -#if REGION_PAL +#if TARGET_PC || REGION_PAL if (i_value == 1 || (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_FRENCH && i_value == 0)) { -#elif !REGION_USA +#elif !REGION_USA // Dusk TODO: What? This checks for Spanish when *not* PAL or USA? if (i_value == 1 || (dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_SPANISH && i_value == 0)) { diff --git a/src/d/d_name.cpp b/src/d/d_name.cpp index e4232eff34..1f3ccbb1d7 100644 --- a/src/d/d_name.cpp +++ b/src/d/d_name.cpp @@ -8,9 +8,15 @@ #include "m_Do/m_Do_controller_pad.h" #include #include + #include "JSystem/J2DGraph/J2DAnmLoader.h" +#include "dusk/version.hpp" #include "f_op/f_op_msg_mng.h" +static bool isPalOrJpn() { + return dusk::version::isRegionPal() || dusk::version::isRegionJpn(); +} + static const char* l_mojiHira[65] = { "あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と", "な", "に", "ぬ", "ね", "の", "は", @@ -66,15 +72,31 @@ static const char* l_mojiEisu[65] = { "X", "k", "x", ",", "L", "Y", "l", "y", ".", "M", "Z", "m", "z", " ", }; -#if REGION_PAL -static char* l_mojiEisuPal_1[65] = { +#if TARGET_PC +// The game normally mutates this string list to fill in the real character codes. +// That can't work on a modern platform, so instead I've filled them out ahead of time. +static const char* l_mojiEisuPal_1[65] = { + "A", "N", "\xC0", "\xCF", "1", "B", "O", "\xC1", "\xD0", "2", "C", "P", "\xC2", "\xD1", "3", "D", "Q", + "\xC3", "\xD2", "4", "E", "R", "\xC4", "\xD3", "5", "F", "S", "\xC5", "\xD4", "6", "G", "T", "\xC6", "\xD5", + "7", "H", "U", "\xC7", "\xD6", "8", "I", "V", "\xC8", "\xD7", "9", "J", "W", "\xC9", "\xD8", "0", "K", + "X", "\xCA", "\xD9", ",", "L", "Y", "\xCB", "\xDA", ".", "M", "Z", "\xCC", "\xDB", " ", +}; + +static const char* l_mojiEisuPal_2[65] = { + "a", "n", "\xE0", "\xEF", "1", "b", "o", "\xE1", "\xF0", "2", "c", "p", "\xE2", "\xF1", "3", "d", "q", + "\xE3", "\xF2", "4", "e", "r", "\xE4", "\xF3", "5", "f", "s", "\xE5", "\xF4", "6", "g", "t", "\xE6", + "\xF5", "7", "h", "u", "\xE7", "\xF6", "8", "i", "v", "\xE8", "\xF7", "9", "j", "w", "\xE9", "\xF8", "0", + "k", "x", "\xEA", "\xF9", ",", "l", "y", "\xEB", "\xFA", ".", "m", "z", "\xEC", "\xFB", " ", +}; +#elif REGION_PAL +static const char* l_mojiEisuPal_1[65] = { "A", "N", "AA", "BB", "1", "B", "O", "CC", "DD", "2", "C", "P", "EE", "FF", "3", "D", "Q", "GG", "HH", "4", "E", "R", "II", "JJ", "5", "F", "S", "KK", "LL", "6", "G", "T", "MM", "NN", "7", "H", "U", "OO", "PP", "8", "I", "V", "QQ", "RR", "9", "J", "W", "SS", "TT", "0", "K", "X", "UU", "VV", ",", "L", "Y", "WW", "XX", ".", "M", "Z", "YY", "ZZ", " ", }; -static char* l_mojiEisuPal_2[65] = { +static const char* l_mojiEisuPal_2[65] = { "a", "n", "aa", "bb", "1", "b", "o", "cc", "dd", "2", "c", "p", "ee", "ff", "3", "d", "q", "gg", "hh", "4", "e", "r", "ii", "jj", "5", "f", "s", "kk", "ll", "6", "g", "t", "mm", "nn", "7", "h", "u", "oo", "pp", "8", "i", "v", "qq", "rr", "9", "j", "w", "ss", "tt", "0", @@ -82,6 +104,16 @@ static char* l_mojiEisuPal_2[65] = { }; #endif +#if TARGET_PC +// ' ' (full-width space) +#define SPACE_MAYBE_FULL (dusk::version::isRegionJpn() ? '\x81\x40' : ' ') +#elif REGION_JPN +// ' ' (full-width space) +#define SPACE_MAYBE_FULL '\x81\x40' +#else +#define SPACE_MAYBE_FULL ' ' +#endif + static dNm_HIO_c g_nmHIO; typedef void (dName_c::*selProcFunc)(void); @@ -163,13 +195,18 @@ void dName_c::init() { field_0x2ac = mSelProc; field_0x2ad = mSelProc; field_0x2ae = field_0x2ac; - #if REGION_PAL || REGION_JPN + #if TARGET_PC + mMojiSet = isPalOrJpn() ? MOJI_HIRA : MOJI_EIGO; + #elif REGION_PAL || REGION_JPN mMojiSet = MOJI_HIRA; #else mMojiSet = MOJI_EIGO; #endif mPrevMojiSet = 255; - #if REGION_PAL || REGION_JPN + #if TARGET_PC + mSelMenu = isPalOrJpn() ? MENU_HIRA : MENU_END; + mPrevSelMenu = isPalOrJpn() ? MENU_HIRA : MENU_END; + #elif REGION_PAL || REGION_JPN mSelMenu = MENU_HIRA; mPrevSelMenu = MENU_HIRA; #else @@ -187,7 +224,8 @@ void dName_c::initial() { mNextNameStr[0] = 0; } - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN + IF_DUSK_BLOCK(isPalOrJpn()) if (mSelProc == PROC_MOJI_SELECT) { mMenuIcon[mMojiSet]->scale(g_nmHIO.mMenuScale, g_nmHIO.mMenuScale); mMenuText[mMojiSet]->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); @@ -196,6 +234,7 @@ void dName_c::initial() { mMenuText[mPrevMojiSet]->setWhite(JUtility::TColor(0x96, 0x96, 0x96, 0xFF)); } } + IF_DUSK_BLOCK_END #endif } @@ -230,8 +269,8 @@ void dName_c::_move() { stick->checkTrigger(); (this->*SelProc[mSelProc])(); - #if REGION_PAL || REGION_JPN - if (mDoCPd_c::getTrigY(PAD_1)) { + #if TARGET_PC || REGION_PAL || REGION_JPN + if (IF_DUSK(isPalOrJpn() &&) mDoCPd_c::getTrigY(PAD_1)) { mDoAud_seStart(Z2SE_SY_DUMMY, 0, 0, 0); mPrevMojiSet = mMojiSet; mMojiSet++; @@ -245,8 +284,8 @@ void dName_c::_move() { mojiListChange(); } else { #endif - #if REGION_JPN - if (mDoCPd_c::getTrigX(PAD_1)) { + #if TARGET_PC || REGION_JPN + if (IF_DUSK(dusk::version::isRegionJpn() &&) mDoCPd_c::getTrigX(PAD_1)) { if (mCurPos != 0) { if (mojiChange(mCurPos - 1) == 1) { mDoAud_seStart(Z2SE_SY_DUMMY, 0, 0, 0); @@ -283,12 +322,14 @@ void dName_c::_move() { backSpace(); } } else if (mDoCPd_c::getTrigStart(PAD_1)) { +#define EIGO_OR_END DUSK_IF_ELSE((dusk::version::isRegionPal() ? MENU_EIGO : MENU_END), MENU_END) + #if REGION_PAL if ((mSelProc != PROC_MENU_SELECT || mSelMenu != MENU_EIGO) && (mSelProc == PROC_MENU_SELECT || mSelProc == PROC_MOJI_SELECT)) { #else - if ((mSelProc != PROC_MENU_SELECT || mSelMenu != MENU_END) && + if ((mSelProc != PROC_MENU_SELECT || mSelMenu != EIGO_OR_END) && (mSelProc == PROC_MENU_SELECT || mSelProc == PROC_MOJI_SELECT)) { #endif @@ -297,7 +338,7 @@ void dName_c::_move() { #if REGION_PAL mSelMenu = MENU_EIGO; #else - mSelMenu = MENU_END; + mSelMenu = EIGO_OR_END; #endif switch (mSelProc) { @@ -314,10 +355,10 @@ void dName_c::_move() { } } } - #if REGION_JPN + #if TARGET_PC || REGION_JPN } #endif - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN } #endif @@ -327,10 +368,9 @@ void dName_c::_move() { int dName_c::nameCheck() { for (int i = 8, len = 7; i > 0; i--) { #if REGION_JPN - // ' ' (full-width space) if (mChrInfo[len].mCharacter != ' ' && mChrInfo[len].mCharacter != '\x81\x40') { #else - if (mChrInfo[len].mCharacter != ' ') { + if (mChrInfo[len].mCharacter != ' ' IF_DUSK(&& (!dusk::version::isRegionJpn() || mChrInfo[len].mCharacter != '\x81\x40'))) { #endif return len + 1; } @@ -344,8 +384,8 @@ void dName_c::playNameSet(int nameLength) { char* str = mInputStr; for (int i = 0; i < nameLength; i++) { - #if REGION_JPN - if (mChrInfo[i].mMojiSet == 2) { + #if TARGET_PC || REGION_JPN + if (!dusk::version::isRegionJpn() || mChrInfo[i].mMojiSet == 2) { *str = mChrInfo[i].mCharacter; str += 1; } else { @@ -727,7 +767,34 @@ int dName_c::getMoji() { int result = -1; const char* moji; - #if REGION_PAL + #if TARGET_PC + if (dusk::version::isRegionPal()) { + switch (mMojiSet) { + case MOJI_HIRA: + moji = l_mojiEisuPal_1[mCharRow + mCharColumn * 5]; + break; + case MOJI_KATA: + moji = l_mojiEisuPal_2[mCharRow + mCharColumn * 5]; + break; + default: + abort(); + } + } else { + switch (mMojiSet) { + case MOJI_HIRA: + moji = l_mojiHira[mCharRow + mCharColumn * 5]; + break; + case MOJI_KATA: + moji = l_mojikata[mCharRow + mCharColumn * 5]; + break; + case MOJI_EIGO: + moji = l_mojiEisu[mCharRow + mCharColumn * 5]; + break; + default: + abort(); + } + } + #elif REGION_PAL switch (mMojiSet) { case MOJI_HIRA: moji = l_mojiEisuPal_1[mCharRow + mCharColumn * 5]; @@ -750,7 +817,17 @@ int dName_c::getMoji() { } #endif - #if REGION_JPN + #if TARGET_PC + if (dusk::version::isRegionJpn()) { + if (*(u8*)moji >> 4 == 0x8 || *(u8*)moji >> 4 == 0x9) { + result = *(u16*)moji; + } else { + result = *moji; + } + } else { + result = *moji; + } + #elif REGION_JPN if (*(u8*)moji >> 4 == 0x8 || *(u8*)moji >> 4 == 0x9) { result = *(u16*)moji; } else { @@ -763,6 +840,14 @@ int dName_c::getMoji() { return result; } +#if TARGET_PC +#define CHAR_TRUNC(val) (dusk::version::isRegionPal() ? val & 0xFF : val) +#elif REGION_PAL +#define CHAR_TRUNC(val) (val & 0xFF) +#else +#define CHAR_TRUNC(val) val +#endif + void dName_c::setMoji(int moji) { if (mCurPos == 8 || nameCheck() == 8) { mDoAud_seStart(Z2SE_SYS_ERROR, NULL, 0, 0); @@ -771,24 +856,14 @@ void dName_c::setMoji(int moji) { s32 notEmpty = false; for (int i = mCurPos; i < 8; i++) { - #if REGION_JPN - // ' ' (full-width space) - if (mChrInfo[i].mCharacter != '\x81\x40') { - #else - if (mChrInfo[i].mCharacter != ' ') { - #endif + if (mChrInfo[i].mCharacter != SPACE_MAYBE_FULL) { notEmpty = true; break; } } if (notEmpty) { - #if REGION_JPN - // ' ' (full-width space) - if (mChrInfo[7].mCharacter == '\x81\x40') { - #else - if (mChrInfo[7].mCharacter == ' ') { - #endif + if (mChrInfo[7].mCharacter == SPACE_MAYBE_FULL) { for (int i = 6; i >= mCurPos; i--) { mChrInfo[i + 1] = mChrInfo[i]; } @@ -797,11 +872,7 @@ void dName_c::setMoji(int moji) { mChrInfo[mCurPos].mRow = mCharRow; mChrInfo[mCurPos].mMojiSet = mMojiSet; mChrInfo[mCurPos].field_0x3 = 1; - #if REGION_PAL - mChrInfo[mCurPos].mCharacter = moji & 0xFF; - #else - mChrInfo[mCurPos].mCharacter = moji; - #endif + mChrInfo[mCurPos].mCharacter = CHAR_TRUNC(moji); if (mCurPos != 8) { mLastCurPos = mCurPos; @@ -814,11 +885,7 @@ void dName_c::setMoji(int moji) { mChrInfo[mCurPos].mRow = mCharRow; mChrInfo[mCurPos].mMojiSet = mMojiSet; mChrInfo[mCurPos].field_0x3 = 1; - #if REGION_PAL - mChrInfo[mCurPos].mCharacter = moji & 0xFF; - #else - mChrInfo[mCurPos].mCharacter = moji; - #endif + mChrInfo[mCurPos].mCharacter = CHAR_TRUNC(moji); if (mCurPos != 8) { mLastCurPos = mCurPos; @@ -844,13 +911,8 @@ void dName_c::setNameText() { "CR\x1b" "CC[000000]\x1bGM[0]%c\x1bHM\x1b" "CC[ffffff]\x1bGM[0]%c", - #if REGION_PAL - (u8)mChrInfo[i].mCharacter & 0xFF, - (u8)mChrInfo[i].mCharacter & 0xFF - #else - (u8)mChrInfo[i].mCharacter, - (u8)mChrInfo[i].mCharacter - #endif + CHAR_TRUNC((u8)mChrInfo[i].mCharacter), + CHAR_TRUNC((u8)mChrInfo[i].mCharacter) ); #if REGION_JPN } else { @@ -889,7 +951,29 @@ void dName_c::nameCursorMove() { void dName_c::selectCursorMove() { int idx; - #if REGION_PAL + #if TARGET_PC + if (dusk::version::isRegionPal()) { + if (mCharColumn < 3) { + idx = 0; + } else if (mCharColumn < 6) { + idx = 1; + } else if (mCharColumn >= 6) { + idx = 2; + } + } else if (dusk::version::isRegionJpn()) { + if (mCharColumn < 3) { + idx = 0; + } else if (mCharColumn < 6) { + idx = 1; + } else if (mCharColumn < 8) { + idx = 2; + } else if (mCharColumn >= 8) { + idx = 3; + } + } else { + idx = 3; + } + #elif REGION_PAL if (mCharColumn < 3) { idx = 0; } else if (mCharColumn < 6) { @@ -930,7 +1014,36 @@ void dName_c::selectCursorMove() { void dName_c::menuCursorPosSet() { mPrevSelMenu = mSelMenu; - #if REGION_PAL + #if TARGET_PC + if (dusk::version::isRegionPal()) { + if (mCharColumn < 3) { + mSelMenu = MENU_HIRA; + } else if (mCharColumn < 6) { + mSelMenu = MENU_KATA; + } else if (mCharColumn >= 6) { + mSelMenu = MENU_EIGO; + } + } else if (dusk::version::isRegionJpn()) { + if (mCharColumn < 3) { + mSelMenu = MENU_HIRA; + return; + } + if (mCharColumn < 6) { + mSelMenu = MENU_KATA; + return; + } + if (mCharColumn < 8) { + mSelMenu = MENU_EIGO; + return; + } + if (mCharColumn >= 8) { + mSelMenu = MENU_END; + return; + } + } else { + mSelMenu = MENU_END; + } + #elif REGION_PAL if (mCharColumn < 3) { mSelMenu = MENU_HIRA; } else if (mCharColumn < 6) { @@ -961,28 +1074,28 @@ void dName_c::menuCursorPosSet() { } void dName_c::MenuSelect() { - #if REGION_PAL || REGION_JPN - if (stick->checkRightTrigger()) { + #if TARGET_PC || REGION_PAL || REGION_JPN + if (isPalOrJpn() && stick->checkRightTrigger()) { mDoAud_seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0); mPrevSelMenu = mSelMenu; mSelMenu++; #if REGION_PAL if (mSelMenu > MENU_EIGO) { #else - if (mSelMenu > MENU_END) { + if (mSelMenu > EIGO_OR_END) { #endif mSelMenu = MENU_HIRA; } MenuSelectAnmInit(); mSelProc = PROC_MENU_SEL_ANM; - } else if (stick->checkLeftTrigger()) { + } else if (isPalOrJpn() && stick->checkLeftTrigger()) { mDoAud_seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0); mPrevSelMenu = mSelMenu; if (mSelMenu == MENU_HIRA) { #if REGION_JPN mSelMenu = MENU_END; #else - mSelMenu = MENU_EIGO; + mSelMenu = dusk::version::isRegionJpn() ? MENU_END : MENU_EIGO; #endif } else { mSelMenu--; @@ -1009,7 +1122,7 @@ void dName_c::MenuSelect() { #if REGION_PAL if (mSelMenu == MENU_EIGO) { #else - if (mSelMenu == MENU_END) { + if (mSelMenu == EIGO_OR_END) { #endif if (nameCheck() != 0) { mDoAud_seStart(Z2SE_SY_NAME_OK, NULL, 0, 0); @@ -1024,7 +1137,7 @@ void dName_c::MenuSelect() { #if REGION_PAL if (mSelMenu == MENU_EIGO) { #else - if (mSelMenu == MENU_END) { + if (mSelMenu == EIGO_OR_END) { #endif if (nameCheck() != 0) { mDoAud_seStart(Z2SE_SY_NAME_OK, NULL, 0, 0); @@ -1067,9 +1180,11 @@ void dName_c::MenuSelectAnm2() { if (canMove == true) { if (prevMenu_i != mojiSet_i) { mMenuText[prevMenu_i]->setWhite(JUtility::TColor(0x96, 0x96, 0x96, 0xFF)); - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN + IF_DUSK_BLOCK(isPalOrJpn()) mMenuIcon[mojiSet_i]->scale(g_nmHIO.mMenuScale, g_nmHIO.mMenuScale); mMenuText[mojiSet_i]->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); + IF_DUSK_BLOCK_END #endif } selectCursorMove(); @@ -1081,6 +1196,11 @@ void dName_c::MenuSelectAnm2() { void dName_c::MenuSelectAnm3() {} void dName_c::menuAbtnSelect() { +#if TARGET_PC + if (dusk::version::isRegionPal() && mSelMenu == MENU_EIGO) { + goto pal_eigo; + } +#endif switch (mSelMenu) { case MENU_HIRA: case MENU_KATA: @@ -1098,6 +1218,7 @@ void dName_c::menuAbtnSelect() { #else case MENU_END: #endif + IF_DUSK(pal_eigo:) int nameNum = nameCheck(); if (nameNum != 0) { playNameSet(nameNum); @@ -1116,44 +1237,33 @@ void dName_c::backSpace() { if (mCurPos != 0) { mDoAud_seStart(Z2SE_SY_NAME_DELETE, NULL, 0, 0); - #if REGION_JPN - // ' ' (full-width space) - if (mCurPos == 8 && mChrInfo[7].mCharacter != '\x81\x40') { - #else - if (mCurPos == 8 && mChrInfo[7].mCharacter != ' ') { - #endif + if (mCurPos == 8 && mChrInfo[7].mCharacter != SPACE_MAYBE_FULL) { mChrInfo[7].mColumn = 7; mChrInfo[7].mRow = 1; - #if REGION_PAL || REGION_JPN + #if TARGET_PC + mChrInfo[7].mMojiSet = isPalOrJpn() ? MOJI_HIRA : MOJI_EIGO; + #elif REGION_PAL || REGION_JPN mChrInfo[7].mMojiSet = MOJI_HIRA; #else mChrInfo[7].mMojiSet = MOJI_EIGO; #endif mChrInfo[7].field_0x3 = 1; - #if REGION_JPN - // ' ' (full-width space) - mChrInfo[7].mCharacter = '\x81\x40'; - #else - mChrInfo[7].mCharacter = ' '; - #endif + mChrInfo[7].mCharacter = SPACE_MAYBE_FULL; } else { for (int i = mCurPos - 1; i < 7; i++) { mChrInfo[i] = mChrInfo[i + 1]; } mChrInfo[7].mColumn = 7; mChrInfo[7].mRow = 1; - #if REGION_PAL || REGION_JPN +#if TARGET_PC + mChrInfo[7].mMojiSet = isPalOrJpn() ? MOJI_HIRA : MOJI_EIGO; +#elif REGION_PAL || REGION_JPN mChrInfo[7].mMojiSet = MOJI_HIRA; #else mChrInfo[7].mMojiSet = MOJI_EIGO; #endif mChrInfo[7].field_0x3 = 1; - #if REGION_JPN - // ' ' (full-width space) - mChrInfo[7].mCharacter = '\x81\x40'; - #else - mChrInfo[7].mCharacter = ' '; - #endif + mChrInfo[7].mCharacter = SPACE_MAYBE_FULL; } setNameText(); @@ -1164,7 +1274,31 @@ void dName_c::backSpace() { } void dName_c::mojiListChange() { - #if REGION_PAL + #if TARGET_PC + const char** mojiSet; + if (dusk::version::isRegionPal()) { + switch (mMojiSet) { + case MOJI_HIRA: + mojiSet = l_mojiEisuPal_1; + break; + case MOJI_KATA: + mojiSet = l_mojiEisuPal_2; + break; + } + } else { + switch (mMojiSet) { + case MOJI_HIRA: + mojiSet = l_mojiHira; + break; + case MOJI_KATA: + mojiSet = l_mojikata; + break; + case MOJI_EIGO: + mojiSet = l_mojiEisu; + break; + } + } + #elif REGION_PAL char** mojiSet; switch (mMojiSet) { @@ -1212,7 +1346,8 @@ void dName_c::mojiListChange() { strcpy(mMojiText[i], buf); } - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN + IF_DUSK_BLOCK(isPalOrJpn()) if (mSelProc == PROC_MOJI_SELECT) { mMenuIcon[mMojiSet]->scale(g_nmHIO.mMenuScale, g_nmHIO.mMenuScale); mMenuText[mMojiSet]->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); @@ -1221,6 +1356,7 @@ void dName_c::mojiListChange() { mMenuText[mPrevMojiSet]->setWhite(JUtility::TColor(0x96, 0x96, 0x96, 0xFF)); } } + IF_DUSK_BLOCK_END #endif } @@ -1241,9 +1377,11 @@ void dName_c::menuCursorMove2() { if (menu_i != mojiSet_i) { mMenuIcon[menu_i]->scale(g_nmHIO.mMenuScale, g_nmHIO.mMenuScale); mMenuText[menu_i]->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); - #if REGION_PAL || REGION_JPN + #if TARGET_PC || REGION_PAL || REGION_JPN + IF_DUSK_BLOCK(isPalOrJpn()) mMenuIcon[mojiSet_i]->scale(1.0f, 1.0f); mMenuText[mojiSet_i]->setWhite(JUtility::TColor(0x96, 0x96, 0x96, 0xFF)); + IF_DUSK_BLOCK_END #endif } @@ -1265,7 +1403,9 @@ void dName_c::selectCursorPosSet(int row) { mCharColumn = 3; break; case MENU_EIGO: - #if REGION_PAL + #if TARGET_PC + mCharColumn = dusk::version::isRegionPal() ? 8 : 6; + #elif REGION_PAL mCharColumn = 8; #else mCharColumn = 6; @@ -1481,9 +1621,11 @@ void dName_c::screenSet() { #endif } - #if !(REGION_PAL || REGION_JPN) + #if TARGET_PC || !(REGION_PAL || REGION_JPN) + IF_DUSK_BLOCK(!isPalOrJpn()) mMenuIcon[0]->hide(); mMenuIcon[1]->hide(); + IF_DUSK_BLOCK_END #endif mMojiPane = nameIn.NameInScr->search(MULTI_CHAR('moji_n')); @@ -1501,25 +1643,28 @@ void dName_c::screenSet() { ((J2DTextBox*)nameTagPane[i])->setFont(nameIn.font); ((J2DTextBox*)nameTagPane[i])->setString(72, ""); ((J2DTextBox*)nameTagPane[i])->setWhite(JUtility::TColor(0xC8, 0xC8, 0xC8, 0xFF)); - #if REGION_PAL + #if TARGET_PC || REGION_PAL + IF_DUSK_BLOCK(dusk::version::isRegionPal()) ((J2DTextBox*)nameTagPane[i])->resize(24.0f, 23.0f); + IF_DUSK_BLOCK_END #endif mNameText[i] = ((J2DTextBox*)nameTagPane[i])->getStringPtr(); } - #if REGION_PAL + #if REGION_PAL // DUSK version note: this code mutates strings. We just edit the table. + IF_DUSK_BLOCK(dusk::version::isRegionPal()) int idx = 2; - static u8 palMoji00[13] = { + static const u8 palMoji00[13] = { 0xC0, 0xC1, 0xC2, 0xC4, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, }; - static u8 palMoji01[13] = { + static const u8 palMoji01[13] = { 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD6, 0x8C, 0xD9, 0xDA, 0xDB, 0xDC, 0x2D, }; - static u8 palMoji10[13] = { + static const u8 palMoji10[13] = { 0xE0, 0xE1, 0xE2, 0xE4, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, }; - static u8 palMoji11[13] = { + static const u8 palMoji11[13] = { 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF6, 0x9C, 0xF9, 0xFA, 0xFB, 0xFC, 0xDF, }; @@ -1536,6 +1681,7 @@ void dName_c::screenSet() { l_mojiEisuPal_2[idx + 1][0] = palMoji11[i]; l_mojiEisuPal_2[idx + 1][1] = 0; } + IF_DUSK_BLOCK_END #endif mCharColumn = 0; @@ -1574,18 +1720,15 @@ void dName_c::displayInit() { mNameCursor[i]->hide(); mChrInfo[i].mColumn = 7; mChrInfo[i].mRow = 1; - #if REGION_PAL || REGION_JPN + #if TARGET_PC + mChrInfo[i].mMojiSet = isPalOrJpn() ? MOJI_HIRA : MOJI_EIGO; + #elif REGION_PAL || REGION_JPN mChrInfo[i].mMojiSet = MOJI_HIRA; #else mChrInfo[i].mMojiSet = MOJI_EIGO; #endif mChrInfo[i].field_0x3 = 1; - #if REGION_JPN - // ' ' (full-width space) - mChrInfo[i].mCharacter = '\x81\x40'; - #else - mChrInfo[i].mCharacter = ' '; - #endif + mChrInfo[i].mCharacter = SPACE_MAYBE_FULL; } mIsInputEnd = false; @@ -1596,7 +1739,63 @@ void dName_c::NameStrSet() { int i = 0; while (*moji != 0) { - #if REGION_PAL + #if TARGET_PC + if (dusk::version::isRegionPal()) { + mChrInfo[i].mCharacter = static_cast(*moji); + + for (int j = 0; j < 65; j++) { + if (mChrInfo[i].mCharacter == *(u8*)l_mojiEisuPal_1[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojiEisuPal_2[j]) + { + mChrInfo[i].mColumn = j / 5; + mChrInfo[i].mRow = j % 5; + mChrInfo[i].mMojiSet = MOJI_HIRA; + break; + } + } + moji++; + i++; + } else { + if (*(u8*)moji >> 4 == 8 || *(u8*)moji >> 4 == 9) { + mChrInfo[i].mCharacter = *(u16*)moji; + + for (int j = 0; j < 65; j++) { + if (mChrInfo[i].mCharacter == *(u16*)l_mojiHira[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojiHira2[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojiHira3[j]) + { + mChrInfo[i].mColumn = j / 5; + mChrInfo[i].mRow = j % 5; + mChrInfo[i].mMojiSet = MOJI_HIRA; + break; + } else if (mChrInfo[i].mCharacter == *(u16*)l_mojikata[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojikata2[j] || + mChrInfo[i].mCharacter == *(u16*)l_mojikata3[j]) + { + mChrInfo[i].mColumn = j / 5; + mChrInfo[i].mRow = j % 5; + mChrInfo[i].mMojiSet = MOJI_KATA; + break; + } + } + moji += 2; + i++; + } else { + mChrInfo[i].mCharacter = *moji; + + for (int j = 0; j < 65; j++) { + if (mChrInfo[i].mCharacter == *(u8*)l_mojiEisu[j]) { + mChrInfo[i].mColumn = j / 5; + mChrInfo[i].mRow = j % 5; + mChrInfo[i].mMojiSet = MOJI_EIGO; + break; + } + } + moji++; + i++; + } + } + #elif REGION_PAL mChrInfo[i].mCharacter = static_cast(*moji); for (int j = 0; j < 65; j++) { @@ -1669,7 +1868,9 @@ s32 dName_c::getMenuPosIdx(u8 selPos) { result = 1; break; case 2: - #if REGION_PAL + #if TARGET_PC + result = dusk::version::isRegionPal() ? 3 : 2; + #elif REGION_PAL result = 3; #else result = 2; diff --git a/src/d/d_s_logo.cpp b/src/d/d_s_logo.cpp index f04df0fa39..cc5d0114a4 100644 --- a/src/d/d_s_logo.cpp +++ b/src/d/d_s_logo.cpp @@ -24,6 +24,7 @@ #include "JSystem/JUtility/JUTConsole.h" #include "dusk/logging.h" +#include "dusk/version.hpp" #if !PLATFORM_GCN #include @@ -44,7 +45,12 @@ struct homeBtnData { }; #endif -#if VERSION == VERSION_SHIELD +#if TARGET_PC +using namespace dusk::version; + +#define LOGO_ARC versionSelect({{GameVersion::GcnJpn, "Logo"}, {GameVersion::GcnPal, "LogoPal"}}, "LogoUs") +#define MSG_PATH versionSelect({{GameVersion::GcnJpn, "/res/Msgjp/bmgres.arc"}}, "/res/Msgus/bmgres.arc") +#elif VERSION == VERSION_SHIELD #define LOGO_ARC "LogoUs" #define MSG_PATH "/res/Msgcn/bmgres.arc" #elif VERSION == VERSION_GCN_JPN @@ -789,7 +795,13 @@ dScnLogo_c::~dScnLogo_c() { JKR_DELETE(mProgressiveNo); JKR_DELETE(mProgressiveSel); - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC + if (getGameVersion() == GameVersion::GcnPal) { + mpPalLogoResCommand->getArchive()->removeResourceAll(); + mpPalLogoResCommand->getArchive()->unmount(); + mpPalLogoResCommand->destroy(); + } + #elif VERSION == VERSION_GCN_PAL mpPalLogoResCommand->getArchive()->removeResourceAll(); mpPalLogoResCommand->getArchive()->unmount(); mpPalLogoResCommand->destroy(); @@ -951,7 +963,8 @@ static int phase_0(dScnLogo_c* i_this) { JUT_ASSERT(1528, i_this->mLogo01Heap != NULL); JKRHEAP_NAME(i_this->mLogo01Heap, "Logo01"); - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL + IF_DUSK_BLOCK(getGameVersion() == GameVersion::GcnPal) switch (i_this->getPalLanguage()) { case 1: i_this->mpPalLogoResCommand = mDoDvdThd_mountArchive_c::create("/res/Layout/LogoPalGm.arc", 0, NULL); @@ -970,6 +983,7 @@ static int phase_0(dScnLogo_c* i_this) { i_this->mpPalLogoResCommand = mDoDvdThd_mountArchive_c::create("/res/Layout/LogoPalUk.arc", 0, NULL); break; } + IF_DUSK_BLOCK_END #endif #if PLATFORM_WII || PLATFORM_SHIELD @@ -990,7 +1004,8 @@ static int phase_1(dScnLogo_c* i_this) { } #endif - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC || VERSION == VERSION_GCN_PAL + IF_DUSK_BLOCK(getGameVersion() == GameVersion::GcnPal) if (!mDoDvdThd::SyncWidthSound) { return cPhs_INIT_e; } @@ -998,6 +1013,7 @@ static int phase_1(dScnLogo_c* i_this) { if (!i_this->mpPalLogoResCommand->sync()) { return cPhs_INIT_e; } + IF_DUSK_BLOCK_END #endif int rt; @@ -1237,7 +1253,113 @@ void dScnLogo_c::logoInitGC() { ResTIMG* dolbyImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 3); mDolbyLogo = JKR_NEW dDlst_2D_c(dolbyImg, 189, 150, 232, 112, 255); -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC + if (getGameVersion() == GameVersion::GcnPal) { + u8 language = getPalLanguage(); + if (language >= 5) { + language = 0; + } + + static const char* choice[] = { + "50_60_choice_eng.bti", + "50_60_choice_ger.bti", + "50_60_choice_fra.bti", + "50_60_choice_spa.bti", + "50_60_choice_ita.bti", + }; + + static const char* yes[] = { + "60_set_eng.bti", + "60_set_ger.bti", + "60_set_fra.bti", + "60_set_spa.bti", + "60_set_ita.bti", + }; + + static const char* no[] = { + "50_set_eng.bti", + "50_set_ger.bti", + "50_set_fra.bti", + "50_set_spa.bti", + "50_set_ita.bti", + }; + + static const char* prog[] = { + "progressive_pro.bti", + "progressive_pro_gm.bti", + "progressive_pro_fr.bti", + "progressive_pro_sp.bti", + "progressive_pro_it.bti", + }; + + static const char* intr[] = { + "progressive_inter.bti", + "progressive_inter_gm.bti", + "progressive_inter_fr.bti", + "progressive_inter_sp.bti", + "progressive_inter_it.bti", + }; + + static const char* warning[] = { + "warning.bti", + "warning_gm.bti", + "warning_fr.bti", + "warning_sp.bti", + "warning_it.bti", + }; + + static const char* warningPs[] = { + "warning_pstart.bti", + "warning_pstart_gm.bti", + "warning_pstart_fr.bti", + "warning_pstart_sp.bti", + "warning_pstart_it.bti", + }; + + ResTIMG* warningImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', warning[language]); + mWarning = JKR_NEW dDlst_2D_c(warningImg, 0, 0, FB_WIDTH, FB_HEIGHT, 255); + + ResTIMG* warnStartImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', warningPs[language]); + mWarningStart = JKR_NEW dDlst_2D_c(warnStartImg, 0, 359, FB_WIDTH, 48, 255); + + ResTIMG* progChoiceImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', choice[language]); + mProgressiveChoice = JKR_NEW dDlst_2D_c(progChoiceImg, 113, 143, 416, 210, 255); + + ResTIMG* progYesImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', yes[language]); + mProgressiveYes = JKR_NEW dDlst_2D_c(progYesImg, 121, 352, 200, 72, 255); + mProgressiveYes->getPicture()->setWhite(JUtility::TColor(160, 160, 160, 255)); + + ResTIMG* progNoImg = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', no[language]); + mProgressiveNo = JKR_NEW dDlst_2D_c(progNoImg, 320, 352, 200, 72, 255); + mProgressiveNo->getPicture()->setWhite(JUtility::TColor(160, 160, 160, 255)); + + mProgressivePro = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', prog[language]); + mProgressiveInter = (ResTIMG*)mpPalLogoResCommand->getArchive()->getResource('DAT ', intr[language]); + mProgressiveSel = JKR_NEW dDlst_2D_c(mProgressivePro, 153, 309, 336, 88, 255); + } else { + ResTIMG* warningImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 10); + mWarning = JKR_NEW dDlst_2D_c(warningImg, 0, 0, FB_WIDTH, FB_HEIGHT, 255); + + ResTIMG* warnStartImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 11); + mWarningStart = JKR_NEW dDlst_2D_c(warnStartImg, 0, 359, FB_WIDTH, 48, 255); + + ResTIMG* progChoiceImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 5); + mProgressiveChoice = JKR_NEW dDlst_2D_c(progChoiceImg, 113, 281, 416, 72, 255); + + ResTIMG* progYesImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 9); + mProgressiveYes = JKR_NEW dDlst_2D_c(progYesImg, 211, 372, 80, 32, 255); + mProgressiveYes->getPicture()->setWhite(JUtility::TColor(160, 160, 160, 255)); + + ResTIMG* progNoImg = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 7); + mProgressiveNo = JKR_NEW dDlst_2D_c(progNoImg, 350, 372, 80, 32, 255); + mProgressiveNo->getPicture()->setWhite(JUtility::TColor(160, 160, 160, 255)); + + mProgressivePro = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 8); + mProgressiveInter = (ResTIMG*)dComIfG_getObjectRes(LOGO_ARC, 6); + mProgressiveSel = JKR_NEW dDlst_2D_c(mProgressivePro, 153, 309, 336, 88, 255); + } + +#elif VERSION == VERSION_GCN_PAL u8 language = getPalLanguage(); if (language >= 5) { language = 0; @@ -1395,7 +1517,30 @@ void dScnLogo_c::dvdDataLoad() { mpButtonCommand = aramMount(BUTTON_RES_PATH, mDoExt_getJ2dHeap()); mpCardIconCommand = aramMount(ICON_RES_PATH, mDoExt_getJ2dHeap()); - #if VERSION == VERSION_GCN_PAL + #if TARGET_PC + if (getGameVersion() == GameVersion::GcnPal) { + switch (getPalLanguage()) { + case 1: + mpBmgResCommand = onMemMount("/res/Msgde/bmgres.arc"); + break; + case 2: + mpBmgResCommand = onMemMount("/res/Msgfr/bmgres.arc"); + break; + case 3: + mpBmgResCommand = onMemMount("/res/Msgsp/bmgres.arc"); + break; + case 4: + mpBmgResCommand = onMemMount("/res/Msgit/bmgres.arc"); + break; + case 0: + default: + mpBmgResCommand = onMemMount("/res/Msguk/bmgres.arc"); + break; + } + } else { + mpBmgResCommand = onMemMount(MSG_PATH); + } + #elif VERSION == VERSION_GCN_PAL switch (getPalLanguage()) { case 1: mpBmgResCommand = onMemMount("/res/Msgde/bmgres.arc"); @@ -1435,7 +1580,10 @@ void dScnLogo_c::dvdDataLoad() { mpMsgResCommand[1] = aramMount(MSG_RES1_PATH, mDoExt_getJ2dHeap()); mpMsgResCommand[2] = aramMount(MSG_RES2_PATH, mDoExt_getJ2dHeap()); mpMsgResCommand[3] = aramMount(MSG_RES3_PATH, mDoExt_getJ2dHeap()); -#if VERSION == VERSION_GCN_JPN +#if TARGET_PC + const auto res4Path = versionSelect({{GameVersion::GcnJpn, "/res/Layout/msgres04.arc"}}, "/res/Layout/msgres04F.arc"); + mpMsgResCommand[4] = aramMount( res4Path, mDoExt_getJ2dHeap()); +#elif VERSION == VERSION_GCN_JPN mpMsgResCommand[4] = aramMount("/res/Layout/msgres04.arc", mDoExt_getJ2dHeap()); #else mpMsgResCommand[4] = aramMount("/res/Layout/msgres04F.arc", mDoExt_getJ2dHeap()); @@ -1445,7 +1593,23 @@ void dScnLogo_c::dvdDataLoad() { mpMain2DCommand = onMemMount(MAIN2D_PATH); -#if VERSION == VERSION_GCN_JPN +#if TARGET_PC + const auto fontResPath = versionSelect( + { + {GameVersion::GcnJpn, "/res/Fontjp/fontres.arc"}, + {GameVersion::GcnPal, "/res/Fonteu/fontres.arc"}, + }, "/res/Fontus/fontres.arc"); + const auto fontRubyPath = versionSelect( + { + {GameVersion::GcnJpn, "/res/Fontjp/rubyres.arc"}, + {GameVersion::GcnPal, "/res/Fonteu/rubyres.arc"}, + }, "/res/Fontus/rubyres.arc"); + + // Note: GCN_JPN mounts this archive as tail instead of head. + // I'm guessing this is fine since we have more RAM. + mpFontResCommand = onMemMount(fontResPath); + mpRubyResCommand = onMemMount(fontRubyPath); +#elif VERSION == VERSION_GCN_JPN mpFontResCommand = mDoDvdThd_mountXArchive_c::create("/res/Fontjp/fontres.arc", 1, JKRArchive::MOUNT_MEM, NULL); mpRubyResCommand = onMemMount("/res/Fontjp/rubyres.arc"); #elif VERSION == VERSION_GCN_PAL @@ -1545,7 +1709,7 @@ static int dScnLogo_IsDelete(dScnLogo_c* i_this) { return 1; } -#if VERSION == VERSION_GCN_PAL || PLATFORM_WII || PLATFORM_SHIELD +#if TARGET_PC || VERSION == VERSION_GCN_PAL || PLATFORM_WII || PLATFORM_SHIELD u8 dScnLogo_c::getPalLanguage() { u8 language; diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index 3289a49c64..be95d1f190 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -16,6 +16,8 @@ #include #include +#include "dusk/version.hpp" + #if PLATFORM_WII || PLATFORM_SHIELD #include #include @@ -1027,7 +1029,7 @@ void dSv_player_config_c::init() { mAttentionType = 0; mVibration = 1; -#if DEBUG +#if DEBUG // DUSK VERSION SUPPORT: This field isn't used, so we can ignore it. mLanguage = SCGetLanguage(); #elif REGION_PAL || VERSION >= VERSION_WII_USA_R2 mLanguage = OSGetLanguage(); @@ -1072,7 +1074,8 @@ void dSv_player_config_c::setVibration(u8 i_status) { } u8 dSv_player_config_c::getPalLanguage() const { -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC || VERSION == VERSION_GCN_PAL + IF_DUSK_BLOCK(dusk::version::getGameVersion() == dusk::version::GameVersion::GcnPal) switch (OSGetLanguage()) { case 0: return LANGUAGE_ENGLISH; @@ -1085,6 +1088,7 @@ u8 dSv_player_config_c::getPalLanguage() const { case 4: return LANGUAGE_ITALIAN; } + IF_DUSK_BLOCK_END #elif VERSION >= VERSION_WII_USA_R0 switch (SCGetLanguage()) { case 1: diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 176967359d..856cdce4a3 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -154,6 +154,7 @@ namespace dusk::config { template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; + template class ConfigImpl; } void dusk::config::Register(ConfigVarBase& configVar) { diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 0499f0d6a1..657e5ef5d2 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -167,7 +167,6 @@ void InitializeCrashReporting() { sentry_set_tag("git_branch", DUSK_WC_BRANCH); sentry_set_tag("build_type", DUSK_BUILD_TYPE); - sentry_set_tag("tp_version", DUSK_TP_VERSION); g_sentryInitialized = true; DuskLog.info("Initialized Sentry crash reporting"); diff --git a/src/dusk/dvd_asset.cpp b/src/dusk/dvd_asset.cpp index b9e0729be9..75c82107cc 100644 --- a/src/dusk/dvd_asset.cpp +++ b/src/dusk/dvd_asset.cpp @@ -80,8 +80,19 @@ static s32 DolVaToFileOffset(u32 va) { return -1; } -bool LoadDolAsset(void* dst, u32 virtualAddress, s32 size) { - s32 fileOffset = DolVaToFileOffset(virtualAddress); +static u32 GetOffsetForVersion(std::initializer_list version) { + const auto gameVersion = dusk::version::getGameVersion(); + for (auto elem : version) { + if (elem.mGameVersion == gameVersion) { + return elem.mOffset; + } + } + + DuskLog.fatal("Unable to find offset for this game version!"); +} + +bool LoadDolAsset(void* dst, std::initializer_list virtualAddress, s32 size) { + s32 fileOffset = DolVaToFileOffset(GetOffsetForVersion(virtualAddress)); if (fileOffset < 0) { return false; } @@ -95,13 +106,16 @@ bool LoadDolAsset(void* dst, u32 virtualAddress, s32 size) { return true; } -bool LoadRelAsset(void* dst, const char* dvdPath, s32 offset, s32 size) { - void* p = JKRDvdRipper::loadToMainRAM(dvdPath, (u8*)dst, EXPAND_SWITCH_UNKNOWN1, (u32)size, nullptr, JKRDvdRipper::ALLOC_DIRECTION_FORWARD, (u32)offset, nullptr, nullptr); - if (!p) DuskLog.fatal("dvd_asset: failed to load {} (offset={:#x} size={:#x})", dvdPath, offset, size); +bool LoadRelAsset(void* dst, const char* dvdPath, std::initializer_list offset, s32 size) { + auto resOffset = GetOffsetForVersion(offset); + void* p = JKRDvdRipper::loadToMainRAM(dvdPath, (u8*)dst, EXPAND_SWITCH_UNKNOWN1, (u32)size, nullptr, JKRDvdRipper::ALLOC_DIRECTION_FORWARD, resOffset, nullptr, nullptr); + if (!p) DuskLog.fatal("dvd_asset: failed to load {} (offset={:#x} size={:#x})", dvdPath, resOffset, size); return p != nullptr; } -bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, s32 offset, s32 size) { +bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, std::initializer_list offset, s32 size) { + auto resOffset = GetOffsetForVersion(offset); + // On TARGET_PC, cDyl_InitCallback skips DynamicModuleControl::initialize() due to static linking // Mount RELS.arc on first use so sArchive is available static bool s_mountAttempted = false; @@ -118,7 +132,7 @@ bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, s32 o DuskLog.fatal("dvd_asset: {} not found in RELS archive", relFileName); return false; } - std::memcpy(dst, rel + offset, size); + std::memcpy(dst, rel + resOffset, size); return true; } diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp index 2727e78718..6347e9bc01 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -22,6 +22,10 @@ typedef void (ImGuiPreLaunchWindow::*drawFunc)(); drawFunc drawTable[2] = {&ImGuiPreLaunchWindow::drawMainMenu, &ImGuiPreLaunchWindow::drawOptions}; +static constexpr std::array skLanguageNames = { + "English", "German", "French", "Spanish", "Italian" +}; + static constexpr std::array skGameDiscFileFilters{{ {"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"}, {"All Files", "*"}, @@ -195,6 +199,20 @@ void ImGuiPreLaunchWindow::drawOptions() { false); } + // TODO: Only show if PAL disc selected? + // Language selection + auto selectedLanguage = getSettings().game.language.getValue(); + if (ImGui::BeginCombo("Language", skLanguageNames[static_cast(selectedLanguage)])) { + for (u8 i = 0; i < skLanguageNames.size(); ++i) { + if (ImGui::Selectable(skLanguageNames[i])) { + getSettings().game.language.setValue(static_cast(i)); + config::Save(); + } + } + + ImGui::EndCombo(); + } + AuroraBackend configuredBackend = BACKEND_AUTO; const std::string& configuredBackendId = getSettings().backend.graphicsBackend; if (!try_parse_backend(configuredBackendId, configuredBackend)) { diff --git a/src/dusk/iso_validate.cpp b/src/dusk/iso_validate.cpp index 755b4eede0..0b4a19ae9c 100644 --- a/src/dusk/iso_validate.cpp +++ b/src/dusk/iso_validate.cpp @@ -19,6 +19,7 @@ constexpr const char* TP_GAME_IDS[] = { constexpr const char* SUPPORTED_TP_GAME_IDS[] = { "GZ2E01", // GCN USA + "GZ2P01", // GCN PAL }; template diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 3e6ee733fe..c7d19beb31 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -20,6 +20,8 @@ UserSettings g_userSettings = { }, .game = { + .language { "game.language", GameLanguage::English }, + // Quality of Life .enableQuickTransform {"game.enableQuickTransform", false}, .hideTvSettingsScreen {"game.hideTvSettingsScreen", false}, @@ -123,6 +125,7 @@ void registerSettings() { Register(g_userSettings.audio.enableReverb); // Game + Register(g_userSettings.game.language); Register(g_userSettings.game.enableQuickTransform); Register(g_userSettings.game.hideTvSettingsScreen); Register(g_userSettings.game.skipWarningScreen); diff --git a/src/dusk/version.cpp b/src/dusk/version.cpp new file mode 100644 index 0000000000..e925dabaf8 --- /dev/null +++ b/src/dusk/version.cpp @@ -0,0 +1,82 @@ +#include "dusk/version.hpp" + +#include "dusk/logging.h" + +namespace dusk::version { + +using namespace std::string_view_literals; + +static bool versionInitialized; +static GameVersion gameVersion; +static DVDDiskID diskId; + +void init() { + versionInitialized = true; + + if (!DVDLowReadDiskID(&diskId, nullptr)) { + DuskLog.fatal("DVDLowReadDiskID failed to return instantly."); + } + + std::string_view company(diskId.company, sizeof(diskId.company)); + std::string_view game(diskId.gameName, sizeof(diskId.gameName)); + + if (company != "01"sv) { + DuskLog.fatal("Wrong company ID in disc: {}", company); + } + + if (game == "GZ2E"sv) { + gameVersion = GameVersion::GcnUsa; + } else if (game == "GZ2P") { + gameVersion = GameVersion::GcnPal; + } else { + // TODO: Handle remaining valid versions. + DuskLog.fatal("Unknown/unsupported game version in disc: {}", game); + } + + DuskLog.info("Loaded game disc is {}{}", game, company); +} + +bool isGcn() { + return getGameVersion() == GameVersion::GcnUsa + || getGameVersion() == GameVersion::GcnPal + || getGameVersion() == GameVersion::GcnJpn; +} + +bool isWii() { + return getGameVersion() == GameVersion::WiiUsaRev0 + || getGameVersion() == GameVersion::WiiUsa + || getGameVersion() == GameVersion::WiiPal + || getGameVersion() == GameVersion::WiiJpn + || getGameVersion() == GameVersion::WiiKor; +} + +bool isPalOrAtLeastWiiR2() { + return getGameVersion() == GameVersion::GcnPal || getGameVersion() >= GameVersion::WiiUsa; +} + +bool isRegionJpn() { + return getGameVersion() == GameVersion::WiiJpn || getGameVersion() == GameVersion::GcnJpn; +} + +bool isRegionPal() { + return getGameVersion() == GameVersion::WiiPal || getGameVersion() == GameVersion::GcnPal; +} + +bool isRegionUsa() { + return getGameVersion() == GameVersion::WiiUsa || getGameVersion() == GameVersion::WiiUsaRev0 + || getGameVersion() == GameVersion::GcnUsa; +} + +GameVersion getGameVersion() { + if (!versionInitialized) { + abort(); + } + + return gameVersion; +} + +const DVDDiskID& getDiskID() { + return diskId; +} + +} // namespace dusk::version \ No newline at end of file diff --git a/src/m_Do/m_Do_MemCard.cpp b/src/m_Do/m_Do_MemCard.cpp index 5cb612f128..dbf962b173 100644 --- a/src/m_Do/m_Do_MemCard.cpp +++ b/src/m_Do/m_Do_MemCard.cpp @@ -12,6 +12,7 @@ #include "os_report.h" #include "dusk/os.h" #include "dusk/main.h" +#include "dusk/version.hpp" #if PLATFORM_WII || PLATFORM_SHIELD #include @@ -79,7 +80,11 @@ void mDoMemCd_Ctrl_c::ThdInit() { #if !PLATFORM_SHIELD CARDSetLoadType((CARDFileType)dusk::getSettings().backend.cardFileType.getValue()); - CARDInit(DUSK_GAME_NAME, DUSK_GAME_VERSION); + char version[5] = {}; + char maker[3] = {}; + std::memcpy(version, dusk::version::getDiskID().gameName, 4); + std::memcpy(maker, dusk::version::getDiskID().company, 2); + CARDInit(version, maker); #endif mCopyToPos = 0; diff --git a/src/m_Do/m_Do_MemCardRWmng.cpp b/src/m_Do/m_Do_MemCardRWmng.cpp index 518bdd27e9..d7739d6158 100644 --- a/src/m_Do/m_Do_MemCardRWmng.cpp +++ b/src/m_Do/m_Do_MemCardRWmng.cpp @@ -11,6 +11,8 @@ #include #include +#include "dusk/version.hpp" + #if VERSION == VERSION_GCN_JPN #define HEADER_TITLE "ゼルダの伝説 トワイライトプリンセス" #define HEADER_COMMENT "%d月%d日のセーブデータです" @@ -313,12 +315,44 @@ s32 mDoMemCdRWm_StoreBannerNAND(NANDFileInfo* file) { #endif static void mDoMemCdRWm_BuildHeader(mDoMemCdRWm_HeaderData* header) { +#if !TARGET_PC snprintf(header->mTitle, sizeof(header->mTitle), HEADER_TITLE); +#endif OSCalendarTime time; OSTicksToCalendarTime(OSGetTime(), &time); -#if VERSION == VERSION_GCN_PAL +#if TARGET_PC + if (dusk::version::isRegionPal()) { + snprintf(header->mTitle, sizeof(header->mTitle), HEADER_TITLE); + + switch (dComIfGs_getPalLanguage()) { + case dSv_player_config_c::LANGUAGE_ENGLISH: + snprintf(header->mComment, sizeof(header->mComment), "%d/%d Save Data", time.mon + 1, time.mday); + break; + case dSv_player_config_c::LANGUAGE_GERMAN: + snprintf(header->mComment, sizeof(header->mComment), "%d/%d Spielstand", time.mday, time.mon + 1); + break; + case dSv_player_config_c::LANGUAGE_FRENCH: + snprintf(header->mComment, sizeof(header->mComment), "Donn%ces de jeu %d/%d", 0xE9, time.mday, time.mon + 1); + break; + case dSv_player_config_c::LANGUAGE_SPANISH: + snprintf(header->mComment, sizeof(header->mComment), "Datos guardados el %d/%d", time.mday, time.mon + 1); + break; + case dSv_player_config_c::LANGUAGE_ITALIAN: + snprintf(header->mComment, sizeof(header->mComment), "Dati salvati: %d/%d", time.mday, time.mon + 1); + break; + } + } else if (dusk::version::isRegionUsa()) { + snprintf(header->mTitle, sizeof(header->mTitle), HEADER_TITLE); + + snprintf(header->mComment, sizeof(header->mComment), HEADER_COMMENT, time.mon + 1, time.mday); + } else { + // TODO JPN SHIFT-JIS + // snprintf(header->mTitle, sizeof(header->mTitle), "ゼルダの伝説 トワイライトプリンセス"); + // snprintf(header->mComment, sizeof(header->mComment), "%d月%d日のセーブデータです", time.mon + 1, time.mday); + } +#elif VERSION == VERSION_GCN_PAL switch (dComIfGs_getPalLanguage()) { case dSv_player_config_c::LANGUAGE_ENGLISH: snprintf(header->mComment, sizeof(header->mComment), "%d/%d Save Data", time.mon + 1, time.mday); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 579cbd8c8b..d25debb38e 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -71,6 +71,7 @@ #include "dusk/config.hpp" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" +#include "dusk/version.hpp" #include "dusk/discord_presence.hpp" #include "tracy/Tracy.hpp" #include "f_pc/f_pc_draw.h" @@ -465,6 +466,23 @@ static constexpr PADDefaultMapping defaultPadMapping = { static bool mainCalled = false; +static u8 selectedLanguage; + +u8 OSGetLanguage() { + return selectedLanguage; +} + +static void LanguageInit() { + // Keep language at 0 (English) if not on a PAL disk. + // Doubt this matters, but avoid funky shit. + if (!dusk::version::isRegionPal()) { + return; + } + + // Cache this to avoid funky shenanigans. + selectedLanguage = static_cast(dusk::getSettings().game.language.getValue()); +} + // ========================================================================= // PC ENTRY POINT // ========================================================================= @@ -597,6 +615,9 @@ int game_main(int argc, char* argv[]) { } } + dusk::version::init(); + LanguageInit(); + OSInit(); mDoMain::sPowerOnTime = OSGetTime(); From 0e5ea0d3b1552cd00beffa05b41b889cc9a8c539 Mon Sep 17 00:00:00 2001 From: Howard Luck Date: Sat, 25 Apr 2026 15:48:45 -0600 Subject: [PATCH 17/70] fix d_meter_button flashing (#549) --- include/d/d_meter_button.h | 5 +++++ src/d/d_meter_button.cpp | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/include/d/d_meter_button.h b/include/d/d_meter_button.h index 3028abeab5..0f05d32440 100644 --- a/include/d/d_meter_button.h +++ b/include/d/d_meter_button.h @@ -343,6 +343,11 @@ public: /* 0x624 */ f32 mMidonaPosX; /* 0x628 */ f32 mMidonaPosY; /* 0x62C */ f32 mMidonaScale; + +#ifdef TARGET_PC + bool mWasListen[2]; + bool mWasRepeat[2]; +#endif }; #endif /* D_METER_D_METER_BUTTON_H */ diff --git a/src/d/d_meter_button.cpp b/src/d/d_meter_button.cpp index d36a124d17..e55b42bc1a 100644 --- a/src/d/d_meter_button.cpp +++ b/src/d/d_meter_button.cpp @@ -368,8 +368,22 @@ void dMeterButton_c::draw() { } if (var_r3) { +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) { + mWasListen[i] = var_r22; + mWasRepeat[i] = var_r23; + } else { + var_r22 = mWasListen[i]; + var_r23 = mWasRepeat[i]; + } +#endif if (var_r22) { - if (field_0x2e8[i] == 18.0f) { +#ifdef TARGET_PC + if (field_0x2e8[i] == 18.0f && dusk::frame_interp::get_ui_tick_pending()) +#else + if (field_0x2e8[i] == 18.0f) +#endif + { mDoAud_seStart(Z2SE_SY_HINT_BUTTON_BLINK, NULL, 0, 0); } From 1b792eb964d6c77ea73c485f2da256455edf5456 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 25 Apr 2026 16:18:16 -0600 Subject: [PATCH 18/70] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 8d9618dc4b..135e976867 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 8d9618dc4bd82e476754c5b841b85fba6ee6ddab +Subproject commit 135e976867490380356878c8bb8d1956932489c5 From ca9330b412da1e6d84ff0ae9d017206e89e1c237 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sat, 25 Apr 2026 20:40:29 -0400 Subject: [PATCH 19/70] Register interp callback for d_a_obj_lv8OptiLift Fixes disappearing platforms --- src/d/actor/d_a_obj_lv8OptiLift.cpp | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/d/actor/d_a_obj_lv8OptiLift.cpp b/src/d/actor/d_a_obj_lv8OptiLift.cpp index a8610989a7..6b6c806d64 100644 --- a/src/d/actor/d_a_obj_lv8OptiLift.cpp +++ b/src/d/actor/d_a_obj_lv8OptiLift.cpp @@ -11,6 +11,10 @@ #include "d/d_com_inf_game.h" #include "d/d_path.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + daOptiLift_HIO_c::daOptiLift_HIO_c() { mStopDisappearTime = 30; mStartMoveTime = 30; @@ -412,7 +416,44 @@ void daOptiLift_c::setNextPoint() { mCurrentPoint = next_point; } +#if TARGET_PC +static void daOptiLift_interp_callback(bool isSimFrame, void* pUserWork) { + daOptiLift_c* lift = static_cast(pUserWork); + if (lift == NULL || lift->mpModel == NULL) { + return; + } + + g_env_light.settingTevStruct(0x10, &lift->current.pos, &lift->tevStr); + g_env_light.setLightTevColorType_MAJI(lift->mpModel, &lift->tevStr); + + J3DModelData* modelData = lift->mpModel->getModelData(); + J3DMaterial* materialp = modelData->getMaterialNodePointer(0); + + if (materialp->getTexGenBlock()->getTexMtx(1) != NULL) { + J3DTexMtxInfo* mtx_info = &materialp->getTexGenBlock()->getTexMtx(1)->getTexMtxInfo(); + if (mtx_info != NULL) { + Mtx m; + C_MTXLightOrtho(m, 100.0f, -100.0f, -100.0f, 100.0f, 1.0f, 1.0f, 0.0f, 0.0f); + mDoMtx_stack_c::XrotS(0x4000); + mDoMtx_stack_c::transM(-lift->current.pos.x, -lift->current.pos.y, -lift->current.pos.z); + cMtx_concat(m, mDoMtx_stack_c::get(), mtx_info->mEffectMtx); + } + } + + lift->mBtk.entry(modelData); + + J3DGXColor* color = materialp->getTevKColor(1); + color->r = l_HIO.mColorR; + color->g = l_HIO.mColorG; + color->b = l_HIO.mColorB; +} +#endif + int daOptiLift_c::Draw() { +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&daOptiLift_interp_callback, this); +#endif + g_env_light.settingTevStruct(0x10, ¤t.pos, &tevStr); g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr); From 6fc862814419dd36e3e06e388d3575f97b8ccb03 Mon Sep 17 00:00:00 2001 From: Howard Luck Date: Sat, 25 Apr 2026 18:44:38 -0600 Subject: [PATCH 20/70] fix gdorf horseback reins + demo cuts (#551) --- include/d/actor/d_a_b_gnd.h | 9 ++++++ src/d/actor/d_a_b_gnd.cpp | 60 +++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/include/d/actor/d_a_b_gnd.h b/include/d/actor/d_a_b_gnd.h index b9c8781ec6..0827102eec 100644 --- a/include/d/actor/d_a_b_gnd.h +++ b/include/d/actor/d_a_b_gnd.h @@ -188,6 +188,15 @@ public: /* 0x273C */ f32 mKankyoBlend; /* 0x2740 */ u8 field_0x2740; /* 0x2744 */ dMsgFlow_c mMsgFlow; +#if TARGET_PC + cXyz mReinsInterpPrev[2][16]; + cXyz mReinsInterpCurr[2][16]; + cXyz mReinsTexInterpPrev[2]; + cXyz mReinsTexInterpCurr[2]; + bool mReinsInterpPrevValid; + bool mReinsInterpCurrValid; + s8 mDemoCamSyncTicks; +#endif }; STATIC_ASSERT(sizeof(b_gnd_class) == 0x2790); diff --git a/src/d/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index 2272c35376..cd6bce78c4 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -17,6 +17,9 @@ #include "Z2AudioLib/Z2Instances.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" + class daB_GND_HIO_c : public JORReflexible { public: daB_GND_HIO_c(); @@ -279,6 +282,30 @@ static int h_nodeCallBack(J3DJoint* i_joint, int param_2) { return 1; } +#if TARGET_PC +static void b_gnd_rein_interp_callback(bool isSimFrame, void* pUserWork) { + b_gnd_class* i_this = (b_gnd_class*)pUserWork; + if (!i_this->mReinsInterpPrevValid || !i_this->mReinsInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + for (int r = 0; r < 2; r++) { + cXyz* dst = i_this->mHorseReins[r].getPos(0); + for (int i = 0; i < 16; i++) { + const cXyz& p0 = i_this->mReinsInterpPrev[r][i]; + const cXyz& p1 = i_this->mReinsInterpCurr[r][i]; + dst[i] = p0 + (p1 - p0) * alpha; + } + } + cXyz* dst = i_this->field_0x21e8.getPos(0); + for (int i = 0; i < 2; i++) { + const cXyz& p0 = i_this->mReinsTexInterpPrev[i]; + const cXyz& p1 = i_this->mReinsTexInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daB_GND_Draw(b_gnd_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -366,6 +393,21 @@ static int daB_GND_Draw(b_gnd_class* i_this) { i_this->field_0x21e8.update(2, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->field_0x21e8); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mReinsInterpCurrValid) { + memcpy(i_this->mReinsInterpPrev, i_this->mReinsInterpCurr, sizeof(i_this->mReinsInterpCurr)); + memcpy(i_this->mReinsTexInterpPrev, i_this->mReinsTexInterpCurr, sizeof(i_this->mReinsTexInterpCurr)); + i_this->mReinsInterpPrevValid = true; + } + for (int r = 0; r < 2; r++) { + memcpy(i_this->mReinsInterpCurr[r], i_this->mHorseReins[r].getPos(0), 16 * sizeof(cXyz)); + } + memcpy(i_this->mReinsTexInterpCurr, i_this->field_0x21e8.getPos(0), 2 * sizeof(cXyz)); + i_this->mReinsInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&b_gnd_rein_interp_callback, i_this); + } +#endif } return 1; @@ -1189,10 +1231,16 @@ static void b_gnd_h_end(b_gnd_class* i_this) { if (i_this->mDemoCamMode < 32) { i_this->mDemoCamMode = 32; +#if TARGET_PC + i_this->mDemoCamSyncTicks = 2; +#endif } else { i_this->mDemoCamMode = 34; i_this->mDemoCamTimer = 0; i_this->mMoveMode = 2; +#if TARGET_PC + i_this->mDemoCamSyncTicks = 2; +#endif } } break; @@ -2887,6 +2935,9 @@ static void demo_camera(b_gnd_class* i_this) { cXyz spF0; s8 sp8 = false; +#if TARGET_PC + const s16 entry_demo_cam_mode = i_this->mDemoCamMode; +#endif switch (i_this->mDemoCamMode) { case 0: break; @@ -3725,6 +3776,15 @@ static void demo_camera(b_gnd_class* i_this) { i_this->mDemoCamTimer = 10000; } } +#if TARGET_PC + if (entry_demo_cam_mode != i_this->mDemoCamMode) { + i_this->mDemoCamSyncTicks = 2; + } + if (i_this->mDemoCamSyncTicks > 0) { + dusk::frame_interp::request_presentation_sync(); + i_this->mDemoCamSyncTicks--; + } +#endif } static void anm_se_set(b_gnd_class* i_this) { From 574b6197a23619ed8f59b6e7bb949d69f4a7a016 Mon Sep 17 00:00:00 2001 From: madeline Date: Sat, 25 Apr 2026 18:23:12 -0700 Subject: [PATCH 21/70] achievements --- files.cmake | 3 + include/dusk/achievements.h | 65 ++++ include/dusk/settings.h | 1 + src/dusk/achievements.cpp | 467 +++++++++++++++++++++++++ src/dusk/imgui/ImGuiAchievements.cpp | 228 ++++++++++++ src/dusk/imgui/ImGuiAchievements.hpp | 23 ++ src/dusk/imgui/ImGuiConsole.cpp | 11 + src/dusk/imgui/ImGuiFirstRunPreset.cpp | 1 + src/dusk/imgui/ImGuiMenuGame.cpp | 1 + src/dusk/imgui/ImGuiMenuTools.cpp | 10 + src/dusk/imgui/ImGuiMenuTools.hpp | 6 + src/dusk/settings.cpp | 2 + 12 files changed, 818 insertions(+) create mode 100644 include/dusk/achievements.h create mode 100644 src/dusk/achievements.cpp create mode 100644 src/dusk/imgui/ImGuiAchievements.cpp create mode 100644 src/dusk/imgui/ImGuiAchievements.hpp diff --git a/files.cmake b/files.cmake index af101b6c47..01b3863505 100644 --- a/files.cmake +++ b/files.cmake @@ -1460,6 +1460,9 @@ set(DUSK_FILES src/dusk/imgui/ImGuiSaveEditor.cpp src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStateShare.cpp + src/dusk/imgui/ImGuiAchievements.hpp + src/dusk/imgui/ImGuiAchievements.cpp + src/dusk/achievements.cpp src/dusk/iso_validate.cpp src/dusk/offset_ptr.cpp src/dusk/OSContext.cpp diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h new file mode 100644 index 0000000000..cd4294b6f1 --- /dev/null +++ b/include/dusk/achievements.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "nlohmann/json.hpp" + +namespace dusk { + +enum class AchievementCategory : uint8_t { + Story, + Collection, + Challenge, + Minigame, + Glitched +}; + +struct Achievement { + const char* key; + const char* name; + const char* description; + AchievementCategory category; + bool isCounter; + int32_t goal; + int32_t progress; + bool unlocked; +}; + +// Responsible for updating a.progress. +// Use extra for any per-achievement state that must survive across frames or sessions, extra is saved +using AchievementCheckFn = std::function; + +class AchievementSystem { +public: + static AchievementSystem& get(); + + void load(); + void save(); + void tick(); + void clearAll(); + + std::vector getAchievements() const; + bool hasPendingUnlock() const { return !m_pendingUnlocks.empty(); } + std::string consumePendingUnlock(); + +private: + struct Entry { + Achievement achievement; + AchievementCheckFn check; + nlohmann::json extra; + }; + + AchievementSystem(); + static std::vector makeEntries(); + void processEntry(Entry& e); + + std::vector m_entries; + bool m_loaded = false; + bool m_dirty = false; + std::queue m_pendingUnlocks; +}; + +} // namespace dusk diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 039e51af7d..8d67805ff3 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -70,6 +70,7 @@ struct UserSettings { ConfigVar disableMainHUD; ConfigVar pauseOnFocusLost; ConfigVar enableLinkDollRotation; + ConfigVar enableAchievementNotifications; // Graphics diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp new file mode 100644 index 0000000000..071332a7c2 --- /dev/null +++ b/src/dusk/achievements.cpp @@ -0,0 +1,467 @@ +#include "dusk/achievements.h" +#include "dusk/io.hpp" +#include "dusk/main.h" +#include "d/d_com_inf_game.h" +#include "d/d_meter2_info.h" +#include "d/actor/d_a_alink.h" +#include "d/actor/d_a_npc4.h" +#include "d/actor/d_a_player.h" +#include "d/d_demo.h" +#include "f_pc/f_pc_name.h" + +#include +#include + +namespace dusk { + +using json = nlohmann::json; + +static void checkGoatHerding(Achievement& a, int32_t threshMs) { + if (dMeter2Info_getMaxCount() != 20 || dMeter2Info_getNowCount() != 20) { + return; + } + const int32_t elapsed = dMeter2Info_getTimeMs(); + if (elapsed > 0 && elapsed <= threshMs) { + a.progress = 1; + } +} + +static constexpr auto ACHIEVEMENTS_FILENAME = "achievements.json"; + +std::vector AchievementSystem::makeEntries() { + return { + { + { + "hero_of_twilight", + "Hero of Twilight", + "Deliver the finishing blow to Ganondorf.", + AchievementCategory::Story, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link != nullptr && link->mProcID == daAlink_c::PROC_GANON_FINISH) { + a.progress = 1; + } + }, + {} + }, + { + { + "rollgoal_8", + "Rollgoal Novice", + "Complete the first 8 rollgoal stages.", + AchievementCategory::Minigame, + true, 8, 0, false + }, + [](Achievement& a, json&) { + a.progress = std::min((int)dComIfGs_getEventReg(0xf63f), 8); + }, + {} + }, + { + { + "rollgoal_all", + "Lost Your Marbles", + "Complete all rollgoal stages.", + AchievementCategory::Minigame, + true, 64, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isEventBit(dSv_event_flag_c::KORO2_ALLCLEAR)) { + a.progress = 64; + } else { + a.progress = dComIfGs_getEventReg(0xf63f); + } + }, + {} + }, + { + { + "goat_30s", + "Ranch Hand", + "Herd all 20 goats into the pen in under 30 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + checkGoatHerding(a, 30000); + }, + {} + }, + { + { + "goat_20s", + "Bane of Howard", + "Herd all 20 goats into the pen in under 20 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + checkGoatHerding(a, 20000); + }, + {} + }, + { + { + "goat_18s", + "King of the Ranch", + "Herd all 20 goats into the pen in under 18 seconds.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + checkGoatHerding(a, 18000); + }, + {} + }, + { + { + "cave_of_ordeals", + "Conqueror of Ordeals", + "Clear all 50 floors of the Cave of Ordeals.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daNpcF_chkEvtBit(0x1F9)) { + a.progress = 1; + } + }, + {} + }, + { + { + "cave_of_ordeals_heartless", + "Indomitable", + "Clear all 50 floors of the Cave of Ordeals with only 3 heart containers.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) { + a.progress = 1; + } + }, + {} + }, + { + { + "speedrun_12h", + "Been There Done That", + "Defeat Ganondorf with a total save file play time under 12 hours.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); + if (ticks / OS_TIMER_CLOCK < 12 * 3600) { + a.progress = 1; + } + }, + {} + }, + { + { + "speedrun_8h", + "Swift Blade", + "Defeat Ganondorf with a total save file play time under 6 hours.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); + if (ticks / OS_TIMER_CLOCK < 8 * 3600) { + a.progress = 1; + } + }, + {} + }, + { + { + "princess_of_bugs", + "The Princess of Bugs", + "Deliver all 24 golden bugs to Agitha.", + AchievementCategory::Collection, + true, 24, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_checkGetInsectNum(); + }, + {} + }, + { + { + "all_poes", + "Poe Collector", + "Collect all 60 Poe Souls.", + AchievementCategory::Collection, + true, 60, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_getPohSpiritNum(); + }, + {} + }, + { + { + "hylian_loach", + "Legendary Catch", + "Catch a Hylian Loach.", + AchievementCategory::Collection, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_getFishNum(1) > 0) { + a.progress = 1; + } + }, + {} + }, + { + { + "all_fish", + "Gone Fishin'", + "Catch all 6 species of fish.", + AchievementCategory::Collection, + true, 6, 0, false + }, + [](Achievement& a, json&) { + int nUniqueFish = 0; + for (int i = 0; i < 6; ++i) { + if (dComIfGs_getFishNum(i) != 0) { + nUniqueFish++; + } + } + a.progress = nUniqueFish; + }, + {} + }, + { + { + "a_big_heart", + "A Big Heart", + "Reach maximum health with all 20 heart containers.", + AchievementCategory::Collection, + true, 20, 0, false + }, + [](Achievement& a, json&) { + a.progress = dComIfGs_getMaxLife() / 5; + }, + {} + }, + { + { + "back_in_time", + "Back in Time", + "Perform the Back in Time glitch to play on the title screen.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + static int titleNoDemoFrames = 0; + if (fopAcM_SearchByName(fpcNm_TITLE_e) == nullptr) { + titleNoDemoFrames = 0; + return; + } + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link != nullptr && dDemo_c::getMode() == 0) { + if (++titleNoDemoFrames >= 60) { + a.progress = 1; + } + } else { + titleNoDemoFrames = 0; + } + }, + {} + }, + { + { + "early_master_sword", + "Early Master Sword", + "Obtain the Master Sword before completing Midna's Desperate Hour.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isCollectSword(COLLECT_MASTER_SWORD) && !dComIfGs_isEventBit(0x1E08)) { + a.progress = 1; + } + }, + {} + }, + { + { + "earliest_master_sword", + "Earliest Master Sword", + "Obtain the Master Sword before meeting Midna.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isCollectSword(COLLECT_MASTER_SWORD) && !dComIfGs_isTransformLV(0)) { + a.progress = 1; + } + }, + {} + }, + { + { + "ultimate_delivery", + "The Ultimate Delivery", + "Have all 16 postman letters at the same time.", + AchievementCategory::Glitched, + true, 16, 0, false + }, + [](Achievement& a, json&) { + a.progress = dMeter2Info_getRecieveLetterNum(); + }, + {} + }, + { + { + "speedrun_4h", + "Hero of Time", + "Defeat Ganondorf with a total save file play time under 4 hours.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + const int64_t ticks = (static_cast(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); + if (ticks / OS_TIMER_CLOCK < 4 * 3600) { + a.progress = 1; + } + }, + {} + } + }; +} + +AchievementSystem::AchievementSystem() : m_entries(makeEntries()) {} + +AchievementSystem& AchievementSystem::get() { + static AchievementSystem instance; + return instance; +} + +std::string AchievementSystem::consumePendingUnlock() { + std::string msg = std::move(m_pendingUnlocks.front()); + m_pendingUnlocks.pop(); + return msg; +} + +std::vector AchievementSystem::getAchievements() const { + std::vector result; + result.reserve(m_entries.size()); + for (const auto& e : m_entries) { + result.push_back(e.achievement); + } + return result; +} + +void AchievementSystem::load() { + m_loaded = true; + const auto filePath = dusk::ConfigPath / ACHIEVEMENTS_FILENAME; + if (!std::filesystem::exists(filePath)) { + return; + } + try { + auto data = io::FileStream::ReadAllBytes(filePath.string().c_str()); + auto j = json::parse(data); + if (!j.is_object()) { + return; + } + for (auto& e : m_entries) { + if (!j.contains(e.achievement.key)) { + continue; + } + const auto& entry = j[e.achievement.key]; + if (entry.contains("progress")) { + e.achievement.progress = entry["progress"].get(); + } + if (entry.contains("unlocked")) { + e.achievement.unlocked = entry["unlocked"].get(); + } + if (entry.contains("extra")) { + e.extra = entry["extra"]; + } + } + } catch (const std::exception&) {} +} + +void AchievementSystem::save() { + json j = json::object(); + for (const auto& e : m_entries) { + json entry = { + {"progress", e.achievement.progress}, + {"unlocked", e.achievement.unlocked}, + }; + if (!e.extra.is_null()) { + entry["extra"] = e.extra; + } + j[e.achievement.key] = std::move(entry); + } + try { + io::FileStream::WriteAllText( + (dusk::ConfigPath / ACHIEVEMENTS_FILENAME).string().c_str(), + j.dump(2) + ); + } catch (const std::exception&) {} +} + +void AchievementSystem::clearAll() { + m_entries = makeEntries(); + save(); +} + +void AchievementSystem::processEntry(Entry& e) { + if (e.achievement.unlocked) { + return; + } + const int32_t prevProgress = e.achievement.progress; + e.check(e.achievement, e.extra); + + const bool progressChanged = e.achievement.progress != prevProgress; + const bool nowUnlocked = e.achievement.isCounter ? + e.achievement.progress >= e.achievement.goal : + e.achievement.progress > 0; + + if (nowUnlocked) { + e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1; + e.achievement.unlocked = true; + m_pendingUnlocks.push(e.achievement.name); + m_dirty = true; + } else if (progressChanged) { + m_dirty = true; + } +} + +void AchievementSystem::tick() { + if (!m_loaded) { + load(); + } + if (!dusk::IsGameLaunched) { + return; + } + for (auto& e : m_entries) { + processEntry(e); + } + if (m_dirty) { + save(); + m_dirty = false; + } +} + +} // namespace dusk diff --git a/src/dusk/imgui/ImGuiAchievements.cpp b/src/dusk/imgui/ImGuiAchievements.cpp new file mode 100644 index 0000000000..4b844b8386 --- /dev/null +++ b/src/dusk/imgui/ImGuiAchievements.cpp @@ -0,0 +1,228 @@ +#include "ImGuiAchievements.hpp" +#include "ImGuiConfig.hpp" +#include "dusk/achievements.h" +#include "dusk/settings.h" +#include "fmt/format.h" +#include "imgui.h" + +namespace dusk { + +void ImGuiAchievements::notify(std::string name) { + if (m_notifyTimer <= 0.f) { + m_notifyName = std::move(name); + m_notifyTimer = NOTIFY_DURATION; + } else { + m_notifyQueue.push(std::move(name)); + } +} + +void ImGuiAchievements::showNotification() { + if (!getSettings().game.enableAchievementNotifications.getValue()) { + return; + } + if (m_notifyTimer <= 0.f) { + if (m_notifyQueue.empty()) { + return; + } + m_notifyName = std::move(m_notifyQueue.front()); + m_notifyQueue.pop(); + m_notifyTimer = NOTIFY_DURATION; + } + + m_notifyTimer -= ImGui::GetIO().DeltaTime; + + const float alpha = std::min({ + m_notifyTimer / NOTIFY_FADE_TIME, + (NOTIFY_DURATION - m_notifyTimer) / NOTIFY_FADE_TIME, + 1.0f + }); + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const float padding = 12.0f; + ImGui::SetNextWindowPos( + ImVec2(viewport->WorkPos.x + viewport->WorkSize.x - padding, viewport->WorkPos.y + padding), + ImGuiCond_Always, ImVec2(1.0f, 0.0f) + ); + + ImGui::SetNextWindowBgAlpha(alpha * 0.92f); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.06f, 0.01f, alpha * 0.92f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 0.8f, 0.1f, alpha)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, alpha)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 2.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(14.0f, 10.0f)); + + constexpr ImGuiWindowFlags flags = + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | + ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs; + + if (ImGui::Begin("##achievement_notify", nullptr, flags)) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.82f, 0.1f, alpha)); + ImGui::TextUnformatted("Achievement Unlocked!"); + ImGui::PopStyleColor(); + ImGui::Spacing(); + ImGui::TextUnformatted(m_notifyName.c_str()); + } + ImGui::End(); + + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(3); +} + +void ImGuiAchievements::draw(bool& open) { + showNotification(); + + if (!open) { + return; + } + + ImGui::SetNextWindowSizeConstraints(ImVec2(640, 200), ImVec2(800, 900)); + ImGui::SetNextWindowSize(ImVec2(640, 480), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin( + "Achievements", &open, + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav) + ) + { + ImGui::End(); + return; + } + + const auto achievements = AchievementSystem::get().getAchievements(); + + int unlocked = 0; + for (const auto& a : achievements) { + if (a.unlocked) { + ++unlocked; + } + } + + ImGui::Text("%d / %d achievements unlocked", unlocked, (int)achievements.size()); + ImGui::SameLine(); + config::ImGuiCheckbox("Notifications", getSettings().game.enableAchievementNotifications); + ImGui::Separator(); + + static const struct { + AchievementCategory cat; + const char* label; + ImVec4 color; + } ACHIEVEMENT_CATEGORIES[] = { + {AchievementCategory::Story, "Story", ImVec4(1.0f, 0.82f, 0.1f, 1.0f)}, + {AchievementCategory::Collection, "Collection", ImVec4(0.3f, 0.85f, 0.4f, 1.0f)}, + {AchievementCategory::Challenge, "Challenge", ImVec4(1.0f, 0.65f, 0.15f, 1.0f)}, + {AchievementCategory::Minigame, "Minigame", ImVec4(0.5f, 0.85f, 1.0f, 1.0f)}, + {AchievementCategory::Glitched, "Glitched", ImVec4(0.75f, 0.4f, 1.0f, 1.0f)}, + }; + + const float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); + + if (ImGui::BeginTabBar("##achievement_tabs", ImGuiTabBarFlags_FittingPolicyScroll)) { + for (const auto& catInfo : ACHIEVEMENT_CATEGORIES) { + int catTotal = 0, catUnlocked = 0; + for (const auto& a : achievements) { + if (a.category == catInfo.cat) { + ++catTotal; + if (a.unlocked) { + ++catUnlocked; + } + } + } + if (catTotal == 0) { + continue; + } + + const std::string tabLabel = fmt::format("{} ({}/{})", catInfo.label, catUnlocked, catTotal); + + ImGui::PushStyleColor(ImGuiCol_Text, catInfo.color); + const bool tabOpen = ImGui::BeginTabItem(tabLabel.c_str()); + ImGui::PopStyleColor(); + + if (tabOpen) { + ImGui::BeginChild( + "##cat_list", + ImVec2(0, -footerHeight), + ImGuiChildFlags_None, + ImGuiWindowFlags_NoBackground + ); + + ImGui::Spacing(); + + for (const auto& a : achievements) { + if (a.category != catInfo.cat) { + continue; + } + ImGui::PushID(a.key); + + ImGui::PushStyleColor( + ImGuiCol_Text, + a.unlocked ? + ImVec4(1.0f, 0.65f, 0.15f, 1.0f) : + ImGui::GetStyleColorVec4(ImGuiCol_Text) + ); + + ImGui::TextUnformatted(a.name); + ImGui::PopStyleColor(); + + const char* statusLabel = a.unlocked ? "[Unlocked]" : "[Locked]"; + ImGui::SameLine( + ImGui::GetContentRegionMax().x - + ImGui::CalcTextSize(statusLabel).x + ); + + if (a.unlocked) { + ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "%s", statusLabel); + } else { + ImGui::TextColored(ImVec4(0.8f, 0.2f, 0.2f, 1.0f), "%s", statusLabel); + } + + ImGui::TextDisabled("%s", a.description); + + if (a.isCounter) { + const float fraction = a.goal > 0 ? (float)(a.progress) / (float)(a.goal) : 1.0f; + const std::string overlay = fmt::format("{} / {}", a.progress, a.goal); + ImGui::PushStyleColor( + ImGuiCol_PlotHistogram, + a.unlocked ? + ImVec4(0.4f, 0.7f, 0.1f, 1.0f) : + ImVec4(0.2f, 0.45f, 0.8f, 1.0f) + ); + ImGui::ProgressBar(fraction, ImVec2(-1.0f, 0.0f), overlay.c_str()); + ImGui::PopStyleColor(); + } + + ImGui::Spacing(); + ImGui::PopID(); + } + + ImGui::EndChild(); + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + } + + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Clear All Achievements")) { + ImGui::OpenPopup("##confirm_clear"); + } + + if (ImGui::BeginPopup("##confirm_clear")) { + ImGui::Text("Reset all achievement progress?"); + ImGui::Spacing(); + if (ImGui::Button("Yes, reset all")) { + AchievementSystem::get().clearAll(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + ImGui::End(); +} + +} // namespace dusk diff --git a/src/dusk/imgui/ImGuiAchievements.hpp b/src/dusk/imgui/ImGuiAchievements.hpp new file mode 100644 index 0000000000..5ee77373fc --- /dev/null +++ b/src/dusk/imgui/ImGuiAchievements.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace dusk { + +class ImGuiAchievements { +public: + void draw(bool& open); + void notify(std::string name); + +private: + void showNotification(); + + std::string m_notifyName; + float m_notifyTimer = 0.f; + std::queue m_notifyQueue; + static constexpr float NOTIFY_DURATION = 4.0f; + static constexpr float NOTIFY_FADE_TIME = 0.5f; +}; + +} // namespace dusk diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 085b34dc2d..c8a9fa86aa 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -14,6 +14,7 @@ #include "SDL3/SDL_events.h" #include "SDL3/SDL_mouse.h" #include "aurora/lib/window.hpp" +#include "dusk/achievements.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/dusk.h" @@ -295,6 +296,15 @@ namespace dusk { UpdateSettings(); + AchievementSystem::get().tick(); + while (AchievementSystem::get().hasPendingUnlock()) { + if (getSettings().game.enableAchievementNotifications) { + m_menuTools.notifyAchievement(AchievementSystem::get().consumePendingUnlock()); + } else { + AchievementSystem::get().consumePendingUnlock(); + } + } + if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_R)) { @@ -374,6 +384,7 @@ namespace dusk { m_menuTools.ShowSaveEditor(); } m_menuTools.ShowStateShare(); + m_menuTools.ShowAchievements(); DuskDebugPad(); // temporary, remove later // Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds. diff --git a/src/dusk/imgui/ImGuiFirstRunPreset.cpp b/src/dusk/imgui/ImGuiFirstRunPreset.cpp index fb2d181562..8009305870 100644 --- a/src/dusk/imgui/ImGuiFirstRunPreset.cpp +++ b/src/dusk/imgui/ImGuiFirstRunPreset.cpp @@ -35,6 +35,7 @@ static void ApplyPresetDusk() { ApplyPresetHD(); auto& s = getSettings(); + s.game.enableAchievementNotifications.setValue(true); s.game.enableQuickTransform.setValue(true); s.game.instantSaves.setValue(true); s.game.midnasLamentNonStop.setValue(true); diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 7e54f2ffa3..8cb50e143d 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -444,6 +444,7 @@ namespace dusk { void ImGuiMenuGame::drawInterfaceMenu() { if (ImGui::BeginMenu("Interface")) { + config::ImGuiCheckbox("Achievement Notifications", getSettings().game.enableAchievementNotifications); config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); #if DUSK_ENABLE_SENTRY_NATIVE diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 00a03e635b..a82067a438 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -58,6 +58,8 @@ namespace dusk { ImGui::EndDisabled(); } + ImGui::MenuItem("Achievements", nullptr, &m_showAchievements); + #if DUSK_CAN_OPEN_DATA_FOLDER ImGui::Separator(); if (ImGui::MenuItem("Open Data Folder")) { @@ -252,4 +254,12 @@ namespace dusk { ImGui::End(); ImGui::PopFont(); } + + void ImGuiMenuTools::ShowAchievements() { + m_achievementsWindow.draw(m_showAchievements); + } + + void ImGuiMenuTools::notifyAchievement(std::string name) { + m_achievementsWindow.notify(std::move(name)); + } } diff --git a/src/dusk/imgui/ImGuiMenuTools.hpp b/src/dusk/imgui/ImGuiMenuTools.hpp index e48aab2742..de94ad2d8f 100644 --- a/src/dusk/imgui/ImGuiMenuTools.hpp +++ b/src/dusk/imgui/ImGuiMenuTools.hpp @@ -5,6 +5,7 @@ #include #include "imgui.h" +#include "ImGuiAchievements.hpp" #include "ImGuiSaveEditor.hpp" #include "ImGuiStateShare.hpp" @@ -26,6 +27,8 @@ namespace dusk { void ShowAudioDebug(); void ShowSaveEditor(); void ShowStateShare(); + void ShowAchievements(); + void notifyAchievement(std::string name); private: bool m_showDebugOverlay = false; @@ -65,6 +68,9 @@ namespace dusk { bool m_showStateShare = false; ImGuiStateShare m_stateShare; + + bool m_showAchievements = false; + ImGuiAchievements m_achievementsWindow; }; } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 3e6ee733fe..b6dc559610 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -44,6 +44,7 @@ UserSettings g_userSettings = { .disableMainHUD {"game.disableMainHUD", false}, .pauseOnFocusLost {"game.pauseOnFocusLost", false}, .enableLinkDollRotation = {"game.enableLinkDollRotation", false }, + .enableAchievementNotifications {"game.enableAchievementNotifications", false}, // Graphics .bloomMode {"game.bloomMode", BloomMode::Classic}, @@ -153,6 +154,7 @@ void registerSettings() { Register(g_userSettings.game.freeMagicArmor); Register(g_userSettings.game.restoreWiiGlitches); Register(g_userSettings.game.enableLinkDollRotation); + Register(g_userSettings.game.enableAchievementNotifications); Register(g_userSettings.game.noMissClimbing); Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); From 9ddfcc38c9f875f9ddf5fc2d6c8d0a3cd0981864 Mon Sep 17 00:00:00 2001 From: Howard Luck Date: Sat, 25 Apr 2026 20:37:36 -0600 Subject: [PATCH 22/70] fix interp on ooks rope (#553) --- include/d/actor/d_a_e_mb.h | 6 ++++++ src/d/actor/d_a_e_mb.cpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/include/d/actor/d_a_e_mb.h b/include/d/actor/d_a_e_mb.h index f36c744879..527cf30eac 100644 --- a/include/d/actor/d_a_e_mb.h +++ b/include/d/actor/d_a_e_mb.h @@ -44,6 +44,12 @@ public: /* 0x88C */ u8 field_0x88C[0x8C8 - 0x88C]; /* 0x8C8 */ s8 field_0x8c8; /* 0x8C9 */ u8 mInitHIO; +#if TARGET_PC + cXyz mRopeInterpPrev[16]; + cXyz mRopeInterpCurr[16]; + bool mRopeInterpPrevValid; + bool mRopeInterpCurrValid; +#endif }; STATIC_ASSERT(sizeof(e_mb_class) == 0x8cc); diff --git a/src/d/actor/d_a_e_mb.cpp b/src/d/actor/d_a_e_mb.cpp index 939b253475..aa77a99612 100644 --- a/src/d/actor/d_a_e_mb.cpp +++ b/src/d/actor/d_a_e_mb.cpp @@ -12,6 +12,8 @@ #include "d/d_bomb.h" #include "c/c_damagereaction.h" #include "Z2AudioLib/Z2Instances.h" +#include "dusk/frame_interpolation.h" +#include "dusk/settings.h" #define ACTION_STANDBY 0 #define ACTION_WALK1 1 @@ -63,6 +65,22 @@ static void anm_init(e_mb_class* i_this, int i_anmID, f32 i_morf, u8 i_attr, f32 i_this->mAnm = i_anmID; } +#if TARGET_PC +static void e_mb_rope_interp_callback(bool isSimFrame, void* pUserWork) { + e_mb_class* i_this = (e_mb_class*)pUserWork; + if (!i_this->mRopeInterpPrevValid || !i_this->mRopeInterpCurrValid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + cXyz* dst = i_this->mRopeMat.getPos(0); + for (int i = 0; i < 16; i++) { + const cXyz& p0 = i_this->mRopeInterpPrev[i]; + const cXyz& p1 = i_this->mRopeInterpCurr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static int daE_MB_Draw(e_mb_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -86,6 +104,17 @@ static int daE_MB_Draw(e_mb_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->mRopeMat.update(16, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mRopeMat); +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + if (i_this->mRopeInterpCurrValid) { + memcpy(i_this->mRopeInterpPrev, i_this->mRopeInterpCurr, sizeof(i_this->mRopeInterpCurr)); + i_this->mRopeInterpPrevValid = true; + } + memcpy(i_this->mRopeInterpCurr, i_this->mRopeMat.getPos(0), 16 * sizeof(cXyz)); + i_this->mRopeInterpCurrValid = true; + dusk::frame_interp::add_interpolation_callback(&e_mb_rope_interp_callback, i_this); + } +#endif return 1; } From f7b5350d60eb96b92446e2d20a0a331e678c2ac0 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 25 Apr 2026 20:42:02 -0600 Subject: [PATCH 23/70] Update README.md --- README.md | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c2fe53576d..b6d6cec863 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - ### **[Discord](https://discord.gg/QACynxeyna)** # Setup -**⚠️Dusk does NOT provide any copyrighted assets. You must provide your own copy of the game.** +**⚠️ Dusk does NOT provide any copyrighted assets. You must provide your own copy of the game.** ### 1. Verify your ROM dump 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. @@ -22,29 +22,10 @@ First make sure your dump of the game is clean and supported by Dusk. You can do - Select Options, then set the ISO Path to your supported game dump - Press Start Game to play! -![DuskOptions](assets/dusk_options.png) +![Dusk options](assets/dusk_options.png) # Building If you'd like to build Dusk from source, please read the [build instructions](docs/building.md). # Credits -- Taka -- encounter -- Antidote -- caseif -- CraftyBoss -- crowell -- dooplecks -- gymnast86 -- Irastris -- kipcode66 -- Lars -- LunarSoap -- Maddie -- MelonSpeedruns -- Pheenoh -- PJB -- Roeming -- YunataSavior - -Special thanks to the TP Decomp team, the GC/Wii Decomp community, the Aurora developers, and the TP speedrunning community. +Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusk/graphs/contributors). From ba58d2486e5dbae1c98ee363cbac0ea1f1511923 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Sat, 25 Apr 2026 23:35:43 -0400 Subject: [PATCH 24/70] Free Camera (#542) * freecam wip * added manual mode * fix freecam collision * made freecam into its own function * Added settings & Radius modification when camer is higher up --------- Co-authored-by: MelonSpeedruns --- include/d/d_cam_param.h | 6 +++ include/d/d_camera.h | 5 ++ include/dusk/settings.h | 5 +- src/d/d_camera.cpp | 66 ++++++++++++++++++++++++++ src/dusk/imgui/ImGuiFirstRunPreset.cpp | 1 + src/dusk/imgui/ImGuiMenuGame.cpp | 10 +++- src/dusk/settings.cpp | 8 +++- 7 files changed, 98 insertions(+), 3 deletions(-) diff --git a/include/d/d_cam_param.h b/include/d/d_cam_param.h index d6ff7b46d7..867c66e38d 100644 --- a/include/d/d_cam_param.h +++ b/include/d/d_cam_param.h @@ -143,6 +143,12 @@ public: /* 0x20 */ JORFile mFile; #endif +#if TARGET_PC + /* 0x24 */ u8 mManualMode; + /* 0x25 */ f32 freeXAngle; + /* 0x29 */ f32 freeYAngle; +#endif + u32 Id(s32 i_style) { return mCamStyleData[i_style].field_0x0; } int Algorythmn(s32 i_style) { return mCamStyleData[i_style].field_0x4; } int Algorythmn() { return mCurrentStyle->field_0x4; } diff --git a/include/d/d_camera.h b/include/d/d_camera.h index 85e930c395..105c9cae9f 100644 --- a/include/d/d_camera.h +++ b/include/d/d_camera.h @@ -273,6 +273,8 @@ public: /* 0xA4 */ f32 field_0xa4; /* 0xA8 */ int field_0xa8; /* 0xAC */ f32 field_0xac; + f32 xAngle; + f32 yAngle; }; struct LockOnData { @@ -1024,6 +1026,9 @@ public: bool colosseumCamera(s32); bool test1Camera(s32); bool test2Camera(s32); + #if TARGET_PC + bool freeCamera(); + #endif bool towerCamera(s32); bool hookshotCamera(s32); bool railCamera(s32); diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 8a54197317..a7815569d0 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -82,7 +82,6 @@ struct UserSettings { // Preferences ConfigVar enableMirrorMode; - ConfigVar invertCameraXAxis; ConfigVar disableMainHUD; ConfigVar pauseOnFocusLost; ConfigVar enableLinkDollRotation; @@ -112,6 +111,10 @@ struct UserSettings { ConfigVar gyroDeadband; ConfigVar gyroInvertPitch; ConfigVar gyroInvertYaw; + ConfigVar freeCamera; + ConfigVar invertCameraXAxis; + ConfigVar invertCameraYAxis; + ConfigVar freeCameraSensitivity; // Cheats ConfigVar infiniteHearts; diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index f805a6a08b..917b30709a 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -800,6 +800,10 @@ void dCamera_c::updatePad() { } mLockLActive = 1; + + #if TARGET_PC + mCamParam.mManualMode = 0; + #endif } else { mLockLJustActivated = 0; mLockLActive = 0; @@ -833,6 +837,12 @@ void dCamera_c::updatePad() { mHoldB = mDoCPd_c::getHoldB(mPadID) ? true : false; mTrigB = mDoCPd_c::getTrigB(mPadID) ? true : false; + #if TARGET_PC + if (mCamParam.mManualMode) { + return; + } + #endif + bool sp6B = true; bool sp6C = true; int temp1; @@ -1167,6 +1177,13 @@ bool dCamera_c::Run() { } } else { sp0F = (this->*engine_tbl[mCamParam.Algorythmn(mCamStyle)])(mCamStyle); + + #if TARGET_PC + if (mCamParam.Algorythmn(mCamStyle) != 1) { + mCamParam.mManualMode = 0; + } + #endif + field_0x170++; field_0x160++; mCurCamStyleTimer++; @@ -3078,6 +3095,11 @@ bool dCamera_c::bumpCheck(u32 i_flags) { } else { field_0x968 *= mMonitor.field_0xc / 5.0f; } + + #if TARGET_PC + if (!dusk::getSettings().game.freeCamera || !mCamParam.mManualMode) { + #endif + f32 tmp = field_0x96c * (mIsWolf == 1 ? 30.0f : 30.0f); center += vec3.norm() * (tmp * globe.V().Sin()); cSGlobe globe2(vec2 - center); @@ -3091,6 +3113,10 @@ bool dCamera_c::bumpCheck(u32 i_flags) { vec = lin_chk1.GetCross(); } + #if TARGET_PC + } + #endif + #if DEBUG if (mCamSetup.CheckFlag(0x8000)) { dDbVw_Report(20, 235, " U"); @@ -4604,6 +4630,11 @@ bool dCamera_c::chaseCamera(s32 param_0) { sp110 = mViewCache.mDirection.R(); mViewCache.mDirection.R(mViewCache.mDirection.R() + (fVar55 - mViewCache.mDirection.R()) * chase->field_0x74); + + #if TARGET_PC + freeCamera(); + #endif + chase->field_0x64 = mViewCache.mCenter + mViewCache.mDirection.Xyz(); mViewCache.mEye = chase->field_0x64; @@ -7444,6 +7475,41 @@ bool dCamera_c::test2Camera(s32 param_0) { return false; } +#if TARGET_PC +bool dCamera_c::freeCamera() { + if (!dusk::getSettings().game.freeCamera) { + mCamParam.mManualMode = 0; + return false; + } + + cXyz camMovement = {mPadInfo.mCStick.mLastPosX, mPadInfo.mCStick.mLastPosY, 0.0f}; + f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY); + + if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY != 0) { + if (!mCamParam.mManualMode) { + mCamParam.mManualMode = 1; + mCamParam.freeXAngle = mViewCache.mDirection.mAzimuth.Degree(); + mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree(); + } + + camMovement = camMovement.normalize(); + camMovement.x *= (dusk::getSettings().game.invertCameraXAxis ? 1.0f : -1.0f) * dusk::getSettings().game.freeCameraSensitivity * 4.0f; + camMovement.y *= (dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f) * dusk::getSettings().game.freeCameraSensitivity * 4.0f; + mCamParam.freeXAngle += camMovement.x * magnitude * dusk::getSettings().game.freeCameraSensitivity; + mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity; + } + + if (mCamParam.mManualMode) { + mCamParam.freeYAngle = std::clamp(mCamParam.freeYAngle, -35.0f, 60.0f); + mViewCache.mDirection.mAzimuth = cSAngle(mCamParam.freeXAngle); + mViewCache.mDirection.mInclination = cSAngle(mCamParam.freeYAngle); + mViewCache.mDirection.mRadius = std::clamp(mCamParam.freeYAngle * 15.0f, 300.0f, 10000.0f); + } + + return mCamParam.mManualMode; +} +#endif + bool dCamera_c::towerCamera(s32 param_0) { cSAngle stack_444 = cSAngle(mCamSetup.ChargeLatitude()); f32 sp224 = mCamSetup.ChargeBRatio(); diff --git a/src/dusk/imgui/ImGuiFirstRunPreset.cpp b/src/dusk/imgui/ImGuiFirstRunPreset.cpp index 8009305870..2a9bcc5c6e 100644 --- a/src/dusk/imgui/ImGuiFirstRunPreset.cpp +++ b/src/dusk/imgui/ImGuiFirstRunPreset.cpp @@ -29,6 +29,7 @@ static void ApplyPresetHD() { s.game.fastTears.setValue(true); s.game.biggerWallets.setValue(true); s.game.invertCameraXAxis.setValue(true); + s.game.freeCamera.setValue(true); } static void ApplyPresetDusk() { diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 8cb50e143d..0a687233aa 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -383,7 +383,15 @@ namespace dusk { ImGui::SeparatorText("Camera"); - config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); + config::ImGuiCheckbox("Free Camera", getSettings().game.freeCamera); + + if (getSettings().game.freeCamera) { + config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); + config::ImGuiCheckbox("Invert Camera Y Axis", getSettings().game.invertCameraYAxis); + config::ImGuiSliderFloat("Free Camera Sensitivity", getSettings().game.freeCameraSensitivity, 0.5f, 2.0f, "%.1f"); + } else { + config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); + } ImGui::SeparatorText("Gyro"); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index e10e6d9ee9..eb1d513ba9 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -42,7 +42,6 @@ UserSettings g_userSettings = { // Preferences .enableMirrorMode {"game.enableMirrorMode", false}, - .invertCameraXAxis {"game.invertCameraXAxis", false}, .disableMainHUD {"game.disableMainHUD", false}, .pauseOnFocusLost {"game.pauseOnFocusLost", false}, .enableLinkDollRotation = {"game.enableLinkDollRotation", false }, @@ -71,6 +70,10 @@ UserSettings g_userSettings = { .gyroDeadband {"game.gyroDeadband", 0.04f}, .gyroInvertPitch {"game.gyroInvertPitch", false}, .gyroInvertYaw {"game.gyroInvertYaw", false}, + .freeCamera {"game.freeCamera", false}, + .invertCameraXAxis {"game.invertCameraXAxis", false}, + .invertCameraYAxis {"game.invertCameraYAxis", false}, + .freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f}, // Cheats .infiniteHearts {"game.infiniteHearts", false}, @@ -144,6 +147,8 @@ void registerSettings() { Register(g_userSettings.game.sunsSong); Register(g_userSettings.game.enableMirrorMode); Register(g_userSettings.game.invertCameraXAxis); + Register(g_userSettings.game.invertCameraYAxis); + Register(g_userSettings.game.freeCameraSensitivity); Register(g_userSettings.game.disableMainHUD); Register(g_userSettings.game.pauseOnFocusLost); Register(g_userSettings.game.bloomMode); @@ -183,6 +188,7 @@ void registerSettings() { Register(g_userSettings.game.gyroSmoothing); Register(g_userSettings.game.gyroInvertPitch); Register(g_userSettings.game.gyroInvertYaw); + Register(g_userSettings.game.freeCamera); Register(g_userSettings.backend.isoPath); Register(g_userSettings.backend.graphicsBackend); From 34ec362876dc99795c6fdd22b93547ea1365398d Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sat, 25 Apr 2026 21:37:11 -0600 Subject: [PATCH 25/70] fix zant fog spin and culling --- src/d/d_kankyo_rain.cpp | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/d/d_kankyo_rain.cpp b/src/d/d_kankyo_rain.cpp index 64cf2247e0..822612768c 100644 --- a/src/d/d_kankyo_rain.cpp +++ b/src/d/d_kankyo_rain.cpp @@ -5986,7 +5986,12 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { TGXTexObj texobj; dKyr_set_btitex(&texobj, (ResTIMG*)tex[1]); - rot += 0.7f; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + rot += 0.7f; + } MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -6037,7 +6042,14 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { sp34.y = 80.0f; sp34.z = 80.0f; mDoLib_project(&sp7C, &proj); - if (!(proj.x > -sp34.x) || !(proj.x < (FB_WIDTH + sp34.x)) || +#if TARGET_PC + f32 cullMinX = mDoGph_gInf_c::getSafeMinXF() - sp34.x; + f32 cullMaxX = mDoGph_gInf_c::getSafeMinXF() + mDoGph_gInf_c::getSafeWidthF() + sp34.x; +#else + f32 cullMinX = -sp34.x; + f32 cullMaxX = FB_WIDTH + sp34.x; +#endif + if (!(proj.x > cullMinX) || !(proj.x < cullMaxX) || !(proj.y > -sp34.y) || !(proj.y < (458.0f + sp34.z))) { continue; @@ -6208,7 +6220,12 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { TGXTexObj texobj; dKyr_set_btitex(&texobj, (ResTIMG*)tex[0]); - rot += 1.0f; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + rot += 1.0f; + } MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -6262,7 +6279,14 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { sp44.z = 80.0f; mDoLib_project(&spA4, &proj); - if (!(proj.x > -sp44.x) || !(proj.x < (FB_WIDTH + sp44.x)) || +#if TARGET_PC + f32 cullMinX = mDoGph_gInf_c::getSafeMinXF() - sp44.x; + f32 cullMaxX = mDoGph_gInf_c::getSafeMinXF() + mDoGph_gInf_c::getSafeWidthF() + sp44.x; +#else + f32 cullMinX = -sp44.x; + f32 cullMaxX = FB_WIDTH + sp44.x; +#endif + if (!(proj.x > cullMinX) || !(proj.x < cullMaxX) || !(proj.y > -sp44.y) || !(proj.y < (458.0f + sp44.z))) { continue; From 8e121a7e5187fb1f30b13b2d520c4303e2cd81db Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 25 Apr 2026 21:12:51 -0700 Subject: [PATCH 26/70] update timer impl --- files.cmake | 1 + include/dusk/settings.h | 3 +- src/d/actor/d_a_alink_demo.inc | 10 ++ src/d/d_bright_check.cpp | 8 ++ src/d/d_s_name.cpp | 20 +++- src/dusk/imgui/ImGuiConsole.cpp | 9 +- src/dusk/imgui/ImGuiConsole.hpp | 7 +- src/dusk/imgui/ImGuiMenuGame.cpp | 121 +++++++++++++++++++++- src/dusk/imgui/ImGuiMenuGame.hpp | 22 ++++ src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp | 96 ----------------- src/dusk/imgui/ImGuiMenuTools.cpp | 9 ++ src/dusk/settings.cpp | 6 +- src/f_op/f_op_scene_req.cpp | 21 +++- 13 files changed, 215 insertions(+), 118 deletions(-) delete mode 100644 src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp diff --git a/files.cmake b/files.cmake index af101b6c47..91b0c0fab7 100644 --- a/files.cmake +++ b/files.cmake @@ -1461,6 +1461,7 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStateShare.cpp src/dusk/iso_validate.cpp + src/dusk/livesplit.cpp src/dusk/offset_ptr.cpp src/dusk/OSContext.cpp src/dusk/OSThread.cpp diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 522e172e7c..7fb8c85a51 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -119,8 +119,7 @@ struct UserSettings { ConfigVar enableTurboKeybind; // Tools - ConfigVar speedrunTimer; - ConfigVar speedrunTimerOverlay; + ConfigVar speedrunMode; ConfigVar liveSplitEnabled; } game; diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index aa77b24129..b46af5cacb 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -23,6 +23,7 @@ #include "d/actor/d_a_npc_tkc.h" #include +#include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/settings.h" BOOL daAlink_c::checkEventRun() const { @@ -4005,6 +4006,15 @@ int daAlink_c::procGanonFinishInit() { field_0x37c8 = current.pos; onEndResetFlg1(ERFLG1_SHIELD_BACKBONE); + +#if TARGET_PC + if (dusk::getSettings().game.speedrunMode) { + if (dusk::g_imguiConsole.isSpeedrunStart()) { + dusk::g_imguiConsole.stopSpeedrun(); + } + } +#endif + return 1; } diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index 2c683eb819..88d0b7a2d8 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -10,6 +10,7 @@ #include "JSystem/J2DGraph/J2DTextBox.h" #include "d/d_msg_string.h" #include "dusk/livesplit.h" +#include "dusk/imgui/ImGuiConsole.hpp" #include "m_Do/m_Do_controller_pad.h" dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) { @@ -141,6 +142,13 @@ void dBrightCheck_c::modeMove() { mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0); #ifdef TARGET_PC dusk::speedrun::start(); + + if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) { + // start a new run if a run isn't already in progress + if (!dusk::g_imguiConsole.isSpeedrunStart()) { + dusk::g_imguiConsole.startSpeedrun(); + } + } #endif mCompleteCheck = true; mMode = MODE_WAIT_e; diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index 735ce4d0ca..9a20ca6caa 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -5,19 +5,20 @@ #include "d/dolzel.h" // IWYU pragma: keep -#include "d/d_s_name.h" #include "JSystem/JKernel/JKRExpHeap.h" #include "d/d_com_inf_game.h" #include "d/d_meter2_info.h" +#include "d/d_s_name.h" +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/memory.h" +#include "dusk/settings.h" +#include "f_op/f_op_overlap_mng.h" #include "f_op/f_op_scene_mng.h" #include "m_Do/m_Do_Reset.h" #include "m_Do/m_Do_graphic.h" #include "m_Do/m_Do_machine.h" -#include "m_Do/m_Do_mtx.h" #include "m_Do/m_Do_main.h" -#include "f_op/f_op_overlap_mng.h" -#include "dusk/memory.h" -#include "dusk/settings.h" +#include "m_Do/m_Do_mtx.h" #if TARGET_PC #define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) @@ -412,6 +413,15 @@ void dScnName_c::changeGameScene() { dKy_clear_game_init(); dComIfGs_resetDan(); dComIfGs_setRestartRoomParam(0); + +#if TARGET_PC + if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) { + // start a new run on file load if a run isn't already in progress + if (!dusk::g_imguiConsole.isSpeedrunStart()) { + dusk::g_imguiConsole.startSpeedrun(); + } + } +#endif } } diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index c64314a806..955cef3920 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -334,7 +334,6 @@ namespace dusk { if (showMenu && ImGui::BeginMainMenuBar()) { m_menuGame.draw(); m_menuTools.draw(); - m_menuSpeedrunTimer.draw(); const auto fpsLabel = fmt::format(FMT_STRING("FPS: {:.2f}\n"), ImGui::GetIO().Framerate); @@ -366,12 +365,11 @@ namespace dusk { } } - m_menuSpeedrunTimer.drawOverlay(); - UpdateDragScroll(); m_menuGame.windowControllerConfig(); m_menuGame.windowInputViewer(); + m_menuGame.drawSpeedrunTimerOverlay(); if (getSettings().game.liveSplitEnabled) { dusk::speedrun::updateLiveSplit(); @@ -381,7 +379,7 @@ namespace dusk { AddToast("LiveSplit disconnected"); } - if (dusk::IsGameLaunched) { + if (dusk::IsGameLaunched && !dusk::getSettings().game.speedrunMode) { m_menuTools.ShowDebugOverlay(); m_menuTools.ShowCameraOverlay(); m_menuTools.ShowProcessManager(); @@ -392,8 +390,9 @@ namespace dusk { m_menuTools.ShowPlayerInfo(); m_menuTools.ShowAudioDebug(); m_menuTools.ShowSaveEditor(); + m_menuTools.ShowStateShare(); } - m_menuTools.ShowStateShare(); + DuskDebugPad(); // temporary, remove later // Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds. diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 85e65d21a1..5e9f2aa523 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -10,7 +10,6 @@ #include "ImGuiFirstRunPreset.hpp" #include "ImGuiMenuGame.hpp" -#include "ImGuiMenuSpeedrunTimer.hpp" #include "ImGuiMenuTools.hpp" #include "ImGuiPreLaunchWindow.hpp" #include "imgui.h" @@ -30,6 +29,11 @@ public: static bool CheckMenuViewToggle(ImGuiKey key, bool& active); void AddToast(std::string_view message, float duration = 3.f); + bool isSpeedrunStart() const { return m_menuGame.isRunStarted(); } + void startSpeedrun() { m_menuGame.startRun(); } + void stopSpeedrun() { m_menuGame.stopRun(); } + void incSpeedrunTotalLoadTime(OSTime time) { m_menuGame.incTotalLoadTime(time); } + private: struct Toast { std::string message; @@ -53,7 +57,6 @@ private: ImGuiFirstRunPreset m_firstRunPreset; ImGuiMenuGame m_menuGame; - ImGuiMenuSpeedrunTimer m_menuSpeedrunTimer; ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 7e54f2ffa3..fbea49bcf9 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -12,12 +12,15 @@ #include "dusk/main.h" #include "dusk/hotkeys.h" #include "dusk/settings.h" +#include "dusk/livesplit.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include #include +#include "m_Do/m_Do_main.h" + namespace { constexpr int kInternalResolutionScaleMax = 12; } // namespace @@ -199,6 +202,7 @@ namespace dusk { ImGui::SeparatorText("Difficulty"); + ImGui::BeginDisabled(getSettings().game.speedrunMode); config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); @@ -211,6 +215,7 @@ namespace dusk { ImGui::SetTooltip("Hearts will never drop from enemies,\n" "pots and various other places."); } + ImGui::EndDisabled(); ImGui::SeparatorText("Quality of Life"); @@ -280,12 +285,39 @@ namespace dusk { ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); } + ImGui::SeparatorText("Speedrunning"); + if (config::ImGuiCheckbox("Speedrun Mode", getSettings().game.speedrunMode)) { + resetForSpeedrunMode(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables Speedrunning options, while restricting certain gameplay modifiers."); + } + + ImGui::BeginDisabled(!getSettings().game.speedrunMode); + bool prevLiveSplit = getSettings().game.liveSplitEnabled; + config::ImGuiCheckbox("LiveSplit Connection", getSettings().game.liveSplitEnabled); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Connect to LiveSplit server on localhost:16834."); + } + ImGui::EndDisabled(); + + if ((bool)getSettings().game.liveSplitEnabled != prevLiveSplit) { + if (getSettings().game.liveSplitEnabled) { + dusk::speedrun::connectLiveSplit(); + } else { + dusk::speedrun::disconnectLiveSplit(); + DuskToast("LiveSplit disconnected", 3.f); + } + } + ImGui::EndMenu(); } } void ImGuiMenuGame::drawCheatsMenu() { if (ImGui::BeginMenu("Cheats")) { + ImGui::BeginDisabled(getSettings().game.speedrunMode); + ImGui::SeparatorText("Resources"); config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); @@ -293,8 +325,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"); + config::ImGuiCheckbox("No Item Timer", getSettings().game.enableIndefiniteItemDrops); + ImGui::SetItemTooltip("Item drops such as Rupees, Hearts, etc. will never disappear after they drop."); ImGui::SeparatorText("Abilities"); config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); @@ -317,6 +349,8 @@ namespace dusk { ImGui::SetTooltip("Makes the magic armor work without rupees."); } + ImGui::EndDisabled(); + ImGui::EndMenu(); } } @@ -431,10 +465,12 @@ namespace dusk { ImGui::SeparatorText("Tools"); + ImGui::BeginDisabled(getSettings().game.speedrunMode); config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); } + ImGui::EndDisabled(); ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); @@ -971,4 +1007,85 @@ namespace dusk { ImGui::End(); } + + static std::string GetFormattedTime(OSTime ticks) { + OSCalendarTime time; + OSTicksToCalendarTime(ticks, &time); + + return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", time.hour, time.min, time.sec, time.msec); + } + + void ImGuiMenuGame::resetForSpeedrunMode() { + // reset settings that should be off for speedrun mode + mDoMain::developmentMode = -1; + + getSettings().game.damageMultiplier.setValue(1); + getSettings().game.instantDeath.setValue(false); + getSettings().game.noHeartDrops.setValue(false); + + getSettings().game.infiniteHearts.setValue(false); + getSettings().game.infiniteArrows.setValue(false); + getSettings().game.infiniteBombs.setValue(false); + getSettings().game.infiniteOil.setValue(false); + getSettings().game.infiniteOxygen.setValue(false); + getSettings().game.infiniteRupees.setValue(false); + getSettings().game.enableIndefiniteItemDrops.setValue(false); + + getSettings().game.moonJump.setValue(false); + getSettings().game.superClawshot.setValue(false); + getSettings().game.alwaysGreatspin.setValue(false); + getSettings().game.enableFastIronBoots.setValue(false); + getSettings().game.canTransformAnywhere.setValue(false); + getSettings().game.fastSpinner.setValue(false); + getSettings().game.freeMagicArmor.setValue(false); + + getSettings().game.enableTurboKeybind.setValue(false); + } + + void ImGuiMenuGame::drawSpeedrunTimerOverlay() { + if (!getSettings().game.speedrunMode) { + return; + } + + // L+R+A+Start to reset timer + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigStart(PAD_1)) { + m_speedrunInfo.m_endTimestamp = 0; + m_speedrunInfo.m_startTimestamp = 0; + m_speedrunInfo.m_totalLoadTime = 0; + m_speedrunInfo.m_isRunStarted = false; + } + + // L+R+A+Z to manually stop timer + if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) { + if (m_speedrunInfo.m_isRunStarted) { + m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; + m_speedrunInfo.m_isRunStarted = false; + } + } + + ImGui::SetNextWindowBgAlpha(0.65f); + ImGuiWindowFlags flags = + ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoDocking + | ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoScrollbar; + + if (ImGui::Begin("##SpeedrunTimerWindow", nullptr, flags)) { + OSTime elapsedTime = 0; + if (m_speedrunInfo.m_isRunStarted) { + elapsedTime = OSGetTime() - m_speedrunInfo.m_startTimestamp; + } else if (m_speedrunInfo.m_endTimestamp != 0) { + elapsedTime = m_speedrunInfo.m_endTimestamp; + } + + ImGui::Text("RTA"); + ImGui::SameLine(60.0f); + ImGuiStringViewText(GetFormattedTime(elapsedTime)); + + ImGui::Text("IGT"); + ImGui::SameLine(60.0f); + ImGuiStringViewText(GetFormattedTime(elapsedTime - m_speedrunInfo.m_totalLoadTime)); + } + ImGui::End(); + } } diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index e21374c8f4..795cf5f446 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -15,9 +15,23 @@ namespace dusk { void windowInputViewer(); void windowControllerConfig(); + void drawSpeedrunTimerOverlay(); static void ToggleFullscreen(); + static void resetForSpeedrunMode(); + bool isRunStarted() const { return m_speedrunInfo.m_isRunStarted; } + void startRun() { + resetForSpeedrunMode(); + m_speedrunInfo.m_isRunStarted = true; + m_speedrunInfo.m_startTimestamp = OSGetTime(); + } + void stopRun() { + m_speedrunInfo.m_isRunStarted = false; + m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; + } + void incTotalLoadTime(OSTime time) { m_speedrunInfo.m_totalLoadTime += time; } + private: void drawAudioMenu(); void drawInputMenu(); @@ -40,6 +54,14 @@ namespace dusk { bool m_showInputViewerGyro = false; int m_inputOverlayCorner = 3; std::string m_controllerName; + + struct { + bool m_showTimerWindow = false; + bool m_isRunStarted = false; + OSTime m_startTimestamp = 0; + OSTime m_endTimestamp = 0; + OSTime m_totalLoadTime = 0; + } m_speedrunInfo; }; } diff --git a/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp deleted file mode 100644 index 6edbb7e084..0000000000 --- a/src/dusk/imgui/ImGuiMenuSpeedrunTimer.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "fmt/format.h" -#include "imgui.h" - -#include "ImGuiMenuSpeedrunTimer.hpp" -#include "ImGuiConfig.hpp" -#include "ImGuiConsole.hpp" -#include "dusk/livesplit.h" -#include "dusk/settings.h" - -namespace dusk { - static auto formatTime(uint64_t frames) { - const uint64_t totalSec = frames / 30; - - return fmt::format( - FMT_STRING("{:d}:{:02d}:{:02d}.{:02d}"), - totalSec / 3600, - (totalSec / 60) % 60, - totalSec % 60, - (int)(((f32)(frames % 30) / 30.0f) * 100.0f) - ); - } - - void ImGuiMenuSpeedrunTimer::draw() { - if (!getSettings().game.speedrunTimer) return; - - const uint64_t frames = dusk::speedrun::getFrameCount(); - - if (ImGui::BeginMenu("Timer##speedrun_timer")) { - ImGui::TextUnformatted(formatTime(frames).c_str()); - - ImGui::Separator(); - - if (ImGui::MenuItem("Reset")) { - dusk::speedrun::reset(); - } - - config::ImGuiCheckbox("Show Overlay", getSettings().game.speedrunTimerOverlay); - - bool prevLiveSplit = getSettings().game.liveSplitEnabled; - config::ImGuiCheckbox("LiveSplit Connection", getSettings().game.liveSplitEnabled); - - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Connect to LiveSplit server on localhost:16834."); - } - - if ((bool)getSettings().game.liveSplitEnabled != prevLiveSplit) { - if (getSettings().game.liveSplitEnabled) { - dusk::speedrun::connectLiveSplit(); - } else { - dusk::speedrun::disconnectLiveSplit(); - DuskToast("LiveSplit disconnected", 3.f); - } - } - - ImGui::EndMenu(); - } - - } - - void ImGuiMenuSpeedrunTimer::drawOverlay() { - if (!getSettings().game.speedrunTimer || !getSettings().game.speedrunTimerOverlay) { - return; - } - - const uint64_t frames = dusk::speedrun::getFrameCount(); - const ImGuiViewport* viewport = ImGui::GetMainViewport(); - - const float padding = 10.f; - - ImGui::SetNextWindowPos( - ImVec2( - viewport->WorkPos.x + viewport->WorkSize.x - padding, - viewport->WorkPos.y + viewport->WorkSize.y - padding - ), - ImGuiCond_Always, ImVec2(1.f, 1.f) - ); - - ImGui::SetNextWindowBgAlpha(0.65f); - - const float fixedWidth = ImGui::CalcTextSize("9:59:59.99").x; - - ImGui::SetNextWindowContentSize(ImVec2(fixedWidth, 0.f)); - - if ( - ImGui::Begin("##speedrun_overlay", nullptr, - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | - ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoNav - ) - ) { - ImGui::TextUnformatted(formatTime(frames).c_str()); - } - ImGui::End(); - } -} diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 00a03e635b..5dd5a62d84 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -50,10 +50,14 @@ namespace dusk { ImGui::BeginDisabled(); } + ImGui::BeginDisabled(getSettings().game.speedrunMode); + 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::EndDisabled(); + if (!dusk::IsGameLaunched) { ImGui::EndDisabled(); } @@ -69,6 +73,8 @@ namespace dusk { } if (ImGui::BeginMenu("Debug")) { + ImGui::BeginDisabled(getSettings().game.speedrunMode); + bool developmentMode = mDoMain::developmentMode == 1; if (ImGui::Checkbox("Development Mode", &developmentMode)) { mDoMain::developmentMode = developmentMode ? 1 : -1; @@ -117,6 +123,9 @@ namespace dusk { } ImGui::MenuItem("OSReport Force", nullptr, &OSReportReallyForceEnable); + + ImGui::EndDisabled(); + ImGui::EndMenu(); } } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index bddc30cb84..843dd4b133 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -92,8 +92,7 @@ UserSettings g_userSettings = { .enableTurboKeybind {"game.enableTurboKeybind", false}, // Tools - .speedrunTimer {"game.speedrunTimer", false}, - .speedrunTimerOverlay {"game.speedrunTimerOverlay", false}, + .speedrunMode {"game.speedrunMode", false}, .liveSplitEnabled {"game.liveSplitEnabled", false} }, @@ -162,8 +161,7 @@ void registerSettings() { Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.enableTurboKeybind); - Register(g_userSettings.game.speedrunTimer); - Register(g_userSettings.game.speedrunTimerOverlay); + Register(g_userSettings.game.speedrunMode); Register(g_userSettings.game.liveSplitEnabled); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.infiniteHearts); diff --git a/src/f_op/f_op_scene_req.cpp b/src/f_op/f_op_scene_req.cpp index 79b87eee4c..e4ce889266 100644 --- a/src/f_op/f_op_scene_req.cpp +++ b/src/f_op/f_op_scene_req.cpp @@ -4,13 +4,14 @@ */ #include "f_op/f_op_scene_req.h" +#include +#include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/logging.h" #include "f_op/f_op_overlap_mng.h" #include "f_op/f_op_scene.h" #include "f_op/f_op_scene_pause.h" #include "f_pc/f_pc_executor.h" #include "f_pc/f_pc_manager.h" -#include -#include "dusk/logging.h" static cPhs_Step fopScnRq_phase_ClearOverlap(scene_request_class* i_sceneReq) { return fopOvlpM_ClearOfReq() == 1 ? cPhs_NEXT_e : cPhs_INIT_e; @@ -39,6 +40,10 @@ static cPhs_Step fopScnRq_phase_IsDoneOverlap(scene_request_class* i_sceneReq) { static BOOL l_fopScnRq_IsUsingOfOverlap; +#if TARGET_PC +static OSTime l_fopScnRq_StartTime = 0; +#endif + static cPhs_Step fopScnRq_phase_Done(scene_request_class* i_sceneReq) { if (i_sceneReq->create_request.parameters != 1) { @@ -48,6 +53,14 @@ static cPhs_Step fopScnRq_phase_Done(scene_request_class* i_sceneReq) { } l_fopScnRq_IsUsingOfOverlap = FALSE; + #if TARGET_PC + if (dusk::getSettings().game.speedrunMode) { + if (dusk::g_imguiConsole.isSpeedrunStart()) { + dusk::g_imguiConsole.incSpeedrunTotalLoadTime(OSGetTime() - l_fopScnRq_StartTime); + } + } + #endif + return cPhs_NEXT_e; } @@ -88,6 +101,10 @@ static scene_request_class* fopScnRq_FadeRequest(s16 i_procname, u16 i_peektime) req = fopOvlpM_Request(i_procname, i_peektime); if (req != NULL) { l_fopScnRq_IsUsingOfOverlap = TRUE; + + #if TARGET_PC + l_fopScnRq_StartTime = OSGetTime(); + #endif } } From 6cf94b4491c62684e47add925d8c4f70d39dbf90 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 25 Apr 2026 21:18:51 -0700 Subject: [PATCH 27/70] remove unneeded file --- src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp diff --git a/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp b/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp deleted file mode 100644 index 8cb524469f..0000000000 --- a/src/dusk/imgui/ImGuiMenuSpeedrunTimer.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP -#define DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP - -namespace dusk { - class ImGuiMenuSpeedrunTimer { - public: - void draw(); - void drawOverlay(); - private: - }; -} - -#endif // DUSK_IMGUI_MENUSPEEDRUNTIMER_HPP From 02924549be425dd9818aa1df3f8148669d97a934 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sat, 25 Apr 2026 21:55:40 -0600 Subject: [PATCH 28/70] fix idle howl blinking --- src/d/d_msg_scrn_howl.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/d/d_msg_scrn_howl.cpp b/src/d/d_msg_scrn_howl.cpp index 1d39cba1a1..c701ea0b2b 100644 --- a/src/d/d_msg_scrn_howl.cpp +++ b/src/d/d_msg_scrn_howl.cpp @@ -20,6 +20,7 @@ #include "Z2AudioLib/Z2WolfHowlMgr.h" #if TARGET_PC +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #endif @@ -580,9 +581,14 @@ void dMsgScrnHowl_c::drawWave() { f17 = local_60; f18 = local_64; } else { - field_0x2134++; - if (field_0x2134 > 30) { - field_0x2134 = 0; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x2134++; + if (field_0x2134 > 30) { + field_0x2134 = 0; + } } if (field_0x2134 < 15) { local_dc = field_0x2134 / 15.0f; From c1231885fe515f13791ae32f33e48471f9991a31 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Sat, 25 Apr 2026 22:17:17 -0700 Subject: [PATCH 29/70] Add rumble intensity sliders to input configurator (#558) --- src/dusk/imgui/ImGuiMenuGame.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 0a687233aa..42dfc8af65 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -965,7 +965,7 @@ namespace dusk { ImGui::SameLine(); // Options panel - ImGuiBeginGroupPanel("Options", ImVec2(150 * scale, 20 * scale)); + ImGuiBeginGroupPanel("Options", ImVec2(150 * scale, -1)); if (deadZones != nullptr) { if (ImGui::Checkbox("Enable Dead Zones", &deadZones->useDeadzones)) { @@ -975,7 +975,24 @@ namespace dusk { PADSerializeMappings(); } } - + + if (PADSupportsRumbleIntensity(m_controllerConfig.m_selectedPort)) { + ImGuiBeginGroupPanel("Rumble Intensity", ImVec2(150 * scale, -1)); + u16 low; + u16 high; + (void)PADGetRumbleIntensity(m_controllerConfig.m_selectedPort, &low, &high); + float fLow = static_cast(low / 32767) * 100.f; + bool changed = ImGui::SliderFloat("Low", &fLow, 0.f, 100.f, "%.0f%%"); + float fHigh = static_cast(high / 32767) * 100.f; + changed |= ImGui::SliderFloat("High", &fHigh, 0.f, 100.f, "%.0f%%"); + if (changed) { + PADSetRumbleIntensity(m_controllerConfig.m_selectedPort, + static_cast((fLow / 100.f) * 32767), + static_cast((fHigh / 100.f) * 32767)); + PADSerializeMappings(); + } + ImGuiEndGroupPanel(); + } ImGuiEndGroupPanel(); ImGui::End(); From 8784958c40d8205df876d2ef5dd2b3b837d328f5 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Sat, 25 Apr 2026 22:51:39 -0700 Subject: [PATCH 30/70] Quick PAD rumble fixes, add button to test rumble configuration --- extern/aurora | 2 +- src/dusk/imgui/ImGuiMenuGame.cpp | 12 ++++++++---- src/dusk/imgui/ImGuiMenuGame.hpp | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/extern/aurora b/extern/aurora index 135e976867..8af9057689 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 135e976867490380356878c8bb8d1956932489c5 +Subproject commit 8af9057689b9b85a55bbbd6218bc95304558fabd diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 42dfc8af65..dcfcc55657 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -981,16 +981,20 @@ namespace dusk { u16 low; u16 high; (void)PADGetRumbleIntensity(m_controllerConfig.m_selectedPort, &low, &high); - float fLow = static_cast(low / 32767) * 100.f; + float fLow = (static_cast(low) / 32767.f) * 100.f; bool changed = ImGui::SliderFloat("Low", &fLow, 0.f, 100.f, "%.0f%%"); - float fHigh = static_cast(high / 32767) * 100.f; + float fHigh = (static_cast(high) / 32767.f) * 100.f; changed |= ImGui::SliderFloat("High", &fHigh, 0.f, 100.f, "%.0f%%"); if (changed) { PADSetRumbleIntensity(m_controllerConfig.m_selectedPort, - static_cast((fLow / 100.f) * 32767), - static_cast((fHigh / 100.f) * 32767)); + static_cast((fLow / 100) * 32767), + static_cast((fHigh / 100) * 32767)); PADSerializeMappings(); } + if (ImGui::Button(fmt::format("{0}...##rumbleTest", m_controllerConfig.m_isRumbling ? "Stop": "Test").c_str(), {-1, 0})) { + PADControlMotor(m_controllerConfig.m_selectedPort, !m_controllerConfig.m_isRumbling ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD); + m_controllerConfig.m_isRumbling ^= 1; + } ImGuiEndGroupPanel(); } ImGuiEndGroupPanel(); diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index e21374c8f4..fb3f3f10ca 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -32,6 +32,7 @@ namespace dusk { PADButtonMapping* m_pendingButtonMapping = nullptr; PADAxisMapping* m_pendingAxisMapping = nullptr; int m_pendingPort = -1; + bool m_isRumbling = false; } m_controllerConfig; bool m_showControllerConfig = false; From 3498ded9d9c2ae29b032915cdd8122577683b89f Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Sun, 26 Apr 2026 10:18:47 -0400 Subject: [PATCH 31/70] ImGui Button name change + Fix Freecam Invert X Axis option --- src/d/d_camera.cpp | 7 +++---- src/dusk/imgui/ImGuiPreLaunchWindow.cpp | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 917b30709a..8efef81b8e 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -7493,10 +7493,9 @@ bool dCamera_c::freeCamera() { } camMovement = camMovement.normalize(); - camMovement.x *= (dusk::getSettings().game.invertCameraXAxis ? 1.0f : -1.0f) * dusk::getSettings().game.freeCameraSensitivity * 4.0f; - camMovement.y *= (dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f) * dusk::getSettings().game.freeCameraSensitivity * 4.0f; - mCamParam.freeXAngle += camMovement.x * magnitude * dusk::getSettings().game.freeCameraSensitivity; - mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity; + camMovement.y *= dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f; + mCamParam.freeXAngle += camMovement.x * magnitude * dusk::getSettings().game.freeCameraSensitivity * 4.0f; + mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity * 4.0f; } if (mCamParam.mManualMode) { diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp index 6347e9bc01..4bfcce9047 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -193,7 +193,7 @@ void ImGuiPreLaunchWindow::drawOptions() { ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly); ImGui::SameLine(); - if (ImGui::Button("Set")) { + if (ImGui::Button(m_selectedIsoPath == "" ? "Set" : "Change")) { ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, false); From 3859f397294e6e906fdf372ed64edcbcf38a623f Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Sun, 26 Apr 2026 10:25:10 -0400 Subject: [PATCH 32/70] Fix camera radius to be smoother --- src/d/d_camera.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 8efef81b8e..c59cf1c525 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -7502,7 +7502,7 @@ bool dCamera_c::freeCamera() { mCamParam.freeYAngle = std::clamp(mCamParam.freeYAngle, -35.0f, 60.0f); mViewCache.mDirection.mAzimuth = cSAngle(mCamParam.freeXAngle); mViewCache.mDirection.mInclination = cSAngle(mCamParam.freeYAngle); - mViewCache.mDirection.mRadius = std::clamp(mCamParam.freeYAngle * 15.0f, 300.0f, 10000.0f); + mViewCache.mDirection.mRadius = std::clamp((mCamParam.freeYAngle + 35.0f) * 10.0f, 300.0f, 10000.0f); } return mCamParam.mManualMode; From 5c84978c3cfd6a1d15228198d6537a00a8961f99 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 26 Apr 2026 10:44:52 -0700 Subject: [PATCH 33/70] evil perf fix closes #552 --- src/d/d_kankyo_rain.cpp | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/d/d_kankyo_rain.cpp b/src/d/d_kankyo_rain.cpp index 822612768c..09e230c9fd 100644 --- a/src/d/d_kankyo_rain.cpp +++ b/src/d/d_kankyo_rain.cpp @@ -5962,6 +5962,8 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { fopAc_ac_c* player = dComIfGp_getPlayer(0); if (evil_packet != NULL) { + IF_DUSK(GXPushDebugGroup("dKyr_evil_draw2")); + j3dSys.reinitGX(); if (dComIfGd_getView() != NULL) { MTXInverse(dComIfGd_getView()->viewMtxNoTrans, camMtx); @@ -6162,6 +6164,8 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { } } + IF_DUSK(GXPopDebugGroup()); + GXSetClipMode(GX_CLIP_ENABLE); J3DShape::resetVcdVatCache(); } @@ -6199,6 +6203,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { f32 sp60 = fabsf(cM_ssin(g_Counter.mCounter0 * 215)); if (evil_packet != NULL) { + IF_DUSK(GXPushDebugGroup("dKyr_evil_draw")); + j3dSys.reinitGX(); if (dComIfGd_getView() != NULL) { MTXInverse(dComIfGd_getView()->viewMtxNoTrans, camMtx); @@ -6231,8 +6237,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { GXLoadPosMtxImm(drawMtx, GX_PNMTX0); GXSetCurrentMtx(GX_PNMTX0); - GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGBA, GX_F32, 0); - GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_CLR_RGBA, GX_RGBA4, 8); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_RGBA4, 8); GXClearVtxDesc(); GXSetVtxDesc(GX_VA_POS, GX_DIRECT); GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); @@ -6255,6 +6261,19 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { GXSetClipMode(GX_CLIP_DISABLE); GXSetNumIndStages(0); +#if TARGET_PC + // move color_reg0 to vtx for perf + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT); + GXSetNumChans(1); + GXSetChanCtrl(GX_COLOR0A0, GX_FALSE, GX_SRC_REG, GX_SRC_VTX, 0, GX_DF_NONE, GX_AF_NONE); + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_C1, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO); + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_TEXA, GX_CA_RASA, GX_CA_ZERO); + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); +#endif + dComIfG_Ccsp()->PrepareMass(); for (int i = 0; i < g_env_light.field_0x1054; i++) { @@ -6373,7 +6392,7 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { color_reg0.b = (115.0f * sp28) + (15.0f * fabsf(sp2C - sp64)); } - GXSetTevColor(GX_TEVREG0, color_reg0); + IF_NOT_DUSK(GXSetTevColor(GX_TEVREG0, color_reg0)); GXSetTevColor(GX_TEVREG1, color_reg1); spC8 = spA4; @@ -6412,12 +6431,16 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { GXBegin(GX_QUADS, GX_VTXFMT0, 4); GXPosition3f32(pos[0].x, pos[0].y, pos[0].z); + IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a)); GXTexCoord2s16(0, 0); GXPosition3f32(pos[1].x, pos[1].y, pos[1].z); + IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a)); GXTexCoord2s16(0xFF, 0); GXPosition3f32(pos[2].x, pos[2].y, pos[2].z); + IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a)); GXTexCoord2s16(0xFF, 0xFF); GXPosition3f32(pos[3].x, pos[3].y, pos[3].z); + IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a)); GXTexCoord2s16(0, 0xFF); GXEnd(); } @@ -6425,6 +6448,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { } } + IF_DUSK(GXPopDebugGroup()); + J3DShape::resetVcdVatCache(); GXSetClipMode(GX_CLIP_ENABLE); From 50e2d9d1a713afa52cb5fcd533e31a21527c7e8e Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 26 Apr 2026 11:18:23 -0700 Subject: [PATCH 34/70] grass draw fix closes #534 --- src/d/actor/d_flower.inc | 14 ++------------ src/d/actor/d_grass.inc | 14 ++++++-------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 5f54ea4bda..4024d5ad6a 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -7,13 +7,8 @@ #include "dusk/frame_interpolation.h" -#if TARGET_PC -const u16 l_J_Ohana00_64TEX__width = 64; -const u16 l_J_Ohana00_64TEX__height = 64; -#else const u16 l_J_Ohana00_64TEX__width = 63; const u16 l_J_Ohana00_64TEX__height = 63; -#endif #if TARGET_PC #include "dusk/dvd_asset.hpp" @@ -136,13 +131,8 @@ l_matDL__d_a_grass(l_J_Ohana00_64TEX) l_matLight4DL(l_J_Ohana00_64TEX) #endif -#if TARGET_PC -const u16 l_J_Ohana01_64128_0419TEX__width = 64; -const u16 l_J_Ohana01_64128_0419TEX__height = 128; -#else const u16 l_J_Ohana01_64128_0419TEX__width = 63; const u16 l_J_Ohana01_64128_0419TEX__height = 127; -#endif #if TARGET_PC using GameVersion = dusk::version::GameVersion; @@ -592,11 +582,11 @@ dFlower_packet_c::dFlower_packet_c() { #if TARGET_PC GXInitTexObj(&mTexObj_l_J_Ohana00_64TEX, l_J_Ohana00_64TEX, - l_J_Ohana00_64TEX__width, l_J_Ohana00_64TEX__height, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE + l_J_Ohana00_64TEX__width + 1, l_J_Ohana00_64TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE ); GXInitTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, l_J_Ohana01_64128_0419TEX, - l_J_Ohana01_64128_0419TEX__width, l_J_Ohana01_64128_0419TEX__height, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE + l_J_Ohana01_64128_0419TEX__width + 1, l_J_Ohana01_64128_0419TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE ); #endif diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index 9bc8815753..3d67a57007 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -494,11 +494,11 @@ dGrass_packet_c::dGrass_packet_c() { #if TARGET_PC GXInitTexObj(&mTexObj_l_M_kusa05_RGBATEX, l_M_kusa05_RGBATEX, - l_M_kusa05_RGBATEX__width, l_M_kusa05_RGBATEX__height, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE + l_M_kusa05_RGBATEX__width + 1, l_M_kusa05_RGBATEX__height + 1, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE ); GXInitTexObj(&mTexObj_l_M_Hijiki00TEX, l_M_Hijiki00TEX, - l_M_Hijiki00TEX__width, l_M_Hijiki00TEX__height, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE + l_M_Hijiki00TEX__width + 1, l_M_Hijiki00TEX__height + 1, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE ); #endif @@ -646,18 +646,14 @@ void dGrass_packet_c::draw() { } if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) { -#if TARGET_PC - GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0); -#endif + IF_DUSK(GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0)); if (sp48 <= 3) { GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size); } else { GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size); } } else { -#if TARGET_PC - GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0); -#endif + IF_DUSK(GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0)); GXCallDisplayList(l_Tengusa_matDL, 0xA0); } @@ -683,12 +679,14 @@ void dGrass_packet_c::draw() { while (var_r29 != NULL) { if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) { + IF_DUSK(GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0)); if (sp48 <= 2) { GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size); } else { GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size); } } else { + IF_DUSK(GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0)); GXCallDisplayList(l_Tengusa_matDL, 0xA0); } From e75ea18ef01c2be02dc620d691c3eb13ce9105eb Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 26 Apr 2026 11:52:20 -0700 Subject: [PATCH 35/70] map deco perf --- include/dusk/settings.h | 1 + src/d/d_map.cpp | 11 ++++------- src/d/d_map_path.cpp | 34 ++++++++++++++++++++++++-------- src/dusk/imgui/ImGuiMenuGame.cpp | 2 ++ src/dusk/settings.cpp | 2 ++ 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index a7815569d0..875b839d44 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -96,6 +96,7 @@ struct UserSettings { ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; ConfigVar enableDepthOfField; + ConfigVar enableMapBackground; // Audio ConfigVar noLowHpSound; diff --git a/src/d/d_map.cpp b/src/d/d_map.cpp index 7dc5187a88..a5163091fd 100644 --- a/src/d/d_map.cpp +++ b/src/d/d_map.cpp @@ -539,17 +539,14 @@ void renderingAmap_c::rendering(dDrawPath_c::poly_class const* i_poly) { } } -/* Enabling the following definition will modify the following function to - * make the map look worse for extra speed in the emulator, especially in large - * areas such as hyrule field. - */ -#define HYRULE_FIELD_SPEEDHACK bool renderingAmap_c::isDrawOutSideTrim() { bool rt = false; - #ifdef HYRULE_FIELD_SPEEDHACK - return 0; + #if TARGET_PC + if (!dusk::getSettings().game.enableMapBackground) { + return 0; + } #endif if (getDispType() == 0 || getDispType() == 4 || getDispType() == 3 || getDispType() == 2 || diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index 8288a2ef9f..f94cdc9475 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -497,12 +497,6 @@ void dRenderingFDAmap_c::postRenderingMap() { dMpath_n::dTexObjAggregate_c dMpath_n::m_texObjAgg; -/* Enabling the following definition will modify the following function to - * make the map look worse for extra speed in the emulator, especially in large - * areas such as hyrule field. - */ -#define HYRULE_FIELD_SPEEDHACK - void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_line) { s32 width = getDecorationLineWidth(p_line->field_0x1); if (width <= 0) { @@ -527,8 +521,32 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li lineColor.r = lineColor.r - 4; GXSetTevColor(GX_TEVREG1, lineColor); +#if TARGET_PC + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0); + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_KONST); + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXBegin(GX_LINESTRIP, GX_VTXFMT0, 2 * (data_num - 1)); + for (int i = 0; i < data_num - 1; i++) { + GXPosition1x16(data_p[i]); + GXTexCoord2f32(0, 0); + GXPosition1x16(data_p[i + 1]); + GXTexCoord2f32(0, 0); + } + GXEnd(); + + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_KONST, GX_CC_TEXC, GX_CC_C1); + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA); + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXBegin(GX_POINTS, GX_VTXFMT0, data_num); + for (int i = 0; i < data_num; i++) { + GXPosition1x16(data_p[i]); + GXTexCoord2f32(0, 0); + } + GXEnd(); +#else for (int i = 0; i < data_num; i++) { -#ifndef HYRULE_FIELD_SPEEDHACK if (i < data_num - 1) { GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0); GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, @@ -547,7 +565,6 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA); GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); -#endif GXBegin(GX_POINTS, GX_VTXFMT0, 1); GXPosition1x16(data_p[0]); @@ -555,6 +572,7 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li GXEnd(); data_p++; } +#endif setTevSettingNonTextureDirectColor(); GXClearVtxDesc(); diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index dcfcc55657..10b6e65bf2 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -167,6 +167,8 @@ namespace dusk { config::ImGuiCheckbox("Enable Depth of Field", getSettings().game.enableDepthOfField); + config::ImGuiCheckbox("Enable Mini-Map Shadows", getSettings().game.enableMapBackground); + ImGui::EndMenu(); } } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index eb1d513ba9..84e56eda4f 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -55,6 +55,7 @@ UserSettings g_userSettings = { .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, .enableDepthOfField {"game.enableDepthOfField", true}, + .enableMapBackground {"game.enableMapBackground", true}, // Audio .noLowHpSound {"game.noLowHpSound", false}, @@ -157,6 +158,7 @@ void registerSettings() { Register(g_userSettings.game.internalResolutionScale); Register(g_userSettings.game.shadowResolutionMultiplier); Register(g_userSettings.game.enableDepthOfField); + Register(g_userSettings.game.enableMapBackground); Register(g_userSettings.game.enableFastIronBoots); Register(g_userSettings.game.canTransformAnywhere); Register(g_userSettings.game.freeMagicArmor); From f856f871bb937c1aea63369e40e6df7b073a0f0a Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sun, 26 Apr 2026 13:55:32 -0600 Subject: [PATCH 36/70] Only show languages if PAL disc --- src/dusk/imgui/ImGuiPreLaunchWindow.cpp | 22 ++++++++++-------- src/dusk/imgui/ImGuiPreLaunchWindow.hpp | 1 + src/dusk/iso_validate.cpp | 31 +++++++++++++++++++++++++ src/dusk/iso_validate.hpp | 1 + 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp index 6347e9bc01..eef8c08e0d 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -75,6 +75,7 @@ void fileDialogCallback(void* userdata, const char* path, const char* error) { } self->m_selectedIsoPath = path; + self->m_isPal = iso::isPal(path); getSettings().backend.isoPath.setValue(self->m_selectedIsoPath); config::Save(); } @@ -92,6 +93,7 @@ bool ImGuiPreLaunchWindow::isSelectedPathValid() const { void ImGuiPreLaunchWindow::draw() { if (m_IsFirstDraw) { m_selectedIsoPath = getSettings().backend.isoPath; + m_isPal = !m_selectedIsoPath.empty() && iso::isPal(m_selectedIsoPath.c_str()); m_initialGraphicsBackend = getSettings().backend.graphicsBackend; m_IsFirstDraw = false; } @@ -199,18 +201,18 @@ void ImGuiPreLaunchWindow::drawOptions() { false); } - // TODO: Only show if PAL disc selected? - // Language selection - auto selectedLanguage = getSettings().game.language.getValue(); - if (ImGui::BeginCombo("Language", skLanguageNames[static_cast(selectedLanguage)])) { - for (u8 i = 0; i < skLanguageNames.size(); ++i) { - if (ImGui::Selectable(skLanguageNames[i])) { - getSettings().game.language.setValue(static_cast(i)); - config::Save(); + if (m_isPal) { + auto selectedLanguage = getSettings().game.language.getValue(); + if (ImGui::BeginCombo("Language", skLanguageNames[static_cast(selectedLanguage)])) { + for (u8 i = 0; i < skLanguageNames.size(); ++i) { + if (ImGui::Selectable(skLanguageNames[i])) { + getSettings().game.language.setValue(static_cast(i)); + config::Save(); + } } - } - ImGui::EndCombo(); + ImGui::EndCombo(); + } } AuroraBackend configuredBackend = BACKEND_AUTO; diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp b/src/dusk/imgui/ImGuiPreLaunchWindow.hpp index 5d16a6ba0b..6cb078a228 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.hpp @@ -18,5 +18,6 @@ public: std::string m_selectedIsoPath; std::string m_errorString; + bool m_isPal = false; }; } // namespace dusk diff --git a/src/dusk/iso_validate.cpp b/src/dusk/iso_validate.cpp index 0b4a19ae9c..e83b3fd34e 100644 --- a/src/dusk/iso_validate.cpp +++ b/src/dusk/iso_validate.cpp @@ -17,6 +17,11 @@ constexpr const char* TP_GAME_IDS[] = { "RZDK01", // Wii KOR }; +constexpr const char* PAL_GAME_IDS[] = { + "GZ2P01", // GCN PAL + "RZDP01", // Wii PAL +}; + constexpr const char* SUPPORTED_TP_GAME_IDS[] = { "GZ2E01", // GCN USA "GZ2P01", // GCN PAL @@ -124,4 +129,30 @@ ValidationError validate(const char* path) { return ValidationError::Success; } +bool isPal(const char* path) { + NodHandleWrapper disc; + + const auto sdlStream = SDL_IOFromFile(path, "rb"); + if (sdlStream == nullptr) { + return false; + } + + const NodDiscStream nod_stream{ + .user_data = sdlStream, + .read_at = StreamReadAt, + .stream_len = StreamLength, + .close = StreamClose, + }; + + if (nod_disc_open_stream(&nod_stream, nullptr, &disc.handle) != NOD_RESULT_OK || disc.handle == nullptr) { + return false; + } + + NodDiscHeader header{}; + if (nod_disc_header(disc.handle, &header) != NOD_RESULT_OK) { + return false; + } + + return matches(header.game_id, PAL_GAME_IDS); +} } // namespace dusk::iso \ No newline at end of file diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp index da1ef1f2a6..d961f052cd 100644 --- a/src/dusk/iso_validate.hpp +++ b/src/dusk/iso_validate.hpp @@ -13,6 +13,7 @@ namespace dusk::iso { }; ValidationError validate(const char* path); + bool isPal(const char* path); } #endif // DUSK_ISO_VALIDATE_HPP From 90c0bdded0816de41f35e718af70619f58131b8c Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 26 Apr 2026 14:02:28 -0600 Subject: [PATCH 37/70] Use GXDestroyCopyTex on map destroy Resolves #469 --- extern/aurora | 2 +- src/d/d_map.cpp | 6 ++++++ src/d/d_menu_dmap_map.cpp | 6 ++++++ src/d/d_menu_fmap_map.cpp | 6 ++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 8af9057689..a6a3d3a65a 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 8af9057689b9b85a55bbbd6218bc95304558fabd +Subproject commit a6a3d3a65ae0de6de8b60629cf47fd0f446c21cb diff --git a/src/d/d_map.cpp b/src/d/d_map.cpp index a5163091fd..d4e5a98888 100644 --- a/src/d/d_map.cpp +++ b/src/d/d_map.cpp @@ -13,6 +13,9 @@ #include "SSystem/SComponent/c_math.h" #include "d/actor/d_a_player.h" #include "d/d_com_inf_game.h" +#if TARGET_PC +#include +#endif #include #if DEBUG @@ -1215,6 +1218,9 @@ void dMap_c::changeTextureSize(int param_1, int param_2, int param_3) { void dMap_c::_remove() { if (mImage_p != NULL) { +#if TARGET_PC + GXDestroyCopyTex(mImage_p); +#endif JKR_DELETE_ARRAY(mImage_p); mImage_p = NULL; } diff --git a/src/d/d_menu_dmap_map.cpp b/src/d/d_menu_dmap_map.cpp index 8971b2630d..9100a7d63a 100644 --- a/src/d/d_menu_dmap_map.cpp +++ b/src/d/d_menu_dmap_map.cpp @@ -11,6 +11,9 @@ #include "d/d_menu_dmap_map.h" #include "f_op/f_op_msg_mng.h" #include "m_Do/m_Do_graphic.h" +#if TARGET_PC +#include +#endif struct dMdm_HIO_prm_res_dst_s { static void* m_res; @@ -291,6 +294,9 @@ void dMenu_DmapMap_c::_create(u16 param_0, u16 param_1, u16 param_2, u16 param_3 void dMenu_DmapMap_c::_delete() { for (int i = 0; i < 2; i++) { if (mMapImage_p[i] != NULL) { +#if TARGET_PC + GXDestroyCopyTex(mMapImage_p[i]); +#endif JKR_DELETE_ARRAY(mMapImage_p[i]); } diff --git a/src/d/d_menu_fmap_map.cpp b/src/d/d_menu_fmap_map.cpp index 3d1a9e2523..c500aff0be 100644 --- a/src/d/d_menu_fmap_map.cpp +++ b/src/d/d_menu_fmap_map.cpp @@ -8,6 +8,9 @@ #include "d/d_debug_viewer.h" #include "d/d_menu_fmap_map.h" #include "m_Do/m_Do_graphic.h" +#if TARGET_PC +#include +#endif #include static u8 twoValueLineInterpolation(u8 i_value1, u8 i_value2, f32 i_param) { @@ -494,6 +497,9 @@ void dMenu_FmapMap_c::_delete() { mResTIMG = NULL; } if (mMapImage_p != NULL) { +#if TARGET_PC + GXDestroyCopyTex(mMapImage_p); +#endif JKR_DELETE_ARRAY(mMapImage_p); mMapImage_p = NULL; } From 0f9d563a3e68f42e67f6a4bf60c27db4df9482fd Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sun, 26 Apr 2026 15:30:51 -0600 Subject: [PATCH 38/70] cLib_offsetPos param aliasing fix --- src/SSystem/SComponent/c_lib.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/SSystem/SComponent/c_lib.cpp b/src/SSystem/SComponent/c_lib.cpp index 6441d6ceb8..d73dfb8627 100644 --- a/src/SSystem/SComponent/c_lib.cpp +++ b/src/SSystem/SComponent/c_lib.cpp @@ -468,9 +468,20 @@ s16 cLib_targetAngleX(cXyz const* lhs, cXyz const* rhs) { void cLib_offsetPos(cXyz* pdest, cXyz const* psrc, s16 angle, cXyz const* vec) { f32 cos = cM_scos(angle); f32 sin = cM_ssin(angle); + // MWCC loads vec members into registers before writing to pdest; other compilers may not, + // which corrupts results when pdest and vec alias the same memory. +#if !__MWERKS__ + f32 vx = vec->x; + f32 vy = vec->y; + f32 vz = vec->z; + pdest->x = psrc->x + (vx * cos + vz * sin); + pdest->y = psrc->y + vy; + pdest->z = psrc->z + (vz * cos - vx * sin); +#else pdest->x = psrc->x + (vec->x * cos + vec->z * sin); pdest->y = psrc->y + vec->y; pdest->z = psrc->z + (vec->z * cos - vec->x * sin); +#endif } /** From a05d1a9ee61ba350ce26dbcc29f68b382ae171d4 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sun, 26 Apr 2026 17:05:46 -0600 Subject: [PATCH 39/70] d_a_balloon score and interp fixes --- src/d/actor/d_a_balloon_2D.cpp | 17 ++++++++++++++--- src/d/actor/d_a_obj_balloon.cpp | 7 +++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/d/actor/d_a_balloon_2D.cpp b/src/d/actor/d_a_balloon_2D.cpp index 6b5b5a2bd2..8627505371 100644 --- a/src/d/actor/d_a_balloon_2D.cpp +++ b/src/d/actor/d_a_balloon_2D.cpp @@ -6,6 +6,7 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep #include "d/actor/d_a_balloon_2D.h" +#include "dusk/frame_interpolation.h" #include "JSystem/J2DGraph/J2DGrafContext.h" #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" @@ -438,7 +439,12 @@ void daBalloon2D_c::setComboAlpha() { void daBalloon2D_c::drawAddScore() { for (s32 i = 19; i >= 0; i--) { if (field_0x5f8[i].field_0xe != 0) { - field_0x5f8[i].field_0xe--; +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x5f8[i].field_0xe--; + } s32 score3; s32 score2; s32 score = field_0x5f8[i].field_0xc; @@ -446,8 +452,13 @@ void daBalloon2D_c::drawAddScore() { u8 local_88 = 0xff; f32 dVar11 = 30.0f; f32 dVar9 = 30.0f; - field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f; - field_0x5f8[i].field_0x0.y -= 1.0f; +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f; + field_0x5f8[i].field_0x0.y -= 1.0f; + } if (field_0x5f8[i].field_0xe < 10) { f32 fVar5 = field_0x5f8[i].field_0xe / 10.0f; local_88 = fVar5 * 255.0f; diff --git a/src/d/actor/d_a_obj_balloon.cpp b/src/d/actor/d_a_obj_balloon.cpp index 41430f97f0..4425483bef 100644 --- a/src/d/actor/d_a_obj_balloon.cpp +++ b/src/d/actor/d_a_obj_balloon.cpp @@ -205,6 +205,13 @@ int daObj_Balloon_c::_delete() { Z2GetAudioMgr()->seStop(Z2SE_OBJ_WATERMILL_ROUND, 0); if (mHIOInit) { hio_set = false; +#ifdef TARGET_PC + // !@bug d_a_obj_balloon.rel unload used to zero these file-statics; with static linking they dangle across scenes. + m_combo_type = 0xFFFFFFFF; + m_combo_count = 0; + m_combo_next_score = 0; + m_balloon_score = 0; +#endif } return 1; } From 206c02b527993d5dfa4e2e9c1471821dedc47805 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sun, 26 Apr 2026 16:51:10 -0700 Subject: [PATCH 40/70] fix igt timer --- src/d/actor/d_a_alink_demo.inc | 4 +-- src/d/d_bright_check.cpp | 5 +-- src/d/d_s_name.cpp | 5 +-- src/dusk/imgui/ImGuiConsole.hpp | 5 --- src/dusk/imgui/ImGuiMenuGame.cpp | 13 +++++--- src/dusk/imgui/ImGuiMenuGame.hpp | 52 +++++++++++++++++++++----------- src/f_op/f_op_overlap_req.cpp | 20 ++++++++++++ src/f_op/f_op_scene_req.cpp | 16 ---------- 8 files changed, 70 insertions(+), 50 deletions(-) diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc index b46af5cacb..5ac30a3cb9 100644 --- a/src/d/actor/d_a_alink_demo.inc +++ b/src/d/actor/d_a_alink_demo.inc @@ -4009,8 +4009,8 @@ int daAlink_c::procGanonFinishInit() { #if TARGET_PC if (dusk::getSettings().game.speedrunMode) { - if (dusk::g_imguiConsole.isSpeedrunStart()) { - dusk::g_imguiConsole.stopSpeedrun(); + if (dusk::m_speedrunInfo.m_isRunStarted) { + dusk::m_speedrunInfo.stopRun(); } } #endif diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index 88d0b7a2d8..3ee98be5c7 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -145,8 +145,9 @@ void dBrightCheck_c::modeMove() { if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) { // start a new run if a run isn't already in progress - if (!dusk::g_imguiConsole.isSpeedrunStart()) { - dusk::g_imguiConsole.startSpeedrun(); + if (!dusk::m_speedrunInfo.m_isRunStarted) { + dusk::ImGuiMenuGame::resetForSpeedrunMode(); + dusk::m_speedrunInfo.startRun(); } } #endif diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index 9a20ca6caa..6baa07da30 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -417,8 +417,9 @@ void dScnName_c::changeGameScene() { #if TARGET_PC if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) { // start a new run on file load if a run isn't already in progress - if (!dusk::g_imguiConsole.isSpeedrunStart()) { - dusk::g_imguiConsole.startSpeedrun(); + if (!dusk::m_speedrunInfo.m_isRunStarted) { + dusk::ImGuiMenuGame::resetForSpeedrunMode(); + dusk::m_speedrunInfo.startRun(); } } #endif diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 5e9f2aa523..a6c8b48fc7 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -29,11 +29,6 @@ public: static bool CheckMenuViewToggle(ImGuiKey key, bool& active); void AddToast(std::string_view message, float duration = 3.f); - bool isSpeedrunStart() const { return m_menuGame.isRunStarted(); } - void startSpeedrun() { m_menuGame.startRun(); } - void stopSpeedrun() { m_menuGame.stopRun(); } - void incSpeedrunTotalLoadTime(OSTime time) { m_menuGame.incTotalLoadTime(time); } - private: struct Toast { std::string message; diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 83b80869b4..7f114aa015 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -1051,6 +1051,8 @@ namespace dusk { getSettings().game.enableTurboKeybind.setValue(false); } + SpeedrunInfo m_speedrunInfo; + void ImGuiMenuGame::drawSpeedrunTimerOverlay() { if (!getSettings().game.speedrunMode) { return; @@ -1058,10 +1060,7 @@ namespace dusk { // L+R+A+Start to reset timer if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigStart(PAD_1)) { - m_speedrunInfo.m_endTimestamp = 0; - m_speedrunInfo.m_startTimestamp = 0; - m_speedrunInfo.m_totalLoadTime = 0; - m_speedrunInfo.m_isRunStarted = false; + m_speedrunInfo.reset(); } // L+R+A+Z to manually stop timer @@ -1091,9 +1090,13 @@ namespace dusk { ImGui::SameLine(60.0f); ImGuiStringViewText(GetFormattedTime(elapsedTime)); + if (!m_speedrunInfo.m_isPauseIGT) { + m_speedrunInfo.m_igtTimer = elapsedTime - m_speedrunInfo.m_totalLoadTime; + } + ImGui::Text("IGT"); ImGui::SameLine(60.0f); - ImGuiStringViewText(GetFormattedTime(elapsedTime - m_speedrunInfo.m_totalLoadTime)); + ImGuiStringViewText(GetFormattedTime(m_speedrunInfo.m_igtTimer)); } ImGui::End(); } diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index 795cf5f446..4fbfec8f76 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -8,6 +8,39 @@ #include "imgui.h" namespace dusk { + struct SpeedrunInfo { + void startRun() { + m_isRunStarted = true; + m_startTimestamp = OSGetTime(); + } + + void stopRun() { + m_isRunStarted = false; + m_endTimestamp = OSGetTime() - m_startTimestamp; + } + + void reset() { + m_isRunStarted = false; + m_startTimestamp = 0; + m_endTimestamp = 0; + m_isPauseIGT = false; + m_loadStartTimestamp = 0; + m_totalLoadTime = 0; + m_igtTimer = 0; + } + + bool m_isRunStarted = false; + OSTime m_startTimestamp = 0; + OSTime m_endTimestamp = 0; + + bool m_isPauseIGT = false; + OSTime m_loadStartTimestamp = 0; + OSTime m_totalLoadTime = 0; + OSTime m_igtTimer = 0; + }; + + extern SpeedrunInfo m_speedrunInfo; + class ImGuiMenuGame { public: ImGuiMenuGame(); @@ -20,17 +53,6 @@ namespace dusk { static void ToggleFullscreen(); static void resetForSpeedrunMode(); - bool isRunStarted() const { return m_speedrunInfo.m_isRunStarted; } - void startRun() { - resetForSpeedrunMode(); - m_speedrunInfo.m_isRunStarted = true; - m_speedrunInfo.m_startTimestamp = OSGetTime(); - } - void stopRun() { - m_speedrunInfo.m_isRunStarted = false; - m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; - } - void incTotalLoadTime(OSTime time) { m_speedrunInfo.m_totalLoadTime += time; } private: void drawAudioMenu(); @@ -55,13 +77,7 @@ namespace dusk { int m_inputOverlayCorner = 3; std::string m_controllerName; - struct { - bool m_showTimerWindow = false; - bool m_isRunStarted = false; - OSTime m_startTimestamp = 0; - OSTime m_endTimestamp = 0; - OSTime m_totalLoadTime = 0; - } m_speedrunInfo; + bool m_showTimerWindow = false; }; } diff --git a/src/f_op/f_op_overlap_req.cpp b/src/f_op/f_op_overlap_req.cpp index 28d0a743cc..4d410fb2c2 100644 --- a/src/f_op/f_op_overlap_req.cpp +++ b/src/f_op/f_op_overlap_req.cpp @@ -7,6 +7,8 @@ #include "f_op/f_op_overlap_req.h" #include "f_pc/f_pc_manager.h" +#include "dusk/imgui/ImGuiMenuGame.hpp" + void fopOvlpReq_SetPeektime(overlap_request_class*, u16); static int fopOvlpReq_phase_Done(overlap_request_class* i_overlapReq) { @@ -16,6 +18,16 @@ static int fopOvlpReq_phase_Done(overlap_request_class* i_overlapReq) { i_overlapReq->peektime = 0; i_overlapReq->field_0x8 = 0; i_overlapReq->field_0xc = 0; + + #if TARGET_PC + if (dusk::getSettings().game.speedrunMode) { + if (dusk::m_speedrunInfo.m_isRunStarted) { + dusk::m_speedrunInfo.m_isPauseIGT = false; + dusk::m_speedrunInfo.m_totalLoadTime += OSGetTime() - dusk::m_speedrunInfo.m_loadStartTimestamp; + dusk::m_speedrunInfo.m_loadStartTimestamp = OSGetTime(); + } + } + #endif return cPhs_NEXT_e; } @@ -81,6 +93,14 @@ static int fopOvlpReq_phase_Create(overlap_request_class* i_overlapReq) { fpcLy_SetCurrentLayer(i_overlapReq->layer); i_overlapReq->request_id = fpcM_Create(i_overlapReq->procname, NULL, NULL); + +#if TARGET_PC + if (dusk::m_speedrunInfo.m_isRunStarted) { + dusk::m_speedrunInfo.m_isPauseIGT = true; + dusk::m_speedrunInfo.m_loadStartTimestamp = OSGetTime(); + } +#endif + return cPhs_NEXT_e; } diff --git a/src/f_op/f_op_scene_req.cpp b/src/f_op/f_op_scene_req.cpp index e4ce889266..abcb6ce848 100644 --- a/src/f_op/f_op_scene_req.cpp +++ b/src/f_op/f_op_scene_req.cpp @@ -40,10 +40,6 @@ static cPhs_Step fopScnRq_phase_IsDoneOverlap(scene_request_class* i_sceneReq) { static BOOL l_fopScnRq_IsUsingOfOverlap; -#if TARGET_PC -static OSTime l_fopScnRq_StartTime = 0; -#endif - static cPhs_Step fopScnRq_phase_Done(scene_request_class* i_sceneReq) { if (i_sceneReq->create_request.parameters != 1) { @@ -53,14 +49,6 @@ static cPhs_Step fopScnRq_phase_Done(scene_request_class* i_sceneReq) { } l_fopScnRq_IsUsingOfOverlap = FALSE; - #if TARGET_PC - if (dusk::getSettings().game.speedrunMode) { - if (dusk::g_imguiConsole.isSpeedrunStart()) { - dusk::g_imguiConsole.incSpeedrunTotalLoadTime(OSGetTime() - l_fopScnRq_StartTime); - } - } - #endif - return cPhs_NEXT_e; } @@ -101,10 +89,6 @@ static scene_request_class* fopScnRq_FadeRequest(s16 i_procname, u16 i_peektime) req = fopOvlpM_Request(i_procname, i_peektime); if (req != NULL) { l_fopScnRq_IsUsingOfOverlap = TRUE; - - #if TARGET_PC - l_fopScnRq_StartTime = OSGetTime(); - #endif } } From 23a91a37be19b596e9bbd03e513309b84a62c67a Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 26 Apr 2026 21:01:32 -0600 Subject: [PATCH 41/70] Kinda crappy initial controller support --- extern/aurora | 2 +- src/dusk/imgui/ImGuiConsole.cpp | 33 +++++++++- src/dusk/imgui/ImGuiConsole.hpp | 1 + src/dusk/imgui/ImGuiMenuGame.cpp | 100 ++++++++++++++++++++++++++----- src/dusk/imgui/ImGuiMenuGame.hpp | 3 + 5 files changed, 120 insertions(+), 19 deletions(-) diff --git a/extern/aurora b/extern/aurora index a6a3d3a65a..7784b6fc95 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit a6a3d3a65ae0de6de8b60629cf47fd0f446c21cb +Subproject commit 7784b6fc95568551499c87bd093b78d86e194eba diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 6bca83482e..a94c7d1f5b 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -56,6 +56,21 @@ ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) { } return nullptr; } + +void FocusLastMenuBarItem() { + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiWindow* window = ImGui::GetCurrentWindow(); + const ImGuiID itemId = g.LastItemData.ID; + if (window == nullptr || itemId == 0) { + return; + } + + ImGui::FocusWindow(window); + ImGui::SetNavID(itemId, ImGuiNavLayer_Menu, g.CurrentFocusScopeId, + ImGui::WindowRectAbsToRel(window, g.LastItemData.NavRect)); + ImGui::SetNavCursorVisibleAfterMove(); + g.NavHighlightItemUnderNav = true; +} } // namespace namespace dusk { @@ -329,7 +344,17 @@ namespace dusk { } m_isHidden = !getSettings().backend.duskMenuOpen; - bool showMenu = !dusk::IsGameLaunched || !CheckMenuViewToggle(ImGuiKey_F1, m_isHidden); + if (dusk::IsGameLaunched) { + if (ImGui::IsKeyPressed(ImGuiKey_F1)) { + m_isHidden = !m_isHidden; + } + if (ImGui::IsKeyPressed(ImGuiKey_GamepadBack)) { + m_isHidden = !m_isHidden; + m_focusMenuBar = !m_isHidden; + } + } + + bool showMenu = !dusk::IsGameLaunched || !m_isHidden; if (dusk::IsGameLaunched) { const bool menuOpen = !m_isHidden; if (getSettings().backend.duskMenuOpen != menuOpen) { @@ -343,6 +368,10 @@ namespace dusk { ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); if (showMenu && ImGui::BeginMainMenuBar()) { m_menuGame.draw(); + if (m_focusMenuBar) { + FocusLastMenuBarItem(); + m_focusMenuBar = false; + } m_menuTools.draw(); const auto fpsLabel = @@ -367,7 +396,7 @@ namespace dusk { if (dusk::IsGameLaunched && !m_isLaunchInitialized) { m_toasts.emplace_back(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ? "Tap to toggle menu"s : - "Press F1 to toggle menu"s, + "Press F1 or Minus/Back to toggle menu"s, 2.5f); m_isLaunchInitialized = true; if (getSettings().game.liveSplitEnabled) { diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index a6c8b48fc7..fc7b4c51bc 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -41,6 +41,7 @@ private: float mouseHideTimer = 0.0f; bool m_isHidden = true; + bool m_focusMenuBar = false; bool m_isLaunchInitialized = false; bool m_touchTapActive = false; bool m_touchTapMoved = false; diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index d0da643b0c..251fabd227 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -23,6 +23,15 @@ namespace { constexpr int kInternalResolutionScaleMax = 12; + +bool is_controller_neutral(int port) { + if (port < 0) { + return true; + } + + return PADGetNativeButtonPressed(port) == -1 && + PADGetNativeAxisPulled(port).nativeAxis == -1; +} } // namespace namespace aurora::gx { @@ -196,7 +205,7 @@ 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 with the C-Stick"); @@ -642,39 +651,90 @@ namespace dusk { void ImGuiMenuGame::windowControllerConfig() { if (!m_showControllerConfig) { + if (m_controllerConfig.m_isReading || + m_controllerConfig.m_suppressRemapActivationUntilRelease) + { + m_controllerConfig.m_isReading = false; + m_controllerConfig.m_pendingButtonMapping = nullptr; + m_controllerConfig.m_pendingAxisMapping = nullptr; + m_controllerConfig.m_pendingPort = -1; + m_controllerConfig.m_waitForInputRelease = false; + m_controllerConfig.m_suppressRemapActivationUntilRelease = false; + m_controllerConfig.m_suppressRemapActivationPort = -1; + PADBlockInput(false); + } return; } + bool suppressRemapActivationThisFrame = m_controllerConfig.m_suppressRemapActivationUntilRelease; + if (m_controllerConfig.m_suppressRemapActivationUntilRelease && + is_controller_neutral(m_controllerConfig.m_suppressRemapActivationPort)) + { + m_controllerConfig.m_suppressRemapActivationUntilRelease = false; + m_controllerConfig.m_suppressRemapActivationPort = -1; + PADBlockInput(false); + } + + if ((m_controllerConfig.m_pendingButtonMapping != nullptr || + m_controllerConfig.m_pendingAxisMapping != nullptr) && + m_controllerConfig.m_waitForInputRelease) + { + m_controllerConfig.m_waitForInputRelease = + !is_controller_neutral(m_controllerConfig.m_pendingPort); + } + // if pending for a button mapping, check to set new input - if (m_controllerConfig.m_pendingButtonMapping != nullptr) { + if (m_controllerConfig.m_pendingButtonMapping != nullptr && + !m_controllerConfig.m_waitForInputRelease) + { s32 nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort); if (nativeButton != -1) { + const int suppressPort = m_controllerConfig.m_pendingPort; m_controllerConfig.m_pendingButtonMapping->nativeButton = nativeButton; m_controllerConfig.m_pendingButtonMapping = nullptr; m_controllerConfig.m_pendingPort = -1; - PADBlockInput(false); + m_controllerConfig.m_isReading = false; + m_controllerConfig.m_waitForInputRelease = false; + m_controllerConfig.m_suppressRemapActivationUntilRelease = true; + m_controllerConfig.m_suppressRemapActivationPort = suppressPort; + suppressRemapActivationThisFrame = true; + PADBlockInput(true); PADSerializeMappings(); } } // if pending for an axis mapping, check to set new input - if (m_controllerConfig.m_pendingAxisMapping != nullptr) { + if (m_controllerConfig.m_pendingAxisMapping != nullptr && + !m_controllerConfig.m_waitForInputRelease) + { auto nativeAxis = PADGetNativeAxisPulled(m_controllerConfig.m_pendingPort); if (nativeAxis.nativeAxis != -1) { + const int suppressPort = m_controllerConfig.m_pendingPort; m_controllerConfig.m_pendingAxisMapping->nativeAxis = nativeAxis; m_controllerConfig.m_pendingAxisMapping->nativeButton = -1; m_controllerConfig.m_pendingAxisMapping = nullptr; m_controllerConfig.m_pendingPort = -1; - PADBlockInput(false); + m_controllerConfig.m_isReading = false; + m_controllerConfig.m_waitForInputRelease = false; + m_controllerConfig.m_suppressRemapActivationUntilRelease = true; + m_controllerConfig.m_suppressRemapActivationPort = suppressPort; + suppressRemapActivationThisFrame = true; + PADBlockInput(true); PADSerializeMappings(); } else { auto nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort); if (nativeButton != -1) { + const int suppressPort = m_controllerConfig.m_pendingPort; m_controllerConfig.m_pendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE}; m_controllerConfig.m_pendingAxisMapping->nativeButton = nativeButton; m_controllerConfig.m_pendingAxisMapping = nullptr; m_controllerConfig.m_pendingPort = -1; - PADBlockInput(false); + m_controllerConfig.m_isReading = false; + m_controllerConfig.m_waitForInputRelease = false; + m_controllerConfig.m_suppressRemapActivationUntilRelease = true; + m_controllerConfig.m_suppressRemapActivationPort = suppressPort; + suppressRemapActivationThisFrame = true; + PADBlockInput(true); PADSerializeMappings(); } } @@ -710,6 +770,10 @@ namespace dusk { m_controllerConfig.m_pendingButtonMapping = nullptr; m_controllerConfig.m_pendingAxisMapping = nullptr; m_controllerConfig.m_pendingPort = -1; + m_controllerConfig.m_waitForInputRelease = false; + m_controllerConfig.m_isReading = false; + m_controllerConfig.m_suppressRemapActivationUntilRelease = false; + m_controllerConfig.m_suppressRemapActivationPort = -1; PADBlockInput(false); } @@ -786,7 +850,7 @@ namespace dusk { std::string dispName; if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingButtonMapping == &btnMappingList[i]) { - dispName = fmt::format("Press a Key...##{}", btnName); + dispName = fmt::format("{}##{}", m_controllerConfig.m_waitForInputRelease ? "Release..." : "Press a Key...", btnName); } else { const char* nativeName = GetNameForGamepadButton(gamepad, btnMappingList[i].nativeButton); if (nativeName == nullptr) { @@ -797,10 +861,11 @@ namespace dusk { bool pressed = ImGui::Button(dispName.c_str(), btnSize); - if (pressed) { + if (pressed && !m_controllerConfig.m_isReading && !suppressRemapActivationThisFrame) { m_controllerConfig.m_isReading = true; m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; m_controllerConfig.m_pendingButtonMapping = &btnMappingList[i]; + m_controllerConfig.m_waitForInputRelease = true; PADBlockInput(true); } } @@ -830,17 +895,18 @@ namespace dusk { std::string dispName; if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[trigger]) { - dispName = fmt::format("Press a Key...##{}", axisName); + dispName = fmt::format("{}##{}", m_controllerConfig.m_waitForInputRelease ? "Release..." : "Press a Key...", axisName); } else { dispName = fmt::format("{0}##-{1}", PADGetNativeAxisName(axisMappingList[trigger].nativeAxis), trigger); } bool pressed = ImGui::Button(dispName.c_str(), btnSize); - if (pressed) { + if (pressed && !m_controllerConfig.m_isReading && !suppressRemapActivationThisFrame) { m_controllerConfig.m_isReading = true; m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; m_controllerConfig.m_pendingAxisMapping = &axisMappingList[trigger]; + m_controllerConfig.m_waitForInputRelease = true; PADBlockInput(true); } } @@ -897,7 +963,7 @@ namespace dusk { std::string dispName; if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) { - dispName = fmt::format("Press a Key...##{}", label); + dispName = fmt::format("{}##{}", m_controllerConfig.m_waitForInputRelease ? "Release..." : "Press a Key...", label); } else { if (axisMappingList[axis].nativeAxis.nativeAxis != -1) { const char* signStr; @@ -916,10 +982,11 @@ namespace dusk { } bool pressed = ImGui::Button(dispName.c_str(), btnSize); - if (pressed) { + if (pressed && !m_controllerConfig.m_isReading && !suppressRemapActivationThisFrame) { m_controllerConfig.m_isReading = true; m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis]; + m_controllerConfig.m_waitForInputRelease = true; PADBlockInput(true); } } @@ -960,7 +1027,7 @@ namespace dusk { std::string dispName; if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) { - dispName = fmt::format("Press a Key...##sub{}", label); + dispName = fmt::format("{}##sub{}", m_controllerConfig.m_waitForInputRelease ? "Release..." : "Press a Key...", label); } else { if (axisMappingList[axis].nativeAxis.nativeAxis != -1) { const char* signStr; @@ -979,10 +1046,11 @@ namespace dusk { } bool pressed = ImGui::Button(fmt::format("{0}##sub{1}", dispName, label).c_str(), btnSize); - if (pressed) { + if (pressed && !m_controllerConfig.m_isReading && !suppressRemapActivationThisFrame) { m_controllerConfig.m_isReading = true; m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis]; + m_controllerConfig.m_waitForInputRelease = true; PADBlockInput(true); } } @@ -1013,7 +1081,7 @@ namespace dusk { PADSerializeMappings(); } } - + if (PADSupportsRumbleIntensity(m_controllerConfig.m_selectedPort)) { ImGuiBeginGroupPanel("Rumble Intensity", ImVec2(150 * scale, -1)); u16 low; @@ -1032,7 +1100,7 @@ namespace dusk { if (ImGui::Button(fmt::format("{0}...##rumbleTest", m_controllerConfig.m_isRumbling ? "Stop": "Test").c_str(), {-1, 0})) { PADControlMotor(m_controllerConfig.m_selectedPort, !m_controllerConfig.m_isRumbling ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD); m_controllerConfig.m_isRumbling ^= 1; - } + } ImGuiEndGroupPanel(); } ImGuiEndGroupPanel(); diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index e54541a010..c902c92359 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -68,6 +68,9 @@ namespace dusk { PADButtonMapping* m_pendingButtonMapping = nullptr; PADAxisMapping* m_pendingAxisMapping = nullptr; int m_pendingPort = -1; + bool m_waitForInputRelease = false; + bool m_suppressRemapActivationUntilRelease = false; + int m_suppressRemapActivationPort = -1; bool m_isRumbling = false; } m_controllerConfig; From 5f33489465bc3d2da0de6daa7b7eabe6878d674f Mon Sep 17 00:00:00 2001 From: Irastris Date: Mon, 27 Apr 2026 01:04:45 -0400 Subject: [PATCH 42/70] Register interp callback for d_a_obj_lv8Lift --- include/d/actor/d_a_obj_lv8Lift.h | 3 +++ src/d/actor/d_a_obj_lv8Lift.cpp | 41 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/include/d/actor/d_a_obj_lv8Lift.h b/include/d/actor/d_a_obj_lv8Lift.h index c5a8ae9f27..c327f4cf3c 100644 --- a/include/d/actor/d_a_obj_lv8Lift.h +++ b/include/d/actor/d_a_obj_lv8Lift.h @@ -58,6 +58,9 @@ public: void setNextPoint(); int Draw(); int Delete(); +#if TARGET_PC + friend void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork); +#endif u8 getPthID() { return fopAcM_GetParamBit(this, 0, 8); } u8 getMoveSpeed() { return fopAcM_GetParamBit(this, 8, 4); } diff --git a/src/d/actor/d_a_obj_lv8Lift.cpp b/src/d/actor/d_a_obj_lv8Lift.cpp index 4f475a626c..8fa9422325 100644 --- a/src/d/actor/d_a_obj_lv8Lift.cpp +++ b/src/d/actor/d_a_obj_lv8Lift.cpp @@ -10,6 +10,10 @@ #include "d/d_path.h" #include "d/d_bg_w.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + daL8Lift_HIO_c::daL8Lift_HIO_c() { mStopDisappearTime = 30; mStartMoveTime = 60; @@ -380,7 +384,44 @@ void daL8Lift_c::setNextPoint() { mCurrentPoint = next_point; } +#if TARGET_PC +void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork) { + daL8Lift_c* lift = static_cast(pUserWork); + if (lift == NULL || lift->mpModel == NULL) { + return; + } + + g_env_light.settingTevStruct(0x10, &lift->current.pos, &lift->tevStr); + g_env_light.setLightTevColorType_MAJI(lift->mpModel, &lift->tevStr); + + J3DModelData* modelData = lift->mpModel->getModelData(); + J3DMaterial* materialp = modelData->getMaterialNodePointer(0); + + if (materialp->getTexGenBlock()->getTexMtx(1) != NULL) { + J3DTexMtxInfo* mtx_info = &materialp->getTexGenBlock()->getTexMtx(1)->getTexMtxInfo(); + if (mtx_info != NULL) { + Mtx m; + C_MTXLightOrtho(m, 100.0f, -100.0f, -100.0f, 100.0f, 1.0f, 1.0f, 0.0f, 0.0f); + mDoMtx_stack_c::XrotS(0x4000); + mDoMtx_stack_c::transM(-lift->current.pos.x, -lift->current.pos.y, -lift->current.pos.z); + cMtx_concat(m, mDoMtx_stack_c::get(), mtx_info->mEffectMtx); + } + } + + lift->mBtk.entry(modelData); + + J3DGXColor* color = materialp->getTevKColor(1); + color->r = l_HIO.mColorR; + color->g = l_HIO.mColorG; + color->b = l_HIO.mColorB; +} +#endif + int daL8Lift_c::Draw() { +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&daL8Lift_interp_callback, this); +#endif + g_env_light.settingTevStruct(16, ¤t.pos, &tevStr); g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr); J3DModelData* modelData = mpModel->getModelData(); From 9b259143be4e50316d5d415d1566e180ce3f3392 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Mon, 27 Apr 2026 12:05:26 -0400 Subject: [PATCH 43/70] Better Freecam code + Allow Freecam in Target mode --- src/d/d_camera.cpp | 53 +++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index c59cf1c525..471c93bb19 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -794,16 +794,15 @@ void dCamera_c::updatePad() { if (mTriggerLeftLast > mCamSetup.ManualEndVal()) { if (mLockLActive == 0) { + #if TARGET_PC + mCamParam.mManualMode = 0; + #endif mLockLJustActivated = 1; } else { mLockLJustActivated = 0; } mLockLActive = 1; - - #if TARGET_PC - mCamParam.mManualMode = 0; - #endif } else { mLockLJustActivated = 0; mLockLActive = 0; @@ -1178,12 +1177,6 @@ bool dCamera_c::Run() { } else { sp0F = (this->*engine_tbl[mCamParam.Algorythmn(mCamStyle)])(mCamStyle); - #if TARGET_PC - if (mCamParam.Algorythmn(mCamStyle) != 1) { - mCamParam.mManualMode = 0; - } - #endif - field_0x170++; field_0x160++; mCurCamStyleTimer++; @@ -3527,6 +3520,12 @@ void dCamera_c::checkGroundInfo() { } bool dCamera_c::chaseCamera(s32 param_0) { +#if TARGET_PC + if (freeCamera()) { + return 1; + } +#endif + static f32 JumpCushion = 0.9f; f32 charge_latitude = mCamSetup.ChargeLatitude(); int charge_timer = mCamSetup.ChargeTimer(); @@ -4631,10 +4630,6 @@ bool dCamera_c::chaseCamera(s32 param_0) { sp110 = mViewCache.mDirection.R(); mViewCache.mDirection.R(mViewCache.mDirection.R() + (fVar55 - mViewCache.mDirection.R()) * chase->field_0x74); - #if TARGET_PC - freeCamera(); - #endif - chase->field_0x64 = mViewCache.mCenter + mViewCache.mDirection.Xyz(); mViewCache.mEye = chase->field_0x64; @@ -7482,6 +7477,9 @@ bool dCamera_c::freeCamera() { return false; } + mCamParam.freeXAngle = mViewCache.mDirection.mAzimuth.Degree(); + mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree(); + cXyz camMovement = {mPadInfo.mCStick.mLastPosX, mPadInfo.mCStick.mLastPosY, 0.0f}; f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY); @@ -7498,14 +7496,29 @@ bool dCamera_c::freeCamera() { mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity * 4.0f; } - if (mCamParam.mManualMode) { - mCamParam.freeYAngle = std::clamp(mCamParam.freeYAngle, -35.0f, 60.0f); - mViewCache.mDirection.mAzimuth = cSAngle(mCamParam.freeXAngle); - mViewCache.mDirection.mInclination = cSAngle(mCamParam.freeYAngle); - mViewCache.mDirection.mRadius = std::clamp((mCamParam.freeYAngle + 35.0f) * 10.0f, 300.0f, 10000.0f); + if (!mCamParam.mManualMode) { + return false; } - return mCamParam.mManualMode; + f32 minYAngle = -10.0f; + f32 maxAngle = 50.0f; + + mCamParam.freeYAngle = std::clamp(mCamParam.freeYAngle, minYAngle, maxAngle); + mViewCache.mDirection.mAzimuth = cSAngle(mCamParam.freeXAngle); + mViewCache.mDirection.mInclination = cSAngle(mCamParam.freeYAngle); + f32 currentLerp = (mCamParam.freeYAngle - minYAngle) / (maxAngle - minYAngle); + mViewCache.mDirection.mRadius = std::lerp(200.0f, 1000.0f, currentLerp); + + cXyz finalCenter = mpPlayerActor->current.pos; + finalCenter.y += mIsWolf ? 90.0f : 100.0f; + mViewCache.mCenter = finalCenter; + + cXyz finalEye = finalCenter + mViewCache.mDirection.Xyz(); + mViewCache.mEye = finalEye; + + mViewCache.mFovy = 60.0f; + + return true; } #endif From d662db69f0a565e1f296f76eba42e0d160bd90e0 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Mon, 27 Apr 2026 17:04:42 -0400 Subject: [PATCH 44/70] Fix bugs with Disable Main HUD --- src/d/d_meter2.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/d/d_meter2.cpp b/src/d/d_meter2.cpp index 5bd321e7a8..fa6774d0a5 100644 --- a/src/d/d_meter2.cpp +++ b/src/d/d_meter2.cpp @@ -316,6 +316,12 @@ int dMeter2_c::_execute() { } int dMeter2_c::_draw() { + #if TARGET_PC + if (dusk::getSettings().game.disableMainHUD) { + return 1; + } + #endif + if (mpMap != NULL) { mpMap->_draw(); } @@ -424,12 +430,6 @@ void dMeter2_c::setLifeZero() { void dMeter2_c::checkStatus() { mStatus = 0; - #if TARGET_PC - if (dusk::getSettings().game.disableMainHUD) { - mStatus |= 0xF0000000; - } - #endif - field_0x12c = field_0x128; field_0x128 = daPy_py_c::checkNowWolf(); From 0c1372f98694b85e654141c4cb80c00ef6491ba6 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 27 Apr 2026 21:55:57 -0600 Subject: [PATCH 45/70] Revert "Kinda crappy initial controller support" This reverts commit 23a91a37be19b596e9bbd03e513309b84a62c67a. --- extern/aurora | 2 +- src/dusk/imgui/ImGuiConsole.cpp | 33 +--------- src/dusk/imgui/ImGuiConsole.hpp | 1 - src/dusk/imgui/ImGuiMenuGame.cpp | 100 +++++-------------------------- src/dusk/imgui/ImGuiMenuGame.hpp | 3 - 5 files changed, 19 insertions(+), 120 deletions(-) diff --git a/extern/aurora b/extern/aurora index 7784b6fc95..a6a3d3a65a 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 7784b6fc95568551499c87bd093b78d86e194eba +Subproject commit a6a3d3a65ae0de6de8b60629cf47fd0f446c21cb diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index a94c7d1f5b..6bca83482e 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -56,21 +56,6 @@ ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) { } return nullptr; } - -void FocusLastMenuBarItem() { - ImGuiContext& g = *ImGui::GetCurrentContext(); - ImGuiWindow* window = ImGui::GetCurrentWindow(); - const ImGuiID itemId = g.LastItemData.ID; - if (window == nullptr || itemId == 0) { - return; - } - - ImGui::FocusWindow(window); - ImGui::SetNavID(itemId, ImGuiNavLayer_Menu, g.CurrentFocusScopeId, - ImGui::WindowRectAbsToRel(window, g.LastItemData.NavRect)); - ImGui::SetNavCursorVisibleAfterMove(); - g.NavHighlightItemUnderNav = true; -} } // namespace namespace dusk { @@ -344,17 +329,7 @@ namespace dusk { } m_isHidden = !getSettings().backend.duskMenuOpen; - if (dusk::IsGameLaunched) { - if (ImGui::IsKeyPressed(ImGuiKey_F1)) { - m_isHidden = !m_isHidden; - } - if (ImGui::IsKeyPressed(ImGuiKey_GamepadBack)) { - m_isHidden = !m_isHidden; - m_focusMenuBar = !m_isHidden; - } - } - - bool showMenu = !dusk::IsGameLaunched || !m_isHidden; + bool showMenu = !dusk::IsGameLaunched || !CheckMenuViewToggle(ImGuiKey_F1, m_isHidden); if (dusk::IsGameLaunched) { const bool menuOpen = !m_isHidden; if (getSettings().backend.duskMenuOpen != menuOpen) { @@ -368,10 +343,6 @@ namespace dusk { ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); if (showMenu && ImGui::BeginMainMenuBar()) { m_menuGame.draw(); - if (m_focusMenuBar) { - FocusLastMenuBarItem(); - m_focusMenuBar = false; - } m_menuTools.draw(); const auto fpsLabel = @@ -396,7 +367,7 @@ namespace dusk { if (dusk::IsGameLaunched && !m_isLaunchInitialized) { m_toasts.emplace_back(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ? "Tap to toggle menu"s : - "Press F1 or Minus/Back to toggle menu"s, + "Press F1 to toggle menu"s, 2.5f); m_isLaunchInitialized = true; if (getSettings().game.liveSplitEnabled) { diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index fc7b4c51bc..a6c8b48fc7 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -41,7 +41,6 @@ private: float mouseHideTimer = 0.0f; bool m_isHidden = true; - bool m_focusMenuBar = false; bool m_isLaunchInitialized = false; bool m_touchTapActive = false; bool m_touchTapMoved = false; diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 251fabd227..d0da643b0c 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -23,15 +23,6 @@ namespace { constexpr int kInternalResolutionScaleMax = 12; - -bool is_controller_neutral(int port) { - if (port < 0) { - return true; - } - - return PADGetNativeButtonPressed(port) == -1 && - PADGetNativeAxisPulled(port).nativeAxis == -1; -} } // namespace namespace aurora::gx { @@ -205,7 +196,7 @@ 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 with the C-Stick"); @@ -651,90 +642,39 @@ namespace dusk { void ImGuiMenuGame::windowControllerConfig() { if (!m_showControllerConfig) { - if (m_controllerConfig.m_isReading || - m_controllerConfig.m_suppressRemapActivationUntilRelease) - { - m_controllerConfig.m_isReading = false; - m_controllerConfig.m_pendingButtonMapping = nullptr; - m_controllerConfig.m_pendingAxisMapping = nullptr; - m_controllerConfig.m_pendingPort = -1; - m_controllerConfig.m_waitForInputRelease = false; - m_controllerConfig.m_suppressRemapActivationUntilRelease = false; - m_controllerConfig.m_suppressRemapActivationPort = -1; - PADBlockInput(false); - } return; } - bool suppressRemapActivationThisFrame = m_controllerConfig.m_suppressRemapActivationUntilRelease; - if (m_controllerConfig.m_suppressRemapActivationUntilRelease && - is_controller_neutral(m_controllerConfig.m_suppressRemapActivationPort)) - { - m_controllerConfig.m_suppressRemapActivationUntilRelease = false; - m_controllerConfig.m_suppressRemapActivationPort = -1; - PADBlockInput(false); - } - - if ((m_controllerConfig.m_pendingButtonMapping != nullptr || - m_controllerConfig.m_pendingAxisMapping != nullptr) && - m_controllerConfig.m_waitForInputRelease) - { - m_controllerConfig.m_waitForInputRelease = - !is_controller_neutral(m_controllerConfig.m_pendingPort); - } - // if pending for a button mapping, check to set new input - if (m_controllerConfig.m_pendingButtonMapping != nullptr && - !m_controllerConfig.m_waitForInputRelease) - { + if (m_controllerConfig.m_pendingButtonMapping != nullptr) { s32 nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort); if (nativeButton != -1) { - const int suppressPort = m_controllerConfig.m_pendingPort; m_controllerConfig.m_pendingButtonMapping->nativeButton = nativeButton; m_controllerConfig.m_pendingButtonMapping = nullptr; m_controllerConfig.m_pendingPort = -1; - m_controllerConfig.m_isReading = false; - m_controllerConfig.m_waitForInputRelease = false; - m_controllerConfig.m_suppressRemapActivationUntilRelease = true; - m_controllerConfig.m_suppressRemapActivationPort = suppressPort; - suppressRemapActivationThisFrame = true; - PADBlockInput(true); + PADBlockInput(false); PADSerializeMappings(); } } // if pending for an axis mapping, check to set new input - if (m_controllerConfig.m_pendingAxisMapping != nullptr && - !m_controllerConfig.m_waitForInputRelease) - { + if (m_controllerConfig.m_pendingAxisMapping != nullptr) { auto nativeAxis = PADGetNativeAxisPulled(m_controllerConfig.m_pendingPort); if (nativeAxis.nativeAxis != -1) { - const int suppressPort = m_controllerConfig.m_pendingPort; m_controllerConfig.m_pendingAxisMapping->nativeAxis = nativeAxis; m_controllerConfig.m_pendingAxisMapping->nativeButton = -1; m_controllerConfig.m_pendingAxisMapping = nullptr; m_controllerConfig.m_pendingPort = -1; - m_controllerConfig.m_isReading = false; - m_controllerConfig.m_waitForInputRelease = false; - m_controllerConfig.m_suppressRemapActivationUntilRelease = true; - m_controllerConfig.m_suppressRemapActivationPort = suppressPort; - suppressRemapActivationThisFrame = true; - PADBlockInput(true); + PADBlockInput(false); PADSerializeMappings(); } else { auto nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort); if (nativeButton != -1) { - const int suppressPort = m_controllerConfig.m_pendingPort; m_controllerConfig.m_pendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE}; m_controllerConfig.m_pendingAxisMapping->nativeButton = nativeButton; m_controllerConfig.m_pendingAxisMapping = nullptr; m_controllerConfig.m_pendingPort = -1; - m_controllerConfig.m_isReading = false; - m_controllerConfig.m_waitForInputRelease = false; - m_controllerConfig.m_suppressRemapActivationUntilRelease = true; - m_controllerConfig.m_suppressRemapActivationPort = suppressPort; - suppressRemapActivationThisFrame = true; - PADBlockInput(true); + PADBlockInput(false); PADSerializeMappings(); } } @@ -770,10 +710,6 @@ namespace dusk { m_controllerConfig.m_pendingButtonMapping = nullptr; m_controllerConfig.m_pendingAxisMapping = nullptr; m_controllerConfig.m_pendingPort = -1; - m_controllerConfig.m_waitForInputRelease = false; - m_controllerConfig.m_isReading = false; - m_controllerConfig.m_suppressRemapActivationUntilRelease = false; - m_controllerConfig.m_suppressRemapActivationPort = -1; PADBlockInput(false); } @@ -850,7 +786,7 @@ namespace dusk { std::string dispName; if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingButtonMapping == &btnMappingList[i]) { - dispName = fmt::format("{}##{}", m_controllerConfig.m_waitForInputRelease ? "Release..." : "Press a Key...", btnName); + dispName = fmt::format("Press a Key...##{}", btnName); } else { const char* nativeName = GetNameForGamepadButton(gamepad, btnMappingList[i].nativeButton); if (nativeName == nullptr) { @@ -861,11 +797,10 @@ namespace dusk { bool pressed = ImGui::Button(dispName.c_str(), btnSize); - if (pressed && !m_controllerConfig.m_isReading && !suppressRemapActivationThisFrame) { + if (pressed) { m_controllerConfig.m_isReading = true; m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; m_controllerConfig.m_pendingButtonMapping = &btnMappingList[i]; - m_controllerConfig.m_waitForInputRelease = true; PADBlockInput(true); } } @@ -895,18 +830,17 @@ namespace dusk { std::string dispName; if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[trigger]) { - dispName = fmt::format("{}##{}", m_controllerConfig.m_waitForInputRelease ? "Release..." : "Press a Key...", axisName); + dispName = fmt::format("Press a Key...##{}", axisName); } else { dispName = fmt::format("{0}##-{1}", PADGetNativeAxisName(axisMappingList[trigger].nativeAxis), trigger); } bool pressed = ImGui::Button(dispName.c_str(), btnSize); - if (pressed && !m_controllerConfig.m_isReading && !suppressRemapActivationThisFrame) { + if (pressed) { m_controllerConfig.m_isReading = true; m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; m_controllerConfig.m_pendingAxisMapping = &axisMappingList[trigger]; - m_controllerConfig.m_waitForInputRelease = true; PADBlockInput(true); } } @@ -963,7 +897,7 @@ namespace dusk { std::string dispName; if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) { - dispName = fmt::format("{}##{}", m_controllerConfig.m_waitForInputRelease ? "Release..." : "Press a Key...", label); + dispName = fmt::format("Press a Key...##{}", label); } else { if (axisMappingList[axis].nativeAxis.nativeAxis != -1) { const char* signStr; @@ -982,11 +916,10 @@ namespace dusk { } bool pressed = ImGui::Button(dispName.c_str(), btnSize); - if (pressed && !m_controllerConfig.m_isReading && !suppressRemapActivationThisFrame) { + if (pressed) { m_controllerConfig.m_isReading = true; m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis]; - m_controllerConfig.m_waitForInputRelease = true; PADBlockInput(true); } } @@ -1027,7 +960,7 @@ namespace dusk { std::string dispName; if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) { - dispName = fmt::format("{}##sub{}", m_controllerConfig.m_waitForInputRelease ? "Release..." : "Press a Key...", label); + dispName = fmt::format("Press a Key...##sub{}", label); } else { if (axisMappingList[axis].nativeAxis.nativeAxis != -1) { const char* signStr; @@ -1046,11 +979,10 @@ namespace dusk { } bool pressed = ImGui::Button(fmt::format("{0}##sub{1}", dispName, label).c_str(), btnSize); - if (pressed && !m_controllerConfig.m_isReading && !suppressRemapActivationThisFrame) { + if (pressed) { m_controllerConfig.m_isReading = true; m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort; m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis]; - m_controllerConfig.m_waitForInputRelease = true; PADBlockInput(true); } } @@ -1081,7 +1013,7 @@ namespace dusk { PADSerializeMappings(); } } - + if (PADSupportsRumbleIntensity(m_controllerConfig.m_selectedPort)) { ImGuiBeginGroupPanel("Rumble Intensity", ImVec2(150 * scale, -1)); u16 low; @@ -1100,7 +1032,7 @@ namespace dusk { if (ImGui::Button(fmt::format("{0}...##rumbleTest", m_controllerConfig.m_isRumbling ? "Stop": "Test").c_str(), {-1, 0})) { PADControlMotor(m_controllerConfig.m_selectedPort, !m_controllerConfig.m_isRumbling ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD); m_controllerConfig.m_isRumbling ^= 1; - } + } ImGuiEndGroupPanel(); } ImGuiEndGroupPanel(); diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index c902c92359..e54541a010 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -68,9 +68,6 @@ namespace dusk { PADButtonMapping* m_pendingButtonMapping = nullptr; PADAxisMapping* m_pendingAxisMapping = nullptr; int m_pendingPort = -1; - bool m_waitForInputRelease = false; - bool m_suppressRemapActivationUntilRelease = false; - int m_suppressRemapActivationPort = -1; bool m_isRumbling = false; } m_controllerConfig; From 30b7087f30f67de26f94868508e163977eea7ce0 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Mon, 27 Apr 2026 22:04:42 -0700 Subject: [PATCH 46/70] add option for only fishing once for sera's cat --- extern/aurora | 2 +- include/dusk/settings.h | 1 + src/d/actor/d_a_npc_ne.cpp | 5 ++--- src/dusk/imgui/ImGuiFirstRunPreset.cpp | 1 + src/dusk/imgui/ImGuiMenuGame.cpp | 5 +++++ src/dusk/settings.cpp | 2 ++ 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/extern/aurora b/extern/aurora index a6a3d3a65a..7784b6fc95 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit a6a3d3a65ae0de6de8b60629cf47fd0f446c21cb +Subproject commit 7784b6fc95568551499c87bd093b78d86e194eba diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 2acc69e43c..e426e301fa 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -76,6 +76,7 @@ struct UserSettings { ConfigVar fastClimbing; ConfigVar noMissClimbing; ConfigVar fastTears; + ConfigVar no2ndFishForCat; ConfigVar instantSaves; ConfigVar instantText; ConfigVar sunsSong; diff --git a/src/d/actor/d_a_npc_ne.cpp b/src/d/actor/d_a_npc_ne.cpp index 1324c56cf5..90e33adea5 100644 --- a/src/d/actor/d_a_npc_ne.cpp +++ b/src/d/actor/d_a_npc_ne.cpp @@ -956,7 +956,7 @@ static void npc_ne_tame(npc_ne_class* i_this) { i_this->mpMorf->setPlaySpeed(i_this->mAnmSpeed); /* dSv_event_flag_c::F_0470 - Fishing Pond - Reserved for fishing */ - if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) { + if (IF_DUSK(dusk::getSettings().game.no2ndFishForCat) || dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) { if (fpcEx_Search(s_fish_sub, _this) != NULL) { i_this->mAction = npc_ne_class::ACT_HOME; i_this->mMode = 10; @@ -2948,8 +2948,7 @@ static int daNpc_Ne_Execute(npc_ne_class* i_this) { if (i_this->mWantsFish && (i_this->mCounter & 0xf) == 0) { /* dSv_event_flag_c::F_0470 - Fishing Pond - Reserved for fishing */ - if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470]) - && i_this->mDistToTarget < 1500.0f) { + if ((IF_DUSK(dusk::getSettings().game.no2ndFishForCat) || dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) && i_this->mDistToTarget < 1500.0f) { if (fopAcM_SearchByName(fpcNm_MG_ROD_e) != NULL) { i_this->mNoFollow = false; } else { diff --git a/src/dusk/imgui/ImGuiFirstRunPreset.cpp b/src/dusk/imgui/ImGuiFirstRunPreset.cpp index 2a9bcc5c6e..b728b8cd24 100644 --- a/src/dusk/imgui/ImGuiFirstRunPreset.cpp +++ b/src/dusk/imgui/ImGuiFirstRunPreset.cpp @@ -30,6 +30,7 @@ static void ApplyPresetHD() { s.game.biggerWallets.setValue(true); s.game.invertCameraXAxis.setValue(true); s.game.freeCamera.setValue(true); + s.game.no2ndFishForCat.setValue(true); } static void ApplyPresetDusk() { diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index d0da643b0c..deced34b8e 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -267,6 +267,11 @@ namespace dusk { ImGui::SetTooltip("Link won't recoil when his sword hits walls."); } + config::ImGuiCheckbox("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Only need to fish once for Sera's cat to return."); + } + config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index ee6ba663ca..2eac6d5a19 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -36,6 +36,7 @@ UserSettings g_userSettings = { .fastClimbing {"game.fastClimbing", false}, .noMissClimbing {"game.noMissClimbing", false}, .fastTears {"game.fastTears", false}, + .no2ndFishForCat {"game.no2ndFishForCat", false}, .instantSaves {"game.instantSaves", false}, .instantText {"game.instantText", false}, .sunsSong {"game.sunsSong", false}, @@ -147,6 +148,7 @@ void registerSettings() { Register(g_userSettings.game.instantDeath); Register(g_userSettings.game.fastClimbing); Register(g_userSettings.game.fastTears); + Register(g_userSettings.game.no2ndFishForCat); Register(g_userSettings.game.instantSaves); Register(g_userSettings.game.instantText); Register(g_userSettings.game.sunsSong); From ea528ed9d9bdb79489e5a696f8a700dac9e0f153 Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 28 Apr 2026 03:41:04 -0700 Subject: [PATCH 47/70] clear single achievement and fix achievement tab moving around --- include/dusk/achievements.h | 1 + src/dusk/achievements.cpp | 12 ++++++++++++ src/dusk/imgui/ImGuiAchievements.cpp | 18 +++++++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h index cd4294b6f1..fb2da89c69 100644 --- a/include/dusk/achievements.h +++ b/include/dusk/achievements.h @@ -40,6 +40,7 @@ public: void save(); void tick(); void clearAll(); + void clearOne(const char* key); std::vector getAchievements() const; bool hasPendingUnlock() const { return !m_pendingUnlocks.empty(); } diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 071332a7c2..99d4648b98 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -426,6 +426,18 @@ void AchievementSystem::clearAll() { save(); } +void AchievementSystem::clearOne(const char* key) { + for (auto& e : m_entries) { + if (std::string(e.achievement.key) == key) { + e.achievement.progress = 0; + e.achievement.unlocked = false; + e.extra = {}; + break; + } + } + save(); +} + void AchievementSystem::processEntry(Entry& e) { if (e.achievement.unlocked) { return; diff --git a/src/dusk/imgui/ImGuiAchievements.cpp b/src/dusk/imgui/ImGuiAchievements.cpp index 4b844b8386..479e307e30 100644 --- a/src/dusk/imgui/ImGuiAchievements.cpp +++ b/src/dusk/imgui/ImGuiAchievements.cpp @@ -131,7 +131,7 @@ void ImGuiAchievements::draw(bool& open) { continue; } - const std::string tabLabel = fmt::format("{} ({}/{})", catInfo.label, catUnlocked, catTotal); + const std::string tabLabel = fmt::format("{} ({}/{})###{}", catInfo.label, catUnlocked, catTotal, catInfo.label); ImGui::PushStyleColor(ImGuiCol_Text, catInfo.color); const bool tabOpen = ImGui::BeginTabItem(tabLabel.c_str()); @@ -152,6 +152,7 @@ void ImGuiAchievements::draw(bool& open) { continue; } ImGui::PushID(a.key); + ImGui::BeginGroup(); ImGui::PushStyleColor( ImGuiCol_Text, @@ -190,6 +191,21 @@ void ImGuiAchievements::draw(bool& open) { ImGui::PopStyleColor(); } + ImGui::EndGroup(); + + if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + ImGui::OpenPopup("##ctx"); + } + + if (ImGui::BeginPopup("##ctx")) { + ImGui::TextDisabled("%s", a.name); + ImGui::Separator(); + if (ImGui::MenuItem("Clear Achievement")) { + AchievementSystem::get().clearOne(a.key); + } + ImGui::EndPopup(); + } + ImGui::Spacing(); ImGui::PopID(); } From 0d37cb4e5420a9cb61a28f00babeb09ed1528dd5 Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 28 Apr 2026 04:01:22 -0700 Subject: [PATCH 48/70] thank you berry much achievement --- src/dusk/achievements.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 99d4648b98..4a7dac5e4d 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -46,6 +46,21 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "plumm_max", + "Thank You Berry Much", + "Score 61,454 points in the Plumm minigame.", + AchievementCategory::Minigame, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_getBalloonScore() >= 61454) { + a.progress = 1; + } + }, + {} + }, { { "rollgoal_8", From 92391d5eb87a1053fd99722a3f2870ceccb3189b Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Tue, 28 Apr 2026 04:06:29 -0700 Subject: [PATCH 49/70] add overview and ai notice to readme --- README.md | 6 ++++++ extern/aurora | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b6d6cec863..a66a423c5e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ - ### **[Official Website](https://twilitrealm.dev)** - ### **[Discord](https://discord.gg/QACynxeyna)** +# Overview +Dusk is a reverse-engineered reimplementation of Twilight Princess. +It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience. + # Setup **⚠️ Dusk does NOT provide any copyrighted assets. You must provide your own copy of the game.** @@ -27,5 +31,7 @@ First make sure your dump of the game is clean and supported by Dusk. You can do # Building If you'd like to build Dusk from source, please read the [build instructions](docs/building.md). +Pull Requests are welcomed! Note that we do not accept contributions that are primarily AI generated and will close your PR if we suspect as much. + # Credits Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusk/graphs/contributors). diff --git a/extern/aurora b/extern/aurora index 7784b6fc95..8a2b80ecb1 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 7784b6fc95568551499c87bd093b78d86e194eba +Subproject commit 8a2b80ecb104625319c2818fd6d752bab8617679 From 47863b34c260f7d86dd0e194b063e1e8db32b407 Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 28 Apr 2026 05:54:09 -0700 Subject: [PATCH 50/70] achievement signal system --- include/dusk/achievements.h | 8 ++++++++ src/d/actor/d_a_obj_carry.cpp | 9 +++++++++ src/dusk/achievements.cpp | 25 +++++++++++++++++++++++++ src/dusk/imgui/ImGuiAchievements.cpp | 1 + 4 files changed, 43 insertions(+) diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h index fb2da89c69..ca4676ab2d 100644 --- a/include/dusk/achievements.h +++ b/include/dusk/achievements.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include "nlohmann/json.hpp" @@ -14,6 +16,7 @@ enum class AchievementCategory : uint8_t { Collection, Challenge, Minigame, + Misc, Glitched }; @@ -42,6 +45,10 @@ public: void clearAll(); void clearOne(const char* key); + // Signals are visible to all achievement checks within the same tick, then cleared. + void signal(const char* key); + bool hasSignal(const char* key) const; + std::vector getAchievements() const; bool hasPendingUnlock() const { return !m_pendingUnlocks.empty(); } std::string consumePendingUnlock(); @@ -58,6 +65,7 @@ private: void processEntry(Entry& e); std::vector m_entries; + std::unordered_set m_signals; bool m_loaded = false; bool m_dirty = false; std::queue m_pendingUnlocks; diff --git a/src/d/actor/d_a_obj_carry.cpp b/src/d/actor/d_a_obj_carry.cpp index a8265f69f6..b83f9cd82f 100644 --- a/src/d/actor/d_a_obj_carry.cpp +++ b/src/d/actor/d_a_obj_carry.cpp @@ -21,6 +21,9 @@ #include "d/d_lib.h" #include "d/d_debug_viewer.h" #include "f_op/f_op_kankyo_mng.h" +#ifdef TARGET_PC +#include "dusk/achievements.h" +#endif static const cM3dGCylS l_cyl_info[] = { { 0.0f, 0.0f, 0.0f, 30.0f, 100.0f }, @@ -3582,6 +3585,12 @@ bool daObjCarry_c::cc_damage_proc_ironball() { } } +#ifdef TARGET_PC + if (mCannon && mCyl.ChkAtHit() && mCyl.GetAtHitAc() == daPy_getPlayerActorClass()) { + dusk::AchievementSystem::get().signal("iron_ball_hit_player"); + } +#endif + return var_r26; } diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 4a7dac5e4d..58e99a3413 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -8,6 +8,7 @@ #include "d/actor/d_a_player.h" #include "d/d_demo.h" #include "f_pc/f_pc_name.h" +#include "f_op/f_op_actor_mng.h" #include #include @@ -273,6 +274,21 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "friendly_fire", + "Friendly Fire", + "Get hit by your own cannonball.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (AchievementSystem::get().hasSignal("iron_ball_hit_player")) { + a.progress = 1; + } + }, + {} + }, { { "back_in_time", @@ -441,6 +457,14 @@ void AchievementSystem::clearAll() { save(); } +void AchievementSystem::signal(const char* key) { + m_signals.insert(key); +} + +bool AchievementSystem::hasSignal(const char* key) const { + return m_signals.count(key) > 0; +} + void AchievementSystem::clearOne(const char* key) { for (auto& e : m_entries) { if (std::string(e.achievement.key) == key) { @@ -485,6 +509,7 @@ void AchievementSystem::tick() { for (auto& e : m_entries) { processEntry(e); } + m_signals.clear(); if (m_dirty) { save(); m_dirty = false; diff --git a/src/dusk/imgui/ImGuiAchievements.cpp b/src/dusk/imgui/ImGuiAchievements.cpp index 479e307e30..113be5d95d 100644 --- a/src/dusk/imgui/ImGuiAchievements.cpp +++ b/src/dusk/imgui/ImGuiAchievements.cpp @@ -111,6 +111,7 @@ void ImGuiAchievements::draw(bool& open) { {AchievementCategory::Collection, "Collection", ImVec4(0.3f, 0.85f, 0.4f, 1.0f)}, {AchievementCategory::Challenge, "Challenge", ImVec4(1.0f, 0.65f, 0.15f, 1.0f)}, {AchievementCategory::Minigame, "Minigame", ImVec4(0.5f, 0.85f, 1.0f, 1.0f)}, + {AchievementCategory::Misc, "Misc", ImVec4(0.65f, 0.65f, 0.65f, 1.0f)}, {AchievementCategory::Glitched, "Glitched", ImVec4(0.75f, 0.4f, 1.0f, 1.0f)}, }; From d99ed2729b1d499633c9d8c6e6f536ce9683a38b Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 28 Apr 2026 05:57:58 -0700 Subject: [PATCH 51/70] fix achievements window sizing --- src/dusk/imgui/ImGuiAchievements.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dusk/imgui/ImGuiAchievements.cpp b/src/dusk/imgui/ImGuiAchievements.cpp index 113be5d95d..f03e4176ed 100644 --- a/src/dusk/imgui/ImGuiAchievements.cpp +++ b/src/dusk/imgui/ImGuiAchievements.cpp @@ -76,8 +76,8 @@ void ImGuiAchievements::draw(bool& open) { return; } - ImGui::SetNextWindowSizeConstraints(ImVec2(640, 200), ImVec2(800, 900)); - ImGui::SetNextWindowSize(ImVec2(640, 480), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSizeConstraints(ImVec2(800, 200), ImVec2(1280, 900)); + ImGui::SetNextWindowSize(ImVec2(800, 480), ImGuiCond_FirstUseEver); if (!ImGui::Begin( "Achievements", &open, From 3c25633ee9fd1b77740f5c96ae64dacc612764b3 Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 28 Apr 2026 06:08:04 -0700 Subject: [PATCH 52/70] speed_target avoid ub --- src/d/actor/d_a_e_th.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/d/actor/d_a_e_th.cpp b/src/d/actor/d_a_e_th.cpp index f963350da4..43ad8ec19b 100644 --- a/src/d/actor/d_a_e_th.cpp +++ b/src/d/actor/d_a_e_th.cpp @@ -282,6 +282,11 @@ static void e_th_spin_B(e_th_class* i_this) { i_this->current.pos += spC; f32 speed_target; + + #if AVOID_UB + speed_target = 0; + #endif + f32 anm_frame = i_this->mpModelMorf->getFrame(); switch (i_this->mMode) { From 2dc494dc1c6e71e8ebbf07741e9a6417f715a210 Mon Sep 17 00:00:00 2001 From: Julian Maynes Date: Tue, 28 Apr 2026 06:14:56 -0700 Subject: [PATCH 53/70] Finish achievement --- src/d/d_menu_letter.cpp | 8 ++++++++ src/dusk/achievements.cpp | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp index a0155ef6b4..589b2553c0 100644 --- a/src/d/d_menu_letter.cpp +++ b/src/d/d_menu_letter.cpp @@ -17,6 +17,10 @@ #include "d/d_msg_scrn_arrow.h" #include "d/d_lib.h" +#ifdef TARGET_PC +#include "dusk/achievements.h" +#endif + #if VERSION == VERSION_GCN_JPN #define D_MENU_LETTER_LINE_MAX 9 #else @@ -514,6 +518,10 @@ void dMenu_Letter_c::read_open_init() { setAButtonString(0); setBButtonString(0); mpBlackTex->setAlpha(0); + + #ifdef TARGET_PC + dusk::AchievementSystem::get().signal("open_letter"); + #endif } void dMenu_Letter_c::read_open_move() { diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 58e99a3413..88dc43910c 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -376,6 +376,24 @@ std::vector AchievementSystem::makeEntries() { } }, {} + }, + { + { + "email-me", + "Email Me", + "Read a letter during the Dark Beast Ganon fight.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + + void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e); + if(dbgExists && AchievementSystem::get().hasSignal("open_letter")) { + a.progress = 1; + } + + }, + {} } }; } From bb6db3caea11d1bbe89f1d06764322a46c2fc1f5 Mon Sep 17 00:00:00 2001 From: Julian Maynes Date: Tue, 28 Apr 2026 06:18:40 -0700 Subject: [PATCH 54/70] remove newlines --- src/dusk/achievements.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 88dc43910c..5a603f4ac7 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -386,12 +386,10 @@ std::vector AchievementSystem::makeEntries() { false, 0, 0, false }, [](Achievement& a, json&) { - void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e); - if(dbgExists && AchievementSystem::get().hasSignal("open_letter")) { + if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) { a.progress = 1; } - }, {} } From 7566949b424a96188ae73454680ac6fbf250b438 Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 28 Apr 2026 06:21:41 -0700 Subject: [PATCH 55/70] fix friendly fire achievement --- src/d/actor/d_a_alink_damage.inc | 13 +++++++++++++ src/d/actor/d_a_obj_carry.cpp | 9 --------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/d/actor/d_a_alink_damage.inc b/src/d/actor/d_a_alink_damage.inc index ac5175e314..e095fc0393 100644 --- a/src/d/actor/d_a_alink_damage.inc +++ b/src/d/actor/d_a_alink_damage.inc @@ -8,6 +8,10 @@ #include "d/actor/d_a_horse.h" #include "d/actor/d_a_crod.h" #include "d/d_msg_object.h" +#ifdef TARGET_PC +#include "d/actor/d_a_obj_carry.h" +#include "dusk/achievements.h" +#endif #if DEBUG #include "d/d_s_menu.h" @@ -677,6 +681,15 @@ BOOL daAlink_c::checkDamageAction() { } setDamagePoint(dmg, at_mtrl == dCcD_MTRL_FIRE || at_mtrl == dCcD_MTRL_ICE, TRUE, 0); + +#ifdef TARGET_PC + if (tghit_ac_name == fpcNm_Obj_Carry_e) { + auto* carry = static_cast(tghit_ac); + if (carry->prm_chk_type_ironball() && carry->checkCannon()) { + dusk::AchievementSystem::get().signal("iron_ball_hit_player"); + } + } +#endif if (armor_no_dmg && at_mtrl != dCcD_MTRL_ELECTRIC && at_mtrl != dCcD_MTRL_ICE) { setGuardSe(var_r29); diff --git a/src/d/actor/d_a_obj_carry.cpp b/src/d/actor/d_a_obj_carry.cpp index b83f9cd82f..a8265f69f6 100644 --- a/src/d/actor/d_a_obj_carry.cpp +++ b/src/d/actor/d_a_obj_carry.cpp @@ -21,9 +21,6 @@ #include "d/d_lib.h" #include "d/d_debug_viewer.h" #include "f_op/f_op_kankyo_mng.h" -#ifdef TARGET_PC -#include "dusk/achievements.h" -#endif static const cM3dGCylS l_cyl_info[] = { { 0.0f, 0.0f, 0.0f, 30.0f, 100.0f }, @@ -3585,12 +3582,6 @@ bool daObjCarry_c::cc_damage_proc_ironball() { } } -#ifdef TARGET_PC - if (mCannon && mCyl.ChkAtHit() && mCyl.GetAtHitAc() == daPy_getPlayerActorClass()) { - dusk::AchievementSystem::get().signal("iron_ball_hit_player"); - } -#endif - return var_r26; } From ddaf50c01d8e096fa358e23074635a6450dce0ce Mon Sep 17 00:00:00 2001 From: madeline Date: Tue, 28 Apr 2026 06:53:20 -0700 Subject: [PATCH 56/70] long jump attack achievement --- src/dusk/achievements.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 58e99a3413..e0730447d5 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -289,6 +289,43 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "long_jump_attack", + "Long Jump Attack", + "Travel more than 20 meters in a single jump attack before landing.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + static bool inJump = false; + static float startX = 0.0f, startZ = 0.0f; + + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr) { + inJump = false; + return; + } + + if (!inJump) { + if (link->mProcID == daAlink_c::PROC_CUT_JUMP) { + inJump = true; + startX = link->current.pos.x; + startZ = link->current.pos.z; + } + } else if (link->mProcID == daAlink_c::PROC_CUT_JUMP_LAND) { + inJump = false; + const float dx = link->current.pos.x - startX; + const float dz = link->current.pos.z - startZ; + if (dx * dx + dz * dz >= 2000.0f * 2000.0f) { + a.progress = 1; + } + } else if (link->mProcID != daAlink_c::PROC_CUT_JUMP) { + inJump = false; + } + }, + {} + }, { { "back_in_time", From 94a99e8da0f5609de53e03c0ec32f6f2dae0be71 Mon Sep 17 00:00:00 2001 From: Julian Maynes Date: Tue, 28 Apr 2026 07:12:29 -0700 Subject: [PATCH 57/70] Add iron boots achievement --- src/dusk/achievements.cpp | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 5a603f4ac7..7ebe1e7710 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -379,7 +379,7 @@ std::vector AchievementSystem::makeEntries() { }, { { - "email-me", + "email_me", "Email Me", "Read a letter during the Dark Beast Ganon fight.", AchievementCategory::Misc, @@ -392,7 +392,27 @@ std::vector AchievementSystem::makeEntries() { } }, {} + }, + { + { + "heavy-hitter", + "Heavy Hitter", + "Wear the Iron Boots during the end credits.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + if (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) { + a.progress = 1; + } + }, + {} } + }; } From 2ed2268579958bd127df7923d3f6c91f40177c27 Mon Sep 17 00:00:00 2001 From: Julian Maynes Date: Tue, 28 Apr 2026 07:19:18 -0700 Subject: [PATCH 58/70] fix newline --- src/dusk/achievements.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 7ebe1e7710..9a88d947b5 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -412,7 +412,6 @@ std::vector AchievementSystem::makeEntries() { }, {} } - }; } From 4d12cc8ea25ea53c22fc64f13f0d5212a05e4ce0 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Tue, 28 Apr 2026 11:05:29 -0400 Subject: [PATCH 59/70] Fix draw crash with Super Clawshot enabled --- src/d/actor/d_a_alink_hook.inc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc index d152190161..73049af08b 100644 --- a/src/d/actor/d_a_alink_hook.inc +++ b/src/d/actor/d_a_alink_hook.inc @@ -18,6 +18,10 @@ enum { }; void daAlink_c::hsChainShape_c::draw() { + if (dusk::getSettings().game.superClawshot) { + return; + } + static const int dummy = 0; daAlink_c* alink = (daAlink_c*)getUserArea(); From f75faf6b06c60bb642df15d94db86b4dd23d7c1c Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Tue, 28 Apr 2026 18:03:06 -0700 Subject: [PATCH 60/70] fix instant text clearing shop messages too early --- include/d/d_msg_object.h | 3 +++ src/d/d_msg_class.cpp | 9 +-------- src/d/d_msg_object.cpp | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/include/d/d_msg_object.h b/include/d/d_msg_object.h index b55ea73904..d2ac4ecb2a 100644 --- a/include/d/d_msg_object.h +++ b/include/d/d_msg_object.h @@ -67,6 +67,9 @@ public: bool isStaffMessage(); bool isSaveMessage(); bool isTalkMessage(); +#if TARGET_PC + bool isShopItemMessage(); +#endif const char* getSmellName(); const char* getPortalName(); const char* getBombName(); diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 4040eb0d7f..fab6906ee0 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -1987,13 +1987,6 @@ 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 && @@ -2066,7 +2059,7 @@ bool jmessage_tSequenceProcessor::do_isReady() { case 0: case 5: case 6: - if (mDoCPd_c::getTrigA(PAD_1) || field_0xb2 != 0) { + if (mDoCPd_c::getTrigA(PAD_1) || field_0xb2 != 0 IF_DUSK(|| (dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)))) { field_0xa4 = 0; pReference->onBatchFlag(); pReference->setCharCnt(D_MSG_CLASS_CHAR_CNT_MAX); diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index ae0e3d8427..22e722d790 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -32,6 +32,8 @@ #if TARGET_PC #include "dusk/settings.h" +#include +#include #endif static void dMsgObject_addFundRaising(s16 param_0); @@ -1594,7 +1596,7 @@ u8 dMsgObject_c::isSend() { return 2; } } else { - if (IF_DUSK((dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)) ||) + if (IF_DUSK((dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0) && !isShopItemMessage()) ||) mDoCPd_c::getTrigA(0) != 0 || mDoCPd_c::getTrigB(0) != 0) { return 2; } @@ -1866,6 +1868,40 @@ bool dMsgObject_c::isTalkMessage() { return true; } +#if TARGET_PC +bool dMsgObject_c::isShopItemMessage() { + + // Probably a better way to do this than just listing every message id, but this works for now + // Note: Keep contents sorted so we can use binary search + const auto shopMsgIds = std::to_array>({ + {}, + // zel_01.bmg - Seras Shop + {7001, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7013, 7014, 7022, 7023, 7028, 7029, + 7044, 7045, 7053}, + // zel_02.bmg - Kakariko Shops + {5251, 5253, 5254, 5256, 5258, 5259, 5653, 5654, 5656, 5660, 5661, 5664, 5665, 5697, 5698, + 5699, 5803, 5804, 5806, 5810, 5811, 5812, 5814, 5821, 5823, 5824, 5987, 5988, 5989, 5990, + 5991, 5992, 5993, 5994, 5995, 5996, 5997, 5998, 5999}, + // zel_03.bmg - Death Mountain Shop + {5303, 5304, 5306, 5310, 5311, 5314, 5315, 5322, 5323, 5324, 5496, 5497, 5498, 5499}, + // zel_04.bmg - Castle Town Shops + {5407, 5408, 5409, 5410, 5411, 5412, 5413, 5414, 5415, 5416, 5417, 5418, 5419, 5420, 5431, + 5432, 5433, 5434, 5435, 5436, 5437, 5438, 5439, 5440, 5441, 5444, 5449, 5450, 5451, 5452, + 5462}, + // zel_05.bmg - Oocca Shop + {9428, 9429, 9430, 9431, 9432, 9437, 9443, 9448, 9449, 9451, 9459} + }); + + u16 id = mMessageID; + s16 group = dMsgObject_getGroupID(); + if (group < shopMsgIds.size()) { + return std::ranges::binary_search(shopMsgIds[group], id); + } + return false; + +} +#endif + const char* dMsgObject_c::getSmellName() { JMSMesgInfo_c* info_header_p = (JMSMesgInfo_c*)((char*)mpMsgRes + 0x20); char* data_ptr = (char*)info_header_p + info_header_p->header.size; From b26896cad53142d4dde73d6df6fc4869b4ab28da Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Tue, 28 Apr 2026 18:05:40 -0700 Subject: [PATCH 61/70] includes --- src/d/d_msg_object.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index 22e722d790..00dfc6626e 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -34,6 +34,7 @@ #include "dusk/settings.h" #include #include +#include #endif static void dMsgObject_addFundRaising(s16 param_0); From e15f5bcee9a48932c66d4a4499a4a2ee23e26d3f Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:40:49 +0000 Subject: [PATCH 62/70] Improve map outline rendering --- src/d/d_map_path.cpp | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index f94cdc9475..911dd09cdd 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -16,6 +16,7 @@ #ifdef TARGET_PC constexpr u16 kMapResolutionMultiplier = 4; +constexpr u16 kMapCircleSize = 16 * kMapResolutionMultiplier; #endif void dMpath_n::dTexObjAggregate_c::create() { @@ -32,6 +33,47 @@ void dMpath_n::dTexObjAggregate_c::create() { JUT_ASSERT(74, image->magFilter == GX_NEAR); mDoLib_setResTimgObj(image, mp_texObj[lp1], 0, NULL); } + +#if TARGET_PC + auto hqCircle = JKR_NEW TGXTexObj(); + + static bool hqCircleDrawn = false; + static u8 hqCircleData[kMapCircleSize * kMapCircleSize]; + + if (!hqCircleDrawn) { + const auto center = kMapCircleSize / 2.0f; + const auto radiusSq = center * center; + const auto blocksAcross = kMapCircleSize >> 3; + + for (u16 y = 0; y < kMapCircleSize; y++) { + for (u16 x = 0; x < kMapCircleSize; x++) { + // swizzle raster order to I8 blocks + auto blockX = x >> 3; + auto blockY = y >> 2; + auto blockIdx = (blockY * blocksAcross) + blockX; + + auto localX = x & 7; + auto localY = y & 3; + auto localIdx = (localY << 3) + localX; + + auto finalOffset = (blockIdx << 5) + localIdx; + + auto dx = (x + 0.5f) - center; + auto dy = (y + 0.5f) - center; + + // the original texture is in I4 format and uses 1 to indicate if inside the circle + // so we scale to I8 range: 255 / 15 = 17 + hqCircleData[finalOffset] = (dx * dx + dy * dy < radiusSq) ? 17 : 0; + } + } + hqCircleDrawn = true; + } + + GXInitTexObj(hqCircle, hqCircleData, kMapCircleSize, kMapCircleSize, GX_TF_I8, GX_CLAMP, + GX_CLAMP, GX_FALSE); + GXInitTexObjLOD(hqCircle, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); + mp_texObj[6] = hqCircle; +#endif } void dMpath_n::dTexObjAggregate_c::remove() { From afe54f22abe94ad52c56622579f45ae503081907 Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Wed, 29 Apr 2026 04:16:24 +0000 Subject: [PATCH 63/70] Write circle pixels linearly --- src/d/d_map_path.cpp | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index 911dd09cdd..eca620ef98 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -44,33 +44,34 @@ void dMpath_n::dTexObjAggregate_c::create() { const auto center = kMapCircleSize / 2.0f; const auto radiusSq = center * center; const auto blocksAcross = kMapCircleSize >> 3; + const auto totalPixels = sizeof(hqCircleData); - for (u16 y = 0; y < kMapCircleSize; y++) { - for (u16 x = 0; x < kMapCircleSize; x++) { - // swizzle raster order to I8 blocks - auto blockX = x >> 3; - auto blockY = y >> 2; - auto blockIdx = (blockY * blocksAcross) + blockX; + for (size_t i = 0; i < totalPixels; i++) { + // 8x4 block swizzling for I8 + const auto blockIdx = i >> 5; + const auto localIdx = i & 31; - auto localX = x & 7; - auto localY = y & 3; - auto localIdx = (localY << 3) + localX; + const auto blockY = blockIdx / blocksAcross; + const auto blockX = blockIdx % blocksAcross; - auto finalOffset = (blockIdx << 5) + localIdx; + const auto localY = localIdx >> 3; + const auto localX = localIdx & 7; - auto dx = (x + 0.5f) - center; - auto dy = (y + 0.5f) - center; + const auto x = (blockX << 3) + localX; + const auto y = (blockY << 2) + localY; - // the original texture is in I4 format and uses 1 to indicate if inside the circle - // so we scale to I8 range: 255 / 15 = 17 - hqCircleData[finalOffset] = (dx * dx + dy * dy < radiusSq) ? 17 : 0; - } + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + + // the original texture is in I4 format and uses 1 to indicate if inside the circle + // so we scale to I8 range: 255 / 15 = 17 + hqCircleData[i] = (dx * dx + dy * dy < radiusSq) ? 17 : 0; } hqCircleDrawn = true; } GXInitTexObj(hqCircle, hqCircleData, kMapCircleSize, kMapCircleSize, GX_TF_I8, GX_CLAMP, - GX_CLAMP, GX_FALSE); + GX_CLAMP, GX_FALSE); GXInitTexObjLOD(hqCircle, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); mp_texObj[6] = hqCircle; #endif From 24dd02fc810c5406e1338b6e14105bcca6741fd5 Mon Sep 17 00:00:00 2001 From: madeline Date: Wed, 29 Apr 2026 05:28:28 -0700 Subject: [PATCH 64/70] better back in time condition --- src/dusk/achievements.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index e0730447d5..dd9907be3f 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -335,18 +335,13 @@ std::vector AchievementSystem::makeEntries() { false, 0, 0, false }, [](Achievement& a, json&) { - static int titleNoDemoFrames = 0; if (fopAcM_SearchByName(fpcNm_TITLE_e) == nullptr) { - titleNoDemoFrames = 0; return; } - const auto* link = static_cast(daPy_getPlayerActorClass()); - if (link != nullptr && dDemo_c::getMode() == 0) { - if (++titleNoDemoFrames >= 60) { + const auto* player = static_cast(daPy_getPlayerActorClass()); + + if (player != nullptr && player->mDemo.getDemoMode() == 1) { a.progress = 1; - } - } else { - titleNoDemoFrames = 0; } }, {} From c803bfb545c551b298d8b50a818bd27d5777bde4 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Wed, 29 Apr 2026 09:14:51 -0700 Subject: [PATCH 65/70] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 8a2b80ecb1..c77a4d0c3c 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 8a2b80ecb104625319c2818fd6d752bab8617679 +Subproject commit c77a4d0c3c6a0d9f584a30e6ab1661634959c32b From 36dc43c6024c3719e86eabe6137cc875628d402f Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Wed, 29 Apr 2026 20:13:07 -0400 Subject: [PATCH 66/70] Fix changing tunics crash while on top of mirror (#596) * Fix changing tunics while reflection is active * Revert "Fix changing tunics while reflection is active" This reverts commit 89927dc7a638342f2a9a79511e2445789fb62006. * Really fix changing tunics while reflection is active * Fix transforming on ice again --------- Co-authored-by: MelonSpeedruns Co-authored-by: Irastris --- include/d/actor/d_a_mirror.h | 1 + src/d/actor/d_a_mirror.cpp | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/include/d/actor/d_a_mirror.h b/include/d/actor/d_a_mirror.h index 06c5603899..e2f9a51e30 100644 --- a/include/d/actor/d_a_mirror.h +++ b/include/d/actor/d_a_mirror.h @@ -27,6 +27,7 @@ public: /* 0x17C */ cXyz mViewScale; #if TARGET_PC bool mbReset = false; + bool mbHadEntry = false; #endif }; diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index 86ef4314f3..5c5218328e 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -40,6 +40,7 @@ dMirror_packet_c::dMirror_packet_c() { void dMirror_packet_c::reset() { #if TARGET_PC mbReset = true; + mbHadEntry = false; #else mModelCount = 0; #endif @@ -84,11 +85,21 @@ void dMirror_packet_c::calcMinMax() { } int dMirror_packet_c::entryModel(J3DModel* i_model) { +#if TARGET_PC + if (mbReset) { + mModelCount = 0; + mbReset = false; + } +#endif + if (mModelCount >= 0x40) { return 0; } mModels[mModelCount++] = i_model; +#if TARGET_PC + mbHadEntry = true; +#endif return 1; } @@ -592,13 +603,6 @@ int daMirror_c::execute() { return 1; } -#if TARGET_PC - if (mPacket.mbReset) { - mPacket.mModelCount = 0; - mPacket.mbReset = false; - } -#endif - daPy_py_c* player = daPy_getLinkPlayerActorClass(); JUT_ASSERT(0, player != NULL); @@ -624,6 +628,12 @@ int daMirror_c::draw() { mDoExt_modelUpdateDL(mpModel); } +#if TARGET_PC + if (mPacket.mbReset && !mPacket.mbHadEntry) { + mPacket.mModelCount = 0; + } + mPacket.mbHadEntry = true; +#endif dComIfGd_getOpaListBG()->entryImm(&mPacket, 0); return 1; } From fbf63b075ad5e2c484c4f7e3f2308a5ff7eaa886 Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:28:47 +0000 Subject: [PATCH 67/70] Correct JPADrawInfo proj matrix on widescreen Fixes #337. --- src/m_Do/m_Do_graphic.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index 425d1e25fe..9bd6e5e327 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -2146,6 +2146,7 @@ int mDoGph_Painter() { // FRAME INTERP NOTE: Call setViewMtx earlier so that it's interpolated in time for draw_info to use it j3dSys.setViewMtx(camera_p->view.viewMtx); JPADrawInfo draw_info(j3dSys.getViewMtx(), camera_p->view.fovy, camera_p->view.aspect); + mDoGph_gInf_c::setWideZoomLightProjection(draw_info.mPrjMtx); #else JPADrawInfo draw_info(camera_p->view.viewMtx, camera_p->view.fovy, camera_p->view.aspect); #endif From fecd1d568373aa7903e0bb92b9db459ec9d03041 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Wed, 29 Apr 2026 22:33:02 -0700 Subject: [PATCH 68/70] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index c77a4d0c3c..d3f34bfea8 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit c77a4d0c3c6a0d9f584a30e6ab1661634959c32b +Subproject commit d3f34bfea84b8b70bd1c8b13d78a3d67a3e4d332 From 1ac6df8de7956a790b7782e74a1503568f922904 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Thu, 30 Apr 2026 07:52:44 -0700 Subject: [PATCH 69/70] mirror clip fix closes #581 --- src/d/actor/d_a_obj_mirror_chain.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/d/actor/d_a_obj_mirror_chain.cpp b/src/d/actor/d_a_obj_mirror_chain.cpp index 92c062f7c8..da4a09e1c5 100644 --- a/src/d/actor/d_a_obj_mirror_chain.cpp +++ b/src/d/actor/d_a_obj_mirror_chain.cpp @@ -59,7 +59,8 @@ void dScissorBegin_packet_c::draw() { } if (sp50 >= 4) { - GXSetScissor(FB_WIDTH + 1, FB_HEIGHT + 1, 0, 0); + IF_DUSK(GXSetColorUpdate(GX_FALSE)); + IF_NOT_DUSK(GXSetScissor(FB_WIDTH + 1, FB_HEIGHT + 1, 0, 0)); return; } @@ -170,7 +171,8 @@ void dScissorBegin_packet_c::draw() { } if (spBC.z < 0.0f || var_f31 > sp6C || sp7C < sp70 || var_f30 > sp64 || sp78 < sp68) { - GXSetScissor(FB_WIDTH + 1, FB_HEIGHT + 1, 0, 0); + IF_DUSK(GXSetColorUpdate(GX_FALSE)); + IF_NOT_DUSK(GXSetScissor(FB_WIDTH + 1, FB_HEIGHT + 1, 0, 0)); } else { var_f31 = cLib_minLimit(var_f31, sp70); sp7C = cLib_maxLimit(sp7C, sp6C); @@ -187,6 +189,7 @@ void dScissorEnd_packet_c::draw() { #endif GXSetScissor(l_scissor[0], l_scissor[1], l_scissor[2], l_scissor[3]); + IF_DUSK(GXSetColorUpdate(GX_TRUE)); } static int createSolidHeap(fopAc_ac_c* i_this) { From 9b6b344ecf4e80389918ee7bae981b178bede215 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 30 Apr 2026 12:09:18 -0600 Subject: [PATCH 70/70] Revert "mirror clip fix" This reverts commit 1ac6df8de7956a790b7782e74a1503568f922904. --- src/d/actor/d_a_obj_mirror_chain.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/d/actor/d_a_obj_mirror_chain.cpp b/src/d/actor/d_a_obj_mirror_chain.cpp index da4a09e1c5..92c062f7c8 100644 --- a/src/d/actor/d_a_obj_mirror_chain.cpp +++ b/src/d/actor/d_a_obj_mirror_chain.cpp @@ -59,8 +59,7 @@ void dScissorBegin_packet_c::draw() { } if (sp50 >= 4) { - IF_DUSK(GXSetColorUpdate(GX_FALSE)); - IF_NOT_DUSK(GXSetScissor(FB_WIDTH + 1, FB_HEIGHT + 1, 0, 0)); + GXSetScissor(FB_WIDTH + 1, FB_HEIGHT + 1, 0, 0); return; } @@ -171,8 +170,7 @@ void dScissorBegin_packet_c::draw() { } if (spBC.z < 0.0f || var_f31 > sp6C || sp7C < sp70 || var_f30 > sp64 || sp78 < sp68) { - IF_DUSK(GXSetColorUpdate(GX_FALSE)); - IF_NOT_DUSK(GXSetScissor(FB_WIDTH + 1, FB_HEIGHT + 1, 0, 0)); + GXSetScissor(FB_WIDTH + 1, FB_HEIGHT + 1, 0, 0); } else { var_f31 = cLib_minLimit(var_f31, sp70); sp7C = cLib_maxLimit(sp7C, sp6C); @@ -189,7 +187,6 @@ void dScissorEnd_packet_c::draw() { #endif GXSetScissor(l_scissor[0], l_scissor[1], l_scissor[2], l_scissor[3]); - IF_DUSK(GXSetColorUpdate(GX_TRUE)); } static int createSolidHeap(fopAc_ac_c* i_this) {