Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fdf954994 | |||
| 230868af3c | |||
| 66154d9de8 | |||
| 93ec3c7dbd | |||
| 945ce3e4bc | |||
| 782a8573e9 | |||
| 4e0ca51159 | |||
| 3cd160e1b2 | |||
| 93a236a9d2 | |||
| eaf3bc2f40 | |||
| 5ca0a2ba06 | |||
| ccd2bdbaac | |||
| 08321699cd | |||
| 7300c0e0f5 | |||
| 7e562824fe | |||
| ed8b5c96b9 | |||
| 14bccdffa6 | |||
| 39e465bcab | |||
| e53bb3a12d | |||
| 50fccd393f | |||
| 7993740ac8 | |||
| 8e5bb8ae59 | |||
| 8fefdd4114 | |||
| 64c8cee21b | |||
| 25fe686573 | |||
| 1c1ea98fdd | |||
| e098104f8f | |||
| 3c5ade5565 | |||
| b2ad75027e | |||
| fdfbf83b88 | |||
| 1b9ca0949e | |||
| 827037f0fa | |||
| d84c5790f5 | |||
| 49eb2282af | |||
| 741f9ecfab | |||
| 37b8122962 | |||
| 5f2cf68e80 | |||
| 74f2c58b29 | |||
| 208433047a | |||
| 2c01430035 | |||
| 5121437bcf | |||
| f61bd3e5ad | |||
| 835e409b32 | |||
| 010bdb7e25 | |||
| 7ba22b7714 | |||
| efcb19a3d0 | |||
| 55455bb1b5 | |||
| e49be12297 | |||
| 75f4940f5e | |||
| 8047330952 | |||
| 9105dcb078 | |||
| b8e38e03e2 | |||
| 331352878e | |||
| 62a88f1e9a | |||
| 43b603e70b | |||
| 95e6ac54cf | |||
| c4b2e2e501 | |||
| dccba23980 | |||
| bf27d10519 | |||
| 6c27011e32 | |||
| 6220990dc5 | |||
| 93e9767c9f | |||
| c774f53dad | |||
| 7fbfe5ad88 | |||
| ef02037990 | |||
| 23cc18ba0e | |||
| 924dbc7715 | |||
| 742f4938f2 | |||
| 02e0f586d3 | |||
| 5717aeef85 | |||
| da9b99f650 | |||
| 901ce2ee4c | |||
| dd2b993cd5 | |||
| 83577d3b82 | |||
| eaae8b6137 | |||
| d9a0ef760f | |||
| 95470b830f |
@@ -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
|
||||
|
||||
@@ -1,37 +1,55 @@
|
||||

|
||||
<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!
|
||||
|
||||

|
||||
- 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>
|
||||
|
||||
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 82 KiB |
@@ -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 |
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -56,6 +56,7 @@ struct UserSettings {
|
||||
ConfigVar<int> fanfareVolume;
|
||||
ConfigVar<bool> enableReverb;
|
||||
ConfigVar<bool> enableHrtf;
|
||||
ConfigVar<bool> menuSounds;
|
||||
} audio;
|
||||
|
||||
// Game settings
|
||||
@@ -66,13 +67,11 @@ struct UserSettings {
|
||||
// QoL
|
||||
ConfigVar<bool> enableQuickTransform;
|
||||
ConfigVar<bool> hideTvSettingsScreen;
|
||||
ConfigVar<bool> skipWarningScreen;
|
||||
ConfigVar<bool> biggerWallets;
|
||||
ConfigVar<bool> noReturnRupees;
|
||||
ConfigVar<bool> disableRupeeCutscenes;
|
||||
ConfigVar<bool> noSwordRecoil;
|
||||
ConfigVar<int> damageMultiplier;
|
||||
ConfigVar<bool> hyperEnemies;
|
||||
ConfigVar<bool> noHeartDrops;
|
||||
ConfigVar<bool> instantDeath;
|
||||
ConfigVar<bool> fastClimbing;
|
||||
@@ -120,6 +119,7 @@ struct UserSettings {
|
||||
ConfigVar<bool> invertCameraXAxis;
|
||||
ConfigVar<bool> invertCameraYAxis;
|
||||
ConfigVar<float> freeCameraSensitivity;
|
||||
ConfigVar<bool> debugFlyCam;
|
||||
|
||||
// Cheats
|
||||
ConfigVar<bool> infiniteHearts;
|
||||
@@ -155,7 +155,6 @@ struct UserSettings {
|
||||
ConfigVar<bool> showPipelineCompilation;
|
||||
ConfigVar<bool> wasPresetChosen;
|
||||
ConfigVar<bool> enableCrashReporting;
|
||||
ConfigVar<bool> duskMenuOpen;
|
||||
ConfigVar<int> cardFileType;
|
||||
} backend;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 48 KiB |
@@ -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("" center center);
|
||||
}
|
||||
|
||||
icon.trophy {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
font-size: 24dp;
|
||||
decorator: text("" 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("" center center);
|
||||
}
|
||||
|
||||
#disc-status[status=bad] icon {
|
||||
decorator: text("" 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,
|
||||
|
||||
@@ -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("" 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%);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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("" 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%;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ¤t.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, ...) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -1,14 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
|
||||
class ImGuiFirstRunPreset {
|
||||
public:
|
||||
void draw();
|
||||
|
||||
private:
|
||||
bool m_opened = false;
|
||||
bool m_done = false;
|
||||
};
|
||||
|
||||
} // namespace dusk
|
||||
@@ -583,7 +583,6 @@ namespace dusk {
|
||||
getSettings().game.damageMultiplier.setValue(1);
|
||||
getSettings().game.instantDeath.setValue(false);
|
||||
getSettings().game.noHeartDrops.setValue(false);
|
||||
getSettings().game.hyperEnemies.setValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setValue(false);
|
||||
getSettings().game.infiniteArrows.setValue(false);
|
||||
@@ -602,6 +601,7 @@ namespace dusk {
|
||||
getSettings().game.freeMagicArmor.setValue(false);
|
||||
|
||||
getSettings().game.enableTurboKeybind.setValue(false);
|
||||
getSettings().game.debugFlyCam.setValue(false);
|
||||
}
|
||||
|
||||
SpeedrunInfo m_speedrunInfo;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,14 +26,12 @@ 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},
|
||||
.noSwordRecoil {"game.noSwordRecoil", false},
|
||||
.damageMultiplier {"game.damageMultiplier", 1},
|
||||
.hyperEnemies {"game.hyperEnemies", false},
|
||||
.noHeartDrops {"game.noHeartDrops", false},
|
||||
.instantDeath {"game.instantDeath", false},
|
||||
.fastClimbing {"game.fastClimbing", false},
|
||||
@@ -48,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},
|
||||
@@ -79,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},
|
||||
@@ -114,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)}
|
||||
}
|
||||
};
|
||||
@@ -137,18 +136,17 @@ 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);
|
||||
Register(g_userSettings.game.noSwordRecoil);
|
||||
Register(g_userSettings.game.damageMultiplier);
|
||||
Register(g_userSettings.game.hyperEnemies);
|
||||
Register(g_userSettings.game.noHeartDrops);
|
||||
Register(g_userSettings.game.instantDeath);
|
||||
Register(g_userSettings.game.fastClimbing);
|
||||
@@ -205,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);
|
||||
@@ -212,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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", "");
|
||||
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;
|
||||
}
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class PrelaunchOptions : public Window {
|
||||
public:
|
||||
PrelaunchOptions();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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("• {}", 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("• {}", 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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||