From 95470b830fa346a64540148bddafc45360602c77 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Thu, 16 Apr 2026 05:55:46 -0700 Subject: [PATCH 01/71] wip map mirror --- include/d/d_menu_fmap2D.h | 6 ++++++ include/d/d_menu_map_common.h | 10 ++++++++++ src/d/d_menu_fmap2D.cpp | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/include/d/d_menu_fmap2D.h b/include/d/d_menu_fmap2D.h index 9b237f2771..d6d869c645 100644 --- a/include/d/d_menu_fmap2D.h +++ b/include/d/d_menu_fmap2D.h @@ -165,6 +165,12 @@ public: void mapBlink() {} + #if PLATFORM_WII || TARGET_PC + f32 getMirrorPosX(f32 param_0, f32 param_1) { + return (field_0x11dc * 2.0f - (param_0 + param_1)) - param_1; + } + #endif + // Unknown name struct RegionTexData { /* 0x00 */ float mMinX; diff --git a/include/d/d_menu_map_common.h b/include/d/d_menu_map_common.h index 989aee7d8b..de50d775ca 100644 --- a/include/d/d_menu_map_common.h +++ b/include/d/d_menu_map_common.h @@ -66,6 +66,16 @@ public: _c90 = param_2; } +#if PLATFORM_WII || TARGET_PC + f32 getMirrorCenterPosX(f32 param_0, f32 param_1) { + if (_c90) { + return (mCenterPosX * 2.0f - (param_0 + param_1)) - param_1; + } + + return param_0; + } +#endif + struct Stage_c { // Incomplete class diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 558ad05d6d..e6078e06c2 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -1393,6 +1393,15 @@ void dMenu_Fmap2DBack_c::regionTextureDraw() { if (uVar10 != uVar9) { bool b = 0; f32 v = mTransX + (dVar14 + (mRegionMinMapX[uVar10] + field_0xf0c[uVar10])); + + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + b = true; + v = getMirrorPosX(mTransX + (dVar14 + (mRegionMinMapX[uVar10] + field_0xf0c[uVar10])), + mRegionMapSizeX[uVar10] * mZoom * 0.5f); + } + #endif + mpAreaTex[uVar10]->draw( v, mTransZ + (dVar13 + (mRegionMinMapY[uVar10] + field_0xf2c[uVar10])), mRegionMapSizeX[uVar10] * mZoom, mRegionMapSizeY[uVar10] * mZoom, b, false, @@ -1400,6 +1409,15 @@ void dMenu_Fmap2DBack_c::regionTextureDraw() { } else { bool b = 0; f32 v = mTransX + (dVar14 + (mRegionMinMapX[uVar9] + field_0xf0c[uVar9])); + + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + b = true; + v = getMirrorPosX(mTransX + (dVar14 + (mRegionMinMapX[uVar9] + field_0xf0c[uVar9])), + mRegionMapSizeX[uVar9] * mZoom * 0.5f); + } + #endif + mpAreaTex[uVar9]->draw( v, mTransZ + (dVar13 + (mRegionMinMapY[uVar9] + field_0xf2c[uVar9])), mRegionMapSizeX[uVar9] * mZoom, mRegionMapSizeY[uVar9] * mZoom, b, false, From d9a0ef760fc7865814134059705f964425a36dd1 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Thu, 16 Apr 2026 19:46:02 -0700 Subject: [PATCH 02/71] fix icon positions --- src/d/d_menu_map_common.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/d/d_menu_map_common.cpp b/src/d/d_menu_map_common.cpp index e7d0fef7ad..0017975c4e 100644 --- a/src/d/d_menu_map_common.cpp +++ b/src/d/d_menu_map_common.cpp @@ -343,6 +343,11 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4 } f32 pos_x = icon_pos_x + i_posX; + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + pos_x = getMirrorCenterPosX(pos_x, 0.0f); + } + #endif mpDrawCursor->setPos(pos_x, icon_pos_y + i_posY); mpDrawCursor->setScale(mIconInfo[info_idx].scale * g_fmapHIO.mMapIconHIO.mPortalCursorScale); mpDrawCursor->draw(); @@ -364,6 +369,12 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4 } f32 pos_x = (icon_pos_x + i_posX); + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + pos_x = getMirrorCenterPosX(pos_x, 0.0f); + } + #endif + mpPortalIcon->setPos(pos_x, icon_pos_y + i_posY); mpPortalIcon->setScale(mIconInfo[info_idx].scale * g_fmapHIO.mMapIconHIO.mPortalIconScale); mpPortalIcon->draw(); @@ -399,6 +410,12 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4 } f32 pos_x = i_posX + (icon_pos_x - (icon_size_x / 2)); + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + pos_x = getMirrorCenterPosX(i_posX + (icon_pos_x - (icon_size_x / 2)), icon_size_x / 2); + } + #endif + mPictures[mIconInfo[info_idx].icon_no]->draw(pos_x, (i_posY + (icon_pos_y - icon_size_y / 2)), icon_size_x, icon_size_y, false, false, false); From 39e465bcab9152578f5d7dbf5a2241f7ed7b4b8d Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Tue, 5 May 2026 06:22:49 -0700 Subject: [PATCH 03/71] fix mirror map region selection --- src/d/d_menu_fmap.cpp | 11 +++++++++++ src/d/d_menu_fmap2D.cpp | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp index 149e03349d..43cb2fa3ae 100644 --- a/src/d/d_menu_fmap.cpp +++ b/src/d/d_menu_fmap.cpp @@ -919,9 +919,20 @@ void dMenu_Fmap_c::region_map_proc() { } mpDraw2DBack->regionMapMove(mpStick); int stage_no, room_no; + +#if TARGET_PC + f32 arrow_pos_x = mpDraw2DBack->getArrowPos2DX(); + if (dusk::getSettings().game.enableMirrorMode) { + arrow_pos_x = mpDraw2DBack->getMirrorPosX(arrow_pos_x, 0.0f); + } + + f32 pos_x = arrow_pos_x - mDoGph_gInf_c::getMinXF() - mDoGph_gInf_c::getWidthF() * 0.5f; +#else f32 pos_x = mpDraw2DBack->getArrowPos2DX() - mDoGph_gInf_c::getMinXF() - mDoGph_gInf_c::getWidthF() * 0.5f; +#endif f32 pos_y = mpDraw2DBack->getArrowPos2DY() - mDoGph_gInf_c::getHeightF() * 0.5f; + mpMenuFmapMap->getPointStagePathInnerNo(getNowFmapRegionData(), pos_x, pos_y, mStayStageNo, &stage_no, &room_no); if (mStageCursor != stage_no || mRoomCursor != room_no || mResetAreaName) { diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index b2e5415c55..ffec184d4e 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -1043,6 +1043,12 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) { calcAllMapPos2D((mArrowPos3DX + control_xpos) - mStageTransX, (mArrowPos3DZ + control_ypos) - mStageTransZ, &sp14, &sp10); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + sp14 = getMirrorPosX(sp14, 0.0f); + } +#endif + mSelectRegion = 0xff; for (int i = 7; i >= 0; i--) { int val = field_0x1230[i]; From 14bccdffa6fa7e191c04fe18a2a5ad326fb4ab70 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Tue, 5 May 2026 06:25:46 -0700 Subject: [PATCH 04/71] fix mirror map portal selection --- src/d/d_menu_fmap.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp index 43cb2fa3ae..d2b06e962c 100644 --- a/src/d/d_menu_fmap.cpp +++ b/src/d/d_menu_fmap.cpp @@ -2475,6 +2475,13 @@ void dMenu_Fmap_c::portalWarpMapMove(STControl* i_stick) { f32 arrow_y = mpDraw2DBack->getArrowPos2DY(); u8 uVar6 = 0xff; +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + arrow_x = mpDraw2DBack->getMirrorPosX(arrow_x, 0.0f); + } +#endif + + for (int i = 0; i < portal_dat->mCount; i++) { if (portals[i].mRegionNo == mpDraw2DBack->getRegionCursor() + 1 && checkDrawPortalIcon(portals[i].mStageNo, portals[i].mSwitchNo)) From ed8b5c96b99a9cf426bd310589c5286ca7f141d6 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 5 May 2026 11:28:05 -0600 Subject: [PATCH 05/71] Update aurora & add logo to README.md --- README.md | 6 ++++++ assets/aurora-powered.png | Bin 0 -> 27368 bytes extern/aurora | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 assets/aurora-powered.png diff --git a/README.md b/README.md index 02b1ded875..cb61bb674c 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,9 @@ Pull requests are welcomed! Note that we do not accept contributions that are pr # 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/assets/aurora-powered.png b/assets/aurora-powered.png new file mode 100644 index 0000000000000000000000000000000000000000..35ff01777678ee9a9e1301c0e0fabe3e2df3d6a5 GIT binary patch literal 27368 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 N)adA;C%7|V{|B?H(DMKQ literal 0 HcmV?d00001 diff --git a/extern/aurora b/extern/aurora index 518747aa86..31ad8443c4 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 518747aa86a50be62ecf92aeb309309b0d58a54a +Subproject commit 31ad8443c42818c9d8b6c26bde6657aa6d94674e From 7e562824fe8888ef4db1cc157c6ef444c30f8df6 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 5 May 2026 11:30:49 -0600 Subject: [PATCH 06/71] Add a
--- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cb61bb674c..30da5db29c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Pull requests are welcomed! Note that we do not accept contributions that are pr 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). +
Powered by Aurora From 7300c0e0f500476956005a9b719b5a7a213c8383 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 5 May 2026 12:15:01 -0600 Subject: [PATCH 07/71] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 31ad8443c4..f845a5a58c 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 31ad8443c42818c9d8b6c26bde6657aa6d94674e +Subproject commit f845a5a58cfb576049e9ba8392c5c5ca61d550b6 From 08321699cdfae1fe7e8864b3c5a48442e56cb157 Mon Sep 17 00:00:00 2001 From: Irastris Date: Tue, 5 May 2026 14:18:25 -0400 Subject: [PATCH 08/71] Display the real title screen behind the prelaunch menu (#638) * Start game execution as soon as a disk image is available * Do not update dDemo_c if prelaunch document is visible * Prevent intro music until prelaunch has popped * Replace "Start Game" references with "Play" * Make prelaunch layout respect mirror mode * Add drop shadow to prelaunch disk-status and version-info * Remove ImGui prelaunch * Add "Change Disk Image" button to prelaunch options * Actually validate discs and make prelaunch very betterer :) * Check your build before pushing dumbass, and go to sleep * "Disc" consistency, adjust restart notice logic * Better LanguageSelect logic * Add restart notice to SaveTypeSelect * Added wind sounds to the pre-launch menu * Add Modal document, use it for disc verification * Consolidate Modal and PresetWindow * Squash various bugs, rearrange document flow * Allow Window inheritors to opt-out of being toggleable * Tweak focus behavior/syntax * Implement "Restart Now" option * Tweaks * Remove a bunch of dynamic_cast * Update README.md --------- Co-authored-by: Luke Street --- README.md | 4 +- files.cmake | 12 +- include/dusk/main.h | 14 ++ res/rml/prelaunch.rcss | 74 ++++++- res/rml/window.rcss | 39 ++++ src/d/d_demo.cpp | 73 ++++++ src/d/d_s_play.cpp | 13 +- src/dusk/imgui/ImGuiConsole.cpp | 4 - src/dusk/imgui/ImGuiConsole.hpp | 2 - src/dusk/imgui/ImGuiPreLaunchWindow.cpp | 282 ------------------------ src/dusk/imgui/ImGuiPreLaunchWindow.hpp | 23 -- src/dusk/main.cpp | 117 +++++++++- src/dusk/ui/component.cpp | 10 - src/dusk/ui/component.hpp | 1 - src/dusk/ui/modal.cpp | 75 +++++++ src/dusk/ui/modal.hpp | 38 ++++ src/dusk/ui/prelaunch.cpp | 150 ++++++++++--- src/dusk/ui/prelaunch.hpp | 17 +- src/dusk/ui/prelaunch_options.cpp | 205 +++++++++++++++-- src/dusk/ui/prelaunch_options.hpp | 9 + src/dusk/ui/preset.cpp | 57 +---- src/dusk/ui/preset.hpp | 9 +- src/dusk/ui/ui.cpp | 19 ++ src/dusk/ui/ui.hpp | 4 +- src/dusk/ui/window.cpp | 64 +++++- src/dusk/ui/window.hpp | 17 ++ src/m_Do/m_Do_main.cpp | 64 ++++-- 27 files changed, 911 insertions(+), 485 deletions(-) delete mode 100644 src/dusk/imgui/ImGuiPreLaunchWindow.cpp delete mode 100644 src/dusk/imgui/ImGuiPreLaunchWindow.hpp create mode 100644 src/dusk/ui/modal.cpp create mode 100644 src/dusk/ui/modal.hpp diff --git a/README.md b/README.md index 30da5db29c..5e7412e6aa 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d - Extract the .zip file - Launch Dusk -- Press **Select Disc Image**, navigate to your game dump, and select the file -- Press **Start Game** to play! +- Press **Select Disc Image** and provide the path to your supported game dump. +- Press **Play**! # Building diff --git a/files.cmake b/files.cmake index a463ce5129..fe7947eaab 100644 --- a/files.cmake +++ b/files.cmake @@ -1447,8 +1447,6 @@ set(DUSK_FILES src/dusk/imgui/ImGuiBloomWindow.hpp src/dusk/imgui/ImGuiMenuTools.cpp src/dusk/imgui/ImGuiMenuTools.hpp - src/dusk/imgui/ImGuiPreLaunchWindow.cpp - src/dusk/imgui/ImGuiPreLaunchWindow.hpp src/dusk/imgui/ImGuiProcessOverlay.cpp src/dusk/imgui/ImGuiCameraOverlay.cpp src/dusk/imgui/ImGuiHeapOverlay.cpp @@ -1459,6 +1457,8 @@ set(DUSK_FILES src/dusk/imgui/ImGuiSaveEditor.cpp src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStateShare.cpp + src/dusk/ui/achievements.cpp + src/dusk/ui/achievements.hpp src/dusk/ui/bool_button.cpp src/dusk/ui/bool_button.hpp src/dusk/ui/button.cpp @@ -1469,16 +1469,14 @@ set(DUSK_FILES src/dusk/ui/controller_config.hpp src/dusk/ui/document.cpp src/dusk/ui/document.hpp - src/dusk/ui/achievements.cpp - src/dusk/ui/achievements.hpp - src/dusk/ui/preset.cpp - src/dusk/ui/preset.hpp src/dusk/ui/editor.cpp src/dusk/ui/editor.hpp src/dusk/ui/event.cpp src/dusk/ui/event.hpp src/dusk/ui/input.cpp src/dusk/ui/input.hpp + src/dusk/ui/modal.cpp + src/dusk/ui/modal.hpp src/dusk/ui/nav_types.hpp src/dusk/ui/number_button.cpp src/dusk/ui/number_button.hpp @@ -1492,6 +1490,8 @@ set(DUSK_FILES src/dusk/ui/prelaunch.hpp src/dusk/ui/prelaunch_options.cpp src/dusk/ui/prelaunch_options.hpp + src/dusk/ui/preset.cpp + src/dusk/ui/preset.hpp src/dusk/ui/select_button.cpp src/dusk/ui/select_button.hpp src/dusk/ui/settings.cpp diff --git a/include/dusk/main.h b/include/dusk/main.h index 065f507d36..d6b9c9927f 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -1,6 +1,10 @@ #ifndef DUSK_MAIN_H #define DUSK_MAIN_H +#if defined(__APPLE__) +#include +#endif + #include namespace dusk { @@ -8,7 +12,17 @@ namespace dusk { extern bool IsShuttingDown; extern bool IsGameLaunched; extern bool IsFocusPaused; + extern bool RestartRequested; extern std::filesystem::path ConfigPath; + +#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ + (defined(TARGET_OS_TV) && TARGET_OS_TV) + inline constexpr bool SupportsProcessRestart = false; +#else + inline constexpr bool SupportsProcessRestart = true; +#endif + + void RequestRestart() noexcept; } #endif // DUSK_MAIN_H diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 9556e74ef2..1c217e466c 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -9,17 +9,44 @@ body { font-weight: normal; font-size: 20dp; color: #FFFFFF; - background-color: #000000; - decorator: image(../prelaunch-bg.png cover left center); filter: opacity(0); transition: filter 1s 0.2s linear-in-out; z-index: -1; } +.gradient { + position: absolute; + width: 100%; + height: 100%; + /* The color gradient from the Figma bands really badly. A fully black gradient does as well, but not as badly. */ + decorator: horizontal-gradient(#000000FF #00000000); +} + +body.mirrored .gradient { + decorator: horizontal-gradient(#00000000 #000000FF); +} + +.background { + position: absolute; + width: 100%; + height: 100%; + decorator: image(../prelaunch-bg.png cover left center); + opacity: 0; + transition: opacity 1s linear-in-out; +} + body[open] { filter: opacity(1); } +body[open] .background { + opacity: 1; +} + +body.disc-ready .background { + opacity: 0; +} + content { display: block; width: 100%; @@ -35,6 +62,7 @@ content[open] { menu { position: absolute; left: 96dp; + right: auto; top: 50%; transform: translateY(-50%); /* Scale based on a reference screen width, 428/1216 */ @@ -47,6 +75,11 @@ menu { gap: 48dp; } +body.mirrored menu { + left: auto; + right: 96dp; +} + hero { display: flex; flex-direction: column; @@ -55,6 +88,10 @@ hero { gap: 8dp; } +body.mirrored hero { + align-items: flex-end; +} + hero img { width: 100%; } @@ -79,6 +116,7 @@ hero img { display: flex; flex-direction: column; gap: 12dp; + align-items: flex-start; } #menu-list button { @@ -86,6 +124,7 @@ hero img { height: 54dp; padding: 8dp 16dp; border-radius: 8dp; + text-align: left; text-transform: uppercase; font-family: "Fira Sans Condensed"; font-size: 32dp; @@ -105,25 +144,56 @@ hero img { decorator: horizontal-gradient(#FEE685FF #FEE68500); } +body.mirrored #menu-list { + align-items: flex-end; +} + +body.mirrored #menu-list button { + text-align: right; +} + +body.mirrored #menu-list button:hover, +body.mirrored #menu-list button:focus-visible { + decorator: horizontal-gradient(#FEE68500 #FEE685FF); +} + disc-info { position: absolute; left: 96dp; + right: auto; bottom: 72dp; display: flex; flex-direction: column; gap: 12dp; font-size: 24dp; + font-effect: glow(0dp 4dp 0dp 4dp black); + text-align: left; +} + +body.mirrored disc-info { + left: auto; + right: 96dp; + text-align: right; } version-info { position: absolute; right: 96dp; + left: auto; bottom: 72dp; display: flex; flex-direction: column; gap: 12dp; text-align: right; font-size: 24dp; + font-effect: glow(0dp 4dp 0dp 4dp black); + text-align: right; +} + +body.mirrored version-info { + right: auto; + left: 96dp; + text-align: left; } #disc-status { diff --git a/res/rml/window.rcss b/res/rml/window.rcss index db3558778a..c72a9aed57 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -43,11 +43,19 @@ window.preset { min-width: 650dp; } +window.modal { + max-width: 816dp; +} + window[open] { filter: opacity(1); transform: scale(1); } +window[open].blurred { + filter: blur(2dp); +} + @media (max-height: 640dp) { body { padding: 16dp; @@ -108,6 +116,12 @@ window content pane > spacer { pointer-events: none; } +window modal { + padding: 32dp; + gap: 20dp; + flex: 0 1 auto; +} + scrollbarvertical { width: 8dp; margin: 4dp 4dp 4dp 0; @@ -194,6 +208,12 @@ button:not(:disabled):active { box-shadow: #C2A42D 0 0 0 2dp; } +button.modal-btn { + font-size: 20dp; + padding: 16dp 10dp; + flex: 1 1 0; +} + select-button { display: flex; align-items: center; @@ -399,3 +419,22 @@ button.preset-btn { color: rgba(224, 219, 200, 65%); text-align: center; } + +.modal-dialog { + display: flex; + flex-flow: column; + padding: 16dp; + gap: 20dp; + flex: 0 1 auto; + min-width: 0; +} + +.modal-actions { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + gap: 12dp; + padding-top: 12dp; + width: 100%; +} diff --git a/src/d/d_demo.cpp b/src/d/d_demo.cpp index d6425848d3..2f099f2c2a 100644 --- a/src/d/d_demo.cpp +++ b/src/d/d_demo.cpp @@ -11,6 +11,62 @@ #include "JSystem/JGadget/define.h" #include +#include "dusk/logging.h" + +#if TARGET_PC +#include "dusk/ui/ui.hpp" + +namespace { +static int sJaiSkip = -1; + +static JSUList* get_stream_list() { + return Z2GetSoundMgr()->getStreamMgr()->getStreamList(); +} + +static int get_stream_count(JSUList* list) { + int i = 0; + for (JSULink* l = list != nullptr ? list->getFirst() : nullptr; l != nullptr; + l = l->getNext()) { + i++; + } + return i; +} + +static void pause_stream(int skip_first, bool paused) { + int i = 0; + JSUList* list = get_stream_list(); + for (JSULink* l = list->getFirst(); l != nullptr; l = l->getNext(), ++i) { + if (i >= skip_first) { + l->getObject()->pause(paused); + } + } +} + +static void pause_streams(int skip_first) { + if (!dusk::ui::is_prelaunch_open()) { + return; + } + JSUList* list = get_stream_list(); + if (list == nullptr || get_stream_count(list) <= skip_first) { + return; + } + pause_stream(skip_first, true); + sJaiSkip = skip_first; +} + +static void unpause_streams(bool require_prelaunch_hidden) { + if (sJaiSkip < 0) { + return; + } + if (require_prelaunch_hidden && dusk::ui::is_prelaunch_open()) { + return; + } + pause_stream(sJaiSkip, false); + sJaiSkip = -1; +} +} // namespace +#endif + s16 dDemo_c::m_branchId = -1; namespace { @@ -1006,7 +1062,16 @@ int dDemo_c::start(u8 const* p_data, cXyz* p_translation, f32 rotationY) { m_control->setSuspend(0); } +#if TARGET_PC + const int existing_streams = get_stream_count(get_stream_list()); +#endif + m_control->forward(0); + +#if TARGET_PC + pause_streams(existing_streams); +#endif + m_translation = p_translation; if (m_translation != NULL) { @@ -1034,6 +1099,10 @@ static void dummyString2() { void dDemo_c::end() { JUT_ASSERT(1956, m_system != NULL); +#if TARGET_PC + unpause_streams(false); +#endif + m_control->destroyObject_all(); m_object->remove(); m_data = NULL; @@ -1054,6 +1123,10 @@ void dDemo_c::branch() { int dDemo_c::update() { JUT_ASSERT(2064, m_system != NULL); +#if TARGET_PC + unpause_streams(true); +#endif + if (m_data == NULL) { if (m_branchData == NULL) { return 0; diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp index f0e2330a15..f7ebf6a20d 100644 --- a/src/d/d_s_play.cpp +++ b/src/d/d_s_play.cpp @@ -40,8 +40,9 @@ #include "JSystem/JKernel/JKRAramArchive.h" #if TARGET_PC +#include "dusk/autosave.h" #include "dusk/memory.h" -#include +#include "dusk/ui/ui.hpp" #endif #if DEBUG @@ -794,7 +795,17 @@ static int dScnPly_Execute(dScnPly_c* i_this) { dJprev_c::get()->update(); #endif +#if TARGET_PC + if (!dusk::ui::is_prelaunch_open()) { + dDemo_c::update(); + } else if (dusk::getSettings().audio.menuSounds) { + s8 reverb = dComIfGp_getReverb(dComIfGp_roomControl_getStayNo()); + f32 fxMix = reverb / 127.0f; + g_mEnvSeMgr.field_0x144.startEnvSeDirLevel(JA_SE_ATM_WIND_1, fxMix, 1.0f); + } +#else dDemo_c::update(); +#endif #if DEBUG dJcame_c::get()->update(); diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 355ef14450..7530a7f613 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -274,10 +274,6 @@ namespace dusk { ImGuiMenuGame::ToggleFullscreen(); } - // if (!dusk::IsGameLaunched) { - // m_preLaunchWindow.draw(); - // } - if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) { m_isHidden = !m_isHidden; } diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 1755c02856..1aee9df373 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -9,7 +9,6 @@ #include "ImGuiMenuGame.hpp" #include "ImGuiMenuTools.hpp" -#include "ImGuiPreLaunchWindow.hpp" #include "imgui.h" union SDL_Event; @@ -45,7 +44,6 @@ private: std::deque m_toasts; ImGuiMenuGame m_menuGame; - ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last ImGuiMenuTools m_menuTools; diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp deleted file mode 100644 index a0006f0ad3..0000000000 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ /dev/null @@ -1,282 +0,0 @@ -#include "imgui.h" - -#include "ImGuiConfig.hpp" -#include "ImGuiEngine.hpp" -#include "ImGuiPreLaunchWindow.hpp" - -#include "../file_select.hpp" -#include "../iso_validate.hpp" -#include "ImGuiConsole.hpp" -#include "dusk/main.h" -#include "dusk/settings.h" - -#include -#include - -#include "aurora/lib/internal.hpp" -#include "aurora/lib/window.hpp" - -namespace dusk { - -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", "*"}, -}}; - -static std::string ShowIsoInvalidError(const iso::ValidationError code) { - using namespace std::literals::string_literals; - - switch (code) { - case iso::ValidationError::IOError: - return "Unknown IO error occurred"s; - case iso::ValidationError::InvalidImage: - return "Unable to interpret selected file as a disc image"s; - case iso::ValidationError::WrongGame: - return "Selected disc image is for a different game"s; - case iso::ValidationError::WrongVersion: - return "Selected disc image is for an unsupported version of the game. Only North American GameCube (NTSC/GZ2E01) is supported at this time."s; - case iso::ValidationError::ExecutableMismatch: - return "Selected disc image contains modified executable files."s; - default: - return "Unknown error"s; - } -} - -static std::string_view card_type_name(CARDFileType type) { - switch (type) { - case CARD_GCIFOLDER: - return "GCI Folder"sv; - case CARD_RAWIMAGE: - return "Card Image"sv; - default: - return ""sv; - } -} - -void fileDialogCallback(void* userdata, const char* path, const char* error) { - auto* self = static_cast(userdata); - if (error != nullptr) { - self->m_selectedIsoPath.clear(); - self->m_errorString = fmt::format("File dialog error: {}", error); - return; - } - - if (path == nullptr) { - self->m_selectedIsoPath.clear(); - return; - } - - self->m_selectedIsoPath = path; - self->m_isPal = iso::isPal(path); - getSettings().backend.isoPath.setValue(self->m_selectedIsoPath); - config::Save(); -} - -ImGuiPreLaunchWindow::ImGuiPreLaunchWindow() = default; - -bool ImGuiPreLaunchWindow::isSelectedPathValid() const { -#if TARGET_ANDROID - return !m_selectedIsoPath.empty(); // unsure why SDL_GetPathInfo doesnt work here -#else - return !m_selectedIsoPath.empty() && SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr); -#endif -} - -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; - } - - if (isSelectedPathValid() && getSettings().backend.skipPreLaunchUI) { - dusk::IsGameLaunched = true; - return; - } - - auto& io = ImGui::GetIO(); - - ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y)); - ImGui::SetNextWindowPos(ImVec2(0, 0)); - ImGui::SetNextWindowBgAlpha(0.65f); - - ImGui::Begin("Pre Launch Window", nullptr, - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoBringToFrontOnFocus); - - const auto& windowSize = ImGui::GetWindowSize(); - - for (int i = 0; i < 5; i++) - ImGui::NewLine(); - - float iconSize = 150.f; - ImGui::SameLine(windowSize.x / 2 - iconSize + (iconSize / 2)); - if (ImGuiEngine::orgIcon != 0) { - ImGui::Image(ImGuiEngine::orgIcon, ImVec2{iconSize, iconSize}); - } - ImGuiTextCenter("Twilit Realm presents"); - if (ImGuiEngine::duskLogo) { - ImGui::NewLine(); - float width = iconSize * 2.5f; - ImGui::SameLine(windowSize.x / 2 - width + (width / 2)); - ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize}); - } else { - ImGui::PushFont(ImGuiEngine::fontExtraLarge); - ImGuiTextCenter("Dusk"); - ImGui::PopFont(); - } - - (this->*drawTable[m_CurMenu])(); - - ImGui::End(); -} - -void ImGuiPreLaunchWindow::drawMainMenu() { - const auto& windowSize = ImGui::GetWindowSize(); - ImGui::SetCursorPosY(windowSize.y - 200); - - ImGui::PushFont(ImGuiEngine::fontLarge); - - if (!isSelectedPathValid()) { - if (!m_errorString.empty()) { - ImGuiTextCenter(m_errorString); - } - - if (ImGuiButtonCenter("Select disc image...")) { - ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), - skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, - false); - } - } else { - if (ImGuiButtonCenter("Start game")) { - dusk::IsGameLaunched = true; - } - } - - if (ImGuiButtonCenter("Options")) { - m_CurMenu = 1; - } - - ImGui::PopFont(); -} - -void ImGuiPreLaunchWindow::drawOptions() { - const auto& windowSize = ImGui::GetWindowSize(); - - ImGui::NewLine(); - - ImGui::PushFont(ImGuiEngine::fontLarge); - ImGuiTextCenter("Options"); - ImGui::Separator(); - ImGui::PopFont(); - - auto cursorY = ImGui::GetCursorPosY(); - float endCursorY = windowSize.y - 100; - - float childWidth = windowSize.x - 400; - - ImGui::SetCursorPosX(windowSize.x / 2 - (childWidth / 2)); - if (ImGui::BeginChild("OptionsChild", ImVec2(childWidth, endCursorY - cursorY), - ImGuiChildFlags_None, ImGuiWindowFlags_NoBackground)) - { - if (!m_errorString.empty()) { - ImGuiTextCenter(m_errorString); - } - - ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly); - ImGui::SameLine(); - if (ImGui::Button(m_selectedIsoPath == "" ? "Set" : "Change")) { - ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(), - skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr, - false); - } - - 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(); - } - } - - AuroraBackend configuredBackend = BACKEND_AUTO; - const std::string& configuredBackendId = getSettings().backend.graphicsBackend; - if (!try_parse_backend(configuredBackendId, configuredBackend)) { - configuredBackend = BACKEND_AUTO; - } - - if (ImGui::BeginCombo("Graphics Backend", backend_name(configuredBackend).data())) { - if (ImGui::Selectable("Auto", configuredBackend == BACKEND_AUTO)) { - getSettings().backend.graphicsBackend.setValue("auto"); - config::Save(); - } - - size_t backendCount = 0; - const AuroraBackend* availableBackends = aurora_get_available_backends(&backendCount); - for (size_t i = 0; i < backendCount; ++i) { - const AuroraBackend backend = availableBackends[i]; - const bool isSelected = configuredBackend == backend; - if (ImGui::Selectable(backend_name(backend).data(), isSelected)) { - getSettings().backend.graphicsBackend.setValue( - std::string(backend_id(backend))); - config::Save(); - } - if (isSelected) { - ImGui::SetItemDefaultFocus(); - } - } - - ImGui::EndCombo(); - } - if (configuredBackendId != m_initialGraphicsBackend) { - ImGui::TextDisabled("Restart Required"); - } - auto curFileType = (CARDFileType)getSettings().backend.cardFileType.getValue(); - - if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) { - - if (ImGui::Selectable("GCI Folder", curFileType == CARD_GCIFOLDER)) { - getSettings().backend.cardFileType.setValue(CARD_GCIFOLDER); - config::Save(); - } - - if (ImGui::Selectable("Card Image", curFileType == CARD_RAWIMAGE)) { - getSettings().backend.cardFileType.setValue(CARD_RAWIMAGE); - config::Save(); - } - - ImGui::EndCombo(); - } - - ImGui::EndChild(); - } - - ImGui::SetCursorPosY(endCursorY); - ImGui::NewLine(); - - ImGui::Separator(); - - ImGui::PushFont(ImGuiEngine::fontLarge); - if (ImGuiButtonCenter("Back")) { - m_CurMenu = 0; - } - ImGui::PopFont(); -} - -} // namespace dusk diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp b/src/dusk/imgui/ImGuiPreLaunchWindow.hpp deleted file mode 100644 index 6cb078a228..0000000000 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -namespace dusk { -class ImGuiPreLaunchWindow { -private: - int m_CurMenu = 0; - bool m_IsFirstDraw = true; - std::string m_initialGraphicsBackend; - - bool isSelectedPathValid() const; - -public: - ImGuiPreLaunchWindow(); - void draw(); - - void drawMainMenu(); - void drawOptions(); - - std::string m_selectedIsoPath; - std::string m_errorString; - bool m_isPal = false; -}; -} // namespace dusk diff --git a/src/dusk/main.cpp b/src/dusk/main.cpp index 22cd5a9fc6..e1b2fd0b6e 100644 --- a/src/dusk/main.cpp +++ b/src/dusk/main.cpp @@ -5,17 +5,110 @@ #endif #include +#include "dusk/main.h" +#include +#include +#include #include #include +#include +#include #include #include #include +#if !defined(_WIN32) +#include +#if defined(__APPLE__) +#include +#endif +#endif + int game_main(int argc, char* argv[]); namespace { +bool RestartProcess(int argc, char* argv[]) { +#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ + (defined(TARGET_OS_TV) && TARGET_OS_TV) + (void)argc; + (void)argv; + return false; +#elif _WIN32 + std::wstring commandLine = GetCommandLineW(); + STARTUPINFOW startupInfo{}; + startupInfo.cb = sizeof(startupInfo); + PROCESS_INFORMATION processInfo{}; + if (!CreateProcessW(nullptr, commandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, + &startupInfo, &processInfo)) + { + fprintf(stderr, "Failed to restart Dusk: CreateProcessW error %lu\n", GetLastError()); + return false; + } + + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); + return true; +#else + std::filesystem::path executablePath; + +#if defined(__APPLE__) + uint32_t pathSize = 0; + _NSGetExecutablePath(nullptr, &pathSize); + if (pathSize > 0) { + std::string path(pathSize, '\0'); + if (_NSGetExecutablePath(path.data(), &pathSize) == 0) { + path.resize(std::strlen(path.c_str())); + std::error_code ec; + executablePath = std::filesystem::weakly_canonical(path, ec); + if (ec) { + executablePath = path; + } + } + } +#elif defined(__linux__) + std::array path{}; + const ssize_t len = readlink("/proc/self/exe", path.data(), path.size() - 1); + if (len > 0) { + path[static_cast(len)] = '\0'; + executablePath = path.data(); + } +#endif + + if (executablePath.empty() && argc > 0 && argv[0] != nullptr && argv[0][0] != '\0') { + std::error_code ec; + executablePath = std::filesystem::absolute(argv[0], ec); + if (ec) { + executablePath = argv[0]; + } + } + + if (executablePath.empty()) { + fprintf(stderr, "Failed to restart Dusk: unable to resolve executable path\n"); + return false; + } + + std::vector args; + args.reserve(static_cast(std::max(argc, 1))); + args.push_back(executablePath.string()); + for (int i = 1; i < argc; ++i) { + args.emplace_back(argv[i] != nullptr ? argv[i] : ""); + } + + std::vector execArgv; + execArgv.reserve(args.size() + 1); + for (auto& arg : args) { + execArgv.push_back(arg.data()); + } + execArgv.push_back(nullptr); + + execv(executablePath.c_str(), execArgv.data()); + fprintf(stderr, "Failed to restart Dusk: execv failed: %s\n", std::strerror(errno)); + return false; +#endif +} + #if _WIN32 bool ShouldShowWindowsConsole(int argc, char* argv[]) { if (const auto* env = std::getenv("DUSK_CONSOLE")) { @@ -53,19 +146,25 @@ void WindowsSetupConsole(bool showConsole) { SetConsoleOutputCP(CP_UTF8); if (const HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); - stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) { + stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) + { DWORD consoleMode = 0; if (GetConsoleMode(stdoutHandle, &consoleMode)) { SetConsoleMode(stdoutHandle, - consoleMode | ENABLE_PROCESSED_OUTPUT - | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + consoleMode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); } } } int DuskMain(int argc, char* argv[]) { WindowsSetupConsole(ShouldShowWindowsConsole(argc, argv)); - return game_main(argc, argv); + const int result = game_main(argc, argv); + if constexpr (dusk::SupportsProcessRestart) { + if (dusk::RestartRequested) { + return RestartProcess(argc, argv) ? 0 : result; + } + } + return result; } std::vector WideArgsToUtf8(int argc, wchar_t** argv) { @@ -81,8 +180,8 @@ std::vector WideArgsToUtf8(int argc, wchar_t** argv) { } std::vector utf8Buffer(static_cast(requiredSize)); - WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr, - nullptr); + WideCharToMultiByte( + CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr, nullptr); utf8Args.emplace_back(utf8Buffer.data()); } @@ -109,7 +208,11 @@ int RunWindowsGuiEntryPoint() { } #else int DuskMain(int argc, char* argv[]) { - return game_main(argc, argv); + const int result = game_main(argc, argv); + if (dusk::RestartRequested && RestartProcess(argc, argv)) { + return 0; + } + return result; } #endif diff --git a/src/dusk/ui/component.cpp b/src/dusk/ui/component.cpp index 466420eb1e..748df848be 100644 --- a/src/dusk/ui/component.cpp +++ b/src/dusk/ui/component.cpp @@ -59,16 +59,6 @@ void Component::set_disabled(bool value) { } } -Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) { - if (parent == nullptr) { - return nullptr; - } - auto* doc = parent->GetOwnerDocument(); - if (doc == nullptr) { - return nullptr; - } - return parent->AppendChild(doc->CreateElement(tag)); -} void Component::listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback, bool capture) { if (element == nullptr) { diff --git a/src/dusk/ui/component.hpp b/src/dusk/ui/component.hpp index 0c49ce3254..e0a602e7fd 100644 --- a/src/dusk/ui/component.hpp +++ b/src/dusk/ui/component.hpp @@ -47,7 +47,6 @@ public: Rml::Element* root() const { return mRoot; } protected: - static Rml::Element* append(Rml::Element* parent, const Rml::String& tag); void clear_children(); Rml::Element* mRoot = nullptr; diff --git a/src/dusk/ui/modal.cpp b/src/dusk/ui/modal.cpp new file mode 100644 index 0000000000..93ea060b51 --- /dev/null +++ b/src/dusk/ui/modal.cpp @@ -0,0 +1,75 @@ +#include "modal.hpp" + +namespace dusk::ui { + +Modal::Modal(Props props) + : WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) { + auto* title = append(mDialog, "div"); + title->SetClass("preset-title", true); + title->SetInnerRML(mProps.title); + + auto* body = append(mDialog, "div"); + body->SetClass("preset-intro", true); + body->SetInnerRML(mProps.bodyRml); + + auto* actions = append(mDialog, "div"); + actions->SetClass("modal-actions", true); + + for (auto& action : mProps.actions) { + auto btn = std::make_unique +
@@ -114,6 +123,44 @@ struct DiscVerificationTask { std::unique_ptr sDiscVerificationTask; bool sDiscVerificationModalPushed = false; +struct UpdateCheckTask { + UpdateCheckTask() { + worker = std::thread([this] { + try { + result = update_check::check_latest_github_release("TwilitRealm", "dusk"); + } catch (const std::exception& e) { + result = { + .status = update_check::Status::Failed, + .message = fmt::format("Update check failed with exception: {}", e.what()), + }; + } catch (...) { + result = { + .status = update_check::Status::Failed, + .message = "Update check failed with an unknown exception", + }; + } + done.store(true, std::memory_order_release); + }); + } + + ~UpdateCheckTask() { join(); } + + void join() { + if (worker.joinable()) { + worker.join(); + } + } + + [[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); } + + update_check::Result result; + std::atomic_bool done = false; + std::thread worker; +}; + +std::unique_ptr sUpdateCheckTask; +std::optional sUpdateCheckResult; + bool verification_state_allows_launch(iso::ValidationError validation) noexcept { return validation == iso::ValidationError::Unknown || validation == iso::ValidationError::Success || @@ -185,6 +232,52 @@ std::optional take_finished_disc_verification() { return result; } +void begin_update_check() { + if (!getSettings().backend.checkForUpdates.getValue()) { + return; + } + if (sUpdateCheckTask != nullptr || sUpdateCheckResult.has_value()) { + return; + } + sUpdateCheckTask = std::make_unique(); +} + +std::optional take_finished_update_check() { + if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) { + return std::nullopt; + } + + sUpdateCheckTask->join(); + auto result = std::move(sUpdateCheckTask->result); + sUpdateCheckTask.reset(); + return result; +} + +std::string update_release_label(const update_check::Release& release) { + std::string_view tagName = release.tagName; + if (!tagName.empty() && tagName.front() == 'v') { + tagName.remove_prefix(1); + } + return std::string(tagName); +} + +void open_update_release() { + if (!sUpdateCheckResult.has_value() || + sUpdateCheckResult->status != update_check::Status::UpdateAvailable) + { + return; + } + + const std::string url = sUpdateCheckResult->latest.htmlUrl; + if (url.empty()) { + PrelaunchLog.warn("Update is available, but the release did not include a download URL"); + return; + } + if (!SDL_OpenURL(url.c_str())) { + PrelaunchLog.warn("Failed to open update URL '{}': {}", url, SDL_GetError()); + } +} + std::string get_error_msg(iso::ValidationError error) { switch (error) { default: @@ -582,6 +675,7 @@ void try_apply_mirrored_layout(Rml::Element* body) { Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) { ensure_initialized(); + begin_update_check(); if (auto* menuList = mDocument->GetElementById("menu-list")) { auto& state = prelaunch_state(); @@ -629,6 +723,23 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB mDiscStatus = mDocument->GetElementById("disc-status"); mDiscDetail = mDocument->GetElementById("disc-version"); mVersion = mDocument->GetElementById("version-text"); + mUpdateStatus = mDocument->GetElementById("update-status"); + mUpdateMessage = mDocument->GetElementById("update-message"); + mUpdateDownload = mDocument->GetElementById("update-download"); + mUpdateDownloadLabel = mDocument->GetElementById("update-download-label"); + + if (mUpdateDownload != nullptr) { + listen(mUpdateDownload, Rml::EventId::Click, [](Rml::Event& event) { + open_update_release(); + event.StopPropagation(); + }); + listen(mUpdateDownload, Rml::EventId::Keydown, [](Rml::Event& event) { + if (map_nav_event(event) == NavCommand::Confirm) { + open_update_release(); + event.StopPropagation(); + } + }); + } try_apply_mirrored_layout(mDocument); @@ -767,6 +878,34 @@ void Prelaunch::update() { } mVersion->SetInnerRML(escape(versionStr)); } + if (mUpdateStatus != nullptr && mUpdateMessage != nullptr) { + if (auto result = take_finished_update_check()) { + if (result->status == update_check::Status::Failed) { + PrelaunchLog.error("Failed to check for updates: {}", result->message); + } + sUpdateCheckResult = std::move(*result); + } + + if (sUpdateCheckTask != nullptr) { + mUpdateStatus->SetAttribute("state", "checking"); + mUpdateMessage->SetInnerRML("Checking for updates..."); + } else if (!sUpdateCheckResult.has_value() || + sUpdateCheckResult->status == update_check::Status::UpToDate) + { + mUpdateStatus->RemoveAttribute("state"); + mUpdateMessage->SetInnerRML(""); + } else if (sUpdateCheckResult->status == update_check::Status::UpdateAvailable) { + mUpdateStatus->SetAttribute("state", "available"); + mUpdateMessage->SetInnerRML("Update available!"); + if (mUpdateDownloadLabel != nullptr) { + mUpdateDownloadLabel->SetInnerRML(escape( + fmt::format("Download {}", update_release_label(sUpdateCheckResult->latest)))); + } + } else { + mUpdateStatus->SetAttribute("state", "failed"); + mUpdateMessage->SetInnerRML("Failed to check for updates"); + } + } Document::update(); } diff --git a/src/dusk/ui/prelaunch.hpp b/src/dusk/ui/prelaunch.hpp index 95ba2238f4..0fbb64ded9 100644 --- a/src/dusk/ui/prelaunch.hpp +++ b/src/dusk/ui/prelaunch.hpp @@ -31,6 +31,10 @@ private: Rml::Element* mDiscStatus = nullptr; Rml::Element* mDiscDetail = nullptr; Rml::Element* mVersion = nullptr; + Rml::Element* mUpdateStatus = nullptr; + Rml::Element* mUpdateMessage = nullptr; + Rml::Element* mUpdateDownload = nullptr; + Rml::Element* mUpdateDownloadLabel = nullptr; }; class PrelaunchOptions; diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 972dc1421b..6f42f555c7 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -917,6 +917,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .key = "Show Pipeline Compilation", .helpText = "Show an overlay when shaders are being compiled for your hardware.", }); + config_bool_select(leftPane, rightPane, getSettings().backend.checkForUpdates, + { + .key = "Check for Updates", + .helpText = "Checks GitHub releases for a new Dusk version on startup.

" + "No personal information is transmitted or collected.", + }); config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings, { .key = "Enable Advanced Settings", diff --git a/src/dusk/update_check.cpp b/src/dusk/update_check.cpp new file mode 100644 index 0000000000..11c11795af --- /dev/null +++ b/src/dusk/update_check.cpp @@ -0,0 +1,196 @@ +#include "update_check.hpp" + +#include "dusk/http/http.hpp" +#include "fmt/format.h" +#include "nlohmann/json.hpp" +#include "version.h" + +#include +#include +#include + +namespace dusk::update_check { +namespace { + +using json = nlohmann::json; + +constexpr std::string_view GitHubApiVersion = "2026-03-10"; + +struct Version { + int major = 0; + int minor = 0; + int patch = 0; + + friend auto operator<=>(const Version&, const Version&) = default; +}; + +std::string json_string(const json& value, const char* key) { + const auto iter = value.find(key); + if (iter == value.end() || !iter->is_string()) { + return {}; + } + return iter->get(); +} + +std::optional parse_component(std::string_view& value) { + if (value.empty() || value.front() < '0' || value.front() > '9') { + return std::nullopt; + } + + int parsed = 0; + const char* begin = value.data(); + const char* end = value.data() + value.size(); + const auto [ptr, ec] = std::from_chars(begin, end, parsed); + if (ec != std::errc()) { + return std::nullopt; + } + + value.remove_prefix(static_cast(ptr - begin)); + return parsed; +} + +bool consume(std::string_view& value, char expected) { + if (value.empty() || value.front() != expected) { + return false; + } + value.remove_prefix(1); + return true; +} + +std::optional parse_version(std::string_view value) { + if (!value.empty() && value.front() == 'v') { + value.remove_prefix(1); + } + + Version version; + auto major = parse_component(value); + if (!major || !consume(value, '.')) { + return std::nullopt; + } + auto minor = parse_component(value); + if (!minor || !consume(value, '.')) { + return std::nullopt; + } + auto patch = parse_component(value); + if (!patch) { + return std::nullopt; + } + if (!value.empty() && value.front() != '-' && value.front() != '+') { + return std::nullopt; + } + + version.major = *major; + version.minor = *minor; + version.patch = *patch; + return version; +} + +Release parse_release(const json& value) { + Release release{ + .tagName = json_string(value, "tag_name"), + .name = json_string(value, "name"), + .htmlUrl = json_string(value, "html_url"), + .body = json_string(value, "body"), + }; + + const auto assets = value.find("assets"); + if (assets != value.end() && assets->is_array()) { + for (const auto& asset : *assets) { + if (!asset.is_object()) { + continue; + } + release.assets.push_back({ + .name = json_string(asset, "name"), + .browserDownloadUrl = json_string(asset, "browser_download_url"), + .digest = json_string(asset, "digest"), + }); + } + } + + return release; +} + +std::string release_url(std::string_view owner, std::string_view repo) { + return fmt::format("https://api.github.com/repos/{}/{}/releases/latest", owner, repo); +} + +std::string user_agent() { + return fmt::format("Dusk/{}", DUSK_WC_DESCRIBE); +} + +} // namespace + +Result check_latest_github_release(std::string_view owner, std::string_view repo) { + if (!http::available()) { + return { + .status = Status::Disabled, + .message = "No HTTP backend is available", + }; + } + if (owner.empty() || repo.empty()) { + return { + .status = Status::Failed, + .message = "GitHub owner and repo are required", + }; + } + + http::Request request{ + .url = release_url(owner, repo), + .headers = + { + {.name = "User-Agent", .value = user_agent()}, + {.name = "Accept", .value = "application/vnd.github+json"}, + {.name = "X-GitHub-Api-Version", .value = std::string(GitHubApiVersion)}, + }, + }; + + http::Result result = http::get(request); + if (result.error != http::Error::None) { + return { + .status = Status::Failed, + .message = result.message, + }; + } + if (result.response.statusCode != 200) { + return { + .status = Status::Failed, + .message = fmt::format("GitHub returned HTTP {}", result.response.statusCode), + }; + } + + Release latest; + try { + latest = parse_release(json::parse(result.response.body)); + } catch (const std::exception& e) { + return { + .status = Status::Failed, + .message = fmt::format("Failed to parse GitHub release JSON: {}", e.what()), + }; + } + + const std::optional latestVersion = parse_version(latest.tagName); + const std::optional currentVersion = parse_version(DUSK_WC_DESCRIBE); + if (!latestVersion) { + return { + .status = Status::Failed, + .message = fmt::format("Failed to parse release tag '{}'", latest.tagName), + .latest = std::move(latest), + }; + } + if (!currentVersion) { + return { + .status = Status::Failed, + .message = fmt::format("Failed to parse Dusk version '{}'", DUSK_WC_DESCRIBE), + .latest = std::move(latest), + }; + } + + const bool updateAvailable = *latestVersion > *currentVersion; + return { + .status = updateAvailable ? Status::UpdateAvailable : Status::UpToDate, + .message = updateAvailable ? "Update available" : "Dusk is up to date", + .latest = std::move(latest), + }; +} + +} // namespace dusk::update_check diff --git a/src/dusk/update_check.hpp b/src/dusk/update_check.hpp new file mode 100644 index 0000000000..72c66449dc --- /dev/null +++ b/src/dusk/update_check.hpp @@ -0,0 +1,41 @@ +#ifndef DUSK_UPDATE_CHECK_HPP +#define DUSK_UPDATE_CHECK_HPP + +#include +#include +#include + +namespace dusk::update_check { + +enum class Status { + Disabled, + UpToDate, + UpdateAvailable, + Failed, +}; + +struct Asset { + std::string name; + std::string browserDownloadUrl; + std::string digest; +}; + +struct Release { + std::string tagName; + std::string name; + std::string htmlUrl; + std::string body; + std::vector assets; +}; + +struct Result { + Status status = Status::Failed; + std::string message; + Release latest; +}; + +Result check_latest_github_release(std::string_view owner, std::string_view repo); + +} // namespace dusk::update_check + +#endif // DUSK_UPDATE_CHECK_HPP From 73a3bd9ae83f76bb61bba36a4ce568f717276ce5 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Fri, 8 May 2026 10:53:01 -0400 Subject: [PATCH 68/71] super clawshot warning about chains (#716) Co-authored-by: MelonSpeedruns --- src/dusk/ui/settings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 6f42f555c7..7158a2460a 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -871,7 +871,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { addCheat( "Moon Jump (R+A)", getSettings().game.moonJump, "Hold R and A to rise into the air."); addCheat("Super Clawshot", getSettings().game.superClawshot, - "Extends clawshot behavior beyond the normal game rules."); + "Extends Clawshot behavior beyond the normal game rules.
This will disable chains from rendering to prevent crashes."); addCheat("Always Greatspin", getSettings().game.alwaysGreatspin, "Allows the Great Spin attack without requiring full health."); addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots, From 97a11907135ae90e0cd6a6a1861bc6cf5a734320 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Fri, 8 May 2026 08:59:13 -0600 Subject: [PATCH 69/71] set max chain links to 600 --- src/d/actor/d_a_alink_hook.inc | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc index 73049af08b..b45adddf9a 100644 --- a/src/d/actor/d_a_alink_hook.inc +++ b/src/d/actor/d_a_alink_hook.inc @@ -17,11 +17,11 @@ enum { HS_MODE_RETURN_e = 6, }; -void daAlink_c::hsChainShape_c::draw() { - if (dusk::getSettings().game.superClawshot) { - return; - } +#if TARGET_PC +static const int HS_CHAIN_MAX_LINKS = 600; +#endif +void daAlink_c::hsChainShape_c::draw() { static const int dummy = 0; daAlink_c* alink = (daAlink_c*)getUserArea(); @@ -165,7 +165,14 @@ void daAlink_c::hsChainShape_c::draw() { } (void)0; - while (maxDistanceF > var_f30) { +#if TARGET_PC + int chainLinks = 0; +#endif + while (maxDistanceF > var_f30 +#if TARGET_PC + && chainLinks < HS_CHAIN_MAX_LINKS +#endif + ) { temp_f27 = var_f28 * cM_fsin(sp34 * var_f30); s16 spC = cM_atan2s(temp_f27 - var_f26, 5.0f); sp64.x = sp6C.x + spC; @@ -187,6 +194,9 @@ void daAlink_c::hsChainShape_c::draw() { var_f26 = temp_f27; var_f30 += fabsf(cM_scos(spC)) * 5.0f; +#if TARGET_PC + chainLinks++; +#endif } } @@ -202,7 +212,14 @@ void daAlink_c::hsChainShape_c::draw() { sp98 = subChainTopPos; sp6C.set(maxDistance.atan2sY_XZ(), maxDistance.atan2sX_Z(), 0); - while (maxDistanceF > var_f30) { +#if TARGET_PC + int subChainLinks = 0; +#endif + while (maxDistanceF > var_f30 +#if TARGET_PC + && subChainLinks < HS_CHAIN_MAX_LINKS +#endif + ) { mDoMtx_stack_c::copy(j3dSys.getViewMtx()); mDoMtx_stack_c::transM(sp98); mDoMtx_stack_c::ZXYrotM(sp6C); @@ -215,6 +232,9 @@ void daAlink_c::hsChainShape_c::draw() { sp98 += maxDistance * 5.0f; ANGLE_ADD_2(sp6C.z, 0x3000); var_f30 += 5.0f; +#if TARGET_PC + subChainLinks++; +#endif } } } From 6fd3762ffcd624bad5cd2b22ba501c9ad52a827e Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Fri, 8 May 2026 12:06:38 -0400 Subject: [PATCH 70/71] optimized code --- src/d/actor/d_a_alink_hook.inc | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc index b45adddf9a..c960d37a7b 100644 --- a/src/d/actor/d_a_alink_hook.inc +++ b/src/d/actor/d_a_alink_hook.inc @@ -168,11 +168,8 @@ void daAlink_c::hsChainShape_c::draw() { #if TARGET_PC int chainLinks = 0; #endif - while (maxDistanceF > var_f30 -#if TARGET_PC - && chainLinks < HS_CHAIN_MAX_LINKS -#endif - ) { + + while (maxDistanceF > var_f30 IF_DUSK(&&chainLinks < HS_CHAIN_MAX_LINKS)) { temp_f27 = var_f28 * cM_fsin(sp34 * var_f30); s16 spC = cM_atan2s(temp_f27 - var_f26, 5.0f); sp64.x = sp6C.x + spC; @@ -194,6 +191,7 @@ void daAlink_c::hsChainShape_c::draw() { var_f26 = temp_f27; var_f30 += fabsf(cM_scos(spC)) * 5.0f; + #if TARGET_PC chainLinks++; #endif @@ -215,11 +213,8 @@ void daAlink_c::hsChainShape_c::draw() { #if TARGET_PC int subChainLinks = 0; #endif - while (maxDistanceF > var_f30 -#if TARGET_PC - && subChainLinks < HS_CHAIN_MAX_LINKS -#endif - ) { + + while (maxDistanceF > var_f30 IF_DUSK(&&subChainLinks < HS_CHAIN_MAX_LINKS)) { mDoMtx_stack_c::copy(j3dSys.getViewMtx()); mDoMtx_stack_c::transM(sp98); mDoMtx_stack_c::ZXYrotM(sp6C); From 3934e09c8fca590a02f8075b47b315c9a2efb662 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Fri, 8 May 2026 12:07:00 -0400 Subject: [PATCH 71/71] remove mention of chains not rendering --- src/dusk/ui/settings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 7158a2460a..e41dbbe46a 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -871,7 +871,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { addCheat( "Moon Jump (R+A)", getSettings().game.moonJump, "Hold R and A to rise into the air."); addCheat("Super Clawshot", getSettings().game.superClawshot, - "Extends Clawshot behavior beyond the normal game rules.
This will disable chains from rendering to prevent crashes."); + "Extends Clawshot behavior beyond the normal game rules."); addCheat("Always Greatspin", getSettings().game.alwaysGreatspin, "Allows the Great Spin attack without requiring full health."); addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots,