Compare commits

...

77 Commits

Author SHA1 Message Date
Luke Street 5fdf954994 Reduce autosave spinner opacity 2026-05-05 23:25:06 -06:00
Luke Street 230868af3c Add autosave spinner & make RmlUi respect 4:3
Resolves #627
2026-05-05 23:18:11 -06:00
doop 66154d9de8 Remove post-trim scissor (#684)
* Remove post-trim scissor

Fixes #679

* Add comment
2026-05-05 19:41:59 -06:00
Luke Street 93ec3c7dbd New UI: Achievement toasts (#690)
* Rename Overlay -> GraphicsTuner, Popup -> MenuBar

* Update GraphicsTuner CSS

* WIP Overlay document & toasts

* Achievement toasts

* Cleanup
2026-05-05 19:29:42 -06:00
MelonSpeedruns 945ce3e4bc Alternate RMLUI Menu Sounds (#686)
* Alternate RMLUI Menu Sounds

Those fit more the game I feel like.

* swapped tab sounds

* pressing A on tab button plays the OK sound

* Fix tab sound + Added menu sounds to prelaunch menu

* Centralize UI sound definitions

* Improvements

* Add "Play" button sound

* Use kSoundItemFocus in prelaunch

* Oops

* Update play/enable/disable sounds

---------

Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
Co-authored-by: Luke Street <luke@street.dev>
2026-05-05 16:44:11 -06:00
TakaRikka 782a8573e9 Merge pull request #570 from TwilitRealm/mapmirror
fix main map for mirror mode
2026-05-05 14:22:25 -07:00
Irastris 4e0ca51159 Remove restart notice from cardFileType's rightPane 2026-05-05 17:03:04 -04:00
Irastris 3cd160e1b2 Defer mDoMemCd_ThdInit if necessary, removing restart requirement
Thanks, Melon
2026-05-05 16:56:41 -04:00
Luke Street 93a236a9d2 Prelaunch settings: actually save config 2026-05-05 14:47:53 -06:00
Irastris eaf3bc2f40 Remove skipWarningScreen config option, always skip logos 2026-05-05 16:27:41 -04:00
Luke Street 5ca0a2ba06 UI: Fix focus reconciliation for context root 2026-05-05 14:17:57 -06:00
Luke Street ccd2bdbaac Add full Settings window to prelaunch 2026-05-05 14:12:53 -06:00
Irastris 08321699cd 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 <luke@street.dev>
2026-05-05 12:18:25 -06:00
Luke Street 7300c0e0f5 Update aurora 2026-05-05 12:15:01 -06:00
Luke Street 7e562824fe Add a <br> 2026-05-05 11:30:49 -06:00
Luke Street ed8b5c96b9 Update aurora & add logo to README.md 2026-05-05 11:28:05 -06:00
TakaRikka 14bccdffa6 fix mirror map portal selection 2026-05-05 06:25:46 -07:00
TakaRikka 39e465bcab fix mirror map region selection 2026-05-05 06:22:49 -07:00
TakaRikka e53bb3a12d Merge branch 'main' of github.com:TwilitRealm/dusk into mapmirror 2026-05-05 04:59:25 -07:00
TakaRikka 50fccd393f fix mirror mode text / rodeo direction 2026-05-05 04:44:14 -07:00
TakaRikka 7993740ac8 fix kargorok mirror mode controls 2026-05-05 01:16:01 -07:00
TakaRikka 8e5bb8ae59 Merge pull request #676 from TwilitRealm/instant-text-tweaks
Instant text tweaks
2026-05-04 23:43:43 -07:00
TakaRikka 8fefdd4114 Merge pull request #669 from TwilitRealm/remove-discord-rpc
Remove discord-rpc/rapidjson; roll our own
2026-05-04 23:43:24 -07:00
gymnast86 64c8cee21b revert to checking for Hold B at the start of do_isReady 2026-05-04 23:08:01 -07:00
gymnast86 25fe686573 add message ids for not being able to buy bombs from barnes 2026-05-04 23:07:23 -07:00
Luke Street 1c1ea98fdd Update aurora 2026-05-04 23:35:21 -06:00
TakaRikka e098104f8f Merge pull request #642 from TwilitRealm/debug-fly-cam
Add fly mode option to debug cam
2026-05-04 19:25:19 -07:00
Pheenoh 3c5ade5565 don't allow activation if paused or in event 2026-05-04 20:15:02 -06:00
Pheenoh b2ad75027e Merge branch 'main' into debug-fly-cam 2026-05-04 19:26:31 -06:00
TakaRikka fdfbf83b88 Merge pull request #660 from TwilitRealm/fix/fix-ub
Fix a UB in Morpheel
2026-05-04 18:24:16 -07:00
SuperDude88 1b9ca0949e Clear Description on More Hovers (#673)
- Properly clear the previous option description when you hover over "toggle fullscreen" or "restore default window size"

Fixes #671
2026-05-04 19:17:39 -06:00
Luke Street 827037f0fa Update aurora for RmlUi base path fix 2026-05-04 18:27:50 -06:00
Luke Street d84c5790f5 NOMINMAX 2026-05-04 16:27:12 -06:00
Luke Street 49eb2282af Forgor to add 2026-05-04 16:20:30 -06:00
Luke Street 741f9ecfab Remove discord-rpc/rapidjson; roll our own 2026-05-04 16:14:57 -06:00
Luke Street 37b8122962 UI: Refactor pane control to allow hover for help text 2026-05-04 14:16:35 -06:00
doop 5f2cf68e80 Minor UI nits 2026-05-04 19:38:11 +00:00
Luke Street 74f2c58b29 UI: Preset tweaks 2026-05-04 13:08:20 -06:00
madeline 208433047a Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-05-04 12:06:28 -07:00
madeline 2c01430035 fix stereo audio with mirror mode fixes #666 2026-05-04 12:06:24 -07:00
Luke Street 5121437bcf UI: Avoid looping at end of window content 2026-05-04 11:49:12 -06:00
Luke Street f61bd3e5ad UI: Reorder settings tabs 2026-05-04 11:31:09 -06:00
Luke Street 835e409b32 UI: Fix prelaunch options close anim 2026-05-04 11:27:31 -06:00
Luke Street 010bdb7e25 UI: Fix left/right nav from content to change tabs 2026-05-04 11:17:48 -06:00
Luke Street 7ba22b7714 UI: Refocus top document automatically 2026-05-04 11:14:40 -06:00
Luke Street efcb19a3d0 Revert hide change to popup menu 2026-05-04 10:49:57 -06:00
Luke Street 55455bb1b5 Adjust presets & default settings 2026-05-04 10:43:42 -06:00
qwertyquerty e49be12297 RmlUi improvements (#663)
* rmlui audio

* fix menu select sound

* Fixes #662

* fix reset logic and fix popup getting stuck closed

* fix X button on menu popup

* rmlui achievements, and fix open/close bug

* presets, achievements css, and menu sounds toggle

* forgor

* fix b button causing audio when menu not visible
2026-05-04 10:25:13 -06:00
MelonSpeedruns 75f4940f5e set field_0xbaf variable like all the other conditions 2026-05-04 11:29:29 -04:00
MelonSpeedruns 8047330952 oops i suck 2026-05-04 09:55:01 -04:00
MelonSpeedruns 9105dcb078 remove empty line 2026-05-04 09:54:24 -04:00
MelonSpeedruns b8e38e03e2 Apply PJB request 2026-05-04 09:54:00 -04:00
MelonSpeedruns 331352878e Fix a UB in morpheel 2026-05-04 08:41:19 -04:00
Luke Street 62a88f1e9a UI: Add warning icons to experimental settings 2026-05-03 23:44:38 -06:00
Luke Street 43b603e70b UI: 3-finger tap to toggle menu on mobile 2026-05-03 22:39:31 -06:00
doop 95e6ac54cf Random UI and README tweaks (#653) 2026-05-03 22:02:09 -06:00
Luke Street c4b2e2e501 Make prelaunch fade out smoother 2026-05-03 22:01:02 -06:00
Luke Street dccba23980 Update aurora 2026-05-03 20:47:14 -06:00
Luke Street bf27d10519 Revert "Convert UI fonts to WOFF2 (#647)" (#648)
This reverts commit 6c27011e32.
2026-05-03 20:41:24 -06:00
doop 6c27011e32 Convert UI fonts to WOFF2 (#647)
* Convert UI fonts to WOFF2

* ...except for the ones we use for ImGui
2026-05-03 20:36:55 -06:00
Luke Street 6220990dc5 UI: Use decorator: text for close button 2026-05-03 19:01:44 -06:00
Phillip Stephens 93e9767c9f Fix OSReport crash on linux, harden FormatToString against malformed strings (#640)
* Fix OSReport crash on linux, harden FormatToString against malformed strings

* Move size increment to after error check

* Update aurora and impl OSPanic

---------

Co-authored-by: Luke Street <luke@street.dev>
2026-05-03 18:50:49 -06:00
Luke Street c774f53dad UI: Fix L/R to switch tabs
Resolves #645
2026-05-03 18:36:06 -06:00
Luke Street 7fbfe5ad88 UI: Add close button to tab bar 2026-05-03 18:29:08 -06:00
Luke Street ef02037990 Remove ESC to exit fullscreen 2026-05-03 17:26:17 -06:00
Luke Street 23cc18ba0e UI: Press ESC to unbind a button
Resolves #562
2026-05-03 17:26:17 -06:00
Luke Street 924dbc7715 UI: Implement controller config
Resolves #622
2026-05-03 17:26:17 -06:00
TakaRikka 742f4938f2 Merge pull request #641 from TwilitRealm/fix/klift00
Frame Interp: Fix obj_klift00 chains
2026-05-03 15:32:35 -07:00
TakaRikka 02e0f586d3 Merge pull request #639 from TwilitRealm/fix/fyer_cs
Frame Interp: Fix Fyer canon repair cutscene
2026-05-03 15:31:09 -07:00
Luke Street 5717aeef85 UI: Bold modified settings values 2026-05-03 16:01:33 -06:00
Luke Street da9b99f650 Update aurora 2026-05-03 15:28:03 -06:00
Pheenoh 901ce2ee4c add debug fly cam option 2026-05-03 15:23:59 -06:00
Pheenoh dd2b993cd5 frame interp: fix obj_klift00 chains 2026-05-03 09:58:31 -06:00
Pheenoh 83577d3b82 frame interp: fix fyer canon repair cs 2026-05-03 09:20:41 -06:00
TakaRikka eaae8b6137 Merge branch 'main' of github.com:TwilitRealm/dusk into mapmirror 2026-04-26 17:37:30 -07:00
TakaRikka d9a0ef760f fix icon positions 2026-04-16 19:46:02 -07:00
TakaRikka 95470b830f wip map mirror 2026-04-16 05:55:46 -07:00
108 changed files with 6146 additions and 2767 deletions
+10 -41
View File
@@ -297,8 +297,10 @@ set(GAME_INCLUDE_DIRS
extern
${CMAKE_BINARY_DIR})
find_package(Threads REQUIRED)
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 fmt::fmt)
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
Threads::Threads)
list(APPEND GAME_LIBS libzstd_static)
@@ -320,46 +322,13 @@ if (DUSK_MOVIE_SUPPORT)
list(APPEND GAME_COMPILE_DEFS MOVIE_SUPPORT=1)
endif ()
option(DUSK_ENABLE_DISCORD_RPC "Enable Discord Rich Presence support" ON)
if (DUSK_ENABLE_DISCORD_RPC AND NOT ANDROID AND NOT IOS AND NOT TVOS)
FetchContent_Populate(discord_rpc
URL https://github.com/discord/discord-rpc/archive/refs/tags/v3.4.0.tar.gz
URL_HASH SHA256=e13427019027acd187352dacba6c65953af66fdf3c35fcf38fc40b454a9d7855
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# RapidJSON is a git submodule absent from the discord-rpc tarball; fetch separately.
FetchContent_Populate(rapidjson
URL https://github.com/Tencent/rapidjson/archive/refs/tags/v1.1.0.tar.gz
URL_HASH SHA256=bf7ced29704a1e696fbccf2a2b4ea068e7774fa37f6d7dd4039d0787f8bed98e
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
if (NOT TARGET discord-rpc)
set(_drpc ${discord_rpc_SOURCE_DIR}/src)
set(_drpc_src
${_drpc}/discord_rpc.cpp
${_drpc}/rpc_connection.cpp
${_drpc}/serialization.cpp
)
if (WIN32)
list(APPEND _drpc_src ${_drpc}/connection_win.cpp ${_drpc}/discord_register_win.cpp)
elseif (APPLE)
list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_osx.m)
else ()
list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_linux.cpp)
endif ()
add_library(discord-rpc STATIC ${_drpc_src})
target_include_directories(discord-rpc PUBLIC
${discord_rpc_SOURCE_DIR}/include
${rapidjson_SOURCE_DIR}/include
)
if (UNIX)
target_link_libraries(discord-rpc PUBLIC pthread)
endif ()
endif ()
list(APPEND GAME_LIBS discord-rpc)
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD_RPC=1)
set(DUSK_ENABLE_DISCORD_DEFAULT ON)
if (DEFINED DUSK_ENABLE_DISCORD_RPC AND NOT DEFINED DUSK_ENABLE_DISCORD)
set(DUSK_ENABLE_DISCORD_DEFAULT ${DUSK_ENABLE_DISCORD_RPC})
endif ()
option(DUSK_ENABLE_DISCORD "Enable Discord Rich Presence support" ${DUSK_ENABLE_DISCORD_DEFAULT})
if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
endif ()
# Edit & Continue
+34 -16
View File
@@ -1,37 +1,55 @@
![DuskLogo](res/logo-mascot.png)
<div align="center">
<img src="res/logo-mascot.png" alt="Logo" width="640">
- ### **[Official Website](https://twilitrealm.dev)**
- ### **[Discord](https://discord.gg/QACynxeyna)**
<p align="center">
<a href="https://twilitrealm.dev">Official Website</a>
<a href="https://discord.gg/QACynxeyna">Discord</a>
</p>
</div>
# 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.**
### 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.
> [!IMPORTANT]
> Dusk does *not* provide any copyrighted assets. You must provide your own copy of the original game.
| Version | sha1 hash |
|--------------| ---------------------------------------- |
| GameCube USA | 75edd3ddff41f125d1b4ce1a40378f1b565519e7 |
| GameCube PAL | 2601822a488eeb86fb89db16ca8f29c2c953e1ca |
### 1. Verify your dump
First, make sure your dump of the game is clean and supported by Dusk. You can do this by checking the SHA-1 hash of your dump against this list of supported versions:
| Version | SHA-1 hash |
|--------------| ------------------------------------------ |
| GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` |
| GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` |
### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases)
### 3. Setup the game
- Extract the zip folder
- Launch Dusk
- Select Options, then set the ISO Path to your supported game dump
- Press Start Game to play!
![Dusk options](assets/dusk_options.png)
- Extract the .zip file
- Launch Dusk
- Press **Select Disc Image** and provide the path to your supported game dump.
- Press **Play**!
# 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.
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).
<br/>
<div align="center">
<a href="https://github.com/encounter/aurora">
<img src="assets/aurora-powered.png" alt="Powered by Aurora" width="800">
</a>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

+66
View File
@@ -0,0 +1,66 @@
<svg width="600" height="600" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
<circle cx="150" cy="150" r="105" fill="none" stroke="white" stroke-width="4"/>
<circle cx="150" cy="150" r="95" fill="none" stroke="white" stroke-width="4"/>
<circle cx="150" cy="150" r="60" fill="none" stroke="white" stroke-width="4"/>
<circle cx="150" cy="150" r="75" fill="none" stroke="white" stroke-width="4"/>
<defs>
<line id="ray" x1="150" y1="55" x2="150" y2="45"/>
<clipPath id="zigzag-clip">
<circle cx="150" cy="150" r="75"/>
</clipPath>
</defs>
<g stroke="white" stroke-width="3">
<use href="#ray"/>
<use href="#ray" transform="rotate(18 150 150)"/>
<use href="#ray" transform="rotate(36 150 150)"/>
<use href="#ray" transform="rotate(54 150 150)"/>
<use href="#ray" transform="rotate(72 150 150)"/>
<use href="#ray" transform="rotate(90 150 150)"/>
<use href="#ray" transform="rotate(108 150 150)"/>
<use href="#ray" transform="rotate(126 150 150)"/>
<use href="#ray" transform="rotate(144 150 150)"/>
<use href="#ray" transform="rotate(162 150 150)"/>
<use href="#ray" transform="rotate(180 150 150)"/>
<use href="#ray" transform="rotate(198 150 150)"/>
<use href="#ray" transform="rotate(216 150 150)"/>
<use href="#ray" transform="rotate(234 150 150)"/>
<use href="#ray" transform="rotate(252 150 150)"/>
<use href="#ray" transform="rotate(270 150 150)"/>
<use href="#ray" transform="rotate(288 150 150)"/>
<use href="#ray" transform="rotate(306 150 150)"/>
<use href="#ray" transform="rotate(324 150 150)"/>
<use href="#ray" transform="rotate(342 150 150)"/>
</g>
<polygon fill="none" stroke="white" stroke-width="4" opacity="1" clip-path="url(#zigzag-clip)"
points="
126.82,78.67
150,90
173.18,78.67
185.27,101.46
210.68,105.92
207.06,131.46
225,150
207.06,168.54
210.68,194.08
185.27,198.54
173.18,221.33
150,210
126.82,221.33
114.73,198.54
89.32,194.08
92.94,168.54
75,150
92.94,131.46
89.32,105.92
114.73,101.46
"/>
<g fill="none" stroke="white" stroke-width="4">
<polygon points="150,105 130,140 170,140"/>
<polygon points="130,140 110,175 150,175"/>
<polygon points="170,140 150,175 190,175"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+1 -1
+14 -10
View File
@@ -1447,10 +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/ImGuiFirstRunPreset.hpp
src/dusk/imgui/ImGuiFirstRunPreset.cpp
src/dusk/imgui/ImGuiProcessOverlay.cpp
src/dusk/imgui/ImGuiCameraOverlay.cpp
src/dusk/imgui/ImGuiHeapOverlay.cpp
@@ -1461,22 +1457,28 @@ 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/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
src/dusk/ui/button.hpp
src/dusk/ui/component.cpp
src/dusk/ui/component.hpp
src/dusk/ui/controller_config.cpp
src/dusk/ui/controller_config.hpp
src/dusk/ui/document.cpp
src/dusk/ui/document.hpp
src/dusk/ui/editor.cpp
src/dusk/ui/editor.hpp
src/dusk/ui/event.cpp
src/dusk/ui/event.hpp
src/dusk/ui/graphics_tuner.cpp
src/dusk/ui/graphics_tuner.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
@@ -1484,12 +1486,12 @@ set(DUSK_FILES
src/dusk/ui/overlay.hpp
src/dusk/ui/pane.cpp
src/dusk/ui/pane.hpp
src/dusk/ui/popup.cpp
src/dusk/ui/popup.hpp
src/dusk/ui/menu_bar.cpp
src/dusk/ui/menu_bar.hpp
src/dusk/ui/prelaunch.cpp
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
@@ -1510,6 +1512,8 @@ set(DUSK_FILES
src/dusk/OSReport.cpp
src/dusk/OSThread.cpp
src/dusk/OSMutex.cpp
src/dusk/discord.cpp
src/dusk/discord.hpp
src/dusk/discord_presence.cpp
src/dusk/version.cpp
)
+11
View File
@@ -25,6 +25,10 @@ public:
int Draw();
int Delete();
#if TARGET_PC
void onInterpCallback();
#endif
enum Param_e {
LOCK_e = (1 << 6), NO_BASE_DISP = (1 << 7)
};
@@ -50,6 +54,13 @@ private:
/* 0x1020 */ dCcD_Cyl mCylinderCollider;
/* 0x115C */ s32 mStopSwingingFrames;
#if TARGET_PC
cXyz mChainInterpPrev[64];
cXyz mChainInterpCurr[64];
bool mChainInterpPrevValid;
bool mChainInterpCurrValid;
#endif
// Number of chain models
u32 getArg0() {
return fopAcM_GetParamBit(this, 0, 6);
+18
View File
@@ -118,6 +118,18 @@ class camera_class;
class dCamera_c;
typedef bool (dCamera_c::*engine_fn)(s32);
#if TARGET_PC
struct DebugFlyCam {
bool initialized;
f32 pitch;
f32 yaw;
cXyz savedCenter;
cXyz savedEye;
f32 savedFovy;
cSAngle savedBank;
};
#endif
class dCamera_c {
public:
class dCamInfo_c {
@@ -1028,6 +1040,8 @@ public:
bool test2Camera(s32);
#if TARGET_PC
bool freeCamera();
bool executeDebugFlyCam();
void deactivateDebugFlyCam();
#endif
bool towerCamera(s32);
bool hookshotCamera(s32);
@@ -1376,6 +1390,10 @@ public:
/* 0x970 */ dCamSetup_c mCamSetup;
/* 0xAEC */ dCamParam_c mCamParam;
/* 0xB0C */ u8 field_0xb0c;
#if TARGET_PC
DebugFlyCam mDebugFlyCam;
#endif
}; // Size: 0xB10
dCamera_c* dCam_getBody();
+6
View File
@@ -169,6 +169,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;
+10
View File
@@ -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
-3
View File
@@ -50,8 +50,6 @@ public:
bool hasSignal(const char* key) const;
std::vector<Achievement> getAchievements() const;
bool hasPendingUnlock() const { return !m_pendingUnlocks.empty(); }
std::string consumePendingUnlock();
private:
struct Entry {
@@ -68,7 +66,6 @@ private:
std::unordered_set<std::string_view> m_signals;
bool m_loaded = false;
bool m_dirty = false;
std::queue<std::string> m_pendingUnlocks;
};
} // namespace dusk
+8 -12
View File
@@ -1,18 +1,14 @@
#pragma once
#ifdef DUSK_DISCORD_RPC
#ifdef DUSK_DISCORD
namespace dusk {
namespace discord {
namespace dusk::discord {
void Initialize();
void initialize();
void run_callbacks();
void update_presence();
void shutdown();
void RunCallbacks();
} // namespace dusk::discord
void UpdatePresence();
void Shutdown();
}
}
#endif // DUSK_DISCORD_RPC
#endif // DUSK_DISCORD
+14
View File
@@ -1,6 +1,10 @@
#ifndef DUSK_MAIN_H
#define DUSK_MAIN_H
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include <filesystem>
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
+2 -2
View File
@@ -56,6 +56,7 @@ struct UserSettings {
ConfigVar<int> fanfareVolume;
ConfigVar<bool> enableReverb;
ConfigVar<bool> enableHrtf;
ConfigVar<bool> menuSounds;
} audio;
// Game settings
@@ -66,7 +67,6 @@ struct UserSettings {
// QoL
ConfigVar<bool> enableQuickTransform;
ConfigVar<bool> hideTvSettingsScreen;
ConfigVar<bool> skipWarningScreen;
ConfigVar<bool> biggerWallets;
ConfigVar<bool> noReturnRupees;
ConfigVar<bool> disableRupeeCutscenes;
@@ -119,6 +119,7 @@ struct UserSettings {
ConfigVar<bool> invertCameraXAxis;
ConfigVar<bool> invertCameraYAxis;
ConfigVar<float> freeCameraSensitivity;
ConfigVar<bool> debugFlyCam;
// Cheats
ConfigVar<bool> infiniteHearts;
@@ -154,7 +155,6 @@ struct UserSettings {
ConfigVar<bool> showPipelineCompilation;
ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> enableCrashReporting;
ConfigVar<bool> duskMenuOpen;
ConfigVar<int> cardFileType;
} backend;
};
+13
View File
@@ -5,6 +5,7 @@
#include "Z2AudioLib/Z2EnvSeMgr.h"
#include "Z2AudioLib/Z2LinkMgr.h"
#include "dusk/audio.h"
#include "dusk/settings.h"
class mDoAud_zelAudio_c : public Z2AudioMgr {
public:
@@ -132,6 +133,18 @@ inline void mDoAud_seStart(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_re
-1.0f, -1.0f, 0);
}
#if TARGET_PC
inline void mDoAud_seStartMenu(u32 i_sfxID) {
if (!mDoAud_zelAudio_c::isInitFlag()) {
return;
}
if (!dusk::getSettings().audio.menuSounds.getValue()) {
return;
}
mDoAud_seStart(i_sfxID, nullptr, 0, 0);
}
#endif
inline void mDoAud_seStartLevel(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_reverb) {
DUSK_AUDIO_SKIP()
Z2AudioMgr::getInterface()->seStartLevel(i_sfxID, i_sePos, param_2, i_reverb, 1.0f, 1.0f,
@@ -556,8 +556,8 @@ void J3DModelLoader::readVertexData(const J3DVertexBlock& block, J3DVertexData&
if (attr == GX_VA_POS) {
// can be a little off due to 0x20 alignment, account for that
u32 expect = ((data.mVtxNum * vertStride) + 0x1F) & ~0x1F;
JUT_ASSERT(1234, expect == addrDiff);
// u32 expect = ((data.mVtxNum * vertStride) + 0x1F) & ~0x1F;
// JUT_ASSERT(1234, expect == addrDiff);
} else if (attr == GX_VA_NRM) {
data.mNrmNum = num;
} else if (attr == GX_VA_CLR0) {
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 48 KiB

+126 -122
View File
@@ -8,140 +8,144 @@ body {
height: 100%;
margin: 0;
padding: 0;
font-family: "Fira Sans Condensed";
font-size: 24dp;
color: #FFFFFF;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: stretch;
}
.overlay-root {
width: 100%;
min-height: 45%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: stretch;
decorator: vertical-gradient(#00000000 #151610F2);
padding: 48dp 0 40dp 0;
filter: opacity(0);
transition: filter 0.2s linear-in-out;
}
.overlay-root[open] {
filter: opacity(1);
}
.overlay {
width: 100%;
max-width: 1216dp;
margin-left: auto;
margin-right: auto;
display: flex;
flex-direction: column;
gap: 24dp;
padding: 0 32dp;
}
@media (max-height: 800dp) {
.overlay-root {
min-height: 38%;
padding: 32dp 0 28dp 0;
}
.overlay {
gap: 16dp;
padding: 0 24dp;
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24dp;
}
.carousel-container {
flex: 1 1 auto;
display: flex;
justify-content: flex-end;
min-width: 0;
}
.description {
font-size: 18dp;
line-height: 22dp;
color: rgba(255, 255, 255, 50%);
}
.divider {
margin: 1dp 0;
border-top: 1dp rgba(217, 217, 217, 50%);
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24dp;
}
footer-button {
display: block;
width: 100%;
max-width: 220dp;
border: 0;
padding: 0;
background-color: transparent;
font-family: "Fira Sans Condensed";
font-weight: bold;
font-family: "Fira Sans";
font-weight: normal;
font-size: 20dp;
line-height: 24dp;
text-transform: uppercase;
color: #FFFFFF;
opacity: 1;
color: #E0DBC8;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: stretch;
z-index: 1;
pointer-events: none;
}
toast {
position: absolute;
top: 40dp;
right: 40dp;
display: flex;
flex-flow: column;
border-radius: 14dp;
overflow: hidden;
border: 1dp #92875B;
backdrop-filter: blur(5dp);
box-shadow: 0 0 15dp 3dp;
background-color: rgba(21, 22, 16, 80%);
filter: opacity(0);
transform: scale(0.9);
transform-origin: center;
transition: filter transform 0.2s cubic-in-out;
padding: 18dp 24dp;
gap: 8dp;
}
toast[open] {
filter: opacity(1);
transform: scale(1);
}
/*toast:hover {
cursor: pointer;
background-color: rgba(61, 59, 36, 80%);
}
footer-button.return {
text-align: left;
toast:active {
background-color: rgba(45, 43, 26, 80%);
}*/
toast heading {
display: flex;
gap: 18dp;
align-items: center;
font-family: "Fira Sans Condensed";
font-size: 18dp;
font-weight: bold;
text-transform: uppercase;
color: #92875B;
}
footer-button.reset {
text-align: right;
}
.stepped-carousel {
toast message {
display: flex;
align-items: center;
justify-content: center;
gap: 16dp;
width: auto;
min-width: 246dp;
padding: 0;
background-color: transparent;
font-family: "Fira Sans Condensed";
font-weight: bold;
justify-content: start;
gap: 8dp;
}
.stepped-carousel-value {
line-height: 29dp;
min-width: 166dp;
text-align: center;
white-space: nowrap;
opacity: 0.9;
toast progress {
height: 4dp;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
.stepped-carousel-arrow {
toast progress fill {
background-color: rgba(194, 164, 45, 80%);
}
toast.achievement {
border: 1dp #C2A42D;
}
toast.achievement heading {
color: #C2A42D;
}
icon {
font-family: "Material Symbols Rounded";
font-weight: normal;
display: inline-block;
vertical-align: middle;
}
icon.arrow-forward {
width: 24dp;
height: 24dp;
min-width: 24dp;
padding: 0;
border: 0;
background-color: transparent;
opacity: 1;
cursor: pointer;
font-size: 24dp;
decorator: text("&#xe5c8;" center center);
}
icon.trophy {
width: 24dp;
height: 24dp;
font-size: 24dp;
decorator: text("&#xe71a;" center center);
}
logo {
position: absolute;
width: 100dp;
height: 100dp;
bottom: 40dp;
left: 40dp;
opacity: 0;
transition: opacity 0.5s linear-in-out;
}
logo[open] {
opacity: 0.65;
}
logo img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
filter: drop-shadow(#0008 0 0 14dp);
}
logo img.outer {
transform-origin: center;
animation: 8s linear-in-out infinite logo-outer-spin;
}
@keyframes logo-outer-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+107 -12
View File
@@ -9,16 +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.1s linear-in-out;
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%;
@@ -34,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 */
@@ -46,6 +75,11 @@ menu {
gap: 48dp;
}
body.mirrored menu {
left: auto;
right: 96dp;
}
hero {
display: flex;
flex-direction: column;
@@ -54,6 +88,10 @@ hero {
gap: 8dp;
}
body.mirrored hero {
align-items: flex-end;
}
hero img {
width: 100%;
}
@@ -78,6 +116,7 @@ hero img {
display: flex;
flex-direction: column;
gap: 12dp;
align-items: flex-start;
}
#menu-list button {
@@ -85,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;
@@ -104,45 +144,100 @@ hero img {
decorator: horizontal-gradient(#FEE685FF #FEE68500);
}
disk-status {
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: 8dp;
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: 8dp;
gap: 12dp;
text-align: right;
font-size: 24dp;
font-effect: glow(0dp 4dp 0dp 4dp black);
text-align: right;
}
.status,
.version {
font-size: 24dp;
body.mirrored version-info {
right: auto;
left: 96dp;
text-align: left;
}
.status,
.update {
#disc-status {
display: flex;
align-items: center;
gap: 8dp;
}
#disc-status[status=good] {
color: #D8F999;
}
.status[bad] {
#disc-status[status=bad] {
color: #FFC9C9;
}
#disc-status icon {
display: block;
width: 24dp;
height: 24dp;
font-family: "Material Symbols Rounded";
font-weight: normal;
font-size: 24dp;
}
#disc-status[status=good] icon {
decorator: text("&#xe5ca;" center center);
}
#disc-status[status=bad] icon {
decorator: text("&#xe5cd;" center center);
}
#disc-version {
font-size: 20dp;
}
/* TODO: Hidden until an actual update checker is introduced */
.update {
display: none;
font-size: 16dp;
font-weight: bold;
cursor: pointer;
color: #D8F999;
}
.detail,
+48
View File
@@ -1,10 +1,21 @@
tab-bar {
display: flex;
position: relative;
min-width: 0;
overflow: auto hidden;
clip: always;
text-transform: uppercase;
}
tab-bar scrollbarhorizontal,
tab-bar scrollbarhorizontal sliderarrowdec,
tab-bar scrollbarhorizontal sliderarrowinc,
tab-bar scrollbarhorizontal slidertrack,
tab-bar scrollbarhorizontal sliderbar {
width: 0;
height: 0;
}
tab-bar tab {
flex: 0 0 auto;
padding: 0 24dp;
@@ -31,3 +42,40 @@ tab-bar tab:hover {
tab-bar tab:active {
decorator: vertical-gradient(#c2a42d10 #c2a42d40);
}
tab-bar[closable] tab-end-spacer {
display: block;
flex: 0 0 64dp;
width: 64dp;
pointer-events: none;
}
tab-bar[closable] close {
display: block;
position: fixed;
top: 8dp;
right: 8dp;
z-index: 1;
width: 48dp;
height: 48dp;
font-family: "Material Symbols Rounded";
font-weight: normal;
font-size: 24dp;
color: rgba(224, 219, 200, 70%);
backdrop-filter: blur(2dp);
border-radius: 6dp;
decorator: text("&#xe5cd;" center center);
transition: color background-color 0.12s linear-in-out;
cursor: pointer;
}
tab-bar[closable] close:hover,
tab-bar[closable] close:focus-visible {
color: #fff;
background-color: rgba(194, 164, 45, 24%);
}
tab-bar[closable] close:active {
color: #fff;
background-color: rgba(194, 164, 45, 40%);
}
+147
View File
@@ -0,0 +1,147 @@
*, *:before, *:after {
box-sizing: border-box;
}
body {
overflow: visible;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-family: "Fira Sans Condensed";
font-size: 24dp;
color: #FFFFFF;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: stretch;
}
.tuner-root {
width: 100%;
min-height: 45%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: stretch;
decorator: vertical-gradient(#00000000 #151610F2);
filter: opacity(0);
transition: filter 0.2s linear-in-out;
}
.tuner-root[open] {
filter: opacity(1);
}
.tuner {
width: 100%;
max-width: 1216dp;
margin-left: auto;
margin-right: auto;
display: flex;
flex-direction: column;
gap: 24dp;
padding: 48dp 64dp;
}
@media (max-height: 800dp) {
.tuner-root {
min-height: 38%;
}
.tuner {
gap: 16dp;
padding: 32dp 48dp;
}
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24dp;
}
.carousel-container {
flex: 1 1 auto;
display: flex;
justify-content: flex-end;
min-width: 0;
}
.description {
font-size: 18dp;
line-height: 22dp;
color: rgba(255, 255, 255, 50%);
}
.divider {
margin: 1dp 0;
border-top: 1dp rgba(217, 217, 217, 50%);
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24dp;
}
footer-button {
display: block;
width: 100%;
max-width: 220dp;
border: 0;
padding: 0;
background-color: transparent;
font-family: "Fira Sans Condensed";
font-weight: bold;
font-size: 20dp;
line-height: 24dp;
text-transform: uppercase;
color: #FFFFFF;
opacity: 1;
cursor: pointer;
}
footer-button.return {
text-align: left;
}
footer-button.reset {
text-align: right;
}
.stepped-carousel {
display: flex;
align-items: center;
justify-content: center;
gap: 16dp;
width: auto;
min-width: 246dp;
padding: 0;
background-color: transparent;
font-family: "Fira Sans Condensed";
font-weight: bold;
}
.stepped-carousel-value {
line-height: 29dp;
min-width: 166dp;
text-align: center;
white-space: nowrap;
opacity: 0.9;
}
.stepped-carousel-arrow {
width: 24dp;
height: 24dp;
min-width: 24dp;
padding: 0;
border: 0;
background-color: transparent;
opacity: 1;
cursor: pointer;
font-family: "Material Symbols Rounded";
font-weight: normal;
}
+201 -4
View File
@@ -3,6 +3,7 @@
}
body {
display: flex;
width: 100%;
height: 100%;
padding: 64dp;
@@ -17,6 +18,7 @@ window {
display: flex;
flex-flow: column;
height: 100%;
width: 100%;
max-width: 1088dp;
max-height: 768dp;
margin: auto;
@@ -32,6 +34,19 @@ window {
transition: filter transform 0.2s cubic-in-out;
}
window.small {
height: auto;
width: auto;
}
window.preset {
min-width: 650dp;
}
window.modal {
max-width: 816dp;
}
window[open] {
filter: opacity(1);
transform: scale(1);
@@ -62,7 +77,7 @@ window tab-bar tab {
window content {
display: flex;
flex: 1 1 0;
flex: 1 1 auto;
min-width: 0;
min-height: 0;
overflow: hidden;
@@ -72,7 +87,6 @@ window content pane {
display: flex;
flex-flow: column;
flex: 1 1 0;
height: 100%;
min-width: 0;
min-height: 0;
padding: 24dp;
@@ -98,6 +112,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;
@@ -184,6 +204,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;
@@ -225,15 +251,186 @@ select-button key {
font-weight: bold;
font-size: 18dp;
text-transform: uppercase;
flex: 1 0 auto;
flex: 0 1 auto;
}
select-button value {
margin-left: auto;
flex: 1 1 auto;
text-align: right;
font-size: 20dp;
}
select-button value.modified {
font-weight: bold;
}
select-button input {
text-align: right;
font-size: 20dp;
}
icon {
font-family: "Material Symbols Rounded";
font-weight: normal;
display: inline-block;
vertical-align: middle;
}
icon.warning {
width: 1em;
height: 1em;
decorator: text("&#xe002;" center center);
color: #ffcc00;
}
.achievement-row {
display: flex;
align-items: flex-start;
gap: 10dp;
padding: 12dp 0;
border-bottom: 1dp rgba(146, 135, 91, 30%);
}
.achievement-info {
display: block;
flex: 1 1 0;
min-width: 0;
}
.achievement-header {
display: flex;
align-items: center;
}
.achievement-name {
flex: 1;
font-weight: bold;
}
.achievement-name.unlocked {
color: #ffa826;
}
.achievement-badge {
font-size: 14dp;
opacity: 0.7;
}
.achievement-badge.unlocked {
color: #44cc55;
opacity: 1;
}
.achievement-badge.locked {
color: #cc4444;
opacity: 1;
}
.achievement-desc {
display: block;
color: rgba(224, 219, 200, 55%);
font-size: 16dp;
margin: 4dp 0 0 0;
}
.achievement-progress {
display: block;
font-size: 13dp;
color: rgba(224, 219, 200, 45%);
}
progressbar {
display: block;
width: 100%;
height: 6dp;
border-radius: 3dp;
background-color: rgba(255, 255, 255, 10%);
margin: 6dp 0 2dp 0;
}
progressbar.progress-done fill {
background-color: #44aa22;
border-radius: 3dp;
}
progressbar.progress-ongoing fill {
background-color: #2255bb;
border-radius: 3dp;
}
button.achievement-clear {
flex: 0 0 auto;
align-self: center;
font-size: 14dp;
padding: 2dp 8dp;
opacity: 0.45;
}
.preset-dialog {
display: flex;
flex-flow: column;
padding: 32dp;
gap: 20dp;
flex: 0 1 auto;
}
.preset-title {
display: block;
font-family: "Fira Sans Condensed";
font-weight: bold;
font-size: 30dp;
text-align: center;
}
.preset-intro {
display: block;
font-size: 18dp;
text-align: center;
color: rgba(224, 219, 200, 65%);
}
.preset-grid {
display: flex;
flex-direction: row;
gap: 20dp;
flex: 0 1 auto;
align-items: flex-start;
}
.preset-col {
display: flex;
flex-flow: column;
gap: 12dp;
flex: 1 1 0;
}
button.preset-btn {
font-size: 22dp;
padding: 20dp 16dp;
}
.preset-desc {
display: block;
font-size: 16dp;
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%;
}
+6
View File
@@ -2,6 +2,7 @@
#include "Z2AudioLib/Z2SoundInfo.h"
#if TARGET_PC
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/settings.h"
#include <cmath>
#endif
#include "Z2AudioLib/Z2Calc.h"
@@ -705,6 +706,11 @@ f32 Z2Audience::calcRelPosVolume(const Vec& param_0, f32 param_1, int camID) {
f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) {
Vec local_54 = param_0;
local_54.y = 0.0f;
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
local_54.x = -local_54.x;
}
#endif
f32 dVar6 = VECMag(&local_54);
if (dVar6 < 0.1f) {
+2 -2
View File
@@ -2721,7 +2721,7 @@ int daAlink_c::procHorseRun() {
}
if (mProcVar2.field_0x300c == 0) {
set3DStatus(BUTTON_STATUS_HOLD_ON, 4);
set3DStatus(BUTTON_STATUS_HOLD_ON, IF_DUSK(dusk::getSettings().game.enableMirrorMode ? 1 :) 4);
}
} else {
if (mProcVar3.field_0x300e != 0) {
@@ -2731,7 +2731,7 @@ int daAlink_c::procHorseRun() {
}
if (mProcVar2.field_0x300c == 0) {
set3DStatus(BUTTON_STATUS_HOLD_ON, 1);
set3DStatus(BUTTON_STATUS_HOLD_ON, IF_DUSK(dusk::getSettings().game.enableMirrorMode ? 4 :) 1);
}
}
+6
View File
@@ -517,6 +517,12 @@ void daE_OctBg_c::core_fish_attack() {
field_0xbaf = cM_rndFX(80.0f) + 100.0f;
}
}
#if AVOID_UB
else {
in_f31 = cM_rndF(400.0f) + 80.0f;
field_0xbaf = cM_rndFX(80.0f) + 100.0f;
}
#endif
} else if (current.pos.abs(cStack_5c) < 400.0f) {
in_f31 = cM_rndF(50.0f) + 20.0f;
field_0xbaf = cM_rndFX(20.0f) + 40.0f;
+8
View File
@@ -3519,7 +3519,15 @@ void daKago_c::action() {
checkSizeBg();
setFlyEffect();
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
mStickX = -mDoCPd_c::getStickX3D(PAD_1);
} else {
mStickX = mDoCPd_c::getStickX3D(PAD_1);
}
#else
mStickX = mDoCPd_c::getStickX3D(PAD_1);
#endif
mStickY = mDoCPd_c::getStickY(PAD_1);
u8 prevIsWaterfall = mIsWaterfall;
+2
View File
@@ -14,6 +14,7 @@
#include "d/actor/d_a_obj_automata.h"
#include "d/d_msg_object.h"
#include "d/actor/d_a_obj_scannon.h"
#include "dusk/frame_interpolation.h"
#include <cstring>
const daNpc_Toby_HIOParam daNpc_Toby_Param_c::m = {
@@ -1398,6 +1399,7 @@ int daNpc_Toby_c::cutRepairSCannon(int arg0) {
old.pos = current.pos;
setAngle(cM_deg2s(5.0f * f32(mPath.getArg0())));
mEventTimer = mPath.getArg2();
dusk::frame_interp::request_presentation_sync();
}
} else if (!mHide) {
mHide = 1;
+51
View File
@@ -11,6 +11,8 @@
#include "d/d_bg_w.h"
#include "d/d_cc_uty.h"
#include "d/d_com_inf_game.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
struct daObjKLift00_HIO_c : public mDoHIO_entry_c {
daObjKLift00_HIO_c();
@@ -295,6 +297,11 @@ int daObjKLift00_c::Create() {
if(getLock())
mStopSwingingFrames = 5;
#if TARGET_PC
mChainInterpPrevValid = false;
mChainInterpCurrValid = false;
#endif
return 1;
}
@@ -436,6 +443,34 @@ int daObjKLift00_c::Execute(Mtx** i_mtx) {
return 1;
}
#if TARGET_PC
static void klift00_interp_callback(bool isSimFrame, void* pUserWork) {
static_cast<daObjKLift00_c*>(pUserWork)->onInterpCallback();
}
void daObjKLift00_c::onInterpCallback() {
if (!mChainInterpPrevValid || !mChainInterpCurrValid) {
return;
}
const f32 alpha = dusk::frame_interp::get_interpolation_step();
cXyz savedPositions[64];
for (int i = 0; i < mNumChains; i++) {
savedPositions[i] = mChainPositions[i].mCurrentPos;
const cXyz& p0 = mChainInterpPrev[i];
const cXyz& p1 = mChainInterpCurr[i];
mChainPositions[i].mCurrentPos = p0 + (p1 - p0) * alpha;
}
setMtx();
for (int i = 0; i < mNumChains; i++) {
mChainPositions[i].mCurrentPos = savedPositions[i];
}
}
#endif
int daObjKLift00_c::Draw() {
g_env_light.settingTevStruct(16, &current.pos, &tevStr);
g_env_light.setLightTevColorType_MAJI(mpLiftPlatform, &tevStr);
@@ -457,6 +492,22 @@ int daObjKLift00_c::Draw() {
dComIfGd_setList();
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation) {
if (mChainInterpCurrValid) {
memcpy(mChainInterpPrev, mChainInterpCurr, mNumChains * sizeof(cXyz));
mChainInterpPrevValid = true;
}
for (int i = 0; i < mNumChains; i++) {
mChainInterpCurr[i] = mChainPositions[i].mCurrentPos;
}
mChainInterpCurrValid = true;
dusk::frame_interp::add_interpolation_callback(&klift00_interp_callback, this);
}
#endif
return 1;
}
+102
View File
@@ -1040,6 +1040,11 @@ void dCamera_c::debugDrawInit() {
bool dCamera_c::Run() {
#if TARGET_PC
ResetView();
if (executeDebugFlyCam()) {
mFrameCounter++;
mTicks++;
return true;
}
#endif
daAlink_c* link = daAlink_getAlinkActorClass();
@@ -7474,7 +7479,104 @@ bool dCamera_c::test2Camera(s32 param_0) {
return false;
}
static constexpr f32 FLYCAM_SPEED = 0.5f;
static constexpr f32 FLYCAM_FAST_SPEED = 4.0f;
static constexpr f32 FLYCAM_ROTATION_SPEED = 0.002f;
static constexpr f32 FLYCAM_TRIGGER_DEADZONE = 20.0f;
#if TARGET_PC
bool dCamera_c::executeDebugFlyCam() {
if (!dusk::getSettings().game.debugFlyCam) {
if (mDebugFlyCam.initialized) {
deactivateDebugFlyCam();
}
return false;
}
dEvt_control_c* event = dComIfGp_getEvent();
if (event == nullptr) {
return false;
}
if (!mDebugFlyCam.initialized && (event->mEventStatus != 0 || dComIfGp_isPauseFlag())) {
dusk::getSettings().game.debugFlyCam.setValue(false);
return false;
}
if (!mDebugFlyCam.initialized) {
mDebugFlyCam.savedCenter = mCenter;
mDebugFlyCam.savedEye = mEye;
mDebugFlyCam.savedFovy = mFovy;
mDebugFlyCam.savedBank = mBank;
f32 dx = mCenter.x - mEye.x;
f32 dy = mCenter.y - mEye.y;
f32 dz = mCenter.z - mEye.z;
mDebugFlyCam.yaw = atan2f(dz, dx);
f32 horizontal = sqrtf(dx * dx + dz * dz);
mDebugFlyCam.pitch = atan2f(dy, horizontal);
mDebugFlyCam.initialized = true;
}
event->mEventStatus = 1;
dComIfGp_getEventManager().setCameraPlay(1);
interface_of_controller_pad& pad = mDoCPd_c::getCpadInfo(0);
f32 stickY = pad.mMainStickPosY * 72.0f;
f32 stickX = pad.mMainStickPosX * 72.0f;
f32 cStickY = pad.mCStickPosY * 59.0f;
f32 cStickX = pad.mCStickPosX * 59.0f;
f32 trigL = pad.mTriggerLeft * 150.0f;
f32 trigR = pad.mTriggerRight * 150.0f;
f32 verticalDisp = 0.0f;
if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
verticalDisp += trigR;
}
if (trigL >= FLYCAM_TRIGGER_DEADZONE) {
verticalDisp -= trigL;
}
f32 moveDy = stickY * sinf(mDebugFlyCam.pitch) + verticalDisp;
f32 moveDx = stickY * cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) - stickX * sinf(mDebugFlyCam.yaw);
f32 moveDz = stickY * sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) + stickX * cosf(mDebugFlyCam.yaw);
f32 speed = mDoCPd_c::getHoldZ(PAD_1) ? FLYCAM_FAST_SPEED : FLYCAM_SPEED;
mEye.x += speed * moveDx;
mEye.y += speed * moveDy;
mEye.z += speed * moveDz;
static constexpr f32 FLYCAM_TARGET_DIST = 100.0f;
mCenter.x = mEye.x + cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
mCenter.z = mEye.z + sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
mCenter.y = mEye.y + sinf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
Reset(mCenter, mEye);
f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? cStickX : -cStickX;
mDebugFlyCam.yaw += yawInput * FLYCAM_ROTATION_SPEED;
mDebugFlyCam.yaw = fmodf(mDebugFlyCam.yaw + 2.0f * (f32)M_PI, 2.0f * (f32)M_PI);
f32 maxPitch = (f32)M_PI / 2.0f - 0.1f;
f32 minPitch = -(f32)M_PI / 2.0f + 0.1f;
mDebugFlyCam.pitch = std::clamp(mDebugFlyCam.pitch + cStickY * FLYCAM_ROTATION_SPEED, minPitch, maxPitch);
return true;
}
void dCamera_c::deactivateDebugFlyCam() {
Reset(mDebugFlyCam.savedCenter, mDebugFlyCam.savedEye, mDebugFlyCam.savedFovy, mDebugFlyCam.savedBank.Val());
dEvt_control_c* event = dComIfGp_getEvent();
if (event != nullptr) {
event->mEventStatus = 0;
}
dComIfGp_getEventManager().setCameraPlay(0);
mDebugFlyCam.initialized = false;
}
bool dCamera_c::freeCamera() {
if (dusk::getSettings().game.freeCamera && mGear == 1) {
mGear = 0;
+73
View File
@@ -11,6 +11,62 @@
#include "JSystem/JGadget/define.h"
#include <cstring>
#include "dusk/logging.h"
#if TARGET_PC
#include "dusk/ui/ui.hpp"
namespace {
static int sJaiSkip = -1;
static JSUList<JAIStream>* get_stream_list() {
return Z2GetSoundMgr()->getStreamMgr()->getStreamList();
}
static int get_stream_count(JSUList<JAIStream>* list) {
int i = 0;
for (JSULink<JAIStream>* 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<JAIStream>* list = get_stream_list();
for (JSULink<JAIStream>* 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<JAIStream>* 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;
+18
View File
@@ -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) {
@@ -2464,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))
+24
View File
@@ -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];
@@ -1397,6 +1403,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,
@@ -1404,6 +1419,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,
+17
View File
@@ -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);
+7
View File
@@ -1987,6 +1987,13 @@ bool jmessage_tSequenceProcessor::do_isReady() {
}
#endif
#if TARGET_PC
if (dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)) {
field_0xb2 = 1;
pReference->setSendTimer(0);
}
#endif
if (dComIfGp_checkMesgBgm()) {
bool isItemMusicPlaying = true;
if (mDoAud_checkPlayingSubBgmFlag() != Z2BGM_ITEM_GET &&
+14 -5
View File
@@ -427,6 +427,16 @@ static void dummyStrings() {
dMsgObject_HIO_c g_MsgObject_HIO_c;
int dMsgObject_c::_execute() {
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
// enable wii message index override
g_MsgObject_HIO_c.mMessageDisplay = 1;
} else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) {
g_MsgObject_HIO_c.mMessageDisplay = 0;
}
#endif
field_0x4c7 = 0;
if (mpTalkHeap != NULL) {
field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap);
@@ -1880,15 +1890,14 @@ bool dMsgObject_c::isShopItemMessage() {
{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},
{5181, 5182, 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},
5432, 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}
});
+2 -16
View File
@@ -1120,26 +1120,12 @@ int dScnLogo_c::create() {
checkProgSelect();
if (field_0x20a != 0) {
mExecCommand = EXEC_PROG_IN;
#if TARGET_PC
mTimer = dusk::getSettings().game.skipWarningScreen ? 1 : 30;
#else
mTimer = 30;
#endif
field_0x218 = getProgressiveMode();
} else {
#if TARGET_PC
if (dusk::getSettings().game.skipWarningScreen) {
mTimer = 0; // Possibly unnecessary but just in case
mExecCommand = EXEC_DVD_WAIT;
} else {
if (mDoRst::getWarningDispFlag()) {
mTimer = 90;
mExecCommand = EXEC_NINTENDO_IN;
} else {
mTimer = 120;
mExecCommand = EXEC_WARNING_IN;
}
}
mTimer = 0; // Possibly unnecessary but just in case
mExecCommand = EXEC_DVD_WAIT;
#else
if (mDoRst::getWarningDispFlag()) {
mTimer = 90;
+12 -1
View File
@@ -40,8 +40,9 @@
#include "JSystem/JKernel/JKRAramArchive.h"
#if TARGET_PC
#include "dusk/autosave.h"
#include "dusk/memory.h"
#include <dusk/autosave.h>
#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();
+31 -4
View File
@@ -1,3 +1,5 @@
#include <memory>
#include "aurora/lib/logging.hpp"
#include "os_report.h"
@@ -21,10 +23,35 @@ static bool checkEnabled() {
static std::string FormatToString(const char* msg, va_list list) {
int ret = vsnprintf(nullptr, 0, msg, list);
std::string buf(ret, '\0');
vsnprintf(buf.data(), buf.size(), msg, list);
buf.pop_back();
return buf;
if (ret <= 0) {
return {};
}
++ret;
std::unique_ptr<char[]> buf(new char[ret]);
vsnprintf(buf.get(), ret, msg, list);
buf[ret - 1] = '\0';
return {buf.get()};
}
void OSReport(const char* fmt, ...) {
if (!checkEnabled()) {
return;
}
va_list args;
va_start(args, fmt);
const auto str = FormatToString(fmt, args);
va_end(args);
Log.info("{}", str);
}
void OSPanic(const char* file, int line, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
const auto str = FormatToString(fmt, args);
va_end(args);
Log.fatal("[{}:{}] {}", file, line, str);
}
void OSReport_Error(const char* fmt, ...) {
+14 -12
View File
@@ -1,14 +1,15 @@
#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_com_inf_game.h"
#include "d/d_demo.h"
#include "f_pc/f_pc_name.h"
#include "d/d_meter2_info.h"
#include "dusk/io.hpp"
#include "dusk/main.h"
#include "dusk/ui/ui.hpp"
#include "f_op/f_op_actor_mng.h"
#include "f_pc/f_pc_name.h"
#include <filesystem>
#include <algorithm>
@@ -454,12 +455,6 @@ AchievementSystem& AchievementSystem::get() {
return instance;
}
std::string AchievementSystem::consumePendingUnlock() {
std::string msg = std::move(m_pendingUnlocks.front());
m_pendingUnlocks.pop();
return msg;
}
std::vector<Achievement> AchievementSystem::getAchievements() const {
std::vector<Achievement> result;
result.reserve(m_entries.size());
@@ -559,7 +554,14 @@ void AchievementSystem::processEntry(Entry& e) {
if (nowUnlocked) {
e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1;
e.achievement.unlocked = true;
m_pendingUnlocks.push(e.achievement.name);
if (getSettings().game.enableAchievementNotifications) {
ui::push_toast({
.type = "achievement",
.title = "Achievement Unlocked!",
.content = e.achievement.name,
.duration = std::chrono::seconds(5),
});
}
m_dirty = true;
} else if (progressChanged) {
m_dirty = true;
+5 -1
View File
@@ -1,4 +1,5 @@
#include "dusk/autosave.h"
#include "dusk/ui/ui.hpp"
#include "imgui/ImGuiConsole.hpp"
u8 mSaveBuffer[QUEST_LOG_SIZE * 3];
@@ -83,6 +84,9 @@ void waitingForWrite() {
}
void endAutoSave() {
dusk::g_imguiConsole.AddToast("Saving...", 2.0f);
dusk::ui::push_toast({
.type = "autosave",
.duration = std::chrono::milliseconds(1500),
});
mAutoSaveProc = 0;
}
+867
View File
@@ -0,0 +1,867 @@
#ifdef DUSK_DISCORD
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "discord.hpp"
#include "dusk/logging.h"
#include "nlohmann/json.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <cstring>
#include <deque>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMCX
#define NOSERVICE
#define NOIME
#include <windows.h>
#else
#include <cerrno>
#include <cstdlib>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
#endif
namespace dusk::discord::rpc {
namespace {
using json = nlohmann::json;
constexpr uint32_t kRpcVersion = 1;
constexpr size_t kFrameHeaderSize = sizeof(uint32_t) * 2;
constexpr size_t kMaxFramePayloadSize = 64 * 1024;
constexpr auto kIoWait = std::chrono::milliseconds(500);
constexpr auto kShutdownClearTimeout = std::chrono::milliseconds(200);
constexpr auto kInitialReconnectDelay = std::chrono::milliseconds(500);
constexpr auto kMaxReconnectDelay = std::chrono::milliseconds(60 * 1000);
enum class Opcode : uint32_t {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
};
enum class ConnectionState {
Disconnected,
SentHandshake,
Connected,
};
enum class DisconnectCode : int {
PipeClosed = 1,
ReadCorrupt = 2,
BadFrame = 3,
};
struct Frame {
Opcode opcode = Opcode::Frame;
std::string payload;
};
struct QueuedEvent {
enum class Type {
Ready,
Disconnected,
Error,
};
Type type = Type::Ready;
User user;
int code = 0;
std::string message;
};
class Backoff {
public:
std::chrono::milliseconds next_delay() {
const auto delay = currentDelay;
currentDelay = std::min(currentDelay * 2, kMaxReconnectDelay);
return delay;
}
void reset() { currentDelay = kInitialReconnectDelay; }
private:
std::chrono::milliseconds currentDelay = kInitialReconnectDelay;
};
class IpcConnection {
public:
IpcConnection() = default;
~IpcConnection() { close(); }
IpcConnection(const IpcConnection&) = delete;
IpcConnection& operator=(const IpcConnection&) = delete;
bool open() {
#ifdef _WIN32
wchar_t pipeName[] = L"\\\\?\\pipe\\discord-ipc-0";
constexpr size_t kPipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
for (wchar_t pipeNumber = L'0'; pipeNumber <= L'9'; ++pipeNumber) {
pipeName[kPipeDigit] = pipeNumber;
pipe = CreateFileW(
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (pipe != INVALID_HANDLE_VALUE) {
return true;
}
if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(pipeName, 10000)) {
--pipeNumber;
}
}
return false;
#else
const auto tempPaths = get_temp_paths();
for (const std::string& tempPath : tempPaths) {
for (int pipeNumber = 0; pipeNumber < 10; ++pipeNumber) {
socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socketFd == -1) {
return false;
}
sockaddr_un pipeAddress{};
pipeAddress.sun_family = AF_UNIX;
const std::string socketPath =
tempPath + "/discord-ipc-" + std::to_string(pipeNumber);
if (socketPath.size() >= sizeof(pipeAddress.sun_path)) {
close();
continue;
}
std::strncpy(
pipeAddress.sun_path, socketPath.c_str(), sizeof(pipeAddress.sun_path) - 1);
if (connect(socketFd, reinterpret_cast<const sockaddr*>(&pipeAddress),
sizeof(pipeAddress)) == 0)
{
fcntl(socketFd, F_SETFL, O_NONBLOCK);
#ifdef SO_NOSIGPIPE
int optval = 1;
setsockopt(socketFd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
#endif
return true;
}
close();
}
}
return false;
#endif
}
void close() {
#ifdef _WIN32
if (pipe != INVALID_HANDLE_VALUE) {
CloseHandle(pipe);
pipe = INVALID_HANDLE_VALUE;
}
#else
if (socketFd != -1) {
::close(socketFd);
socketFd = -1;
}
#endif
pendingRead.clear();
}
bool is_open() const {
#ifdef _WIN32
return pipe != INVALID_HANDLE_VALUE;
#else
return socketFd != -1;
#endif
}
bool write_frame(const Frame& frame) {
std::array<uint8_t, kFrameHeaderSize> header{};
write_u32(header.data(), static_cast<uint32_t>(frame.opcode));
write_u32(header.data() + sizeof(uint32_t), static_cast<uint32_t>(frame.payload.size()));
return write_all(header.data(), header.size()) &&
write_all(
reinterpret_cast<const uint8_t*>(frame.payload.data()), frame.payload.size());
}
enum class ReadStatus {
None,
Frame,
Closed,
Corrupt,
};
ReadStatus read_frame(Frame& frame) {
if (!read_available()) {
return is_open() ? ReadStatus::None : ReadStatus::Closed;
}
if (pendingRead.size() < kFrameHeaderSize) {
return ReadStatus::None;
}
const uint32_t payloadLength = read_u32(pendingRead.data() + sizeof(uint32_t));
if (payloadLength > kMaxFramePayloadSize) {
return ReadStatus::Corrupt;
}
const size_t frameLength = kFrameHeaderSize + payloadLength;
if (pendingRead.size() < frameLength) {
return ReadStatus::None;
}
frame.opcode = static_cast<Opcode>(read_u32(pendingRead.data()));
frame.payload.assign(
reinterpret_cast<const char*>(pendingRead.data() + kFrameHeaderSize), payloadLength);
pendingRead.erase(
pendingRead.begin(), pendingRead.begin() + static_cast<std::ptrdiff_t>(frameLength));
return ReadStatus::Frame;
}
private:
#ifndef _WIN32
static std::vector<std::string> get_temp_paths() {
std::vector<std::string> paths;
for (const char* name : {"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"}) {
if (const char* value = std::getenv(name); value && value[0] != '\0') {
if (std::find(paths.begin(), paths.end(), value) == paths.end()) {
paths.emplace_back(value);
}
}
}
if (std::find(paths.begin(), paths.end(), "/tmp") == paths.end()) {
paths.emplace_back("/tmp");
}
return paths;
}
#endif
static void write_u32(uint8_t* out, uint32_t value) {
out[0] = static_cast<uint8_t>(value & 0xff);
out[1] = static_cast<uint8_t>((value >> 8) & 0xff);
out[2] = static_cast<uint8_t>((value >> 16) & 0xff);
out[3] = static_cast<uint8_t>((value >> 24) & 0xff);
}
static uint32_t read_u32(const uint8_t* in) {
return static_cast<uint32_t>(in[0]) | (static_cast<uint32_t>(in[1]) << 8) |
(static_cast<uint32_t>(in[2]) << 16) | (static_cast<uint32_t>(in[3]) << 24);
}
bool write_all(const uint8_t* data, size_t length) {
size_t written = 0;
while (written < length) {
#ifdef _WIN32
DWORD bytesWritten = 0;
if (WriteFile(pipe, data + written, static_cast<DWORD>(length - written), &bytesWritten,
nullptr) == FALSE ||
bytesWritten == 0)
{
close();
return false;
}
written += bytesWritten;
#else
#ifdef MSG_NOSIGNAL
constexpr int kMsgFlags = MSG_NOSIGNAL;
#else
constexpr int kMsgFlags = 0;
#endif
const ssize_t bytesWritten =
send(socketFd, data + written, length - written, kMsgFlags);
if (bytesWritten < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
}
close();
return false;
}
if (bytesWritten == 0) {
close();
return false;
}
written += static_cast<size_t>(bytesWritten);
#endif
}
return true;
}
bool read_available() {
std::array<uint8_t, 4096> buffer{};
bool readAny = false;
for (;;) {
#ifdef _WIN32
DWORD bytesAvailable = 0;
if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr) == FALSE) {
close();
return readAny;
}
if (bytesAvailable == 0) {
return readAny;
}
const DWORD bytesToRead =
std::min<DWORD>(bytesAvailable, static_cast<DWORD>(buffer.size()));
DWORD bytesRead = 0;
if (ReadFile(pipe, buffer.data(), bytesToRead, &bytesRead, nullptr) == FALSE) {
close();
return readAny;
}
if (bytesRead == 0) {
close();
return readAny;
}
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
readAny = true;
#else
#ifdef MSG_NOSIGNAL
constexpr int kMsgFlags = MSG_NOSIGNAL;
#else
constexpr int kMsgFlags = 0;
#endif
const ssize_t bytesRead = recv(socketFd, buffer.data(), buffer.size(), kMsgFlags);
if (bytesRead < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return readAny;
}
close();
return readAny;
}
if (bytesRead == 0) {
close();
return readAny;
}
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
readAny = true;
#endif
}
}
#ifdef _WIN32
HANDLE pipe = INVALID_HANDLE_VALUE;
#else
int socketFd = -1;
#endif
std::vector<uint8_t> pendingRead;
};
int current_process_id() {
#ifdef _WIN32
return static_cast<int>(GetCurrentProcessId());
#else
return static_cast<int>(getpid());
#endif
}
std::string next_nonce() {
static std::atomic_uint64_t sNonce{1};
return std::to_string(sNonce.fetch_add(1));
}
const json* find_member(const json& object, const char* key) {
if (!object.is_object()) {
return nullptr;
}
const auto member = object.find(key);
if (member == object.end()) {
return nullptr;
}
return &*member;
}
std::string json_string_member(const json& object, const char* key) {
const json* member = find_member(object, key);
if (!member || !member->is_string()) {
return {};
}
return member->get<std::string>();
}
int json_int_member(const json& object, const char* key, int defaultValue) {
const json* member = find_member(object, key);
if (!member || !member->is_number_integer()) {
return defaultValue;
}
try {
return member->get<int>();
} catch (const json::exception&) {
return defaultValue;
}
}
json make_presence_activity(const Presence& presence) {
json activity = json::object();
if (!presence.state.empty()) {
activity["state"] = presence.state;
}
if (!presence.details.empty()) {
activity["details"] = presence.details;
}
if (presence.startTimestamp != 0) {
activity["timestamps"] = {{"start", presence.startTimestamp}};
}
if (!presence.largeImageKey.empty() || !presence.largeImageText.empty()) {
json assets = json::object();
if (!presence.largeImageKey.empty()) {
assets["large_image"] = presence.largeImageKey;
}
if (!presence.largeImageText.empty()) {
assets["large_text"] = presence.largeImageText;
}
activity["assets"] = std::move(assets);
}
return activity;
}
Frame make_handshake_frame(std::string_view applicationId) {
return {
Opcode::Handshake,
json{
{"v", kRpcVersion},
{"client_id", std::string(applicationId)},
}
.dump(),
};
}
Frame make_set_activity_frame(std::string nonce, int pid, std::optional<Presence> presence) {
json args = {{"pid", pid}};
if (presence) {
args["activity"] = make_presence_activity(*presence);
} else {
args["activity"] = nullptr;
}
return {
Opcode::Frame,
json{
{"cmd", "SET_ACTIVITY"},
{"nonce", std::move(nonce)},
{"args", std::move(args)},
}
.dump(),
};
}
class Client {
public:
void initialize(std::string applicationId, EventHandlers handlers) {
shutdown();
{
std::lock_guard lock(mutex);
this->applicationId = std::move(applicationId);
this->handlers = std::move(handlers);
shouldRun = true;
queuedPresence.reset();
hasQueuedPresence = false;
clearRequested = false;
sentInitialConnectLog = false;
}
ioThread = std::thread([this] { io_loop(); });
}
void run_callbacks() {
std::deque<QueuedEvent> events;
EventHandlers localHandlers;
{
std::lock_guard lock(mutex);
events.swap(queuedEvents);
localHandlers = handlers;
}
for (const QueuedEvent& event : events) {
switch (event.type) {
case QueuedEvent::Type::Ready:
if (localHandlers.ready) {
localHandlers.ready(event.user);
}
break;
case QueuedEvent::Type::Disconnected:
if (localHandlers.disconnected) {
localHandlers.disconnected(event.code, event.message);
}
break;
case QueuedEvent::Type::Error:
if (localHandlers.error) {
localHandlers.error(event.code, event.message);
}
break;
}
}
}
void update_presence(Presence presence) {
{
std::lock_guard lock(mutex);
if (!shouldRun) {
return;
}
queuedPresence = std::move(presence);
hasQueuedPresence = true;
}
cv.notify_all();
}
void clear_presence() {
{
std::lock_guard lock(mutex);
if (!shouldRun) {
return;
}
queuedPresence.reset();
hasQueuedPresence = false;
clearRequested = true;
}
cv.notify_all();
}
void shutdown() {
{
std::lock_guard lock(mutex);
if (!shouldRun && !ioThread.joinable()) {
return;
}
shouldRun = false;
clearRequested = true;
}
cv.notify_all();
if (ioThread.joinable()) {
ioThread.join();
}
std::lock_guard lock(mutex);
queuedPresence.reset();
hasQueuedPresence = false;
clearRequested = false;
queuedEvents.clear();
handlers = {};
applicationId.clear();
}
private:
void io_loop() {
IpcConnection connection;
ConnectionState state = ConnectionState::Disconnected;
Backoff reconnectBackoff;
auto nextConnect = std::chrono::steady_clock::now();
const int pid = current_process_id();
std::string localApplicationId;
for (;;) {
{
std::unique_lock lock(mutex);
if (!shouldRun) {
break;
}
localApplicationId = applicationId;
}
const auto now = std::chrono::steady_clock::now();
if (state == ConnectionState::Disconnected && now >= nextConnect) {
if (connection.open()) {
if (connection.write_frame(make_handshake_frame(localApplicationId))) {
state = ConnectionState::SentHandshake;
} else {
connection.close();
}
}
if (state == ConnectionState::Disconnected) {
log_waiting_for_discord_once();
nextConnect = now + reconnectBackoff.next_delay();
}
}
if (state != ConnectionState::Disconnected) {
process_reads(connection, state, reconnectBackoff, nextConnect);
}
if (state == ConnectionState::Connected) {
flush_pending_presence(connection, pid);
}
std::unique_lock lock(mutex);
if (!shouldRun) {
break;
}
cv.wait_for(lock, kIoWait);
}
flush_shutdown_clear(connection, state, pid);
connection.close();
}
void process_reads(IpcConnection& connection, ConnectionState& state, Backoff& reconnectBackoff,
std::chrono::steady_clock::time_point& nextConnect) {
for (;;) {
Frame frame;
const auto status = connection.read_frame(frame);
if (status == IpcConnection::ReadStatus::None) {
return;
}
if (status == IpcConnection::ReadStatus::Closed) {
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
static_cast<int>(DisconnectCode::PipeClosed), "Pipe closed");
return;
}
if (status == IpcConnection::ReadStatus::Corrupt) {
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
static_cast<int>(DisconnectCode::ReadCorrupt), "Oversized Discord IPC frame");
return;
}
switch (frame.opcode) {
case Opcode::Frame:
process_json_frame(frame.payload, state, reconnectBackoff);
break;
case Opcode::Close:
process_close_frame(
frame.payload, connection, state, reconnectBackoff, nextConnect);
return;
case Opcode::Ping:
connection.write_frame({Opcode::Pong, frame.payload});
break;
case Opcode::Pong:
break;
case Opcode::Handshake:
default:
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
static_cast<int>(DisconnectCode::BadFrame), "Bad Discord IPC frame");
return;
}
}
}
void process_json_frame(
const std::string& payload, ConnectionState& state, Backoff& reconnectBackoff) {
json message;
try {
message = json::parse(payload);
} catch (const json::parse_error&) {
enqueue_error(
static_cast<int>(DisconnectCode::ReadCorrupt), "Invalid Discord IPC JSON");
return;
}
const std::string cmd = json_string_member(message, "cmd");
const std::string evt = json_string_member(message, "evt");
if (state == ConnectionState::SentHandshake && cmd == "DISPATCH" && evt == "READY") {
state = ConnectionState::Connected;
reconnectBackoff.reset();
enqueue_ready(message);
return;
}
if (evt == "ERROR") {
const json* data = find_member(message, "data");
enqueue_error(data ? json_int_member(*data, "code", 0) : 0,
data ? json_string_member(*data, "message") : std::string{});
}
}
void process_close_frame(const std::string& payload, IpcConnection& connection,
ConnectionState& state, Backoff& reconnectBackoff,
std::chrono::steady_clock::time_point& nextConnect) {
int code = static_cast<int>(DisconnectCode::PipeClosed);
std::string message = "Discord closed IPC connection";
try {
const json closePayload = json::parse(payload);
code = json_int_member(closePayload, "code", code);
const std::string closeMessage = json_string_member(closePayload, "message");
if (!closeMessage.empty()) {
message = closeMessage;
}
} catch (const json::exception&) {
}
handle_disconnect(connection, state, reconnectBackoff, nextConnect, code, message);
}
void handle_disconnect(IpcConnection& connection, ConnectionState& state,
Backoff& reconnectBackoff, std::chrono::steady_clock::time_point& nextConnect, int code,
std::string_view message) {
const bool wasConnected =
state == ConnectionState::Connected || state == ConnectionState::SentHandshake;
connection.close();
state = ConnectionState::Disconnected;
nextConnect = std::chrono::steady_clock::now() + reconnectBackoff.next_delay();
if (wasConnected) {
enqueue_disconnected(code, message);
}
}
void flush_pending_presence(IpcConnection& connection, int pid) {
std::optional<Presence> presence;
bool shouldClear = false;
{
std::lock_guard lock(mutex);
if (hasQueuedPresence) {
presence = queuedPresence;
hasQueuedPresence = false;
} else if (clearRequested) {
shouldClear = true;
clearRequested = false;
}
}
if (presence) {
if (!connection.write_frame(
make_set_activity_frame(next_nonce(), pid, std::move(presence))))
{
requeue_presence();
}
} else if (shouldClear) {
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
}
}
void flush_shutdown_clear(IpcConnection& connection, ConnectionState state, int pid) {
if (state != ConnectionState::Connected || !connection.is_open()) {
return;
}
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
const auto deadline = std::chrono::steady_clock::now() + kShutdownClearTimeout;
while (std::chrono::steady_clock::now() < deadline) {
Frame frame;
const auto status = connection.read_frame(frame);
if (status == IpcConnection::ReadStatus::None) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue;
}
if (status != IpcConnection::ReadStatus::Frame) {
break;
}
if (frame.opcode == Opcode::Ping) {
connection.write_frame({Opcode::Pong, frame.payload});
}
}
}
void requeue_presence() {
std::lock_guard lock(mutex);
if (queuedPresence) {
hasQueuedPresence = true;
}
}
void enqueue_ready(const json& readyMessage) {
User user;
const auto data = readyMessage.find("data");
if (data != readyMessage.end() && data->is_object()) {
const auto userIt = data->find("user");
if (userIt != data->end() && userIt->is_object()) {
user.id = json_string_member(*userIt, "id");
user.username = json_string_member(*userIt, "username");
user.discriminator = json_string_member(*userIt, "discriminator");
user.avatar = json_string_member(*userIt, "avatar");
}
}
std::lock_guard lock(mutex);
queuedEvents.push_back({QueuedEvent::Type::Ready, std::move(user)});
}
void enqueue_disconnected(int code, std::string_view message) {
std::lock_guard lock(mutex);
QueuedEvent event;
event.type = QueuedEvent::Type::Disconnected;
event.code = code;
event.message = message;
queuedEvents.push_back(std::move(event));
}
void enqueue_error(int code, std::string_view message) {
std::lock_guard lock(mutex);
QueuedEvent event;
event.type = QueuedEvent::Type::Error;
event.code = code;
event.message = message;
queuedEvents.push_back(std::move(event));
}
void log_waiting_for_discord_once() {
bool shouldLog = false;
{
std::lock_guard lock(mutex);
if (!sentInitialConnectLog) {
sentInitialConnectLog = true;
shouldLog = true;
}
}
if (shouldLog) {
DuskLog.info("Discord: Waiting for local Discord IPC");
}
}
std::mutex mutex;
std::condition_variable cv;
std::thread ioThread;
std::string applicationId;
EventHandlers handlers;
std::deque<QueuedEvent> queuedEvents;
std::optional<Presence> queuedPresence;
bool hasQueuedPresence = false;
bool clearRequested = false;
bool shouldRun = false;
bool sentInitialConnectLog = false;
};
Client& client() {
static Client sClient;
return sClient;
}
} // namespace
void initialize(std::string applicationId, EventHandlers handlers) {
client().initialize(std::move(applicationId), std::move(handlers));
}
void run_callbacks() {
client().run_callbacks();
}
void update_presence(Presence presence) {
client().update_presence(std::move(presence));
}
void clear_presence() {
client().clear_presence();
}
void shutdown() {
client().shutdown();
}
} // namespace dusk::discord::rpc
#endif // DUSK_DISCORD
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#ifdef DUSK_DISCORD
#include <cstdint>
#include <functional>
#include <string>
#include <string_view>
namespace dusk::discord::rpc {
struct User {
std::string id;
std::string username;
std::string discriminator;
std::string avatar;
};
struct Presence {
std::string state;
std::string details;
int64_t startTimestamp = 0;
std::string largeImageKey;
std::string largeImageText;
};
struct EventHandlers {
std::function<void(const User&)> ready;
std::function<void(int, std::string_view)> disconnected;
std::function<void(int, std::string_view)> error;
};
void initialize(std::string applicationId, EventHandlers handlers);
void run_callbacks();
void update_presence(Presence presence);
void clear_presence();
void shutdown();
} // namespace dusk::discord::rpc
#endif // DUSK_DISCORD
+53 -52
View File
@@ -1,38 +1,39 @@
#ifdef DUSK_DISCORD_RPC
#ifdef DUSK_DISCORD
#include "dusk/discord_presence.hpp"
#include "d/d_com_inf_game.h"
#include "discord.hpp"
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/map_loader_definitions.h"
#include "d/d_com_inf_game.h"
#include "discord_rpc.h"
#include "fmt/format.h"
#include <chrono>
#include <cstring>
#include <string>
#include <string_view>
#include <utility>
namespace dusk {
namespace discord {
namespace dusk::discord {
static int64_t g_startTime = 0;
static bool g_initialized = false;
static const char* APPLICATION_ID = "1495632471994405035";
static constexpr const char* kApplicationId = "1495632471994405035";
static void OnReady(const DiscordUser* user) {
DuskLog.info("Discord: Connected as {}", user->username);
static void on_ready(const rpc::User& user) {
DuskLog.info("Discord: Connected as {}", user.username);
}
static void OnDisconnected(int errorCode, const char* message) {
static void on_disconnected(int errorCode, std::string_view message) {
DuskLog.warn("Discord: Disconnected ({}: {})", errorCode, message);
}
static void OnError(int errorCode, const char* message) {
static void on_error(int errorCode, std::string_view message) {
DuskLog.warn("Discord: Error ({}: {})", errorCode, message);
}
static const char* LookupMapName(const char* mapFile) {
if (!mapFile || mapFile[0] == '\0') return nullptr;
static const char* lookup_map_name(const char* mapFile) {
if (!mapFile || mapFile[0] == '\0')
return nullptr;
for (const auto& region : gameRegions) {
for (const auto& map : region.maps) {
if (map.mapFile && strcmp(mapFile, map.mapFile) == 0) {
@@ -43,80 +44,80 @@ static const char* LookupMapName(const char* mapFile) {
return nullptr;
}
void Initialize() {
g_startTime = static_cast<int64_t>(
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
).count()
);
void initialize() {
g_startTime = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
DiscordEventHandlers handlers{};
handlers.ready = OnReady;
handlers.disconnected = OnDisconnected;
handlers.errored = OnError;
Discord_Initialize(APPLICATION_ID, &handlers, 0, nullptr);
rpc::EventHandlers handlers{};
handlers.ready = on_ready;
handlers.disconnected = on_disconnected;
handlers.error = on_error;
rpc::initialize(kApplicationId, std::move(handlers));
g_initialized = true;
DuskLog.info("Discord Rich Presence initialized");
}
void RunCallbacks() {
if (!g_initialized) return;
Discord_RunCallbacks();
void run_callbacks() {
if (!g_initialized)
return;
rpc::run_callbacks();
}
void UpdatePresence() {
if (!g_initialized) return;
void update_presence() {
if (!g_initialized)
return;
static auto lastUpdate = std::chrono::steady_clock::time_point{};
static auto sLastUpdate = std::chrono::steady_clock::time_point{};
const auto now = std::chrono::steady_clock::now();
if (now - lastUpdate < std::chrono::seconds(15)) return;
lastUpdate = now;
if (now - sLastUpdate < std::chrono::seconds(15))
return;
sLastUpdate = now;
static std::string detailsBuf;
static std::string stateBuf;
static std::string sDetailsBuf;
static std::string sStateBuf;
DiscordRichPresence presence{};
rpc::Presence presence{};
presence.startTimestamp = g_startTime;
presence.largeImageKey = "icon";
presence.largeImageText = "Dusk";
if (dusk::IsGameLaunched) {
if (IsGameLaunched) {
const char* stageName = dComIfGp_getLastPlayStageName();
// stageName is empty until a room is actually entered
if (stageName[0] != '\0') {
const char* locationName = LookupMapName(stageName);
const char* locationName = lookup_map_name(stageName);
if (locationName) {
detailsBuf = locationName;
}
else {
detailsBuf = "Twilight Princess";
sDetailsBuf = locationName;
} else {
sDetailsBuf = "Twilight Princess";
}
presence.details = detailsBuf.c_str();
presence.details = sDetailsBuf;
stateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"),
sStateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"),
dComIfGs_getLife() / 4, dComIfGs_getMaxLife() / 5, dComIfGs_getRupee());
presence.state = stateBuf.c_str();
presence.state = sStateBuf;
}
}
Discord_UpdatePresence(&presence);
rpc::update_presence(std::move(presence));
DuskLog.debug("Discord Rich Presence sent");
}
void Shutdown() {
if (!g_initialized) return;
Discord_ClearPresence();
Discord_Shutdown();
void shutdown() {
if (!g_initialized)
return;
rpc::clear_presence();
rpc::shutdown();
g_initialized = false;
DuskLog.info("Discord Rich Presence shut down");
}
} // namespace discord
} // namespace dusk
} // namespace dusk::discord
#endif // DUSK_DISCORD_RPC
#endif // DUSK_DISCORD
-245
View File
@@ -1,245 +0,0 @@
#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(800, 200), ImVec2(1280, 900));
ImGui::SetNextWindowSize(ImVec2(800, 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::Misc, "Misc", ImVec4(0.65f, 0.65f, 0.65f, 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, catInfo.label);
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::BeginGroup();
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::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();
}
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
-23
View File
@@ -1,23 +0,0 @@
#pragma once
#include <queue>
#include <string>
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<std::string> m_notifyQueue;
static constexpr float NOTIFY_DURATION = 4.0f;
static constexpr float NOTIFY_FADE_TIME = 0.5f;
};
} // namespace dusk
+18 -60
View File
@@ -1,9 +1,12 @@
#include "f_op/f_op_camera_mng.h"
#include "SSystem/SComponent/c_xyz.h"
#include "d/d_com_inf_game.h"
#include "imgui.h"
#include "ImGuiConfig.hpp"
#include "ImGuiConsole.hpp"
#include "ImGuiMenuTools.hpp"
#include "dusk/settings.h"
namespace dusk {
void ImGuiMenuTools::ShowCameraOverlay() {
@@ -46,70 +49,25 @@ namespace dusk {
ImGui::InputFloat("Camera FOV", &dCam->mFovy);
ImGui::SeparatorText("Free-look Data");
ImGui::SeparatorText("Options");
static float eyeYawDeg = 0.0f;
static float moveSpeed = 5000.0f;
static float rotSpeed = 5.0f;
static cXyz freeLookPos = cXyz::Zero;
static bool freeLookActive = false;
bool changed = false;
if (ImGui::IsKeyDown(ImGuiKey_LeftArrow)) {
eyeYawDeg += rotSpeed;
if (eyeYawDeg >= 360.0f)
eyeYawDeg -= 360.0f;
changed = true;
bool eventRunning = (dComIfGp_event_runCheck() || dComIfGp_isPauseFlag()) && !getSettings().game.debugFlyCam;
if (eventRunning) {
ImGui::BeginDisabled();
}
else if (ImGui::IsKeyDown(ImGuiKey_RightArrow)) {
eyeYawDeg -= rotSpeed;
if (eyeYawDeg < 0.0f)
eyeYawDeg += 360.0f;
changed = true;
config::ImGuiCheckbox("Fly Mode", getSettings().game.debugFlyCam);
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (eventRunning) {
ImGui::SetTooltip("Cannot enable while paused or during an active event.");
} else {
ImGui::SetTooltip("Detach camera and fly freely.\n"
"Left stick: move, C-stick: look\n"
"L/R triggers: up/down, Z: fast");
}
}
cSAngle yawAngle = cSAngle(eyeYawDeg);
cXyz frontDir = cXyz(yawAngle.Sin(), 0.0f, yawAngle.Cos());
if (ImGui::IsKeyDown(ImGuiKey_UpArrow)) {
freeLookPos -= frontDir * moveSpeed * ImGui::GetIO().DeltaTime;
changed = true;
if (eventRunning) {
ImGui::EndDisabled();
}
else if (ImGui::IsKeyDown(ImGuiKey_DownArrow)) {
freeLookPos += frontDir * moveSpeed * ImGui::GetIO().DeltaTime;
changed = true;
}
if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) {
freeLookPos += cXyz::BaseY * moveSpeed * ImGui::GetIO().DeltaTime;
changed = true;
}
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) {
freeLookPos -= cXyz::BaseY * moveSpeed * ImGui::GetIO().DeltaTime;
changed = true;
}
if (!freeLookActive && changed) {
freeLookPos += dCam->Center();
freeLookActive = true;
}
if (ImGui::IsKeyDown(ImGuiKey_R)) {
freeLookPos = cXyz::Zero;
freeLookActive = false;
}
if (freeLookActive) {
dCam->Reset(freeLookPos, freeLookPos + (frontDir * 100.0f));
}
ImGui::InputFloat("Free-look Yaw", &eyeYawDeg);
ImGui::InputFloat3("Free-look Position", &freeLookPos.x);
ImGui::InputFloat("Free-look Move Speed", &moveSpeed);
ImGui::InputFloat("Free-look Rotation Speed", &rotSpeed);
ShowCornerContextMenu(m_cameraOverlayCorner, 0);
+7 -83
View File
@@ -10,11 +10,9 @@
#include "fmt/format.h"
#include "ImGuiConsole.hpp"
#include "dusk/ui/ui.hpp"
#include "JSystem/JUtility/JUTGamePad.h"
#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"
@@ -22,6 +20,8 @@
#include "dusk/livesplit.h"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "f_pc/f_pc_manager.h"
#include "f_pc/f_pc_name.h"
#include "m_Do/m_Do_controller_pad.h"
#include "m_Do/m_Do_main.h"
#include "tracy/Tracy.hpp"
@@ -35,14 +35,6 @@ using namespace std::string_literals;
using namespace std::string_view_literals;
namespace {
ImVec2 TouchEventToScreenPos(const SDL_TouchFingerEvent& touch) {
const AuroraWindowSize size = aurora::window::get_window_size();
return ImVec2{
touch.x * static_cast<float>(size.width),
touch.y * static_cast<float>(size.height),
};
}
ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) {
while (window != nullptr) {
const bool canScrollX = window->ScrollMax.x > 0.0f;
@@ -241,48 +233,7 @@ namespace dusk {
ImGuiConsole::ImGuiConsole() {}
void ImGuiConsole::HandleSDLEvent(const SDL_Event& event) {
if (!IsGameLaunched) {
return;
}
switch (event.type) {
case SDL_EVENT_FINGER_DOWN:
if (!m_touchTapActive) {
m_touchTapActive = true;
m_touchTapMoved = false;
m_touchTapFingerId = event.tfinger.fingerID;
m_touchTapStartPos = TouchEventToScreenPos(event.tfinger);
}
break;
case SDL_EVENT_FINGER_MOTION:
if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) {
const auto currentPos = TouchEventToScreenPos(event.tfinger);
const auto delta = currentPos - m_touchTapStartPos;
if (ImLengthSqr(delta) > 144.0f) {
m_touchTapMoved = true;
}
}
break;
case SDL_EVENT_FINGER_UP:
if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) {
const bool shouldToggle =
!m_touchTapMoved && (m_isHidden || !ImGui::GetIO().WantCaptureMouse);
m_touchTapActive = false;
m_touchTapMoved = false;
if (shouldToggle) {
m_isHidden = !m_isHidden;
getSettings().backend.duskMenuOpen.setValue(!m_isHidden);
Save();
}
}
break;
case SDL_EVENT_FINGER_CANCELED:
m_touchTapActive = false;
m_touchTapMoved = false;
break;
default:
break;
}
(void)event;
}
void ImGuiConsole::UpdateSettings() {
@@ -301,16 +252,8 @@ 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)) &&
if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) &&
(ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
ImGui::IsKeyPressed(ImGuiKey_R))
{
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
@@ -320,23 +263,10 @@ namespace dusk {
ImGuiMenuGame::ToggleFullscreen();
}
if (ImGui::IsKeyPressed(ImGuiKey_Escape) && getSettings().video.enableFullscreen) {
ImGuiMenuGame::ToggleFullscreen();
}
// if (!dusk::IsGameLaunched) {
// m_preLaunchWindow.draw();
// }
m_isHidden = !getSettings().backend.duskMenuOpen;
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
m_isHidden = !m_isHidden;
}
bool showMenu = !m_isHidden;
if (getSettings().backend.duskMenuOpen != showMenu) {
getSettings().backend.duskMenuOpen.setValue(showMenu);
Save();
}
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
// so make the window bg fully transparent temporarily
@@ -359,14 +289,9 @@ namespace dusk {
}
ImGui::PopStyleColor();
if (!getSettings().backend.wasPresetChosen) {
m_firstRunPreset.draw();
return;
}
if (dusk::IsGameLaunched && !m_isLaunchInitialized) {
AddToast(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ?
"Tap to toggle menu"s :
"3-finger tap to toggle menu"s :
"Press F1 to toggle menu"s,
4.f);
m_isLaunchInitialized = true;
@@ -402,7 +327,6 @@ 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.
-9
View File
@@ -6,12 +6,9 @@
#include <string_view>
#include <aurora/aurora.h>
#include <SDL3/SDL_touch.h>
#include "ImGuiFirstRunPreset.hpp"
#include "ImGuiMenuGame.hpp"
#include "ImGuiMenuTools.hpp"
#include "ImGuiPreLaunchWindow.hpp"
#include "imgui.h"
union SDL_Event;
@@ -42,17 +39,11 @@ private:
bool m_isHidden = true;
bool m_isLaunchInitialized = false;
bool m_touchTapActive = false;
bool m_touchTapMoved = false;
SDL_FingerID m_touchTapFingerId = 0;
ImVec2 m_touchTapStartPos = {};
ImGuiWindow* m_dragScrollWindow = nullptr;
ImVec2 m_dragScrollLastMousePos = {};
std::deque<Toast> m_toasts;
ImGuiFirstRunPreset m_firstRunPreset;
ImGuiMenuGame m_menuGame;
ImGuiPreLaunchWindow m_preLaunchWindow;
// Keep always last
ImGuiMenuTools m_menuTools;
-141
View File
@@ -1,141 +0,0 @@
#include "ImGuiFirstRunPreset.hpp"
#include "imgui.h"
#include "ImGuiConsole.hpp"
#include "ImGuiEngine.hpp"
#include "dusk/settings.h"
#include "dusk/config.hpp"
#include <dusk/dusk.h>
namespace dusk {
static void ApplyPresetClassic() {
auto& s = getSettings();
s.video.lockAspectRatio.setValue(true);
s.game.bloomMode.setValue(BloomMode::Classic);
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
}
static void ApplyPresetHD() {
auto& s = getSettings();
s.game.bloomMode.setValue(BloomMode::Classic);
s.game.hideTvSettingsScreen.setValue(true);
s.game.skipWarningScreen.setValue(true);
s.game.noReturnRupees.setValue(true);
s.game.disableRupeeCutscenes.setValue(true);
s.game.noSwordRecoil.setValue(true);
s.game.fastClimbing.setValue(true);
s.game.noMissClimbing.setValue(true);
s.game.fastTears.setValue(true);
s.game.biggerWallets.setValue(true);
s.game.invertCameraXAxis.setValue(true);
s.game.freeCamera.setValue(true);
s.game.no2ndFishForCat.setValue(true);
}
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);
s.game.enableFrameInterpolation.setValue(true);
s.game.sunsSong.setValue(true);
s.game.bloomMode.setValue(BloomMode::Dusk);
s.game.autoSave.setValue(true);
}
// =========================================================================
void ImGuiFirstRunPreset::draw() {
const char* modalTitle = "Welcome to Dusk!";
if (m_done) return;
if (!m_opened) {
ImGui::OpenPopup(modalTitle);
m_opened = true;
}
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(800.0f * ImGuiScale(), 0.0f), ImGuiCond_Always);
if (!ImGui::BeginPopupModal(modalTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
// force the user to actually pick one, and not just hit escape to skip the dialog
m_opened = false;
return;
}
ImGui::TextWrapped("Choose a preset to get started. You can change any setting later from the Enhancements menu.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
int chosen = -1;
if (ImGui::BeginTable("##presets", 5, ImGuiTableFlags_None)) {
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthFixed, 16.0f * ImGuiScale());
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthFixed, 16.0f * ImGuiScale());
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::PushFont(ImGuiEngine::fontLarge);
ImGui::TableSetColumnIndex(0);
if (ImGui::Button("Classic##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale()))) {
chosen = 0;
}
ImGui::TableSetColumnIndex(2);
if (ImGui::Button("HD##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale()))) {
chosen = 1;
}
ImGui::TableSetColumnIndex(4);
if (ImGui::Button("Dusk##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale())))
{
chosen = 2;
}
ImGui::PopFont();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Spacing();
ImGui::TextWrapped("All enhancements disabled to match the GameCube version. Good for speedrunning or simple nostalgia!");
ImGui::TableSetColumnIndex(2);
ImGui::Spacing();
ImGui::TextWrapped("Some enhancements enabled to match the HD version. A good starting point for most players!");
ImGui::TableSetColumnIndex(4);
ImGui::Spacing();
ImGui::TextWrapped("More enhancements enabled than the HD preset. Veteran players will appreciate the additional tweaks!");
ImGui::EndTable();
}
if (chosen >= 0) {
if (chosen == 0) ApplyPresetClassic();
if (chosen == 1) ApplyPresetHD();
if (chosen == 2) ApplyPresetDusk();
getSettings().backend.wasPresetChosen.setValue(true);
config::Save();
m_done = true;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
} // namespace dusk
-14
View File
@@ -1,14 +0,0 @@
#pragma once
namespace dusk {
class ImGuiFirstRunPreset {
public:
void draw();
private:
bool m_opened = false;
bool m_done = false;
};
} // namespace dusk
+1
View File
@@ -601,6 +601,7 @@ namespace dusk {
getSettings().game.freeMagicArmor.setValue(false);
getSettings().game.enableTurboKeybind.setValue(false);
getSettings().game.debugFlyCam.setValue(false);
}
SpeedrunInfo m_speedrunInfo;
-9
View File
@@ -66,7 +66,6 @@ namespace dusk {
ImGui::EndDisabled();
}
ImGui::MenuItem("Achievements", nullptr, &m_showAchievements);
#if DUSK_CAN_OPEN_DATA_FOLDER
ImGui::Separator();
@@ -268,12 +267,4 @@ 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));
}
}
+1 -6
View File
@@ -2,10 +2,10 @@
#define DUSK_IMGUI_MENUTOOLS_HPP
#include <aurora/aurora.h>
#include <queue>
#include <string>
#include "imgui.h"
#include "ImGuiAchievements.hpp"
#include "ImGuiSaveEditor.hpp"
#include "ImGuiStateShare.hpp"
@@ -27,8 +27,6 @@ namespace dusk {
void ShowAudioDebug();
void ShowSaveEditor();
void ShowStateShare();
void ShowAchievements();
void notifyAchievement(std::string name);
private:
bool m_showDebugOverlay = false;
@@ -68,9 +66,6 @@ namespace dusk {
bool m_showStateShare = false;
ImGuiStateShare m_stateShare;
bool m_showAchievements = false;
ImGuiAchievements m_achievementsWindow;
};
}
-282
View File
@@ -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 <SDL3/SDL_dialog.h>
#include <SDL3/SDL_filesystem.h>
#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<const char*, 5> skLanguageNames = {
"English", "German", "French", "Spanish", "Italian"
};
static constexpr std::array<SDL_DialogFileFilter, 2> 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<ImGuiPreLaunchWindow*>(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<u8>(selectedLanguage)])) {
for (u8 i = 0; i < skLanguageNames.size(); ++i) {
if (ImGui::Selectable(skLanguageNames[i])) {
getSettings().game.language.setValue(static_cast<GameLanguage>(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
-23
View File
@@ -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
+110 -7
View File
@@ -5,17 +5,110 @@
#endif
#include <aurora/main.h>
#include "dusk/main.h"
#include <algorithm>
#include <array>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#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<char, 4096> path{};
const ssize_t len = readlink("/proc/self/exe", path.data(), path.size() - 1);
if (len > 0) {
path[static_cast<size_t>(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<std::string> args;
args.reserve(static_cast<size_t>(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<char*> 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<std::string> WideArgsToUtf8(int argc, wchar_t** argv) {
@@ -81,8 +180,8 @@ std::vector<std::string> WideArgsToUtf8(int argc, wchar_t** argv) {
}
std::vector<char> utf8Buffer(static_cast<size_t>(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
+8 -8
View File
@@ -18,6 +18,7 @@ UserSettings g_userSettings = {
.fanfareVolume {"audio.fanfareVolume", 100},
.enableReverb {"audio.enableReverb", true},
.enableHrtf {"audio.enableHrtf", false},
.menuSounds {"audio.menuSounds", true},
},
.game = {
@@ -25,8 +26,7 @@ UserSettings g_userSettings = {
// Quality of Life
.enableQuickTransform {"game.enableQuickTransform", false},
.hideTvSettingsScreen {"game.hideTvSettingsScreen", false},
.skipWarningScreen {"game.skipWarningScreen", false},
.hideTvSettingsScreen {"game.hideTvSettingsScreen", true},
.biggerWallets {"game.biggerWallets", false},
.noReturnRupees {"game.noReturnRupees", false},
.disableRupeeCutscenes {"game.disableRupeeCutscenes", false},
@@ -47,11 +47,11 @@ UserSettings g_userSettings = {
.enableMirrorMode {"game.enableMirrorMode", false},
.disableMainHUD {"game.disableMainHUD", false},
.pauseOnFocusLost {"game.pauseOnFocusLost", false},
.enableLinkDollRotation = {"game.enableLinkDollRotation", false },
.enableAchievementNotifications {"game.enableAchievementNotifications", false},
.enableLinkDollRotation = {"game.enableLinkDollRotation", false},
.enableAchievementNotifications {"game.enableAchievementNotifications", true},
// Graphics
.bloomMode {"game.bloomMode", BloomMode::Classic},
.bloomMode {"game.bloomMode", BloomMode::Dusk},
.bloomMultiplier {"game.bloomMultiplier", 1.0f},
.disableWaterRefraction {"game.disableWaterRefraction", false},
.enableFrameInterpolation {"game.enableFrameInterpolation", false},
@@ -78,6 +78,7 @@ UserSettings g_userSettings = {
.invertCameraXAxis {"game.invertCameraXAxis", false},
.invertCameraYAxis {"game.invertCameraYAxis", false},
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
.debugFlyCam {"game.debugFlyCam", false},
// Cheats
.infiniteHearts {"game.infiniteHearts", false},
@@ -113,7 +114,6 @@ UserSettings g_userSettings = {
.showPipelineCompilation {"backend.showPipelineCompilation", false},
.wasPresetChosen {"backend.wasPresetChosen", false},
.enableCrashReporting {"backend.enableCrashReporting", true},
.duskMenuOpen {"backend.duskMenuOpen", false},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)}
}
};
@@ -136,12 +136,12 @@ void registerSettings() {
Register(g_userSettings.audio.fanfareVolume);
Register(g_userSettings.audio.enableReverb);
Register(g_userSettings.audio.enableHrtf);
Register(g_userSettings.audio.menuSounds);
// Game
Register(g_userSettings.game.language);
Register(g_userSettings.game.enableQuickTransform);
Register(g_userSettings.game.hideTvSettingsScreen);
Register(g_userSettings.game.skipWarningScreen);
Register(g_userSettings.game.biggerWallets);
Register(g_userSettings.game.noReturnRupees);
Register(g_userSettings.game.disableRupeeCutscenes);
@@ -203,6 +203,7 @@ void registerSettings() {
Register(g_userSettings.game.gyroInvertPitch);
Register(g_userSettings.game.gyroInvertYaw);
Register(g_userSettings.game.freeCamera);
Register(g_userSettings.game.debugFlyCam);
Register(g_userSettings.backend.isoPath);
Register(g_userSettings.backend.graphicsBackend);
@@ -210,7 +211,6 @@ void registerSettings() {
Register(g_userSettings.backend.showPipelineCompilation);
Register(g_userSettings.backend.wasPresetChosen);
Register(g_userSettings.backend.enableCrashReporting);
Register(g_userSettings.backend.duskMenuOpen);
Register(g_userSettings.backend.cardFileType);
}
+208
View File
@@ -0,0 +1,208 @@
#include "achievements.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "dusk/achievements.h"
#include "fmt/format.h"
#include "m_Do/m_Do_audio.h"
#include "nav_types.hpp"
#include "pane.hpp"
namespace dusk::ui {
namespace {
struct CategoryInfo {
AchievementCategory cat;
const char* label;
};
constexpr CategoryInfo kCategories[] = {
{AchievementCategory::Story, "Story"},
{AchievementCategory::Collection, "Collection"},
{AchievementCategory::Challenge, "Challenge"},
{AchievementCategory::Minigame, "Minigame"},
{AchievementCategory::Misc, "Misc"},
{AchievementCategory::Glitched, "Glitched"},
};
Rml::String build_achievement_info_rml(const Achievement& a) {
Rml::String s = fmt::format(
R"(<div class="achievement-header">)"
R"(<span class="achievement-name{}">{}</span>)"
R"(<span class="achievement-badge{}">{}</span>)"
R"(</div>)"
R"(<p class="achievement-desc">{}</p>)",
a.unlocked ? " unlocked" : "",
a.name,
a.unlocked ? " unlocked" : " locked",
a.unlocked ? "Unlocked" : "Locked",
a.description
);
if (a.isCounter) {
float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f;
s += fmt::format(
R"(<progressbar value="{:.3f}" class="{}"/>)"
R"(<span class="achievement-progress">{} / {}</span>)",
fraction,
a.unlocked ? "progress-done" : "progress-ongoing",
a.progress,
a.goal
);
}
return s;
}
class AchievementRow : public FluentComponent<AchievementRow> {
public:
AchievementRow(Rml::Element* parent, const Achievement& a)
: FluentComponent(createRowRoot(parent))
{
auto& btn = add_child<Button>(Button::Props{"×"});
mClearButton = &btn;
btn.root()->SetClass("achievement-clear", true);
btn.on_nav_command([this, key = std::string(a.key)](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (mConfirming) {
mDoAud_seStartMenu(kSoundClick);
AchievementSystem::get().clearOne(key.c_str());
resetConfirm();
} else {
mConfirming = true;
mClearButton->set_text("Clear?");
}
return true;
}
if (cmd == NavCommand::Cancel && mConfirming) {
resetConfirm();
return true;
}
return false;
});
Component::listen(btn.root(), Rml::EventId::Blur, [this](Rml::Event&) {
resetConfirm();
});
auto* infoDiv = append(mRoot, "div");
infoDiv->SetClass("achievement-info", true);
infoDiv->SetInnerRML(build_achievement_info_rml(a));
}
bool focus() override { return mClearButton->focus(); }
private:
static Rml::Element* createRowRoot(Rml::Element* parent) {
auto* doc = parent->GetOwnerDocument();
auto elem = doc->CreateElement("div");
elem->SetClass("achievement-row", true);
return parent->AppendChild(std::move(elem));
}
void resetConfirm() {
mConfirming = false;
mClearButton->set_text("×");
}
Button* mClearButton = nullptr;
bool mConfirming = false;
};
} // namespace
AchievementsWindow::AchievementsWindow() {
const auto all = AchievementSystem::get().getAchievements();
for (const auto& catInfo : kCategories) {
int catTotal = 0;
for (const auto& a : all) {
if (a.category == catInfo.cat) {
++catTotal;
}
}
if (catTotal == 0) {
continue;
}
add_tab(catInfo.label, [this, cat = catInfo.cat](Rml::Element* content) {
const auto achievements = AchievementSystem::get().getAchievements();
int total = 0, unlocked = 0;
for (const auto& a : achievements) {
if (a.category == cat) {
++total;
if (a.unlocked) {
++unlocked;
}
}
}
auto& pane = add_child<Pane>(content, Pane::Type::Controlled);
pane.add_section(fmt::format("{} / {} unlocked", unlocked, total));
for (const auto& a : achievements) {
if (a.category != cat) {
continue;
}
pane.add_child<AchievementRow>(a);
}
pane.add_section("Actions");
auto& clearAllBtn = pane.add_button("Clear All Achievements");
auto* clearAllPtr = &clearAllBtn;
auto confirmingAll = std::make_shared<bool>(false);
clearAllBtn.on_nav_command([clearAllPtr, confirmingAll](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (*confirmingAll) {
mDoAud_seStartMenu(kSoundClick);
AchievementSystem::get().clearAll();
*confirmingAll = false;
clearAllPtr->set_text("Clear All Achievements");
} else {
*confirmingAll = true;
clearAllPtr->set_text("Are you sure?");
}
return true;
}
if (cmd == NavCommand::Cancel && *confirmingAll) {
*confirmingAll = false;
clearAllPtr->set_text("Clear All Achievements");
return true;
}
return false;
});
clearAllBtn.listen(Rml::EventId::Blur, [clearAllPtr, confirmingAll](Rml::Event&) {
*confirmingAll = false;
clearAllPtr->set_text("Clear All Achievements");
});
pane.finalize();
});
}
}
void AchievementsWindow::update() {
const auto current = AchievementSystem::get().getAchievements();
bool dirty = current.size() != mSnapshot.size();
if (!dirty) {
for (size_t i = 0; i < current.size(); ++i) {
if (current[i].progress != mSnapshot[i].progress ||
current[i].unlocked != mSnapshot[i].unlocked)
{
dirty = true;
break;
}
}
}
if (dirty) {
mSnapshot = current;
refresh_active_tab();
}
Window::update();
}
} // namespace dusk::ui
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "dusk/achievements.h"
#include "window.hpp"
#include <vector>
namespace dusk::ui {
class AchievementsWindow : public Window {
public:
AchievementsWindow();
void update() override;
private:
std::vector<Achievement> mSnapshot;
};
} // namespace dusk::ui
+19 -3
View File
@@ -1,11 +1,25 @@
#include "bool_button.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
namespace dusk::ui {
BoolButton::BoolButton(Rml::Element* parent, Props props)
: BaseControlledSelectButton(parent, {std::move(props.key)}),
: BaseControlledSelectButton(parent,
{
.key = std::move(props.key),
.icon = std::move(props.icon),
}),
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
mIsDisabled(std::move(props.isDisabled)) {}
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)) {}
bool BoolButton::modified() const {
if (mIsModified) {
return mIsModified();
}
return BaseControlledSelectButton::modified();
}
bool BoolButton::disabled() const {
if (mIsDisabled) {
@@ -20,7 +34,9 @@ Rml::String BoolButton::format_value() {
bool BoolButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) {
mSetValue(!mGetValue());
const bool newValue = !mGetValue();
mSetValue(newValue);
mDoAud_seStartMenu(newValue ? kSoundItemEnable : kSoundItemDisable);
return true;
}
return false;
+4
View File
@@ -7,13 +7,16 @@ class BoolButton : public BaseControlledSelectButton {
public:
struct Props {
Rml::String key;
Rml::String icon;
std::function<bool()> getValue;
std::function<void(bool)> setValue;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
};
BoolButton(Rml::Element* parent, Props props);
bool modified() const override;
bool disabled() const override;
protected:
@@ -24,6 +27,7 @@ private:
std::function<int()> mGetValue;
std::function<void(int)> mSetValue;
std::function<bool()> mIsDisabled;
std::function<bool()> mIsModified;
};
} // namespace dusk::ui
+3
View File
@@ -2,6 +2,9 @@
#include "ui.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <utility>
namespace dusk::ui {
-10
View File
@@ -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) {
-10
View File
@@ -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;
@@ -66,15 +65,6 @@ public:
return static_cast<Derived&>(*this);
}
Derived& on_focus(ScopedEventListener::Callback callback) {
return listen(
Rml::EventId::Focus, [this, callback = std::move(callback)](Rml::Event& event) {
if (!disabled()) {
callback(event);
}
});
}
Derived& on_nav_command(std::function<bool(Rml::Event&, NavCommand)> callback) {
listen(Rml::EventId::Click, [this, callback](Rml::Event& event) {
if (!disabled() && callback(event, NavCommand::Confirm)) {
+638
View File
@@ -0,0 +1,638 @@
#include "controller_config.hpp"
#include "bool_button.hpp"
#include "button.hpp"
#include "pane.hpp"
#include "select_button.hpp"
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_keyboard.h>
#include <fmt/format.h>
#include <array>
#include <string>
#include <utility>
#include <vector>
namespace dusk::ui {
namespace {
Rml::String current_controller_name(int port) {
const char* name = PADGetName(port);
return name == nullptr ? "None" : name;
}
Rml::String controller_index_name(u32 index) {
const char* name = PADGetNameForControllerIndex(index);
if (name == nullptr) {
return fmt::format("Controller {}", index + 1);
}
return name;
}
SDL_Gamepad* gamepad_for_port(int port) {
const s32 index = PADGetIndexForPort(port);
if (index < 0) {
return nullptr;
}
return PADGetSDLGamepadForIndex(static_cast<u32>(index));
}
struct SpecificButtonName {
SDL_GamepadType type;
const char* name;
};
struct ButtonNames {
SDL_GamepadButton button;
std::vector<SpecificButtonName> names;
};
// clang-format off
const std::vector<ButtonNames> kGamepadButtonNames = {
{ SDL_GAMEPAD_BUTTON_LEFT_STICK, {
{SDL_GAMEPAD_TYPE_PS3, "L3"},
{SDL_GAMEPAD_TYPE_PS4, "L3"},
{SDL_GAMEPAD_TYPE_PS5, "L3"},
{SDL_GAMEPAD_TYPE_XBOX360, "Left Stick"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Left Stick"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Control Stick"},
}},
{ SDL_GAMEPAD_BUTTON_RIGHT_STICK, {
{SDL_GAMEPAD_TYPE_PS3, "R3"},
{SDL_GAMEPAD_TYPE_PS4, "R3"},
{SDL_GAMEPAD_TYPE_PS5, "R3"},
{SDL_GAMEPAD_TYPE_XBOX360, "Right Stick"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Right Stick"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "C Stick"},
}},
{ SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, {
{SDL_GAMEPAD_TYPE_PS3, "L1"},
{SDL_GAMEPAD_TYPE_PS4, "L1"},
{SDL_GAMEPAD_TYPE_PS5, "L1"},
{SDL_GAMEPAD_TYPE_XBOX360, "LB"},
{SDL_GAMEPAD_TYPE_XBOXONE, "LB"},
}},
{ SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, {
{SDL_GAMEPAD_TYPE_PS3, "R1"},
{SDL_GAMEPAD_TYPE_PS4, "R1"},
{SDL_GAMEPAD_TYPE_PS5, "R1"},
{SDL_GAMEPAD_TYPE_XBOX360, "RB"},
{SDL_GAMEPAD_TYPE_XBOXONE, "RB"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Z"},
}},
{ SDL_GAMEPAD_BUTTON_BACK, {
{SDL_GAMEPAD_TYPE_PS3, "Select"},
{SDL_GAMEPAD_TYPE_PS4, "Share"},
{SDL_GAMEPAD_TYPE_PS5, "Create"},
{SDL_GAMEPAD_TYPE_XBOX360, "Back"},
{SDL_GAMEPAD_TYPE_XBOXONE, "View"},
}},
{ SDL_GAMEPAD_BUTTON_START, {
{SDL_GAMEPAD_TYPE_PS3, "Start"},
{SDL_GAMEPAD_TYPE_PS4, "Options"},
{SDL_GAMEPAD_TYPE_PS5, "Options"},
{SDL_GAMEPAD_TYPE_XBOX360, "Start"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Menu"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Start/Pause"},
}},
};
// clang-format on
Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) {
if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) {
return "Not bound";
}
auto button = static_cast<SDL_GamepadButton>(buttonUntyped);
if (gamepad != nullptr) {
switch (SDL_GetGamepadButtonLabel(gamepad, button)) {
case SDL_GAMEPAD_BUTTON_LABEL_A:
return "A";
case SDL_GAMEPAD_BUTTON_LABEL_B:
return "B";
case SDL_GAMEPAD_BUTTON_LABEL_X:
return "X";
case SDL_GAMEPAD_BUTTON_LABEL_Y:
return "Y";
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
return "Cross";
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
return "Circle";
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
return "Triangle";
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
return "Square";
default:
break;
}
}
const SDL_GamepadType type =
gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN;
for (const auto& buttonNames : kGamepadButtonNames) {
if (buttonNames.button != button) {
continue;
}
for (const auto& name : buttonNames.names) {
if (name.type == type) {
return name.name;
}
}
}
switch (button) {
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
return "D-pad left";
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
return "D-pad right";
case SDL_GAMEPAD_BUTTON_DPAD_UP:
return "D-pad up";
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
return "D-pad down";
default:
break;
}
if (const char* name = PADGetNativeButtonName(buttonUntyped)) {
return name;
}
return "Unknown";
}
Rml::String native_axis_name(const PADAxisMapping& mapping, SDL_Gamepad* gamepad) {
if (mapping.nativeAxis.nativeAxis != -1) {
Rml::String value = PADGetNativeAxisName(mapping.nativeAxis);
if (mapping.padAxis != PAD_AXIS_TRIGGER_L && mapping.padAxis != PAD_AXIS_TRIGGER_R) {
value += mapping.nativeAxis.sign == AXIS_SIGN_POSITIVE ? "+" : "-";
}
return value;
}
if (mapping.nativeButton != -1) {
return native_button_name(gamepad, static_cast<u32>(mapping.nativeButton));
}
return "Not bound";
}
bool is_dpad_button(PADButton button) {
return button == PAD_BUTTON_UP || button == PAD_BUTTON_DOWN || button == PAD_BUTTON_LEFT ||
button == PAD_BUTTON_RIGHT;
}
bool is_action_button(PADButton button) {
return button == PAD_BUTTON_A || button == PAD_BUTTON_B || button == PAD_BUTTON_X ||
button == PAD_BUTTON_Y || button == PAD_BUTTON_START || button == PAD_TRIGGER_Z;
}
bool input_neutral(int port) {
if (port < 0) {
return true;
}
return PADGetNativeButtonPressed(port) == -1 && PADGetNativeAxisPulled(port).nativeAxis == -1;
}
// A Keydown event with KI_ESCAPE may have been dispatched from the controller bindings,
// so instead poll the keyboard input directly for Escape-to-unbind
bool keyboard_escape_pressed() {
int keyCount = 0;
const bool* keys = SDL_GetKeyboardState(&keyCount);
return keys != nullptr && SDL_SCANCODE_ESCAPE < keyCount && keys[SDL_SCANCODE_ESCAPE];
}
} // namespace
ControllerConfigWindow::ControllerConfigWindow() {
listen(
Rml::EventId::Keydown,
[this](Rml::Event& event) {
if (capture_active() || mSuppressNavigationUntilNeutral) {
event.StopPropagation();
}
},
true);
if (auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr) {
if (auto* root = context->GetRootElement()) {
mListeners.emplace_back(std::make_unique<ScopedEventListener>(
root, "controllerchange", [this](Rml::Event&) { refresh_controller_page(); }));
}
}
for (int port = PAD_CHAN0; port < PAD_CHANMAX; ++port) {
add_tab(fmt::format("Port {}", port + 1), [this, port](Rml::Element* content) {
if (mPendingPort != -1 && mPendingPort != port) {
cancel_pending_binding();
}
build_port_tab(content, port);
});
}
}
void ControllerConfigWindow::hide(bool close) {
cancel_pending_binding();
Window::hide(close);
}
void ControllerConfigWindow::update() {
poll_pending_binding();
Window::update();
}
void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
mRightPane = &rightPane;
mActivePort = port;
auto addPageButton = [this, &leftPane, &rightPane, port](
Page page, Rml::String key, auto getValue) {
leftPane.register_control(leftPane.add_select_button({
.key = std::move(key),
.getValue = std::move(getValue),
}),
rightPane, [this, port, page](Pane& pane) {
mPage = page;
render_page(pane, port, page);
});
};
addPageButton(Page::Controller, "Controller", [port] { return current_controller_name(port); });
addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); });
addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); });
addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); });
leftPane.add_section("Options");
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
.key = "Enable Dead Zones",
.getValue =
[port] {
PADDeadZones* deadZones = PADGetDeadZones(port);
return deadZones != nullptr && deadZones->useDeadzones;
},
.setValue =
[port](bool value) {
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
deadZones->useDeadzones = value;
PADSerializeMappings();
}
},
.isDisabled = [port] { return PADGetDeadZones(port) == nullptr; },
}),
rightPane, [](Pane& pane) {
pane.add_text("Apply configured dead zones to the sticks and analog triggers.");
});
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
.key = "Emulate Triggers",
.getValue =
[port] {
PADDeadZones* deadZones = PADGetDeadZones(port);
return deadZones != nullptr && deadZones->emulateTriggers;
},
.setValue =
[port](bool value) {
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
deadZones->emulateTriggers = value;
PADSerializeMappings();
}
},
.isDisabled = [port] { return PADGetDeadZones(port) == nullptr; },
}),
rightPane, [](Pane& pane) {
pane.add_text("Treat analog trigger movement as digital L and R button input.");
});
render_page(rightPane, port, mPage);
}
void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
pane.clear();
switch (page) {
case Page::Controller: {
const u32 controllerCount = PADCount();
if (controllerCount == 0) {
pane.add_text("No controllers detected");
break;
}
pane.add_button({
.text = "None",
.isSelected = [port] { return PADGetIndexForPort(port) < 0; },
})
.on_pressed([this, port] {
mDoAud_seStartMenu(kSoundItemChange);
cancel_pending_binding();
PADClearPort(port);
PADSerializeMappings();
});
for (u32 i = 0; i < controllerCount; ++i) {
pane.add_button(
{
.text = controller_index_name(i),
.isSelected =
[port, i] { return PADGetIndexForPort(port) == static_cast<s32>(i); },
})
.on_pressed([this, port, i] {
mDoAud_seStartMenu(kSoundItemChange);
cancel_pending_binding();
PADSetPortForIndex(i, port);
PADSerializeMappings();
});
}
break;
}
case Page::Buttons: {
u32 buttonCount = 0;
PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount);
if (mappings == nullptr) {
pane.add_text("No controller selected");
break;
}
SDL_Gamepad* gamepad = gamepad_for_port(port);
pane.add_section("Buttons");
for (u32 i = 0; i < buttonCount; ++i) {
PADButtonMapping& mapping = mappings[i];
if (!is_action_button(mapping.padButton)) {
continue;
}
pane.add_select_button({
.key = PADGetButtonName(mapping.padButton),
.getValue =
[this, &mapping, gamepad] {
if (mPendingButtonMapping == &mapping) {
return pending_button_label();
}
return native_button_name(
gamepad, mapping.nativeButton);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingButtonMapping = &mapping;
});
}
pane.add_section("D-Pad");
for (u32 i = 0; i < buttonCount; ++i) {
PADButtonMapping& mapping = mappings[i];
if (!is_dpad_button(mapping.padButton)) {
continue;
}
pane.add_select_button({
.key = PADGetButtonName(mapping.padButton),
.getValue =
[this, &mapping, gamepad] {
if (mPendingButtonMapping == &mapping) {
return pending_button_label();
}
return native_button_name(
gamepad, mapping.nativeButton);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingButtonMapping = &mapping;
});
}
break;
}
case Page::Triggers: {
u32 axisCount = 0;
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
u32 buttonCount = 0;
PADButtonMapping* buttons = PADGetButtonMappings(port, &buttonCount);
if (axes == nullptr && buttons == nullptr) {
pane.add_text("No controller selected");
break;
}
SDL_Gamepad* gamepad = gamepad_for_port(port);
pane.add_section("Analog");
constexpr std::array<PADAxis, 2> kTriggerAxes = {PAD_AXIS_TRIGGER_L, PAD_AXIS_TRIGGER_R};
if (axes != nullptr) {
for (PADAxis axis : kTriggerAxes) {
if (axis >= axisCount) {
continue;
}
PADAxisMapping& mapping = axes[axis];
pane.add_select_button({
.key = PADGetAxisName(mapping.padAxis),
.getValue =
[this, &mapping, gamepad] {
if (mPendingAxisMapping == &mapping) {
return pending_axis_label();
}
return native_axis_name(mapping, gamepad);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingAxisMapping = &mapping;
});
}
}
pane.add_section("Digital");
if (buttons != nullptr) {
for (u32 i = 0; i < buttonCount; ++i) {
PADButtonMapping& mapping = buttons[i];
if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) {
continue;
}
pane.add_select_button({
.key = PADGetButtonName(mapping.padButton),
.getValue =
[this, &mapping, gamepad] {
if (mPendingButtonMapping == &mapping) {
return pending_button_label();
}
return native_button_name(
gamepad, mapping.nativeButton);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingButtonMapping = &mapping;
});
}
}
break;
}
case Page::Sticks: {
u32 axisCount = 0;
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
if (axes == nullptr) {
pane.add_text("No controller selected");
break;
}
SDL_Gamepad* gamepad = gamepad_for_port(port);
auto addAxis = [&](PADAxis axis) {
if (axis >= axisCount) {
return;
}
PADAxisMapping& mapping = axes[axis];
pane.add_select_button({
.key = PADGetAxisDirectionLabel(mapping.padAxis),
.getValue =
[this, &mapping, gamepad] {
if (mPendingAxisMapping == &mapping) {
return pending_axis_label();
}
return native_axis_name(mapping, gamepad);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingAxisMapping = &mapping;
});
};
pane.add_section("Control Stick");
addAxis(PAD_AXIS_LEFT_Y_POS);
addAxis(PAD_AXIS_LEFT_Y_NEG);
addAxis(PAD_AXIS_LEFT_X_NEG);
addAxis(PAD_AXIS_LEFT_X_POS);
pane.add_section("C Stick");
addAxis(PAD_AXIS_RIGHT_Y_POS);
addAxis(PAD_AXIS_RIGHT_Y_NEG);
addAxis(PAD_AXIS_RIGHT_X_NEG);
addAxis(PAD_AXIS_RIGHT_X_POS);
break;
}
}
}
void ControllerConfigWindow::refresh_controller_page() {
if (!visible() || mPage != Page::Controller || mRightPane == nullptr) {
return;
}
render_page(*mRightPane, mActivePort, Page::Controller);
}
void ControllerConfigWindow::poll_pending_binding() {
if (mSuppressNavigationUntilNeutral && input_neutral(mSuppressNavigationPort)) {
mSuppressNavigationUntilNeutral = false;
mSuppressNavigationPort = -1;
}
if (!capture_active()) {
return;
}
if (keyboard_escape_pressed()) {
unmap_pending_binding();
return;
}
if (!mPendingBindingArmed) {
if (pending_input_neutral()) {
mPendingBindingArmed = true;
}
return;
}
if (mPendingButtonMapping != nullptr) {
const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort);
if (nativeButton != -1) {
const int completedPort = mPendingPort;
mPendingButtonMapping->nativeButton = static_cast<u32>(nativeButton);
finish_pending_binding(completedPort);
}
return;
}
if (mPendingAxisMapping != nullptr) {
const PADSignedNativeAxis nativeAxis = PADGetNativeAxisPulled(mPendingPort);
if (nativeAxis.nativeAxis != -1) {
const int completedPort = mPendingPort;
mPendingAxisMapping->nativeAxis = nativeAxis;
mPendingAxisMapping->nativeButton = -1;
finish_pending_binding(completedPort);
return;
}
const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort);
if (nativeButton != -1) {
const int completedPort = mPendingPort;
mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
mPendingAxisMapping->nativeButton = nativeButton;
finish_pending_binding(completedPort);
}
}
}
void ControllerConfigWindow::finish_pending_binding(int completedPort) {
mPendingButtonMapping = nullptr;
mPendingAxisMapping = nullptr;
mPendingPort = -1;
mPendingBindingArmed = false;
mSuppressNavigationUntilNeutral = true;
mSuppressNavigationPort = completedPort;
PADSerializeMappings();
}
void ControllerConfigWindow::unmap_pending_binding() {
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr) {
return;
}
const int completedPort = mPendingPort;
if (mPendingButtonMapping != nullptr) {
mPendingButtonMapping->nativeButton = PAD_NATIVE_BUTTON_INVALID;
}
if (mPendingAxisMapping != nullptr) {
mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
mPendingAxisMapping->nativeButton = -1;
}
finish_pending_binding(completedPort);
}
bool ControllerConfigWindow::capture_active() const {
return mPendingButtonMapping != nullptr || mPendingAxisMapping != nullptr;
}
bool ControllerConfigWindow::pending_input_neutral() const {
return input_neutral(mPendingPort);
}
Rml::String ControllerConfigWindow::pending_button_label() const {
return mPendingBindingArmed ? "Press a button..." : "Waiting...";
}
Rml::String ControllerConfigWindow::pending_axis_label() const {
return mPendingBindingArmed ? "Move axis or press a button..." : "Waiting...";
}
void ControllerConfigWindow::cancel_pending_binding() {
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr &&
!mSuppressNavigationUntilNeutral)
{
return;
}
mPendingButtonMapping = nullptr;
mPendingAxisMapping = nullptr;
mPendingPort = -1;
mPendingBindingArmed = false;
mSuppressNavigationUntilNeutral = false;
mSuppressNavigationPort = -1;
}
} // namespace dusk::ui
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "window.hpp"
#include <pad.h>
namespace dusk::ui {
class ControllerConfigWindow : public Window {
public:
ControllerConfigWindow();
void update() override;
void hide(bool close) override;
private:
enum class Page {
Controller,
Buttons,
Triggers,
Sticks,
};
void build_port_tab(Rml::Element* content, int port);
void render_page(class Pane& pane, int port, Page page);
void refresh_controller_page();
void poll_pending_binding();
void finish_pending_binding(int completedPort);
void unmap_pending_binding();
bool capture_active() const;
bool pending_input_neutral() const;
Rml::String pending_button_label() const;
Rml::String pending_axis_label() const;
void cancel_pending_binding();
Page mPage = Page::Controller;
Pane* mRightPane = nullptr;
int mActivePort = 0;
int mPendingPort = -1;
bool mPendingBindingArmed = false;
bool mSuppressNavigationUntilNeutral = false;
int mSuppressNavigationPort = -1;
PADButtonMapping* mPendingButtonMapping = nullptr;
PADAxisMapping* mPendingAxisMapping = nullptr;
};
} // namespace dusk::ui
+9 -2
View File
@@ -3,6 +3,9 @@
#include "aurora/rmlui.hpp"
#include "ui.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
namespace dusk::ui {
namespace {
@@ -17,7 +20,7 @@ Rml::ElementDocument* load_document(const Rml::String& source) {
} // namespace
Document::Document(const Rml::String& source) : mDocument(load_document(source)) {
// Block events while hidden (except for Menu command)
// Block events while hidden (except for Menu command); play nav sounds when visible
listen(
Rml::EventId::Keydown,
[this](Rml::Event& event) {
@@ -38,7 +41,10 @@ Document::Document(const Rml::String& source) : mDocument(load_document(source))
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
if (cmd == NavCommand::None) {
return;
}
if (handle_nav_command(event, cmd)) {
event.StopPropagation();
}
});
@@ -100,6 +106,7 @@ bool Document::visible() const {
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Menu) {
mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen);
toggle();
return true;
}
+4
View File
@@ -31,6 +31,10 @@ public:
show();
}
}
void push(std::unique_ptr<Document> document) {
push_document(std::move(document));
hide(false);
}
void pop() {
hide(true);
show_top_document();
+371 -324
View File
File diff suppressed because it is too large Load Diff
+12 -1
View File
@@ -10,9 +10,20 @@ ScopedEventListener::ScopedEventListener(
mElement->AddEventListener(mEvent, this, mCapture);
}
ScopedEventListener::ScopedEventListener(
Rml::Element* element, Rml::String event, Callback callback, bool capture)
: mElement(element), mEventName(std::move(event)), mCapture(capture),
mCallback(std::move(callback)) {
mElement->AddEventListener(mEventName, this, mCapture);
}
ScopedEventListener::~ScopedEventListener() {
if (mElement != nullptr) {
mElement->RemoveEventListener(mEvent, this, mCapture);
if (!mEventName.empty()) {
mElement->RemoveEventListener(mEventName, this, mCapture);
} else {
mElement->RemoveEventListener(mEvent, this, mCapture);
}
mElement = nullptr;
}
}
+3
View File
@@ -12,6 +12,8 @@ public:
ScopedEventListener(
Rml::Element* element, Rml::EventId event, Callback callback, bool capture = false);
ScopedEventListener(
Rml::Element* element, Rml::String event, Callback callback, bool capture = false);
~ScopedEventListener() override;
ScopedEventListener(const ScopedEventListener&) = delete;
@@ -25,6 +27,7 @@ public:
private:
Rml::Element* mElement = nullptr;
Rml::EventId mEvent = Rml::EventId::Invalid;
Rml::String mEventName;
bool mCapture = false;
Callback mCallback;
};
+283
View File
@@ -0,0 +1,283 @@
#include "graphics_tuner.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <dolphin/gx/GXAurora.h>
#include <dolphin/vi.h>
#include <fmt/format.h>
#include "dusk/config.hpp"
#include "dusk/settings.h"
#include <algorithm>
#include <string>
namespace dusk::ui {
namespace {
const Rml::String kDocumentSource = R"RML(
<rml>
<head>
<link type="text/rcss" href="res/rml/tuner.rcss" />
</head>
<body>
<div id="root" class="tuner-root">
<div class="tuner">
<div class="header">
<div id="title"></div>
<div id="carousel-container" class="carousel-container"></div>
</div>
<div id="description" class="description"></div>
<div class="divider"></div>
<div id="footer" class="footer"></div>
</div>
</div>
</body>
</rml>
)RML";
int get_value(GraphicsOption option) {
switch (option) {
case GraphicsOption::InternalResolution:
return getSettings().game.internalResolutionScale.getValue();
case GraphicsOption::ShadowResolution:
return getSettings().game.shadowResolutionMultiplier.getValue();
case GraphicsOption::BloomMode:
return static_cast<int>(getSettings().game.bloomMode.getValue());
case GraphicsOption::BloomMultiplier:
return std::clamp(
static_cast<int>(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0,
100);
}
return 0;
}
void set_value(GraphicsOption option, int value) {
switch (option) {
case GraphicsOption::InternalResolution:
getSettings().game.internalResolutionScale.setValue(value);
VISetFrameBufferScale(static_cast<float>(value));
break;
case GraphicsOption::ShadowResolution:
getSettings().game.shadowResolutionMultiplier.setValue(value);
break;
case GraphicsOption::BloomMode:
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
value, static_cast<int>(BloomMode::Off), static_cast<int>(BloomMode::Dusk))));
break;
case GraphicsOption::BloomMultiplier:
getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
break;
}
config::Save();
}
Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
auto* doc = parent->GetOwnerDocument();
auto root = doc->CreateElement("div");
root->SetClass("stepped-carousel", true);
root->SetAttribute("tabindex", "0");
return parent->AppendChild(std::move(root));
}
Rml::Element* create_stepped_carousel_arrow(
Rml::Element* parent, const Rml::String& className, const Rml::String& label) {
auto* doc = parent->GetOwnerDocument();
auto button = doc->CreateElement("button");
button->SetClass("stepped-carousel-arrow", true);
button->SetClass(className, true);
button->SetInnerRML(label);
return parent->AppendChild(std::move(button));
}
} // namespace
SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
: Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "&#xe5cb;");
mValueElem = append(mRoot, "div");
mValueElem->SetClass("stepped-carousel-value", true);
Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", "&#xe5cc;");
listen(prevElem, Rml::EventId::Click,
[this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
listen(nextElem, Rml::EventId::Click,
[this](Rml::Event&) { handle_nav_command(NavCommand::Right); });
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::None && handle_nav_command(cmd)) {
event.StopPropagation();
}
});
}
bool SteppedCarousel::focus() {
return Component::focus();
}
void SteppedCarousel::update() {
if (mValueElem == nullptr) {
return;
}
const int value = std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
if (mProps.formatValue) {
mValueElem->SetInnerRML(mProps.formatValue(value));
} else {
mValueElem->SetInnerRML(std::to_string(value));
}
}
bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Left) {
const int value = mProps.getValue ? mProps.getValue() : 0;
apply(std::clamp(value - mProps.step, mProps.min, mProps.max));
return true;
}
if (cmd == NavCommand::Right) {
const int value = mProps.getValue ? mProps.getValue() : 0;
apply(std::clamp(value + mProps.step, mProps.min, mProps.max));
return true;
}
return false;
}
void SteppedCarousel::apply(int value) {
const int nextValue = std::clamp(value, mProps.min, mProps.max);
const int currentValue =
std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
if (nextValue == currentValue) {
return;
}
mDoAud_seStartMenu(kSoundItemChange);
if (mProps.onChange) {
mProps.onChange(nextValue);
}
}
Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
switch (option) {
case GraphicsOption::InternalResolution: {
u32 width = 0;
u32 height = 0;
AuroraGetRenderSize(&width, &height);
if (value <= 0) {
return fmt::format("Auto ({}×{})", width, height);
} else {
return fmt::format("{}× ({}×{})", value, width, height);
}
}
case GraphicsOption::ShadowResolution:
return fmt::format("{}×", value);
case GraphicsOption::BloomMode:
switch (static_cast<BloomMode>(value)) {
case BloomMode::Off:
return "Off";
case BloomMode::Classic:
return "Classic";
case BloomMode::Dusk:
return "Dusk";
}
break;
case GraphicsOption::BloomMultiplier:
return fmt::format("{}%", value);
}
return "";
}
GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
: Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
if (mDocument == nullptr) {
return;
}
if (auto* title = mDocument->GetElementById("title")) {
title->SetInnerRML(escape(props.title));
}
if (auto* description = mDocument->GetElementById("description")) {
description->SetInnerRML(escape(props.helpText));
}
if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
add_component<SteppedCarousel>(carouselParent,
SteppedCarousel::Props{
.min = mValueMin,
.max = mValueMax,
.step = 1,
.getValue = [this] { return get_value(mOption); },
.onChange = [this](int value) { set_value(mOption, value); },
.formatValue =
[this](int value) { return format_graphics_setting_value(mOption, value); },
});
}
if (auto* footer = mDocument->GetElementById("footer")) {
auto& returnButton = add_component<Button>(footer, "\xE2\x86\x90 Return", "footer-button")
.on_pressed([this] { pop(); });
returnButton.root()->SetClass("return", true);
auto& resetButton =
add_component<Button>(footer, "Reset to default", "footer-button").on_pressed([this] {
mDoAud_seStartMenu(kSoundItemChange);
reset_default();
});
resetButton.root()->SetClass("reset", true);
}
// Hide document after transition completion
mRoot = mDocument->GetElementById("root");
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
Document::visible())
{
Document::hide(mPendingClose);
}
});
}
void GraphicsTuner::show() {
Document::show();
mRoot->SetAttribute("open", "");
mDoAud_seStartMenu(kSoundWindowOpen);
}
void GraphicsTuner::hide(bool close) {
mRoot->RemoveAttribute("open");
if (close) {
mPendingClose = true;
mDoAud_seStartMenu(kSoundWindowClose);
}
}
void GraphicsTuner::update() {
for (const auto& component : mComponents) {
component->update();
}
Document::update();
}
bool GraphicsTuner::focus() {
for (const auto& component : mComponents) {
if (component->focus()) {
return true;
}
}
return false;
}
bool GraphicsTuner::visible() const {
return mRoot->HasAttribute("open");
}
bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel) {
pop();
return true;
}
return Document::handle_nav_command(event, cmd);
}
void GraphicsTuner::reset_default() {
set_value(mOption, mDefaultValue);
}
} // namespace dusk::ui
+90
View File
@@ -0,0 +1,90 @@
#pragma once
#include "button.hpp"
#include "component.hpp"
#include "document.hpp"
#include "ui.hpp"
#include <functional>
#include <memory>
#include <type_traits>
#include <utility>
#include <vector>
namespace dusk::ui {
class SteppedCarousel : public Component {
public:
struct Props {
int min = 0;
int max = 0;
int step = 1;
std::function<int()> getValue;
std::function<void(int)> onChange;
std::function<Rml::String(int)> formatValue;
};
SteppedCarousel(Rml::Element* parent, Props props);
bool focus() override;
void update() override;
private:
bool handle_nav_command(NavCommand cmd);
void apply(int value);
Props mProps;
Rml::Element* mValueElem = nullptr;
};
enum class GraphicsOption {
InternalResolution,
ShadowResolution,
BloomMode,
BloomMultiplier,
};
Rml::String format_graphics_setting_value(GraphicsOption option, int value);
struct GraphicsTunerProps {
GraphicsOption option;
Rml::String title;
Rml::String helpText;
int valueMin = 0;
int valueMax = 0;
int defaultValue = 0;
};
class GraphicsTuner : public Document {
public:
explicit GraphicsTuner(GraphicsTunerProps props);
void show() override;
void hide(bool close) override;
void update() override;
bool focus() override;
bool visible() const override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
template <typename T, typename... Args>
requires std::is_base_of_v<Component, T> T& add_component(Args&&... args) {
auto child = std::make_unique<T>(std::forward<Args>(args)...);
T& ref = *child;
mComponents.emplace_back(std::move(child));
return ref;
}
void reset_default();
GraphicsOption mOption;
int mValueMin = 0;
int mValueMax = 0;
int mDefaultValue = 0;
std::vector<std::unique_ptr<Component> > mComponents;
Rml::Element* mRoot;
};
} // namespace dusk::ui
+201 -14
View File
@@ -5,6 +5,7 @@
#include <RmlUi/Core.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL_touch.h>
#include <aurora/rmlui.hpp>
#include <dolphin/pad.h>
@@ -22,6 +23,10 @@ constexpr double kGamepadMenuChordGraceDuration = 0.12;
constexpr Sint16 kGamepadAxisPressThreshold = 16384;
constexpr Sint16 kGamepadAxisReleaseThreshold = 12000;
constexpr int kGamepadAxisDirectionCount = SDL_GAMEPAD_AXIS_COUNT * 2;
constexpr int kMenuTapFingerCount = 3;
constexpr float kMenuTapMoveThreshold = 12.0f;
constexpr double kMenuTapMaxDownSpan = 0.18;
constexpr double kMenuTapMaxDuration = 0.55;
struct GamepadRepeatState {
Rml::Input::KeyIdentifier key = Rml::Input::KI_UNKNOWN;
@@ -32,11 +37,26 @@ struct GamepadRepeatState {
bool pending = false;
};
struct TouchTapFinger {
SDL_FingerID id = 0;
Rml::Vector2f startPosition;
bool active = false;
};
struct TouchTapState {
std::array<TouchTapFinger, kMenuTapFingerCount> fingers;
int activeCount = 0;
double firstDownAt = 0.0;
bool candidate = false;
bool failed = false;
};
bool sPadInputBlocked = false;
std::array<GamepadRepeatState, SDL_GAMEPAD_BUTTON_COUNT> sGamepadButtonRepeats;
std::array<GamepadRepeatState, kGamepadAxisDirectionCount> sGamepadAxisRepeats;
std::array<u32, PAD_MAX_CONTROLLERS> sPadHoldMasks;
std::array<bool, PAD_MAX_CONTROLLERS> sMenuChordConsumed;
TouchTapState sTouchMenuTap;
double now_seconds() noexcept {
return static_cast<double>(SDL_GetTicksNS()) / 1000000000.0;
@@ -55,13 +75,38 @@ bool has_menu_chord_part_held(u32 port) noexcept {
return (held & (PAD_TRIGGER_R | PAD_BUTTON_START)) != 0;
}
bool should_block_pad_for_menu_chord() noexcept {
for (u32 port = 0; port < sPadHoldMasks.size(); ++port) {
if (sMenuChordConsumed[port] && has_menu_chord_part_held(port)) {
return true;
}
const char* controller_change_type(Uint32 eventType) noexcept {
switch (eventType) {
case SDL_EVENT_GAMEPAD_ADDED:
return "added";
case SDL_EVENT_GAMEPAD_REMOVED:
return "removed";
case SDL_EVENT_GAMEPAD_REMAPPED:
return "remapped";
default:
return nullptr;
}
return false;
}
void dispatch_controller_change_event(const SDL_Event& event) noexcept {
const char* type = controller_change_type(event.type);
if (type == nullptr) {
return;
}
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
auto* root = context->GetRootElement();
if (root == nullptr) {
return;
}
Rml::Dictionary parameters;
parameters["type"] = Rml::String(type);
parameters["which"] = static_cast<int>(event.gdevice.which);
root->DispatchEvent("controllerchange", parameters);
}
PADButton pad_button_from_axis(PADAxis axis) noexcept {
@@ -355,6 +400,133 @@ void clear_gamepad_repeats() noexcept {
sMenuChordConsumed.fill(false);
}
void reset_touch_menu_tap() noexcept {
sTouchMenuTap = {};
}
Rml::Vector2f touch_position(const SDL_TouchFingerEvent& event, Rml::Context& context) noexcept {
const auto dimensions = context.GetDimensions();
return {
event.x * static_cast<float>(dimensions.x),
event.y * static_cast<float>(dimensions.y),
};
}
TouchTapFinger* find_touch_finger(SDL_FingerID id) noexcept {
for (auto& finger : sTouchMenuTap.fingers) {
if (finger.active && finger.id == id) {
return &finger;
}
}
return nullptr;
}
TouchTapFinger* find_free_touch_finger() noexcept {
for (auto& finger : sTouchMenuTap.fingers) {
if (!finger.active) {
return &finger;
}
}
return nullptr;
}
bool touch_moved_too_far(
const TouchTapFinger& finger, Rml::Vector2f position, Rml::Context& context) noexcept {
const Rml::Vector2f delta = position - finger.startPosition;
const float threshold =
kMenuTapMoveThreshold * std::max(context.GetDensityIndependentPixelRatio(), 1.0f);
return delta.SquaredMagnitude() > threshold * threshold;
}
void dispatch_menu_key(Rml::Context& context) noexcept {
context.ProcessMouseLeave();
context.ProcessKeyDown(Rml::Input::KI_F1, 0);
context.ProcessKeyUp(Rml::Input::KI_F1, 0);
}
bool handle_touch_menu_tap(Rml::Context& context, const SDL_Event& event) noexcept {
switch (event.type) {
case SDL_EVENT_FINGER_DOWN: {
const double now = now_seconds();
if (sTouchMenuTap.activeCount == 0) {
reset_touch_menu_tap();
sTouchMenuTap.firstDownAt = now;
}
if (sTouchMenuTap.candidate || sTouchMenuTap.activeCount >= kMenuTapFingerCount ||
find_touch_finger(event.tfinger.fingerID) != nullptr)
{
sTouchMenuTap.failed = true;
return false;
}
auto* finger = find_free_touch_finger();
if (finger == nullptr) {
sTouchMenuTap.failed = true;
return false;
}
*finger = TouchTapFinger{
.id = event.tfinger.fingerID,
.startPosition = touch_position(event.tfinger, context),
.active = true,
};
sTouchMenuTap.activeCount++;
if (now - sTouchMenuTap.firstDownAt > kMenuTapMaxDownSpan) {
sTouchMenuTap.failed = true;
}
if (sTouchMenuTap.activeCount == kMenuTapFingerCount) {
sTouchMenuTap.candidate = true;
}
return false;
}
case SDL_EVENT_FINGER_MOTION: {
auto* finger = find_touch_finger(event.tfinger.fingerID);
if (finger == nullptr) {
return false;
}
if (touch_moved_too_far(*finger, touch_position(event.tfinger, context), context)) {
sTouchMenuTap.failed = true;
}
return false;
}
case SDL_EVENT_FINGER_UP: {
auto* finger = find_touch_finger(event.tfinger.fingerID);
if (finger == nullptr) {
return false;
}
const double now = now_seconds();
if (!sTouchMenuTap.candidate ||
touch_moved_too_far(*finger, touch_position(event.tfinger, context), context))
{
sTouchMenuTap.failed = true;
}
*finger = {};
sTouchMenuTap.activeCount--;
if (sTouchMenuTap.activeCount > 0) {
return false;
}
const bool shouldDispatch = sTouchMenuTap.candidate && !sTouchMenuTap.failed &&
now - sTouchMenuTap.firstDownAt <= kMenuTapMaxDuration;
reset_touch_menu_tap();
if (shouldDispatch) {
dispatch_menu_key(context);
return true;
}
return false;
}
case SDL_EVENT_FINGER_CANCELED:
reset_touch_menu_tap();
return false;
default:
return false;
}
}
void begin_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
if (repeat.held) {
return;
@@ -454,7 +626,8 @@ void process_axis_direction(
}
set_pad_button_held(port, heldPadButton, true);
const bool chorded = heldPadButton == PAD_TRIGGER_R && is_menu_chord(port);
const bool chorded = heldPadButton == PAD_TRIGGER_R && is_menu_chord(port) &&
(port >= sMenuChordConsumed.size() || !sMenuChordConsumed[port]);
if (chorded) {
consume_menu_chord(port, context);
}
@@ -476,7 +649,7 @@ void process_axis_direction(
} // namespace
void sync_input_block() noexcept {
const bool shouldBlock = any_document_visible() || should_block_pad_for_menu_chord();
const bool shouldBlock = any_document_visible();
if (sPadInputBlocked == shouldBlock) {
return;
}
@@ -496,25 +669,39 @@ void release_input_block() noexcept {
void reset_input_state() noexcept {
clear_gamepad_repeats();
reset_touch_menu_tap();
}
void handle_event(const SDL_Event& event) noexcept {
if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
reset_input_state();
sync_input_block();
return;
}
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
{
return;
if (event.type != SDL_EVENT_GAMEPAD_REMOVED) {
return;
}
}
dispatch_controller_change_event(event);
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
if (event.type == SDL_EVENT_FINGER_DOWN || event.type == SDL_EVENT_FINGER_MOTION ||
event.type == SDL_EVENT_FINGER_UP || event.type == SDL_EVENT_FINGER_CANCELED)
{
if (handle_touch_menu_tap(*context, event)) {
sync_input_block();
}
return;
}
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
{
return;
}
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
process_axis_direction(*context, event.gaxis, AXIS_SIGN_POSITIVE);
process_axis_direction(*context, event.gaxis, AXIS_SIGN_NEGATIVE);
@@ -1,10 +1,17 @@
#include "popup.hpp"
#include "menu_bar.hpp"
#include <RmlUi/Core.h>
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include "achievements.hpp"
#include "aurora/rmlui.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "editor.hpp"
#include "f_pc/f_pc_manager.h"
#include "f_pc/f_pc_name.h"
#include "imgui.h"
#include "settings.hpp"
#include "ui.hpp"
@@ -23,26 +30,37 @@ const Rml::String kDocumentSource = R"RML(
<link type="text/rcss" href="res/rml/popup.rcss" />
</head>
<body>
<popup id="popup"></div>
<popup id="popup" />
</body>
</rml>
)RML";
}
Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{.autoSelect = false});
mTabBar->add_tab("Settings", [] { push_document(std::make_unique<SettingsWindow>()); });
mTabBar->add_tab("Warp", [] {
// TODO
});
mTabBar->add_tab("Editor", [] { push_document(std::make_unique<EditorWindow>()); });
MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
.onClose =
[this] {
mDoAud_seStartMenu(kSoundMenuClose);
hide(false);
},
.autoSelect = false,
});
mTabBar->add_tab("Settings", [this] { push(std::make_unique<SettingsWindow>()); });
// mTabBar->add_tab("Warp", [] {
// // TODO
// });
mTabBar->add_tab("Editor", [this] { push(std::make_unique<EditorWindow>()); });
mTabBar->add_tab("Achievements", [this] { push(std::make_unique<AchievementsWindow>()); });
mTabBar->add_tab("Reset", [this] {
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
mTabBar->set_active_tab(-1);
if (fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) {
return;
}
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
hide(false);
});
mTabBar->add_tab("Exit", [] { IsRunning = false; });
mTabBar->add_tab("Quit", [] { IsRunning = false; });
// Hide document after transition completion
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
@@ -52,30 +70,31 @@ Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("pop
Document::hide(mPendingClose);
}
});
// We start hidden, but want focus for an open nav event
mDocument->Focus();
}
void Popup::show() {
void MenuBar::show() {
Document::show();
mRoot->SetAttribute("open", "");
mTabBar->set_active_tab(-1);
if (!mTabBar->focus_tab(mFocusedTabIndex)) {
mTabBar->focus();
}
}
void Popup::hide(bool close) {
void MenuBar::hide(bool close) {
mFocusedTabIndex = mTabBar->focused_tab_index();
mRoot->RemoveAttribute("open");
if (close) {
mPendingClose = true;
}
}
void Popup::update() {
void MenuBar::update() {
update_safe_area();
Document::update();
}
void Popup::update_safe_area() noexcept {
void MenuBar::update_safe_area() noexcept {
if (mDocument == nullptr || mTabBar == nullptr) {
return;
}
@@ -106,21 +125,30 @@ void Popup::update_safe_area() noexcept {
Rml::PropertyId::PaddingRight, Rml::Property(safeInsets.right, Rml::Unit::PX));
tabBar->SetProperty(
Rml::PropertyId::PaddingLeft, Rml::Property(safeInsets.left, Rml::Unit::PX));
if (auto* close = tabBar->QuerySelector("close")) {
close->SetProperty(Rml::PropertyId::Right,
Rml::Property(safeInsets.right + 8.0f * context->GetDensityIndependentPixelRatio(),
Rml::Unit::PX));
}
}
bool Popup::visible() const {
bool MenuBar::visible() const {
return mRoot->HasAttribute("open");
}
bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel) {
bool MenuBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (!getSettings().backend.wasPresetChosen) {
return true;
}
if (cmd == NavCommand::Cancel && visible()) {
mDoAud_seStartMenu(kSoundMenuClose);
hide(false);
return true;
}
return Document::handle_nav_command(event, cmd);
}
bool Popup::focus() {
bool MenuBar::focus() {
return mTabBar->focus();
}
@@ -8,12 +8,12 @@
namespace dusk::ui {
class Popup : public Document {
class MenuBar : public Document {
public:
Popup();
MenuBar();
Popup(const Popup&) = delete;
Popup& operator=(const Popup&) = delete;
MenuBar(const MenuBar&) = delete;
MenuBar& operator=(const MenuBar&) = delete;
void show() override;
void hide(bool close) override;
@@ -32,6 +32,7 @@ private:
std::unique_ptr<Button> mCloseButton;
Insets mTabBarPadding;
float mTopMargin = 0.f;
int mFocusedTabIndex = -1;
};
} // namespace dusk::ui
+75
View File
@@ -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<Button>(actions, action.label);
btn->root()->SetClass("modal-btn", true);
btn->on_pressed([this, callback = std::move(action.onPressed)] {
if (callback) {
callback(*this);
}
});
mButtons.push_back(std::move(btn));
}
}
bool Modal::focus() {
if (!mButtons.empty()) {
return mButtons.front()->focus();
}
return false;
}
void Modal::dismiss() {
if (mProps.onDismiss) {
mProps.onDismiss(*this);
return;
}
pop();
}
bool Modal::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
mDoAud_seStartMenu(kSoundWindowClose);
dismiss();
return true;
}
int direction = 0;
if (cmd == NavCommand::Left) {
direction = -1;
} else if (cmd == NavCommand::Right) {
direction = 1;
} else {
return false;
}
auto* target = event.GetTargetElement();
for (int i = 0; i < static_cast<int>(mButtons.size()); ++i) {
if (mButtons[i]->contains(target)) {
const int next = i + direction;
if (next >= 0 && next < static_cast<int>(mButtons.size()) && mButtons[next]->focus()) {
mDoAud_seStartMenu(kSoundItemFocus);
return true;
}
return false;
}
}
return false;
}
} // namespace dusk::ui
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "button.hpp"
#include "window.hpp"
namespace dusk::ui {
class Modal;
struct ModalAction {
Rml::String label;
std::function<void(Modal&)> onPressed;
};
class Modal : public WindowSmall {
public:
struct Props {
Rml::String title;
Rml::String bodyRml;
std::vector<ModalAction> actions;
std::function<void(Modal&)> onDismiss;
};
explicit Modal(Props props);
bool focus() override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
void dismiss();
Props mProps;
std::vector<std::unique_ptr<Button> > mButtons;
};
} // namespace dusk::ui
+20 -7
View File
@@ -1,5 +1,8 @@
#include "number_button.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <charconv>
#include <fmt/format.h>
@@ -8,8 +11,16 @@ namespace dusk::ui {
NumberButton::NumberButton(Rml::Element* parent, Props props)
: BaseStringButton(parent, {.key = std::move(props.key), .type = "number"}),
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
mIsDisabled(std::move(props.isDisabled)), mMin(props.min), mMax(props.max), mStep(props.step),
mPrefix(std::move(props.prefix)), mSuffix(std::move(props.suffix)) {}
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)),
mMin(props.min), mMax(props.max), mStep(props.step), mPrefix(std::move(props.prefix)),
mSuffix(std::move(props.suffix)) {}
bool NumberButton::modified() const {
if (mIsModified) {
return mIsModified();
}
return BaseStringButton::modified();
}
bool NumberButton::disabled() const {
if (mIsDisabled) {
@@ -43,11 +54,13 @@ void NumberButton::set_value(Rml::String value) {
}
bool NumberButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Left) {
mSetValue(std::clamp(mGetValue() - mStep, mMin, mMax));
return true;
} else if (cmd == NavCommand::Right) {
mSetValue(std::clamp(mGetValue() + mStep, mMin, mMax));
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
const int newValue = std::clamp(
mGetValue() + (cmd == NavCommand::Right ? mStep : -mStep), mMin, mMax);
if (newValue != mGetValue()) {
mSetValue(newValue);
mDoAud_seStartMenu(kSoundItemChange);
}
return true;
}
return BaseStringButton::handle_nav_command(cmd);
+3
View File
@@ -11,6 +11,7 @@ public:
std::function<int()> getValue;
std::function<void(int)> setValue;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
int min = 0;
int max = INT_MAX;
int step = 1;
@@ -20,6 +21,7 @@ public:
NumberButton(Rml::Element* parent, Props props);
bool modified() const override;
bool disabled() const override;
protected:
@@ -32,6 +34,7 @@ private:
std::function<int()> mGetValue;
std::function<void(int)> mSetValue;
std::function<bool()> mIsDisabled;
std::function<bool()> mIsModified;
int mMin;
int mMax;
int mStep;
+85 -233
View File
@@ -1,17 +1,15 @@
#include "overlay.hpp"
#include <dolphin/gx/GXAurora.h>
#include <dolphin/vi.h>
#include <fmt/format.h>
#include "dusk/config.hpp"
#include "dusk/settings.h"
#include "aurora/lib/logging.hpp"
#include "magic_enum.hpp"
#include <algorithm>
#include <string>
#include "dusk/achievements.h"
namespace dusk::ui {
namespace {
aurora::Module Log{"dusk::ui::overlay"};
const Rml::String kDocumentSource = R"RML(
<rml>
@@ -19,257 +17,111 @@ const Rml::String kDocumentSource = R"RML(
<link type="text/rcss" href="res/rml/overlay.rcss" />
</head>
<body>
<div id="root" class="overlay-root">
<div class="overlay">
<div class="header">
<div id="title"></div>
<div id="carousel-container" class="carousel-container"></div>
</div>
<div id="description" class="description"></div>
<div class="divider"></div>
<div id="footer" class="footer"></div>
</div>
</div>
</body>
</rml>
)RML";
int get_value(GraphicsOption option) {
switch (option) {
case GraphicsOption::InternalResolution:
return getSettings().game.internalResolutionScale.getValue();
case GraphicsOption::ShadowResolution:
return getSettings().game.shadowResolutionMultiplier.getValue();
case GraphicsOption::BloomMode:
return static_cast<int>(getSettings().game.bloomMode.getValue());
case GraphicsOption::BloomMultiplier:
return std::clamp(
static_cast<int>(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0,
100);
Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
if (toast.type == "autosave") {
Rml::Factory::InstanceElementText(parent, R"RML(
<logo>
<img class="inner" src="res/org-icon-inner.png" />
<img class="outer" src="res/org-icon-outer.png" />
</logo>
)RML");
return parent->GetFirstChild();
} else {
auto* elem = append(parent, "toast");
if (!toast.type.empty()) {
elem->SetClass(toast.type, true);
}
{
auto* heading = append(elem, "heading");
auto* span = append(heading, "span");
span->SetInnerRML(toast.title);
if (toast.type == "achievement") {
auto* icon = append(heading, "icon");
icon->SetClass("trophy", true);
mDoAud_seStartMenu(kSoundAchievementUnlock);
}
}
{
auto* message = append(elem, "message");
auto* span = append(message, "span");
span->SetInnerRML(toast.content);
}
{
auto* progress = append(elem, "progress");
progress->SetAttribute("value", 1.f);
}
return elem;
}
return 0;
}
void set_value(GraphicsOption option, int value) {
switch (option) {
case GraphicsOption::InternalResolution:
getSettings().game.internalResolutionScale.setValue(value);
VISetFrameBufferScale(static_cast<float>(value));
break;
case GraphicsOption::ShadowResolution:
getSettings().game.shadowResolutionMultiplier.setValue(value);
break;
case GraphicsOption::BloomMode:
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
value, static_cast<int>(BloomMode::Off), static_cast<int>(BloomMode::Dusk))));
break;
case GraphicsOption::BloomMultiplier:
getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
break;
}
config::Save();
}
Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
auto* doc = parent->GetOwnerDocument();
auto root = doc->CreateElement("div");
root->SetClass("stepped-carousel", true);
root->SetAttribute("tabindex", "0");
return parent->AppendChild(std::move(root));
}
Rml::Element* create_stepped_carousel_arrow(
Rml::Element* parent, const Rml::String& className, const Rml::String& label) {
auto* doc = parent->GetOwnerDocument();
auto button = doc->CreateElement("button");
button->SetClass("stepped-carousel-arrow", true);
button->SetClass(className, true);
button->SetInnerRML(escape(label));
return parent->AppendChild(std::move(button));
}
} // namespace
SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
: Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "<");
mValueElem = append(mRoot, "div");
mValueElem->SetClass("stepped-carousel-value", true);
Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", ">");
listen(prevElem, Rml::EventId::Click,
[this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
listen(nextElem, Rml::EventId::Click,
[this](Rml::Event&) { handle_nav_command(NavCommand::Right); });
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::None && handle_nav_command(cmd)) {
event.StopPropagation();
}
});
}
bool SteppedCarousel::focus() {
return Component::focus();
}
void SteppedCarousel::update() {
if (mValueElem == nullptr) {
return;
}
const int value = std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
if (mProps.formatValue) {
mValueElem->SetInnerRML(mProps.formatValue(value));
} else {
mValueElem->SetInnerRML(std::to_string(value));
}
}
bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Left) {
const int value = mProps.getValue ? mProps.getValue() : 0;
apply(std::clamp(value - mProps.step, mProps.min, mProps.max));
return true;
}
if (cmd == NavCommand::Right) {
const int value = mProps.getValue ? mProps.getValue() : 0;
apply(std::clamp(value + mProps.step, mProps.min, mProps.max));
return true;
}
return false;
}
void SteppedCarousel::apply(int value) {
const int nextValue = std::clamp(value, mProps.min, mProps.max);
const int currentValue =
std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
if (nextValue == currentValue) {
return;
}
if (mProps.onChange) {
mProps.onChange(nextValue);
}
}
Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
switch (option) {
case GraphicsOption::InternalResolution:
if (value <= 0) {
return "Auto";
} else {
u32 width = 0;
u32 height = 0;
AuroraGetRenderSize(&width, &height);
return fmt::format("{}x ({}x{})", value, width, height);
}
case GraphicsOption::ShadowResolution:
return fmt::format("{}x", value);
case GraphicsOption::BloomMode:
switch (static_cast<BloomMode>(value)) {
case BloomMode::Off:
return "Off";
case BloomMode::Classic:
return "Classic";
case BloomMode::Dusk:
return "Dusk";
}
break;
case GraphicsOption::BloomMultiplier:
return fmt::format("{}%", value);
}
return "";
}
Overlay::Overlay(OverlayProps props)
: Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
if (mDocument == nullptr) {
return;
}
if (auto* title = mDocument->GetElementById("title")) {
title->SetInnerRML(escape(props.title));
}
if (auto* description = mDocument->GetElementById("description")) {
description->SetInnerRML(escape(props.helpText));
}
if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
add_component<SteppedCarousel>(carouselParent,
SteppedCarousel::Props{
.min = mValueMin,
.max = mValueMax,
.step = 1,
.getValue = [this] { return get_value(mOption); },
.onChange = [this](int value) { set_value(mOption, value); },
.formatValue =
[this](int value) { return format_graphics_setting_value(mOption, value); },
});
}
if (auto* footer = mDocument->GetElementById("footer")) {
auto& returnButton = add_component<Button>(footer, "\xE2\x86\x90 Return", "footer-button")
.on_pressed([this] { pop(); });
returnButton.root()->SetClass("return", true);
auto& resetButton =
add_component<Button>(footer, "Reset to default", "footer-button").on_pressed([this] {
reset_default();
});
resetButton.root()->SetClass("reset", true);
}
// Hide document after transition completion
mRoot = mDocument->GetElementById("root");
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
Document::visible())
{
Document::hide(mPendingClose);
Overlay::Overlay() : Document(kDocumentSource) {
listen(mDocument, Rml::EventId::Focus, [](Rml::Event&) { Log.warn("Overlay received focus"); });
listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
if (event.GetTargetElement() == mCurrentToast) {
if (get_toasts().empty() ||
clock::now() >= mCurrentToastStartTime + get_toasts().front().duration)
{
mCurrentToast->SetPseudoClass("done", true);
}
}
});
}
void Overlay::show() {
Document::show();
mRoot->SetAttribute("open", "");
}
void Overlay::hide(bool close) {
mRoot->RemoveAttribute("open");
if (close) {
mPendingClose = true;
if (mDocument != nullptr) {
mDocument->Show(Rml::ModalFlag::None, Rml::FocusFlag::None, Rml::ScrollFlag::None);
}
}
void Overlay::update() {
for (const auto& component : mComponents) {
component->update();
}
Document::update();
}
bool Overlay::focus() {
for (const auto& component : mComponents) {
if (component->focus()) {
return true;
auto& toasts = get_toasts();
if (mCurrentToast == nullptr) {
if (!toasts.empty()) {
const auto& toast = toasts.front();
mCurrentToast = create_toast(mDocument, toast);
mCurrentToastStartTime = clock::now();
}
} else if (!toasts.empty()) {
const auto& toast = toasts.front();
const float duration = std::chrono::duration<float>(toast.duration).count();
const float elapsed =
std::chrono::duration<float>(clock::now() - mCurrentToastStartTime).count();
const float ratio = duration > 0.0f ? std::clamp(elapsed / duration, 0.0f, 1.0f) : 1.0f;
const auto remaining = 1.f - ratio;
Rml::ElementList list;
mDocument->GetElementsByTagName(list, "progress");
for (auto* elem : list) {
elem->SetAttribute("value", remaining);
}
if (remaining == 0.f) {
if (mCurrentToast->IsPseudoClassSet("done") ||
// Fallback for large gaps in time where we never actually opened it
!mCurrentToast->IsPseudoClassSet("opened"))
{
mCurrentToast->GetParentNode()->RemoveChild(mCurrentToast);
mCurrentToast = nullptr;
toasts.pop_front();
} else {
mCurrentToast->RemoveAttribute("open");
}
} else {
mCurrentToast->SetAttribute("open", "");
mCurrentToast->SetPseudoClass("opened", true);
}
}
return false;
}
bool Overlay::visible() const {
return mRoot->HasAttribute("open");
}
bool Overlay::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel) {
pop();
return true;
}
return Document::handle_nav_command(event, cmd);
}
void Overlay::reset_default() {
set_value(mOption, mDefaultValue);
Log.warn("Overlay received nav command: {}", magic_enum::enum_name(cmd));
return false;
}
} // namespace dusk::ui
+4 -71
View File
@@ -1,90 +1,23 @@
#pragma once
#include "button.hpp"
#include "component.hpp"
#include "document.hpp"
#include "ui.hpp"
#include <functional>
#include <memory>
#include <type_traits>
#include <utility>
#include <vector>
#include <chrono>
namespace dusk::ui {
class SteppedCarousel : public Component {
public:
struct Props {
int min = 0;
int max = 0;
int step = 1;
std::function<int()> getValue;
std::function<void(int)> onChange;
std::function<Rml::String(int)> formatValue;
};
SteppedCarousel(Rml::Element* parent, Props props);
bool focus() override;
void update() override;
private:
bool handle_nav_command(NavCommand cmd);
void apply(int value);
Props mProps;
Rml::Element* mValueElem = nullptr;
};
enum class GraphicsOption {
InternalResolution,
ShadowResolution,
BloomMode,
BloomMultiplier,
};
Rml::String format_graphics_setting_value(GraphicsOption option, int value);
struct OverlayProps {
GraphicsOption option;
Rml::String title;
Rml::String helpText;
int valueMin = 0;
int valueMax = 0;
int defaultValue = 0;
};
class Overlay : public Document {
public:
explicit Overlay(OverlayProps props);
Overlay();
void show() override;
void hide(bool close) override;
void update() override;
bool focus() override;
bool visible() const override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
template <typename T, typename... Args>
requires std::is_base_of_v<Component, T> T& add_component(Args&&... args) {
auto child = std::make_unique<T>(std::forward<Args>(args)...);
T& ref = *child;
mComponents.emplace_back(std::move(child));
return ref;
}
void reset_default();
GraphicsOption mOption;
int mValueMin = 0;
int mValueMax = 0;
int mDefaultValue = 0;
std::vector<std::unique_ptr<Component> > mComponents;
Rml::Element* mRoot;
Rml::Element* mCurrentToast = nullptr;
clock::time_point mCurrentToastStartTime;
};
} // namespace dusk::ui
+43
View File
@@ -1,5 +1,7 @@
#include "pane.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include "ui.hpp"
namespace dusk::ui {
@@ -56,6 +58,7 @@ Pane::Pane(Rml::Element* parent, Type type) : FluentComponent(createRoot(parent)
int i = focusedChild + direction;
while (i >= 0 && i < mChildren.size()) {
if (mChildren[i]->focus()) {
mDoAud_seStartMenu(kSoundItemFocus);
event.StopPropagation();
break;
}
@@ -72,6 +75,12 @@ Pane::Pane(Rml::Element* parent, Type type) : FluentComponent(createRoot(parent)
childIndex = i;
}
}
// If item already selected, deselect
if (childIndex >= 0 && childIndex < mChildren.size() &&
mChildren[childIndex]->selected())
{
childIndex = -1;
}
set_selected_item(childIndex);
// If the selection was handled locally, don't allow it to bubble up to window
if (event.GetParameter("handled", false)) {
@@ -95,6 +104,40 @@ void Pane::set_selected_item(int index) {
}
}
Component& Pane::register_control(
Component& component, Pane& nextPane, std::function<void(Pane&)> callback) {
component.listen(component.root(), Rml::EventId::Mouseover,
[this, &component, &nextPane, callback](Rml::Event&) {
if (component.disabled()) {
return;
}
bool anySelected = false;
for (const auto& child : mChildren) {
if (child->selected()) {
anySelected = true;
break;
}
}
if (!anySelected) {
nextPane.clear();
if (callback) {
callback(nextPane);
}
}
});
component.listen(component.root(), Rml::EventId::Focus,
[&component, &nextPane, callback = std::move(callback)](Rml::Event&) {
if (component.disabled()) {
return;
}
nextPane.clear();
if (callback) {
callback(nextPane);
}
});
return component;
}
bool Pane::focus() {
// Focus the first selected child
for (const auto& child : mChildren) {
+2
View File
@@ -19,6 +19,8 @@ public:
void update() override;
void set_selected_item(int index);
Component& register_control(
Component& component, Pane& nextPane, std::function<void(Pane&)> callback);
Rml::Element* add_section(const Rml::String& text);
ControlledButton& add_button(ControlledButton::Props props) {
+190 -48
View File
@@ -4,15 +4,18 @@
#include "dusk/file_select.hpp"
#include "dusk/iso_validate.hpp"
#include "dusk/main.h"
#include "dusk/ui/prelaunch_options.hpp"
#include "dusk/settings.h"
#include "modal.hpp"
#include "preset.hpp"
#include "settings.hpp"
#include "version.h"
#include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_filesystem.h>
#include <aurora/lib/window.hpp>
#include "m_Do/m_Do_MemCard.h"
namespace dusk::ui {
namespace {
const Rml::String kDocumentSource = R"RML(
<rml>
@@ -20,6 +23,8 @@ const Rml::String kDocumentSource = R"RML(
<link type="text/rcss" href="res/rml/prelaunch.rcss" />
</head>
<body>
<div class="gradient" />
<div class="background" />
<content id="root" open>
<menu>
<hero class="intro-item delay-0">
@@ -28,10 +33,13 @@ const Rml::String kDocumentSource = R"RML(
</hero>
<div id="menu-list" />
</menu>
<disk-status class="intro-item delay-4">
<span id="status" class="status" />
<span id="detail" class="detail" />
</disk-status>
<disc-info class="intro-item delay-4">
<div id="disc-status">
<icon />
<span id="disc-status-label" />
</div>
<span id="disc-version" class="detail" />
</disc-info>
<version-info class="intro-item delay-5">
<div class="version">Version <span id="version-text"></span></div>
<div class="update"><span>Update available!</span> Download</div>
@@ -46,6 +54,23 @@ constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
{"All Files", "*"},
}};
static std::string get_error_msg(iso::ValidationError error) {
switch (error) {
case iso::ValidationError::IOError:
return "Unable to read the selected file.";
case iso::ValidationError::InvalidImage:
return "The selected file is not a valid disc image.";
case iso::ValidationError::WrongGame:
return "The selected game is not supported by Dusk.";
case iso::ValidationError::WrongVersion:
return "Dusk currently supports GameCube USA and PAL disc images only.";
case iso::ValidationError::Success:
return "The selected disc image is valid.";
default:
return "The selected disc image could not be validated.";
}
}
void file_dialog_callback(void*, const char* path, const char* error) {
auto& state = prelaunch_state();
if (error != nullptr) {
@@ -55,14 +80,18 @@ void file_dialog_callback(void*, const char* path, const char* error) {
return;
}
state.selectedIsoPath = path;
state.errorString.clear();
refresh_path_state();
getSettings().backend.isoPath.setValue(state.selectedIsoPath);
config::Save();
}
const auto validation = iso::validate(path);
if (validation != iso::ValidationError::Success) {
state.errorString = escape(get_error_msg(validation));
return;
}
} // namespace
state.selectedDiscPath = path;
state.errorString.clear();
getSettings().backend.isoPath.setValue(state.selectedDiscPath);
config::Save();
refresh_state();
}
PrelaunchState sPrelaunchState;
@@ -70,9 +99,15 @@ PrelaunchState& prelaunch_state() noexcept {
return sPrelaunchState;
}
void refresh_path_state() noexcept {
void refresh_state() noexcept {
auto& state = prelaunch_state();
state.isPal = !state.selectedIsoPath.empty() && iso::isPal(state.selectedIsoPath.c_str());
const auto validation = iso::validate(state.selectedDiscPath.c_str());
if (state.selectedDiscPath.empty() || validation != iso::ValidationError::Success) {
state.selectedDiscIsValid = false;
return;
}
state.selectedDiscIsValid = true;
state.selectedDiscIsPal = iso::isPal(state.selectedDiscPath.c_str());
}
void ensure_initialized() noexcept {
@@ -81,16 +116,17 @@ void ensure_initialized() noexcept {
return;
}
state.selectedIsoPath = getSettings().backend.isoPath;
state.selectedDiscPath = getSettings().backend.isoPath;
state.initialDiscPath = state.selectedDiscPath;
if (iso::validate(state.initialDiscPath.c_str()) == iso::ValidationError::Success) {
state.initialDiscIsPal = iso::isPal(state.initialDiscPath.c_str());
}
state.initialLanguage = getSettings().game.language;
state.initialGraphicsBackend = getSettings().backend.graphicsBackend;
state.initialCardFileType = getSettings().backend.cardFileType;
state.errorString.clear();
state.initialized = true;
refresh_path_state();
}
bool is_selected_path_valid() noexcept {
return !prelaunch_state().selectedIsoPath.empty() &&
SDL_GetPathInfo(prelaunch_state().selectedIsoPath.c_str(), nullptr);
refresh_state();
}
void open_iso_picker() noexcept {
@@ -99,6 +135,20 @@ void open_iso_picker() noexcept {
kDiscFileFilters.data(), kDiscFileFilters.size(), nullptr, false);
}
bool is_restart_pending() noexcept {
const auto& state = prelaunch_state();
if (!state.initialDiscPath.empty() && state.selectedDiscPath != state.initialDiscPath) {
return true;
}
if (getSettings().backend.graphicsBackend.getValue() != state.initialGraphicsBackend) {
return true;
}
if (getSettings().game.language.getValue() != state.initialLanguage) {
return true;
}
return false;
}
void apply_intro_animation(Rml::Element* element, const char* delay_class) {
if (element == nullptr || delay_class == nullptr) {
return;
@@ -107,26 +157,53 @@ void apply_intro_animation(Rml::Element* element, const char* delay_class) {
element->SetClass(delay_class, true);
}
void try_apply_mirrored_layout(Rml::Element* body) {
if (body == nullptr) {
return;
}
body->SetClass("mirrored", getSettings().game.enableMirrorMode.getValue());
}
Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) {
ensure_initialized();
if (auto* menuList = mDocument->GetElementById("menu-list")) {
const bool hasValidPath = is_selected_path_valid();
mMenuButtons.push_back(
std::make_unique<Button>(menuList, hasValidPath ? "Start Game" : "Select Disk Image"));
auto& state = prelaunch_state();
mMenuButtons.push_back(std::make_unique<Button>(
menuList, state.selectedDiscIsValid ? "Play" : "Select Disc Image"));
mMenuButtons.back()->on_pressed([this] {
if (!is_selected_path_valid()) {
if (!prelaunch_state().selectedDiscIsValid) {
open_iso_picker();
return;
}
mDoAud_seStartMenu(kSoundPlay);
if (getSettings().audio.menuSounds) {
JAISoundHandle* handle = g_mEnvSeMgr.field_0x144.getHandle();
if (*handle) {
(*handle)->stop(60);
(*handle)->releaseHandle();
}
}
if (g_mDoMemCd_control.mCardCommand == mDoMemCd_Ctrl_c::Command_e::COMM_NONE_e) {
mDoMemCd_ThdInit();
}
IsGameLaunched = true;
if (!getSettings().backend.wasPresetChosen) {
push_document(std::make_unique<dusk::ui::PresetWindow>());
}
hide(true);
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Options"));
mMenuButtons.back()->on_pressed(
[] { push_document(std::make_unique<PrelaunchOptions>()); });
mMenuButtons.back()->on_pressed([this] {
mRestartSuppressed = false;
push(std::make_unique<SettingsWindow>(true));
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit To Desktop"));
@@ -134,10 +211,12 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
}
mDiscStatus = mDocument->GetElementById("status");
mDiscDetail = mDocument->GetElementById("detail");
mDiscStatus = mDocument->GetElementById("disc-status");
mDiscDetail = mDocument->GetElementById("disc-version");
mVersion = mDocument->GetElementById("version-text");
try_apply_mirrored_layout(mDocument);
listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
auto* target = event.GetTargetElement();
if (target == nullptr) {
@@ -155,6 +234,40 @@ void Prelaunch::show() {
Document::show();
mDocument->SetAttribute("open", "");
mRoot->SetAttribute("open", "");
if (is_restart_pending() && !mRestartSuppressed) {
const auto dismiss = [this](Modal& modal) {
mRestartSuppressed = true;
modal.pop();
};
std::vector<ModalAction> actions;
if constexpr (dusk::SupportsProcessRestart) {
actions.push_back(ModalAction{
.label = "Restart later",
.onPressed = dismiss,
});
actions.push_back(ModalAction{
.label = "Restart now",
.onPressed = [](Modal&) { dusk::RequestRestart(); },
});
} else {
actions.push_back(ModalAction{
.label = "OK",
.onPressed = dismiss,
});
}
push(std::make_unique<Modal>(Modal::Props{
.title = "Apply Options",
.bodyRml =
dusk::SupportsProcessRestart ?
"A restart is required to apply selected options.<br/><br/>Restart now to "
"apply them immediately?" :
"A restart is required to apply selected options.<br/><br/>Close and reopen "
"Dusk to apply them.",
.actions = std::move(actions),
.onDismiss = dismiss,
}));
}
}
void Prelaunch::hide(bool close) {
@@ -162,6 +275,8 @@ void Prelaunch::hide(bool close) {
if (!mEntranceAnimationStarted) {
// Close document immediately
Document::hide(true);
} else {
mPendingClose = true;
}
mDocument->RemoveAttribute("open");
} else {
@@ -171,12 +286,34 @@ void Prelaunch::hide(bool close) {
void Prelaunch::update() {
ensure_initialized();
refresh_path_state();
try_apply_mirrored_layout(mDocument);
auto& state = prelaunch_state();
const bool hasValidPath = is_selected_path_valid();
if (hasValidPath && getSettings().backend.skipPreLaunchUI) {
hide(true);
if (!state.errorString.empty() && top_document() == this) {
auto dismiss = [](Modal& modal) {
prelaunch_state().errorString.clear();
modal.pop();
};
push(std::make_unique<Modal>(Modal::Props{
.title = "Invalid disc image",
.bodyRml = state.errorString,
.actions =
{
ModalAction{
.label = "OK",
.onPressed = dismiss,
},
},
.onDismiss = dismiss,
}));
}
const bool hasValidPath = prelaunch_state().selectedDiscIsValid;
mDocument->SetClass("disc-ready", hasValidPath);
if (hasValidPath) {
if (getSettings().backend.skipPreLaunchUI) {
hide(true);
}
IsGameLaunched = true;
}
@@ -186,27 +323,32 @@ void Prelaunch::update() {
}
if (!mMenuButtons.empty()) {
mMenuButtons[0]->set_text(hasValidPath ? "Start Game" : "Select Disk Image");
mMenuButtons[0]->set_text(hasValidPath ? "Play" : "Select Disc Image");
}
if (mDiscStatus != nullptr) {
const auto discStatusLabel = mDiscStatus->GetElementById("disc-status-label");
if (mDiscStatus != nullptr && discStatusLabel != nullptr) {
if (hasValidPath) {
mDiscStatus->RemoveAttribute("bad");
mDiscStatus->SetInnerRML("Disc Ready");
} else {
mDiscStatus->SetAttribute("bad", "");
mDiscStatus->SetInnerRML("Disk Not Found");
mDiscStatus->SetAttribute("status", "good");
discStatusLabel->SetInnerRML("Disc ready.");
}
}
if (mDiscDetail != nullptr) {
if (hasValidPath) {
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Block);
mDiscDetail->SetInnerRML(state.isPal ? "GameCube • PAL" : "GameCube • USA");
mDiscDetail->SetInnerRML(
prelaunch_state().initialDiscIsPal ? "GameCube • EUR" : "GameCube • USA");
} else {
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::None);
}
}
if (mVersion != nullptr) {
mVersion->SetInnerRML(escape(DUSK_WC_DESCRIBE));
std::string_view versionStr(DUSK_WC_DESCRIBE);
if (versionStr[0] == 'v') {
versionStr = versionStr.substr(1);
}
mVersion->SetInnerRML(escape(versionStr));
}
Document::update();
@@ -216,7 +358,7 @@ bool Prelaunch::focus() {
if (mMenuButtons.empty()) {
return false;
}
return mMenuButtons[0]->focus();
return mMenuButtons.front()->focus();
}
bool Prelaunch::visible() const {
@@ -240,11 +382,11 @@ bool Prelaunch::handle_nav_command(Rml::Event& event, NavCommand cmd) {
break;
}
}
const auto buttonCount = static_cast<int>(mMenuButtons.size());
int i = (focusedButton + direction) % buttonCount;
if (i < 0) i += buttonCount;
const auto n = static_cast<int>(mMenuButtons.size());
int i = ((focusedButton + direction) % n + n) % n;
while (i >= 0 && i < mMenuButtons.size()) {
if (mMenuButtons[i]->focus()) {
mDoAud_seStartMenu(kSoundItemFocus);
event.StopPropagation();
return true;
}
+12 -6
View File
@@ -24,6 +24,7 @@ protected:
private:
bool mEntranceAnimationStarted = false;
bool mRestartSuppressed = false;
std::vector<std::unique_ptr<Button>> mMenuButtons;
Rml::Element* mRoot = nullptr;
Rml::Element* mDiscStatus = nullptr;
@@ -34,17 +35,22 @@ private:
class PrelaunchOptions;
struct PrelaunchState {
std::string selectedIsoPath;
std::string errorString;
std::string initialGraphicsBackend;
bool isPal = false;
bool initialized = false;
std::string selectedDiscPath;
bool selectedDiscIsValid = false;
bool selectedDiscIsPal = false;
std::string errorString;
bool initialDiscIsPal = false;
std::string initialDiscPath;
GameLanguage initialLanguage = GameLanguage::English;
std::string initialGraphicsBackend;
int initialCardFileType = 0;
};
PrelaunchState& prelaunch_state() noexcept;
void ensure_initialized() noexcept;
void refresh_path_state() noexcept;
bool is_selected_path_valid() noexcept;
void refresh_state() noexcept;
void open_iso_picker() noexcept;
bool is_restart_pending() noexcept;
} // namespace dusk::ui
-263
View File
@@ -1,263 +0,0 @@
#include "prelaunch_options.hpp"
#include "dusk/config.hpp"
#include "dusk/settings.h"
#include "pane.hpp"
#include "prelaunch.hpp"
namespace dusk::ui {
namespace {
static constexpr std::array<const char*, 5> kLanguageNames = {
"English", "German", "French", "Spanish", "Italian",
};
// TODO: Copied from ImGui prelaunch. Needs a refactor?
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
if (backend == "auto") {
outBackend = BACKEND_AUTO;
return true;
}
if (backend == "d3d11") {
outBackend = BACKEND_D3D11;
return true;
}
if (backend == "d3d12") {
outBackend = BACKEND_D3D12;
return true;
}
if (backend == "metal") {
outBackend = BACKEND_METAL;
return true;
}
if (backend == "vulkan") {
outBackend = BACKEND_VULKAN;
return true;
}
if (backend == "opengl") {
outBackend = BACKEND_OPENGL;
return true;
}
if (backend == "opengles") {
outBackend = BACKEND_OPENGLES;
return true;
}
if (backend == "webgpu") {
outBackend = BACKEND_WEBGPU;
return true;
}
if (backend == "null") {
outBackend = BACKEND_NULL;
return true;
}
return false;
}
std::string_view backend_name(AuroraBackend backend) {
switch (backend) {
default:
return "Auto";
case BACKEND_D3D12:
return "D3D12";
case BACKEND_D3D11:
return "D3D11";
case BACKEND_METAL:
return "Metal";
case BACKEND_VULKAN:
return "Vulkan";
case BACKEND_OPENGL:
return "OpenGL";
case BACKEND_OPENGLES:
return "OpenGL ES";
case BACKEND_WEBGPU:
return "WebGPU";
case BACKEND_NULL:
return "Null";
}
}
std::string_view backend_id(AuroraBackend backend) {
switch (backend) {
default:
return "auto";
case BACKEND_D3D12:
return "d3d12";
case BACKEND_D3D11:
return "d3d11";
case BACKEND_METAL:
return "metal";
case BACKEND_VULKAN:
return "vulkan";
case BACKEND_OPENGL:
return "opengl";
case BACKEND_OPENGLES:
return "opengles";
case BACKEND_WEBGPU:
return "webgpu";
case BACKEND_NULL:
return "null";
}
}
std::vector<AuroraBackend> available_backends() {
std::vector<AuroraBackend> backends;
backends.emplace_back(BACKEND_AUTO);
size_t backendCount = 0;
const AuroraBackend* raw = aurora_get_available_backends(&backendCount);
for (size_t i = 0; i < backendCount; ++i) {
// Do not expose NULL or D3D11
if (raw[i] != BACKEND_NULL && raw[i] != BACKEND_D3D11) {
backends.emplace_back(raw[i]);
}
}
return backends;
}
class LanguageSelect final : public SelectButton {
public:
explicit LanguageSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Language"}) {}
void update() override {
ensure_initialized();
refresh_path_state();
const bool validPath = is_selected_path_valid();
const bool ntscDiscLocked = validPath && !prelaunch_state().isPal;
if (ntscDiscLocked) {
if (getSettings().game.language.getValue() != GameLanguage::English) {
getSettings().game.language.setValue(GameLanguage::English);
config::Save();
}
set_disabled(true);
} else {
set_disabled(false);
}
const auto lang = getSettings().game.language.getValue();
auto value = static_cast<u8>(lang);
if (value >= kLanguageNames.size()) {
getSettings().game.language.setValue(GameLanguage::English);
config::Save();
value = static_cast<u8>(getSettings().game.language.getValue());
}
set_value_label(kLanguageNames[value]);
SelectButton::update();
}
protected:
bool handle_nav_command(NavCommand cmd) override {
if (disabled()) {
return false;
}
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
return false;
}
constexpr int n = static_cast<int>(kLanguageNames.size());
int idx = static_cast<int>(getSettings().game.language.getValue());
const int dir = (cmd == NavCommand::Left) ? -1 : 1;
idx = ((idx + dir) % n + n) % n;
getSettings().game.language.setValue(static_cast<GameLanguage>(idx));
config::Save();
return true;
}
};
class BackendSelect final : public SelectButton {
public:
explicit BackendSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Graphics Backend"}) {}
void update() override {
AuroraBackend configuredBackend = BACKEND_AUTO;
const auto configuredId = getSettings().backend.graphicsBackend.getValue();
if (!try_parse_backend(configuredId, configuredBackend)) {
configuredBackend = BACKEND_AUTO;
}
// Do not expose NULL or D3D11
if (configuredBackend == BACKEND_NULL || configuredBackend == BACKEND_D3D11) {
getSettings().backend.graphicsBackend.setValue("auto");
config::Save();
configuredBackend = BACKEND_AUTO;
}
const auto backend = getSettings().backend.graphicsBackend.getValue();
Rml::String value = backend_name(configuredBackend).data();
if (backend != prelaunch_state().initialGraphicsBackend) {
value += " (restart required)";
}
set_value_label(value);
SelectButton::update();
}
protected:
bool handle_nav_command(NavCommand cmd) override {
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
return false;
}
const auto backends = available_backends();
const int n = static_cast<int>(backends.size());
if (n <= 0) {
return false;
}
AuroraBackend configuredBackend = BACKEND_AUTO;
const auto configuredId = getSettings().backend.graphicsBackend.getValue();
if (!try_parse_backend(configuredId, configuredBackend)) {
configuredBackend = BACKEND_AUTO;
}
int idx = 0;
for (int i = 0; i < n; ++i) {
if (backends[static_cast<size_t>(i)] == configuredBackend) {
idx = i;
break;
}
}
const int dir = (cmd == NavCommand::Left) ? -1 : 1;
idx = ((idx + dir) % n + n) % n;
getSettings().backend.graphicsBackend.setValue(std::string(backend_id(backends[static_cast<size_t>(idx)])));
config::Save();
return true;
}
};
class SaveTypeSelect final : public SelectButton {
public:
explicit SaveTypeSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Save File Type"}) {}
void update() override {
const CARDFileType cft = static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
set_value_label(cft == CARD_GCIFOLDER ? "GCI Folder" : "Card Image");
SelectButton::update();
}
protected:
bool handle_nav_command(NavCommand cmd) override {
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
return false;
}
CARDFileType cft = static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
const CARDFileType newValue = cft == CARD_GCIFOLDER ? CARD_RAWIMAGE : CARD_GCIFOLDER;
getSettings().backend.cardFileType.setValue(newValue);
config::Save();
return true;
}
};
} // namespace
PrelaunchOptions::PrelaunchOptions() {
add_tab("Options", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
leftPane.add_child<LanguageSelect>();
leftPane.add_child<BackendSelect>();
leftPane.add_child<SaveTypeSelect>();
});
}
} // namespace dusk::ui
-12
View File
@@ -1,12 +0,0 @@
#pragma once
#include "window.hpp"
namespace dusk::ui {
class PrelaunchOptions : public Window {
public:
PrelaunchOptions();
};
} // namespace dusk::ui
+140
View File
@@ -0,0 +1,140 @@
#include "preset.hpp"
#include "button.hpp"
#include "dusk/config.hpp"
#include "dusk/settings.h"
#include "ui.hpp"
#include <dolphin/gx/GXAurora.h>
namespace dusk::ui {
namespace {
void applyPresetClassic() {
auto& s = getSettings();
s.video.lockAspectRatio.setValue(true);
s.game.bloomMode.setValue(BloomMode::Classic);
s.game.enableAchievementNotifications.setValue(false);
s.game.internalResolutionScale.setValue(1);
s.game.shadowResolutionMultiplier.setValue(1);
s.game.hideTvSettingsScreen.setValue(false);
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
}
void applyPresetDusk() {
auto& s = getSettings();
s.game.hideTvSettingsScreen.setValue(true);
s.game.noReturnRupees.setValue(true);
s.game.disableRupeeCutscenes.setValue(true);
s.game.noSwordRecoil.setValue(true);
s.game.fastClimbing.setValue(true);
s.game.noMissClimbing.setValue(true);
s.game.fastTears.setValue(true);
s.game.biggerWallets.setValue(true);
s.game.invertCameraXAxis.setValue(true);
s.game.no2ndFishForCat.setValue(true);
s.game.enableAchievementNotifications.setValue(true);
s.game.enableQuickTransform.setValue(true);
s.game.instantSaves.setValue(true);
s.game.midnasLamentNonStop.setValue(true);
s.game.enableFrameInterpolation.setValue(true);
s.game.sunsSong.setValue(true);
s.game.bloomMode.setValue(BloomMode::Dusk);
s.game.internalResolutionScale.setValue(0);
s.game.shadowResolutionMultiplier.setValue(4);
s.game.enableGyroAim.setValue(true);
}
} // namespace
PresetWindow::PresetWindow() : WindowSmall("preset", "preset-dialog") {
auto* title = append(mDialog, "div");
title->SetClass("preset-title", true);
title->SetInnerRML("Welcome to Dusk!");
auto* intro = append(mDialog, "div");
intro->SetClass("preset-intro", true);
intro->SetInnerRML(
"Choose a preset to get started.<br/>"
"You can change any setting later from the Settings menu.");
auto* grid = append(mDialog, "div");
grid->SetClass("preset-grid", true);
struct PresetInfo {
const char* name;
const char* desc;
void (*apply)();
};
static constexpr PresetInfo kPresets[] = {
{"Classic",
"Enhancements disabled to match the GameCube version. "
"Good for speedrunning or simple nostalgia!",
applyPresetClassic},
{"Dusk",
"Graphics & quality of life tweaks, including some from the Wii U version. "
"Our recommended way to play!",
applyPresetDusk},
};
for (const auto& preset : kPresets) {
auto* col = append(grid, "div");
col->SetClass("preset-col", true);
auto btn = std::make_unique<Button>(col, Rml::String(preset.name));
btn->root()->SetClass("preset-btn", true);
btn->on_nav_command([this, apply = preset.apply](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
apply();
getSettings().backend.wasPresetChosen.setValue(true);
config::Save();
hide(true);
return true;
}
return false;
});
mButtons.push_back(std::move(btn));
auto* desc = append(col, "div");
desc->SetClass("preset-desc", true);
desc->SetInnerRML(preset.desc);
}
}
bool PresetWindow::focus() {
if (!mButtons.empty()) {
return mButtons.back()->focus();
}
return false;
}
bool PresetWindow::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
return true;
}
int direction = 0;
if (cmd == NavCommand::Left) {
direction = -1;
} else if (cmd == NavCommand::Right) {
direction = 1;
} else {
return false;
}
auto* target = event.GetTargetElement();
for (int i = 0; i < static_cast<int>(mButtons.size()); ++i) {
if (mButtons[i]->contains(target)) {
const int next = i + direction;
if (next >= 0 && next < static_cast<int>(mButtons.size())) {
if (mButtons[next]->focus()) {
mDoAud_seStartMenu(kSoundItemFocus);
return true;
}
}
return false;
}
}
return false;
}
} // namespace dusk::ui
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "component.hpp"
#include "window.hpp"
#include <memory>
#include <vector>
namespace dusk::ui {
class PresetWindow : public WindowSmall {
public:
PresetWindow();
bool focus() override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
std::vector<std::unique_ptr<Component>> mButtons;
};
} // namespace dusk::ui
+56 -2
View File
@@ -2,6 +2,7 @@
#include "ui.hpp"
#include <fmt/format.h>
#include <utility>
namespace dusk::ui {
@@ -18,28 +19,73 @@ Rml::Element* createRoot(Rml::Element* parent) {
SelectButton::SelectButton(Rml::Element* parent, Props props)
: FluentComponent(createRoot(parent)) {
mKeyElem = append(mRoot, "key");
mIconElem = append(mRoot, "icon");
mValueElem = append(mRoot, "value");
update_props(std::move(props));
on_nav_command([this](Rml::Event&, NavCommand cmd) { return handle_nav_command(cmd); });
}
bool SelectButton::modified() const {
return mProps.modified;
}
void SelectButton::set_modified(bool value) {
if (mProps.modified != value) {
mValueElem->SetClass("modified", value);
if (value) {
mValueElem->SetInnerRML(fmt::format("•&nbsp;{}", escape(mProps.value)));
} else {
mValueElem->SetInnerRML(escape(mProps.value));
}
mProps.modified = value;
}
}
void SelectButton::set_value_label(const Rml::String& value) {
if (mProps.value != value) {
mValueElem->SetInnerRML(escape(value));
if (mProps.modified) {
mValueElem->SetInnerRML(fmt::format("•&nbsp;{}", escape(value)));
} else {
mValueElem->SetInnerRML(escape(value));
}
mProps.value = value;
}
}
SelectButton& SelectButton::on_pressed(SelectButtonCallback callback) {
if (!callback) {
return *this;
}
listen(Rml::EventId::Submit, [this, callback = std::move(callback)](Rml::Event& event) {
if (!disabled() && event.GetTargetElement() == mRoot) {
callback();
event.StopPropagation();
}
});
return *this;
}
void SelectButton::update_props(Props props) {
if (mProps.key != props.key) {
mKeyElem->SetInnerRML(escape(props.key));
}
if (mProps.icon != props.icon) {
Rml::StringList iconClasses;
Rml::StringUtilities::ExpandString(iconClasses, mIconElem->GetClassNames(), ' ', true);
for (const auto& className : iconClasses) {
mIconElem->SetClass(className, false);
}
if (!props.icon.empty()) {
mIconElem->SetClass(props.icon, true);
}
}
set_value_label(props.value);
set_modified(props.modified);
mProps = std::move(props);
}
bool SelectButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (cmd == NavCommand::Confirm && mProps.submit) {
mRoot->DispatchEvent(Rml::EventId::Submit, {});
return true;
}
@@ -49,9 +95,17 @@ bool SelectButton::handle_nav_command(NavCommand cmd) {
void BaseControlledSelectButton::update() {
set_disabled(disabled());
set_value_label(format_value());
set_modified(modified());
SelectButton::update();
}
bool ControlledSelectButton::modified() const {
if (mIsModified) {
return mIsModified();
}
return BaseControlledSelectButton::modified();
}
bool ControlledSelectButton::disabled() const {
if (mIsDisabled) {
return mIsDisabled();
+23 -3
View File
@@ -3,18 +3,29 @@
#include "component.hpp"
#include "ui.hpp"
#include <functional>
#include <utility>
namespace dusk::ui {
using SelectButtonCallback = std::function<void()>;
class SelectButton : public FluentComponent<SelectButton> {
public:
struct Props {
Rml::String key;
Rml::String value;
Rml::String icon;
bool modified = false;
bool submit = true;
};
SelectButton(Rml::Element* parent, Props props);
virtual bool modified() const;
void set_modified(bool value);
void set_value_label(const Rml::String& value);
SelectButton& on_pressed(SelectButtonCallback callback);
protected:
void update_props(Props props);
@@ -22,8 +33,8 @@ protected:
Props mProps;
Rml::Element* mKeyElem = nullptr;
Rml::Element* mIconElem = nullptr;
Rml::Element* mValueElem = nullptr;
std::function<void()> mOnHover;
};
class BaseControlledSelectButton : public SelectButton {
@@ -43,12 +54,20 @@ public:
Rml::String key;
std::function<Rml::String()> getValue;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
bool submit = true;
};
ControlledSelectButton(Rml::Element* parent, Props props)
: BaseControlledSelectButton(parent, {std::move(props.key)}),
mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)) {}
: BaseControlledSelectButton(parent,
{
.key = std::move(props.key),
.submit = props.submit,
}),
mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)),
mIsModified(std::move(props.isModified)) {}
bool modified() const override;
bool disabled() const override;
protected:
@@ -56,6 +75,7 @@ protected:
std::function<Rml::String()> mGetValue;
std::function<bool()> mIsDisabled;
std::function<bool()> mIsModified;
};
} // namespace dusk::ui
+699 -436
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -5,7 +5,12 @@ namespace dusk::ui {
class SettingsWindow : public Window {
public:
SettingsWindow();
SettingsWindow(bool prelaunch = false);
void update() override;
protected:
bool mPrelaunch;
};
} // namespace dusk::ui
+127 -13
View File
@@ -1,5 +1,8 @@
#include "tab_bar.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
namespace dusk::ui {
namespace {
@@ -9,28 +12,97 @@ Rml::Element* createRoot(Rml::Element* parent) {
return parent->AppendChild(std::move(elem));
}
int key_modifiers_from_event(const Rml::Event& event) {
int modifiers = 0;
if (event.GetParameter("ctrl_key", 0)) {
modifiers |= Rml::Input::KM_CTRL;
}
if (event.GetParameter("shift_key", 0)) {
modifiers |= Rml::Input::KM_SHIFT;
}
if (event.GetParameter("alt_key", 0)) {
modifiers |= Rml::Input::KM_ALT;
}
if (event.GetParameter("meta_key", 0)) {
modifiers |= Rml::Input::KM_META;
}
if (event.GetParameter("caps_lock_key", 0)) {
modifiers |= Rml::Input::KM_CAPSLOCK;
}
if (event.GetParameter("num_lock_key", 0)) {
modifiers |= Rml::Input::KM_NUMLOCK;
}
if (event.GetParameter("scroll_lock_key", 0)) {
modifiers |= Rml::Input::KM_SCROLLLOCK;
}
return modifiers;
}
} // namespace
TabBar::TabBar(Rml::Element* parent, Props props)
: FluentComponent(createRoot(parent)), mProps(std::move(props)) {
if (mProps.onClose) {
mRoot->SetAttribute("closable", "");
add_child<Button>(Button::Props{}, "close")
.on_nav_command([this](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
mProps.onClose();
return true;
}
return false;
});
mEndSpacer = append(mRoot, "tab-end-spacer");
}
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
event.StopPropagation();
}
});
// Remap vertical scroll events into horizontal scroll events
listen(Rml::EventId::Mousescroll, [this](Rml::Event& event) {
if (mRedirectingScroll) {
return;
}
if (mRoot->GetScrollWidth() <= mRoot->GetClientWidth() + 0.5f) {
return;
}
const float wheelDeltaX = event.GetParameter("wheel_delta_x", 0.0f);
const float wheelDeltaY = event.GetParameter("wheel_delta_y", 0.0f);
const float absWheelDeltaX = wheelDeltaX < 0.0f ? -wheelDeltaX : wheelDeltaX;
const float absWheelDeltaY = wheelDeltaY < 0.0f ? -wheelDeltaY : wheelDeltaY;
if (absWheelDeltaY == 0.0f || absWheelDeltaX >= absWheelDeltaY) {
return;
}
auto* context = mRoot->GetContext();
if (context == nullptr) {
return;
}
mRedirectingScroll = true;
context->ProcessMouseWheel({wheelDeltaY, 0.0f}, key_modifiers_from_event(event));
mRedirectingScroll = false;
event.StopPropagation();
});
}
bool TabBar::focus() {
if (mProps.selectedTabIndex >= 0 && mProps.selectedTabIndex < mTabs.size()) {
// Try to focus the currently selected tab
if (mTabs[mProps.selectedTabIndex].button.focus()) {
mLastFocusedTabIndex = mProps.selectedTabIndex;
return true;
}
}
// Otherwise, focus the first enabled tab
for (const auto& tab : mTabs) {
if (tab.button.focus()) {
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
if (mTabs[i].button.focus()) {
mLastFocusedTabIndex = i;
return true;
}
}
@@ -44,10 +116,23 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
callback();
}
auto& button = add_child<Button>(Button::Props{title}, "tab");
button.on_pressed([this, index] { set_active_tab(index); });
button.on_nav_command([this, index](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (mProps.autoSelect) {
mDoAud_seStartMenu(kSoundTabChanged);
}
set_active_tab(index);
return true;
}
return false;
});
if (selected) {
button.set_selected(true);
}
if (mEndSpacer != nullptr) {
auto spacer = mRoot->RemoveChild(mEndSpacer);
mEndSpacer = mRoot->AppendChild(std::move(spacer));
}
mTabs.emplace_back(Tab{
.title = title,
.button = button,
@@ -58,8 +143,8 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
bool TabBar::set_active_tab(int index) {
if (index == -1) {
// Clear currently selected tab
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
mTabs[i].button.set_selected(false);
for (auto& tab : mTabs) {
tab.button.set_selected(false);
}
mProps.selectedTabIndex = -1;
return true;
@@ -70,6 +155,7 @@ bool TabBar::set_active_tab(int index) {
}
const auto& tab = mTabs[index];
if (tab.button.focus()) {
mLastFocusedTabIndex = index;
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
mTabs[i].button.set_selected(i == index);
}
@@ -82,11 +168,28 @@ bool TabBar::set_active_tab(int index) {
return false;
}
void TabBar::refresh_active_tab() {
if (mProps.selectedTabIndex >= 0 && mProps.selectedTabIndex < static_cast<int>(mTabs.size())) {
const auto& tab = mTabs[mProps.selectedTabIndex];
if (tab.callback) {
tab.callback();
}
}
}
int TabBar::focused_tab_index() const {
return mLastFocusedTabIndex;
}
bool TabBar::focus_tab(int index) {
if (index < 0 || index >= mTabs.size() || index == mProps.selectedTabIndex) {
return false;
}
return mTabs[index].button.focus();
if (mTabs[index].button.focus()) {
mLastFocusedTabIndex = index;
return true;
}
return false;
}
int TabBar::tab_containing(Rml::Element* element) const {
@@ -103,20 +206,31 @@ bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
cmd == NavCommand::Previous)
{
bool isNext = cmd == NavCommand::Right || cmd == NavCommand::Next;
int currentComponent = tab_containing(event.GetTargetElement());
int currentComponent = mProps.selectedTabIndex;
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
int activeTab = tab_containing(event.GetTargetElement());
if (activeTab != -1) {
currentComponent = activeTab;
}
}
int direction = isNext ? 1 : -1;
int i = currentComponent + direction;
if (currentComponent == -1) {
// If the container itself is focused and right is pressed, focus the first element
if (isNext) {
i = 0;
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
// If the container itself is focused and right is pressed, focus the first element
if (!isNext) {
return false;
}
currentComponent = -1;
} else {
// Otherwise, allow event to bubble
// Next/Previous require a currently selected tab to navigate from
return false;
}
}
int i = currentComponent + direction;
while (i >= 0 && i < mTabs.size()) {
if (mProps.autoSelect ? set_active_tab(i) : focus_tab(i)) {
const bool changed = mProps.autoSelect ? set_active_tab(i) : focus_tab(i);
if (changed) {
mDoAud_seStartMenu(kSoundTabChanged);
return true;
}
i += direction;

Some files were not shown because too many files have changed in this diff Show More