Compare commits

..

46 Commits

Author SHA1 Message Date
Howard Luck 65e8577253 frame interp: fix b&c chain links (#724) 2026-05-08 17:26:33 -06:00
Luke Street a4fcc10f5f Make version logic tolerate prerelease semver 2026-05-08 17:25:09 -06:00
Luke Street a2c2988666 Disable Sentry in CI builds 2026-05-08 17:12:58 -06:00
Irastris abec043249 Add mouse as a gyro input source (#720)
* Add mouse as a gyro input source

* Revisions

* Grammar
2026-05-08 17:11:13 -06:00
SuperDude88 699d069b0a Fix Letter Menu Page Numbers (#722)
This bug comes from `getBounds()` returning a reference to a static variable -- the compiler can load the float right away or later (after the second call), so the value it is referencing may have changed. If it waits to load the first operand, the subtraction results in 0. Further discussion at https://discord.com/channels/1446645861529550869/1446648842387722411/1502415404386091118.

Resolves #719
2026-05-08 17:10:37 -06:00
Tom Lube 4d67033ff8 Remove reset via CTRL + R 2026-05-08 17:08:03 -06:00
Irastris 84ffd67622 Refactor notification settings, allow disabling controller toasts (#721)
* Refactor notification settings, allow disabling controller toasts

* "Toasts" to "Notifications"
2026-05-08 17:01:19 -06:00
Luke Street 1c85ee63eb Many mobile fixes, Android update check, "Background Input" & "Pause On Focus Lost" options 2026-05-08 16:57:33 -06:00
qwertyquerty 81c7213993 Merge pull request #599 from TwilitRealm/50_achievements
More Achievements
2026-05-08 12:27:25 -07:00
madeline be82e606b2 45 achievements 2026-05-08 11:20:17 -07:00
MelonSpeedruns 44da1a9f7d Added sounds to Reset & Quit modals (#718)
Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
2026-05-08 11:51:13 -06:00
doop d0f8ea56f9 Interpolate more things (#699)
* Interpolate trim height

* Interpolate (some) fades
2026-05-08 11:50:44 -06:00
Howard Luck e472b36cef Merge pull request #717 from TwilitRealm/fix/super_clawshot 2026-05-08 10:26:46 -06:00
MelonSpeedruns 3934e09c8f remove mention of chains not rendering 2026-05-08 12:07:00 -04:00
MelonSpeedruns 3136816ce9 Merge remote-tracking branch 'origin/main' into fix/super_clawshot 2026-05-08 12:06:47 -04:00
MelonSpeedruns 6fd3762ffc optimized code 2026-05-08 12:06:38 -04:00
Pheenoh 97a1190713 set max chain links to 600 2026-05-08 08:59:13 -06:00
MelonSpeedruns 73a3bd9ae8 super clawshot warning about chains (#716)
Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
2026-05-08 08:53:01 -06:00
Luke Street fc533dbdc7 UI: Add update checks (#715) 2026-05-08 08:52:36 -06:00
madeline 6217e071d2 Merge branch '50_achievements' of https://github.com/TakaRikka/dusk into 50_achievements 2026-05-08 05:17:48 -07:00
Luke Street 673ca7f686 Update Linux icons & .desktop
Resolves #714
2026-05-08 01:23:53 -06:00
CraftyBoss 29a1cff7ea update aurora, add comment explaining king bulblin 1 strange behavior
the game saves a flag when the player hits king bulblin at least once, which makes subsequent loads (including king bulblin 2) have 1 less "health" as lap_num for the boar is set to 1 during creation, as it checks the flag.
2026-05-08 00:05:16 -07:00
Luke Street 3c5152a67b Disable autosave in speedrun mode 2026-05-08 00:46:02 -06:00
MelonSpeedruns 1c1a849095 Recording Mode & Enable Autosave in Dusk preset (#685)
* branch for recording without a HUD

* no more black bars thanks maddie

* Backslash hotkey

* mute music

* dont check for null twice

* renamed option to recording mode, and added it to the rmlui menu

* Move recording mode to Interface

* re-added & renamed minimal hud option

* Mute all BGM if recording mode is on

* Update Interface section

* Un-experimentalize Autosave & enable in Dusk preset

---------

Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
Co-authored-by: Irastris <irastris15@gmail.com>
Co-authored-by: Luke Street <luke@street.dev>
2026-05-07 23:26:22 -06:00
Phillip Stephens 89d393e57a Add missing climits include 2026-05-07 22:24:31 -07:00
Phillip Stephens 2f27687d80 Fix OSReport strings corruption with non-trivial format (#709)
* Fix OSReport strings corruption with non-trivial format

* Limit format attempts to 3
2026-05-07 23:10:23 -06:00
SuperDude88 167a50c01d Deselect Sub-Menu On Focus Change (#710)
- Unselect any previous items when focusing a new one

This might be way over-inclusive so it should be scrutinized harshly (but fixes #672)
2026-05-07 23:09:25 -06:00
SuperDude88 313f03f5e5 Dim Carousel Arrows
- Dim carousel arrows if there are no further options in that direction (closes #708)

This sets the color property constantly, not sure if that is a concern for perf (would imagine not since it already sets the rml to mValueElem every frame also?)
2026-05-07 23:05:08 -06:00
Howard Luck 6cfdc3d8a3 add keyboard controls + roll support to fly cam (#705) 2026-05-07 23:04:29 -06:00
Luke Street 9c24a0bc4b Disable Development Mode & move Input Viewer
Resolves #712
2026-05-07 23:03:26 -06:00
TakaRikka b99ad920c4 Merge pull request #704 from TwilitRealm/ira/rmlui-confirmation
Add confirmations modals to Reset and Quit
2026-05-07 13:57:03 -07:00
TakaRikka 912b18eca1 Merge pull request #703 from TwilitRealm/ui/advanced-settings
Advanced Settings
2026-05-07 13:55:13 -07:00
TakaRikka 9e651a51db Merge pull request #700 from TwilitRealm/ira/rmlui-fps
Add FPS counter to RmlUi
2026-05-07 13:51:51 -07:00
TakaRikka c8b6e997a7 Merge pull request #702 from TwilitRealm/ira/rmlui-inputconfig
Add remaining controller config options to RmlUi
2026-05-07 13:45:35 -07:00
Irastris 928e187524 Add mobile layout for prelaunch 2026-05-07 15:57:52 -04:00
Irastris cf12d19860 Resolve RmlUi deprecation warning, add L1/R1 tabbing to MenuBar 2026-05-07 13:48:12 -04:00
Irastris e3a3ac56fb Add confirmations modals to Reset and Quit 2026-05-07 13:32:59 -04:00
MelonSpeedruns d15ed81cd5 Only show Editor if Advanced Settings is enabled 2026-05-07 12:39:37 -04:00
TakaRikka ff054f6f47 update aurora and enable MTX_USE_PS 2026-05-07 09:36:32 -07:00
MelonSpeedruns 667cf70fa0 Add Advanced Settings option & Disable debug hotkeys if it's off 2026-05-07 12:01:32 -04:00
Irastris 73eb401c93 Add remaining controller config options, remove ImGui config 2026-05-07 12:00:35 -04:00
Irastris b11f3add06 Don't immediately dismiss prelaunch if skip config is toggled
Prevents a situation where memcard is not init until restarting after the deferral change
2026-05-07 09:26:41 -04:00
Irastris 92f888a152 Remove blur and shadow from counter, consolidate options 2026-05-07 03:28:30 -04:00
Irastris 78ed5cc716 Add FPS counter 2026-05-07 03:13:08 -04:00
madeline 44f3828f68 fix morpheel logic 2026-04-30 02:48:30 -07:00
madeline 3f560b060c 40 achievements 2026-04-30 01:48:03 -07:00
104 changed files with 4680 additions and 1042 deletions
+47 -8
View File
@@ -48,13 +48,15 @@ else ()
message(STATUS "Unable to find git, commit information will not be available")
endif ()
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$")
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
if (CMAKE_MATCH_5)
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}")
else ()
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0")
set(DUSK_VERSION_TWEAK "0")
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
endif ()
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
else ()
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
set(DUSK_VERSION_STRING "0.0.0.0")
@@ -69,7 +71,7 @@ message(STATUS "Dusk version set to ${DUSK_WC_DESCRIBE}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
project(dusk LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
if (APPLE)
enable_language(OBJC)
enable_language(OBJC OBJCXX)
endif ()
if (APPLE AND NOT TVOS AND CMAKE_SYSTEM_NAME STREQUAL tvOS)
# ios.toolchain.cmake hack for SDL
@@ -102,12 +104,14 @@ set(AURORA_ENABLE_DVD ON CACHE BOOL "Enable DVD API support" FORCE)
set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE)
set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE)
add_subdirectory(extern/aurora EXCLUDE_FROM_ALL)
target_compile_definitions(aurora_mtx PRIVATE MTX_USE_PS=1)
add_subdirectory(libs/freeverb)
option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)")
option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.")
option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON)
option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
if(ANDROID)
set(DUSK_MOVIE_SUPPORT OFF)
@@ -283,9 +287,9 @@ set(DUSK_PRODUCT_NAME "Dusk")
set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
source_group("dusk" FILES ${DUSK_FILES})
source_group("dusk" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0)
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
set(GAME_INCLUDE_DIRS
include
@@ -313,6 +317,41 @@ if (WIN32)
list(APPEND GAME_LIBS Ws2_32)
endif ()
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/no_backend.cpp)
if (DUSK_ENABLE_UPDATE_CHECKER)
list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_UPDATE_CHECKER=1)
if (WIN32)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/winhttp.cpp)
list(APPEND GAME_LIBS winhttp)
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_WINHTTP=1)
message(STATUS "dusk: Enabled update checker (WinHTTP)")
elseif (ANDROID)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/android.cpp)
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_ANDROID=1)
message(STATUS "dusk: Enabled update checker (Android)")
elseif (APPLE)
find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm)
set_source_files_properties(src/dusk/http/url_session.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
list(APPEND GAME_LIBS ${FOUNDATION_FRAMEWORK})
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_URLSESSION=1)
message(STATUS "dusk: Enabled update checker (NSURLSession)")
elseif (CMAKE_SYSTEM_NAME STREQUAL Linux)
find_package(CURL QUIET OPTIONAL_COMPONENTS HTTPS SSL)
if (CURL_FOUND AND CURL_HTTPS_FOUND AND CURL_SSL_FOUND)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/curl.cpp)
list(APPEND GAME_LIBS CURL::libcurl)
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_LIBCURL=1)
message(STATUS "dusk: Enabled update checker (libcurl)")
else ()
message(STATUS "dusk: Disabled update checker (libcurl + HTTPS/SSL not found)")
endif ()
else ()
message(STATUS "dusk: Disabled update checker (unsupported platform)")
endif ()
endif ()
list(APPEND DUSK_FILES ${DUSK_HTTP_BACKEND_SOURCE})
if (DUSK_MOVIE_SUPPORT)
if (TARGET libjpeg-turbo::turbojpeg-static)
list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static)
+1 -1
View File
@@ -30,7 +30,7 @@
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_ENABLE_SENTRY_NATIVE": {
"type": "BOOL",
"value": true
"value": false
},
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
"DUSK_SENTRY_ENVIRONMENT": "production"
+1 -1
+10
View File
@@ -1430,11 +1430,14 @@ set(DUSK_FILES
src/dusk/gyro.cpp
src/dusk/gamepad_color.cpp
src/dusk/autosave.cpp
src/dusk/http/http.hpp
src/dusk/io.cpp
src/dusk/layout.cpp
src/dusk/logging.cpp
src/dusk/settings.cpp
src/dusk/stubs.cpp
src/dusk/update_check.cpp
src/dusk/update_check.hpp
#src/dusk/m_Do_ext_dusk.cpp
src/dusk/imgui/ImGuiConfig.hpp
src/dusk/imgui/ImGuiConsole.hpp
@@ -1516,3 +1519,10 @@ set(DUSK_FILES
src/dusk/discord_presence.cpp
src/dusk/version.cpp
)
set(DUSK_HTTP_BACKEND_FILES
src/dusk/http/no_backend.cpp
src/dusk/http/curl.cpp
src/dusk/http/winhttp.cpp
src/dusk/http/url_session.mm
)
+12
View File
@@ -4552,6 +4552,18 @@ public:
void handleWolfHowl();
void handleQuickTransform();
bool checkGyroAimContext();
void onIronBallChainInterpCallback();
static const int IRON_BALL_CHAIN_COUNT = 102;
cXyz mIBChainInterpPrevPos[IRON_BALL_CHAIN_COUNT];
cXyz mIBChainInterpCurrPos[IRON_BALL_CHAIN_COUNT];
csXyz mIBChainInterpPrevAngle[IRON_BALL_CHAIN_COUNT];
csXyz mIBChainInterpCurrAngle[IRON_BALL_CHAIN_COUNT];
cXyz mIBChainInterpPrevHandRoot;
cXyz mIBChainInterpCurrHandRoot;
bool mIBChainInterpPrevValid;
bool mIBChainInterpCurrValid;
#endif
}; // Size: 0x385C
+6
View File
@@ -1845,6 +1845,12 @@ inline void dComIfGs_addDeathCount() {
g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().addDeathCount();
}
#if TARGET_PC
inline u16 dComIfGs_getDeathCount() {
return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getDeathCount();
}
#endif
inline char* dComIfGs_getPlayerName() {
return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getPlayerName();
}
+3
View File
@@ -486,6 +486,9 @@ public:
mDeathCount++;
}
}
#if TARGET_PC
u16 getDeathCount() const { return mDeathCount; }
#endif
char* getPlayerName() const { return const_cast<char*>(mPlayerName); }
void setPlayerName(const char* i_name) {
#if AVOID_UB
+1 -2
View File
@@ -12,9 +12,8 @@
namespace dusk {
enum class AchievementCategory : uint8_t {
Story,
Collection,
Challenge,
Collection,
Minigame,
Misc,
Glitched
+1
View File
@@ -12,6 +12,7 @@ void rollgoalTableOffset(s16& out_ax, s16& out_az);
extern bool s_sensor_keep_alive;
bool get_sensor_keep_alive();
void set_sensor_keep_alive(bool value);
bool rollgoal_gyro_enabled();
} // namespace dusk::gyro
#endif
-1
View File
@@ -11,7 +11,6 @@ namespace dusk {
extern bool IsRunning;
extern bool IsShuttingDown;
extern bool IsGameLaunched;
extern bool IsFocusPaused;
extern bool RestartRequested;
extern std::filesystem::path ConfigPath;
+22 -3
View File
@@ -27,6 +27,11 @@ enum class DiscVerificationState : u8 {
HashMismatch,
};
enum class GyroMode : u8 {
Sensor = 0,
Mouse = 1,
};
namespace config {
template <>
struct ConfigEnumRange<BloomMode> {
@@ -45,6 +50,12 @@ struct ConfigEnumRange<DiscVerificationState> {
static constexpr auto min = DiscVerificationState::Unknown;
static constexpr auto max = DiscVerificationState::HashMismatch;
};
template <>
struct ConfigEnumRange<GyroMode> {
static constexpr auto min = GyroMode::Sensor;
static constexpr auto max = GyroMode::Mouse;
};
}
// Persistent user settings
@@ -57,6 +68,8 @@ struct UserSettings {
ConfigVar<bool> enableFullscreen;
ConfigVar<bool> enableVsync;
ConfigVar<bool> lockAspectRatio;
ConfigVar<bool> enableFpsOverlay;
ConfigVar<int> fpsOverlayCorner;
} video;
struct {
@@ -97,11 +110,11 @@ struct UserSettings {
// Preferences
ConfigVar<bool> enableMirrorMode;
ConfigVar<bool> disableMainHUD;
ConfigVar<bool> minimalHUD;
ConfigVar<bool> pauseOnFocusLost;
ConfigVar<bool> enableLinkDollRotation;
ConfigVar<bool> enableAchievementNotifications;
ConfigVar<bool> enableAchievementToasts;
ConfigVar<bool> enableControllerToasts;
// Graphics
ConfigVar<BloomMode> bloomMode;
@@ -118,6 +131,7 @@ struct UserSettings {
ConfigVar<bool> midnasLamentNonStop;
// Input
ConfigVar<GyroMode> gyroMode;
ConfigVar<bool> enableGyroAim;
ConfigVar<bool> enableGyroRollgoal;
ConfigVar<float> gyroSensitivityX;
@@ -132,6 +146,8 @@ struct UserSettings {
ConfigVar<bool> invertCameraYAxis;
ConfigVar<float> freeCameraSensitivity;
ConfigVar<bool> debugFlyCam;
ConfigVar<bool> debugFlyCamLockEvents;
ConfigVar<bool> allowBackgroundInput;
// Cheats
ConfigVar<bool> infiniteHearts;
@@ -158,6 +174,7 @@ struct UserSettings {
// Tools
ConfigVar<bool> speedrunMode;
ConfigVar<bool> liveSplitEnabled;
ConfigVar<bool> recordingMode;
} game;
struct {
@@ -168,7 +185,9 @@ struct UserSettings {
ConfigVar<bool> showPipelineCompilation;
ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> enableCrashReporting;
ConfigVar<bool> checkForUpdates;
ConfigVar<int> cardFileType;
ConfigVar<bool> enableAdvancedSettings;
} backend;
};
+17 -3
View File
@@ -8,6 +8,10 @@
#include "JSystem/JUtility/JUTFader.h"
#include "JSystem/J2DGraph/J2DOrthoGraph.h"
#ifdef TARGET_PC
#include <algorithm>
#endif
JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor)
: mColor(pColor), mBox(x, y, x + width, y + height) {
mStatus = None;
@@ -63,14 +67,24 @@ void JUTFader::advance() {
void JUTFader::control() {
advance();
#ifndef TARGET_PC
// FRAME INTERP NOTE: Draw is called by JFWDisplay when interpolation is active
draw();
#endif
}
void JUTFader::draw() {
if (mColor.a != 0) {
#ifdef TARGET_PC
if (dusk::frame_interp::is_enabled() && mDuration != 0) {
const auto step = dusk::frame_interp::get_interpolation_step();
const auto progress = static_cast<f32>(mTimer) / static_cast<f32>(mDuration);
const auto timer = mTimer - 1 + step + progress;
auto alpha = timer / mDuration;
if (mStatus == FadeIn) {
alpha = 1.0f - alpha;
}
alpha = std::clamp(alpha, 0.0f, 1.0f);
mColor.a = static_cast<u8>(alpha * 255.0f);
}
#endif
J2DOrthoGraph orthograph;
orthograph.setColor(mColor);
orthograph.fillBox(mBox);
+17 -1
View File
@@ -2,6 +2,16 @@ plugins {
id 'com.android.application'
}
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusk').get().asFile
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
from(new File(duskRepoDir, 'res')) {
into 'res'
exclude '**/.DS_Store'
}
into duskGeneratedAssetsDir
}
android {
namespace 'com.twilitrealm.dusk'
compileSdk 36
@@ -27,7 +37,7 @@ android {
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
assets.srcDirs = ['../../assets']
assets.srcDirs = [duskGeneratedAssetsDir]
}
}
@@ -48,3 +58,9 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
tasks.configureEach { task ->
if (task.name.startsWith('merge') && task.name.endsWith('Assets')) {
task.dependsOn(syncDuskAssets)
}
}
+2
View File
@@ -1,2 +1,4 @@
# Keep SDL activity and related JNI bridge methods.
-keep class org.libsdl.app.** { *; }
-keep class com.twilitrealm.dusk.DuskHttpClient { *; }
-keep class com.twilitrealm.dusk.DuskHttpClient$Response { *; }
@@ -10,6 +10,7 @@
<uses-feature android:name="android.hardware.type.pc" android:required="false" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
@@ -1,18 +1,27 @@
package com.twilitrealm.dusk;
import android.app.ActionBar;
import android.content.ClipData;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import org.libsdl.app.SDLActivity;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class DuskActivity extends SDLActivity {
private static final String TAG = "DuskActivity";
private static String[] splitArgs(String raw) {
List<String> out = new ArrayList<>();
StringBuilder current = new StringBuilder();
@@ -63,7 +72,10 @@ public class DuskActivity extends SDLActivity {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
WindowInsetsController ctrl = getWindow().getDecorView().getWindowInsetsController();
if (ctrl != null) {
ctrl.hide(WindowInsets.Type.systemBars());
}
}else {
View decorView = getWindow().getDecorView();
// Hide the status bar.
@@ -72,7 +84,9 @@ public class DuskActivity extends SDLActivity {
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
actionBar.hide();
if (actionBar != null) {
actionBar.hide();
}
}
}
@@ -108,4 +122,93 @@ public class DuskActivity extends SDLActivity {
}
return new String[0];
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
persistUriPermissions(data);
}
super.onActivityResult(requestCode, resultCode, data);
}
private void persistUriPermissions(Intent data) {
if (data == null) {
return;
}
int permissionFlags =
data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (permissionFlags == 0) {
return;
}
Uri uri = data.getData();
if (uri != null) {
persistUriPermission(uri, permissionFlags);
}
ClipData clipData = data.getClipData();
if (clipData == null) {
return;
}
for (int i = 0; i < clipData.getItemCount(); ++i) {
Uri itemUri = clipData.getItemAt(i).getUri();
if (itemUri != null) {
persistUriPermission(itemUri, permissionFlags);
}
}
}
private void persistUriPermission(Uri uri, int permissionFlags) {
if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read");
}
if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write");
}
}
private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) {
try {
getContentResolver().takePersistableUriPermission(uri, permissionFlag);
} catch (SecurityException | IllegalArgumentException e) {
Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e);
}
}
public String getDisplayNameForUri(String uriString) {
if (uriString == null || uriString.isEmpty()) {
return "";
}
Uri uri = Uri.parse(uriString);
if ("content".equals(uri.getScheme())) {
try (Cursor cursor = getContentResolver().query(
uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null))
{
if (cursor != null && cursor.moveToFirst()) {
int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (displayNameColumn >= 0) {
String displayName = cursor.getString(displayNameColumn);
if (displayName != null && !displayName.isEmpty()) {
return displayName;
}
}
}
} catch (SecurityException | IllegalArgumentException e) {
Log.w(TAG, "Unable to query display name for " + uri, e);
}
} else if ("file".equals(uri.getScheme())) {
String path = uri.getPath();
if (path != null && !path.isEmpty()) {
String name = new File(path).getName();
if (!name.isEmpty()) {
return name;
}
}
}
String lastSegment = uri.getLastPathSegment();
return lastSegment != null ? lastSegment : "";
}
}
@@ -0,0 +1,237 @@
package com.twilitrealm.dusk;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
public final class DuskHttpClient {
public static final int ERROR_NONE = 0;
public static final int ERROR_INVALID_URL = 1;
public static final int ERROR_UNSUPPORTED_SCHEME = 2;
public static final int ERROR_TIMEOUT = 3;
public static final int ERROR_TOO_LARGE = 4;
public static final int ERROR_NETWORK = 5;
private static final int MAX_REDIRECTS = 5;
public static final class Response {
public int error;
public String message;
public int statusCode;
public String[] headerNames;
public String[] headerValues;
public byte[] body;
Response(int error, String message, int statusCode, String[] headerNames,
String[] headerValues, byte[] body) {
this.error = error;
this.message = message;
this.statusCode = statusCode;
this.headerNames = headerNames != null ? headerNames : new String[0];
this.headerValues = headerValues != null ? headerValues : new String[0];
this.body = body != null ? body : new byte[0];
}
}
private DuskHttpClient() {
}
public static Response get(String url, String[] headerNames, String[] headerValues,
int timeoutMs, long maxBodyBytes) {
if (url == null || url.isEmpty()) {
return fail(ERROR_INVALID_URL, "URL is empty");
}
try {
URL currentUrl = new URL(url);
if (!isHttps(currentUrl)) {
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
}
for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) {
HttpsURLConnection connection =
(HttpsURLConnection) currentUrl.openConnection();
try {
connection.setRequestMethod("GET");
connection.setConnectTimeout(timeoutMs);
connection.setReadTimeout(timeoutMs);
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(false);
applyHeaders(connection, headerNames, headerValues);
int statusCode = connection.getResponseCode();
if (isRedirect(statusCode)) {
String location = connection.getHeaderField("Location");
if (location == null || location.isEmpty()) {
return fail(ERROR_NETWORK, "Redirect response did not include Location",
statusCode, connection, new byte[0]);
}
URL nextUrl = new URL(currentUrl, location);
if (!isHttps(nextUrl)) {
return fail(ERROR_UNSUPPORTED_SCHEME,
"Only https:// redirects are supported", statusCode,
connection, new byte[0]);
}
currentUrl = nextUrl;
continue;
}
byte[] body = readBody(connection, statusCode, maxBodyBytes);
return success(statusCode, connection, body);
} catch (ResponseTooLargeException e) {
return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit",
safeStatusCode(connection), connection, e.partialBody);
} finally {
connection.disconnect();
}
}
return fail(ERROR_NETWORK, "Too many redirects");
} catch (MalformedURLException e) {
return fail(ERROR_INVALID_URL, "Failed to parse URL");
} catch (SocketTimeoutException e) {
return fail(ERROR_TIMEOUT, "Request timed out");
} catch (IOException e) {
String message = e.getMessage();
return fail(ERROR_NETWORK, message != null ? message : e.toString());
} catch (ClassCastException e) {
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
}
}
private static void applyHeaders(HttpsURLConnection connection, String[] names,
String[] values) {
if (names == null || values == null) {
return;
}
int count = Math.min(names.length, values.length);
for (int i = 0; i < count; ++i) {
if (names[i] != null && values[i] != null) {
connection.setRequestProperty(names[i], values[i]);
}
}
}
private static boolean isHttps(URL url) {
return "https".equalsIgnoreCase(url.getProtocol());
}
private static boolean isRedirect(int statusCode) {
return statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
statusCode == HttpURLConnection.HTTP_MOVED_TEMP ||
statusCode == HttpURLConnection.HTTP_SEE_OTHER ||
statusCode == 307 ||
statusCode == 308;
}
private static byte[] readBody(HttpsURLConnection connection, int statusCode,
long maxBodyBytes) throws IOException,
ResponseTooLargeException {
InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ?
connection.getErrorStream() : connection.getInputStream();
if (stream == null) {
return new byte[0];
}
try (InputStream bodyStream = stream;
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
long total = 0;
while (true) {
int read = bodyStream.read(buffer);
if (read < 0) {
return out.toByteArray();
}
if (read == 0) {
continue;
}
if (read > maxBodyBytes || total > maxBodyBytes - read) {
throw new ResponseTooLargeException(out.toByteArray());
}
out.write(buffer, 0, read);
total += read;
}
}
}
private static int safeStatusCode(HttpsURLConnection connection) {
try {
return connection.getResponseCode();
} catch (IOException e) {
return 0;
}
}
private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) {
HeaderLists headers = readHeaders(connection);
return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body);
}
private static Response fail(int error, String message) {
return new Response(error, message, 0, null, null, null);
}
private static Response fail(int error, String message, int statusCode,
HttpsURLConnection connection, byte[] body) {
HeaderLists headers = readHeaders(connection);
return new Response(error, message, statusCode, headers.names, headers.values, body);
}
private static HeaderLists readHeaders(HttpsURLConnection connection) {
List<String> names = new ArrayList<>();
List<String> values = new ArrayList<>();
Map<String, List<String>> headerFields = connection.getHeaderFields();
if (headerFields == null) {
return new HeaderLists(new String[0], new String[0]);
}
for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
String name = entry.getKey();
if (name == null) {
continue;
}
List<String> entryValues = entry.getValue();
if (entryValues == null || entryValues.isEmpty()) {
names.add(name);
values.add("");
continue;
}
for (String value : entryValues) {
names.add(name);
values.add(value != null ? value : "");
}
}
return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0]));
}
private static final class HeaderLists {
final String[] names;
final String[] values;
HeaderLists(String[] names, String[] values) {
this.names = names;
this.values = values;
}
}
private static final class ResponseTooLargeException extends Exception {
final byte[] partialBody;
ResponseTooLargeException(byte[] partialBody) {
this.partialBody = partialBody;
}
}
}
@@ -256,6 +256,7 @@ public class HIDDeviceManager {
0x24c6, // PowerA
0x2c22, // Qanba
0x2dc8, // 8BitDo
0x37d7, // Flydigi
0x9886, // ASTRO Gaming
};
@@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
private static final String TAG = "SDL";
private static final int SDL_MAJOR_VERSION = 3;
private static final int SDL_MINOR_VERSION = 4;
private static final int SDL_MICRO_VERSION = 2;
private static final int SDL_MICRO_VERSION = 4;
/*
// Display InputType.SOURCE/CLASS of events and devices
//
@@ -2032,7 +2032,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
try {
ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode);
return pfd != null ? pfd.detachFd() : -1;
} catch (FileNotFoundException e) {
} catch (FileNotFoundException | SecurityException e) {
e.printStackTrace();
return -1;
}
@@ -2227,4 +2227,3 @@ class SDLClipboardHandler implements
SDLActivity.onNativeClipboardChanged();
}
}
@@ -65,17 +65,15 @@ class SDLInputConnection extends BaseInputConnection
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) {
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
if (beforeLength > 0 && afterLength == 0) {
// backspace(s)
while (beforeLength-- > 0) {
nativeGenerateScancodeForUnichar('\b');
}
return true;
}
}
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
if (beforeLength > 0 && afterLength == 0) {
// backspace(s)
while (beforeLength-- > 0) {
nativeGenerateScancodeForUnichar('\b');
}
return true;
}
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
return false;
+35 -16
View File
@@ -14,9 +14,22 @@ if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then
fi
if [[ -n "$ANDROID_NDK_VER" ]]; then
TOOLCHAIN_BIN="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/bin"
if [[ -x "$TOOLCHAIN_BIN/llvm-strip" ]]; then
STRIP_TOOL="$TOOLCHAIN_BIN/llvm-strip"
case "$(uname -s)" in
Darwin) HOST_TAG="darwin-x86_64" ;;
Linux) HOST_TAG="linux-x86_64" ;;
*) HOST_TAG="" ;;
esac
PREBUILT_DIR="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt"
if [[ -n "$HOST_TAG" && -x "$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" ]]; then
STRIP_TOOL="$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip"
else
for candidate in "$PREBUILT_DIR"/*/bin/llvm-strip; do
if [[ -x "$candidate" ]]; then
STRIP_TOOL="$candidate"
break
fi
done
fi
fi
@@ -25,29 +38,35 @@ copy_lib() {
local src="$2"
local dst_dir="$APP_DIR/$abi"
local dst="$dst_dir/libmain.so"
local tmp="$dst_dir/.libmain.so.$$"
if [[ ! -f "$src" ]]; then
echo "Missing native library for $abi: $src" >&2
exit 1
fi
mkdir -p "$dst_dir"
cp -f "$src" "$dst"
cp -f "$src" "$tmp"
if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then
"$STRIP_TOOL" --strip-debug "$dst"
echo "Staged and stripped $src -> $dst"
"$STRIP_TOOL" --strip-unneeded "$tmp"
mv -f "$tmp" "$dst"
echo "Stripped and staged $src -> $dst"
else
mv -f "$tmp" "$dst"
echo "Staged $src -> $dst (strip disabled or strip tool unavailable)"
fi
}
declare -A ABI_TO_LIB=(
["arm64-v8a"]="$ROOT_DIR/build/android-arm64/libmain.so"
["x86_64"]="$ROOT_DIR/build/android-x86_64/libmain.so"
)
# Drop any previously staged ABI directories to avoid stale APK contents.
rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64"
for abi in $ANDROID_STAGE_ABIS; do
src="${ABI_TO_LIB[$abi]:-}"
if [[ -z "$src" ]]; then
echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
exit 1
fi
case "$abi" in
arm64-v8a) src="$ROOT_DIR/build/android-arm64/libmain.so" ;;
x86_64) src="$ROOT_DIR/build/android-x86_64/libmain.so" ;;
*)
echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
exit 1
;;
esac
copy_lib "$abi" "$src"
done
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 928 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 B

After

Width:  |  Height:  |  Size: 8.7 KiB

+1 -1
View File
@@ -6,4 +6,4 @@ Exec=dusk
Icon=dusk
Terminal=false
Type=Application
Categories=Graphics;3DGraphics;Game
Categories=Game;
+68 -14
View File
@@ -20,18 +20,22 @@ body {
pointer-events: none;
}
fps,
toast {
position: absolute;
border: 1dp #92875B;
background-color: rgba(21, 22, 16, 80%);
}
toast {
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;
@@ -159,33 +163,68 @@ icon {
}
icon.arrow-forward {
width: 24dp;
height: 24dp;
font-size: 24dp;
width: 1.2em;
height: 1.2em;
font-size: 1.2em;
decorator: text("&#xe5c8;" center center);
}
icon.trophy {
width: 24dp;
height: 24dp;
font-size: 24dp;
width: 1.2em;
height: 1.2em;
font-size: 1.2em;
decorator: text("&#xe71a;" center center);
}
icon.controller {
width: 24dp;
height: 24dp;
font-size: 24dp;
width: 1.2em;
height: 1.2em;
font-size: 1.2em;
decorator: text("&#xf135;" center center);
}
icon.warning {
width: 24dp;
height: 24dp;
font-size: 24dp;
width: 1.2em;
height: 1.2em;
font-size: 1.2em;
decorator: text("&#xe002;" center center);
}
fps {
display: none;
z-index: 99;
font-size: 18dp;
font-weight: bold;
padding: 9dp 12dp;
border-radius: 7dp;
pointer-events: none;
white-space: nowrap;
}
fps[open] {
display: block;
}
fps[corner=tl] {
top: 12dp;
left: 12dp;
}
fps[corner=tr] {
top: 12dp;
right: 12dp;
}
fps[corner=bl] {
bottom: 12dp;
left: 12dp;
}
fps[corner=br] {
bottom: 12dp;
right: 12dp;
}
logo {
position: absolute;
width: 100dp;
@@ -235,3 +274,18 @@ logo img.outer {
transform: rotate(360deg);
}
}
@media (max-height: 640dp) {
toast {
top: 20dp;
right: 20dp;
}
toast.controller-warning {
bottom: 20dp;
}
toast.menu-notification {
top: 20dp;
}
}
+180 -7
View File
@@ -273,20 +273,60 @@ body.mirrored version-info {
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;
color: #A6A09B;
align-items: center;
justify-content: flex-end;
gap: 8dp;
font-size: 20dp;
}
.detail,
.update span {
.update[state=checking],
.update[state=failed] {
display: block;
}
.update[state=available] {
display: flex;
}
#update-download {
display: none;
margin: 0dp;
padding: 0dp;
border-width: 0dp;
background-color: transparent;
color: #D8F999;
cursor: pointer;
text-transform: uppercase;
font-weight: bold;
decorator: horizontal-gradient(#00000000 #00000000);
}
.update[state=available] #update-download {
display: flex;
align-items: center;
gap: 2dp;
}
#update-download icon {
display: block;
width: 18dp;
height: 18dp;
font-family: "Material Symbols Rounded";
font-weight: normal;
decorator: text("&#xe5c8;" center center);
}
.detail {
color: #A6A09B;
}
body.mirrored .update {
justify-content: flex-start;
}
/* Startup animation */
.intro-item {
opacity: 0;
@@ -322,3 +362,136 @@ body.animate-in .intro-item {
.delay-5 {
transition: opacity transform 0.3s 0.6s cubic-in-out;
}
/* Mobile layout */
@media (max-height: 640dp) {
.gradient {
decorator: horizontal-gradient(#00000000 #000000FF);
}
body.mirrored .gradient {
decorator: horizontal-gradient(#000000FF #00000000);
}
menu {
left: 32dp;
right: 32dp;
width: auto;
min-width: 0;
max-width: none;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 16dp;
}
body.mirrored menu {
left: 32dp;
right: 32dp;
flex-direction: row-reverse;
}
hero {
flex: 1 1 0;
min-width: 0;
max-width: 48%;
margin-left: 32dp;
}
body.mirrored hero {
align-items: flex-end;
}
hero img {
width: 100%;
}
#menu-list {
flex: 1 1 0;
min-width: 0;
max-width: 52%;
align-items: flex-end;
}
#menu-list button {
width: 100%;
max-width: 100%;
text-align: right;
}
#menu-list button:hover,
#menu-list button:focus-visible {
decorator: horizontal-gradient(#FEE68500 #FEE685FF);
}
body.mirrored #menu-list {
align-items: flex-start;
}
body.mirrored #menu-list button {
text-align: left;
}
body.mirrored #menu-list button:hover,
body.mirrored #menu-list button:focus-visible {
decorator: horizontal-gradient(#FEE685FF #FEE68500);
}
.eyebrow {
display: none;
}
disc-info {
right: 32dp;
left: auto;
bottom: 32dp;
top: auto;
text-align: right;
font-size: 16dp;
}
#disc-status {
justify-content: flex-end;
}
#disc-status icon {
font-size: 20dp;
}
#disc-version {
font-size: 16dp;
}
version-info {
right: 32dp;
left: auto;
bottom: auto;
top: 32dp;
text-align: right;
font-size: 16dp;
}
.update {
font-size: 16dp;
}
body.mirrored disc-info {
right: auto;
left: 32dp;
bottom: 32dp;
top: auto;
text-align: left;
}
body.mirrored version-info {
right: auto;
left: 32dp;
bottom: auto;
top: 32dp;
text-align: left;
}
body.mirrored #disc-status {
justify-content: flex-start;
}
}
+27 -4
View File
@@ -17,6 +17,7 @@ body {
window {
display: flex;
flex-flow: column;
position: relative;
height: 100%;
width: 100%;
max-width: 1088dp;
@@ -287,6 +288,23 @@ icon.celebration {
decorator: text("&#xea65;" center center);
}
icon.question-mark {
decorator: text("&#xeb8b;" center center);
}
.achievement-total {
position: absolute;
top: 0;
right: 64dp;
height: 64dp;
line-height: 64dp;
font-family: "Fira Sans Condensed";
font-weight: bold;
font-size: 16dp;
color: rgba(224, 219, 200, 55%);
pointer-events: none;
}
.achievement-row {
display: flex;
align-items: flex-start;
@@ -343,7 +361,7 @@ icon.celebration {
color: rgba(224, 219, 200, 45%);
}
progressbar {
progress {
display: block;
width: 100%;
height: 6dp;
@@ -352,12 +370,12 @@ progressbar {
margin: 6dp 0 2dp 0;
}
progressbar.progress-done fill {
progress.progress-done fill {
background-color: #44aa22;
border-radius: 3dp;
}
progressbar.progress-ongoing fill {
progress.progress-ongoing fill {
background-color: #2255bb;
border-radius: 3dp;
}
@@ -447,6 +465,11 @@ window.modal.danger .modal-header icon {
font-weight: normal;
}
.modal-body span.tip {
font-size: 14dp;
color: #92875B;
}
.verification-progress {
display: flex;
flex-direction: column;
@@ -460,7 +483,7 @@ window.modal.danger .modal-header icon {
color: #FFFFFF;
}
progressbar.verification-progress-bar {
progress.verification-progress-bar {
height: 8dp;
margin: 2dp 0 0 0;
}
+26
View File
@@ -51,6 +51,7 @@
#include "d/actor/d_a_ni.h"
#include "d/d_s_play.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
#include "res/Object/Alink.h"
#include <cstring>
@@ -14787,6 +14788,10 @@ void daAlink_c::deleteEquipItem(BOOL i_isPlaySound, BOOL i_isDeleteKantera) {
mIronBallChainPos = NULL;
mIronBallChainAngle = NULL;
field_0x3848 = NULL;
#if TARGET_PC
mIBChainInterpPrevValid = false;
mIBChainInterpCurrValid = false;
#endif
field_0x0774 = NULL;
field_0x0778 = NULL;
mpHookshotLinChk = NULL;
@@ -19717,6 +19722,27 @@ int daAlink_c::draw() {
)
{
dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0);
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation &&
mEquipItem == dItemNo_IRONBALL_e &&
mIronBallChainPos != NULL && mIronBallChainAngle != NULL)
{
if (mIBChainInterpCurrValid) {
memcpy(mIBChainInterpPrevPos, mIBChainInterpCurrPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz));
memcpy(mIBChainInterpPrevAngle, mIBChainInterpCurrAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz));
mIBChainInterpPrevHandRoot = mIBChainInterpCurrHandRoot;
mIBChainInterpPrevValid = true;
}
memcpy(mIBChainInterpCurrPos, mIronBallChainPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz));
memcpy(mIBChainInterpCurrAngle, mIronBallChainAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz));
mIBChainInterpCurrHandRoot = mHookshotTopPos;
mIBChainInterpCurrValid = true;
dusk::frame_interp::add_interpolation_callback(&ironBallChainInterpCallback, this);
}
#endif
}
}
+3
View File
@@ -205,6 +205,9 @@ int daAlink_c::setDamagePoint(int i_dmgAmount, BOOL i_checkZoraMag, BOOL i_setDm
dComIfGp_setItemLifeCount(-i_dmgAmount, 0);
}
#if TARGET_PC
dusk::AchievementSystem::get().signal("player_damaged");
#endif
onResetFlg1(RFLG1_DAMAGE_IMPACT);
mSwordUpTimer = 0;
+48 -6
View File
@@ -8,6 +8,8 @@
#include "d/actor/d_a_obj_swhang.h"
#include "d/actor/d_a_obj_chandelier.h"
#include "JSystem/J3DGraphBase/J3DMaterial.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
enum {
HS_MODE_NONE_e,
@@ -17,11 +19,11 @@ enum {
HS_MODE_RETURN_e = 6,
};
void daAlink_c::hsChainShape_c::draw() {
if (dusk::getSettings().game.superClawshot) {
return;
}
#if TARGET_PC
static const int HS_CHAIN_MAX_LINKS = 600;
#endif
void daAlink_c::hsChainShape_c::draw() {
static const int dummy = 0;
daAlink_c* alink = (daAlink_c*)getUserArea();
@@ -165,7 +167,11 @@ void daAlink_c::hsChainShape_c::draw() {
}
(void)0;
while (maxDistanceF > var_f30) {
#if TARGET_PC
int chainLinks = 0;
#endif
while (maxDistanceF > var_f30 IF_DUSK(&&chainLinks < HS_CHAIN_MAX_LINKS)) {
temp_f27 = var_f28 * cM_fsin(sp34 * var_f30);
s16 spC = cM_atan2s(temp_f27 - var_f26, 5.0f);
sp64.x = sp6C.x + spC;
@@ -187,6 +193,10 @@ void daAlink_c::hsChainShape_c::draw() {
var_f26 = temp_f27;
var_f30 += fabsf(cM_scos(spC)) * 5.0f;
#if TARGET_PC
chainLinks++;
#endif
}
}
@@ -202,7 +212,11 @@ void daAlink_c::hsChainShape_c::draw() {
sp98 = subChainTopPos;
sp6C.set(maxDistance.atan2sY_XZ(), maxDistance.atan2sX_Z(), 0);
while (maxDistanceF > var_f30) {
#if TARGET_PC
int subChainLinks = 0;
#endif
while (maxDistanceF > var_f30 IF_DUSK(&&subChainLinks < HS_CHAIN_MAX_LINKS)) {
mDoMtx_stack_c::copy(j3dSys.getViewMtx());
mDoMtx_stack_c::transM(sp98);
mDoMtx_stack_c::ZXYrotM(sp6C);
@@ -215,11 +229,39 @@ void daAlink_c::hsChainShape_c::draw() {
sp98 += maxDistance * 5.0f;
ANGLE_ADD_2(sp6C.z, 0x3000);
var_f30 += 5.0f;
#if TARGET_PC
subChainLinks++;
#endif
}
}
}
}
#if TARGET_PC
static void ironBallChainInterpCallback(bool isSimFrame, void* pUserWork) {
static_cast<daAlink_c*>(pUserWork)->onIronBallChainInterpCallback();
}
void daAlink_c::onIronBallChainInterpCallback() {
if (!mIBChainInterpPrevValid || !mIBChainInterpCurrValid) {
return;
}
if (mIronBallChainPos == NULL || mIronBallChainAngle == NULL) {
return;
}
const f32 alpha = dusk::frame_interp::get_interpolation_step();
for (int i = 0; i < IRON_BALL_CHAIN_COUNT; i++) {
mIronBallChainPos[i] = mIBChainInterpPrevPos[i] + (mIBChainInterpCurrPos[i] - mIBChainInterpPrevPos[i]) * alpha;
mIronBallChainAngle[i].x = mIBChainInterpPrevAngle[i].x + (s16)((s16)(mIBChainInterpCurrAngle[i].x - mIBChainInterpPrevAngle[i].x) * alpha);
mIronBallChainAngle[i].y = mIBChainInterpPrevAngle[i].y + (s16)((s16)(mIBChainInterpCurrAngle[i].y - mIBChainInterpPrevAngle[i].y) * alpha);
mIronBallChainAngle[i].z = mIBChainInterpPrevAngle[i].z + (s16)((s16)(mIBChainInterpCurrAngle[i].z - mIBChainInterpPrevAngle[i].z) * alpha);
}
mHookshotTopPos = mIBChainInterpPrevHandRoot + (mIBChainInterpCurrHandRoot - mIBChainInterpPrevHandRoot) * alpha;
}
#endif
void daAlink_c::hookshotAtHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor,
dCcD_GObjInf* i_tgObjInf) {
if (i_tgActor != NULL && fopAcM_IsActor(i_tgActor) && !i_tgObjInf->ChkTgHookshotThrough()) {
+9 -1
View File
@@ -17,6 +17,9 @@
#include "d/actor/d_a_e_pz.h"
#include "d/actor/d_a_horse.h"
#include "d/actor/d_a_hozelda.h"
#if TARGET_PC
#include "dusk/achievements.h"
#endif
int daArrow_c::createHeap() {
J3DModelData* model_data;
@@ -92,7 +95,12 @@ void daArrow_c::atHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor, d
if (dist_to_hitpos < field_0x998) {
field_0x998 = dist_to_hitpos;
mHitAcID = fopAcM_GetID(i_tgActor);
#if TARGET_PC
if (fopAcM_GetGroup(i_tgActor) == fopAc_ENEMY_e &&
current.pos.abs(mStartPos) > 10000.0f) {
dusk::AchievementSystem::get().signal("arrow_hit_100m");
}
#endif
if (mArrowType == 1) {
field_0x9a8 = *hit_pos_p;
} else if (i_tgObjInf->ChkTgShield()) {
+6
View File
@@ -19,6 +19,9 @@
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
#if TARGET_PC
#include "dusk/achievements.h"
#endif
class daB_GND_HIO_c : public JORReflexible {
public:
@@ -1289,6 +1292,9 @@ static void b_gnd_g_wait(b_gnd_class* i_this) {
if (i_this->mMoveMode < 5 && i_this->mPlayerDistXZ < 600.0f) {
i_this->mMoveMode = 5;
i_this->field_0xc44[0] = 10;
#if TARGET_PC
dusk::AchievementSystem::get().signal("ganondorf_fishing_rod");
#endif
}
} else if (i_this->mMoveMode == 5) {
i_this->mMoveMode = 6;
+9
View File
@@ -12,6 +12,9 @@
#include "c/c_damagereaction.h"
#include "f_op/f_op_actor_enemy.h"
#include "f_op/f_op_camera_mng.h"
#if TARGET_PC
#include "dusk/achievements.h"
#endif
class daE_TH_HIO_c : public JORReflexible {
public:
@@ -542,6 +545,7 @@ static void damage_check(e_th_class* i_this) {
if (i_this->field_0x6a4 == 0 && i_this->mAction != ACTION_SPIN) {
daPy_py_c* player = (daPy_py_c*)dComIfGp_getPlayer(0);
OS_REPORT("E_th HP1 %d\n", i_this->health);
s16 prevHealth = i_this->health;
cc_at_check(i_this, &i_this->mAtInfo);
OS_REPORT("E_th HP2 %d\n", i_this->health);
@@ -554,6 +558,11 @@ static void damage_check(e_th_class* i_this) {
dComIfGs_onOneZoneSwitch(3, -1);
if (i_this->health <= 0) {
#if TARGET_PC
if (prevHealth == i_this->field_0x560) {
dusk::AchievementSystem::get().signal("dark_hammer_one_hit");
}
#endif
i_this->mAction = ACTION_END;
i_this->mMode = 0;
i_this->field_0x68a |= 4;
+2
View File
@@ -5852,6 +5852,8 @@ static int daE_WB_Create(fopAc_ac_c* actor) {
daE_WB_Execute(i_this);
c_start = 0;
// Note: this flag makes king bulblin 1 instant die when set, as it only requires 2 laps
// for insta-kill to trigger.
if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[88])) {
i_this->lap_num = 1;
}
+5 -3
View File
@@ -729,10 +729,12 @@ static void koro2_game(fshop_class* i_this) {
cLib_addCalcAngleS2(&i_this->field_0x4020.z, 0, 2, 0x200);
case 2:
#if TARGET_PC
if (dusk::getSettings().game.enableGyroRollgoal) {
if (dusk::gyro::rollgoal_gyro_enabled()) {
if (!dusk::gyro::get_sensor_keep_alive()) {
dusk::gyro::set_sensor_keep_alive(true);
}
} else if (dusk::gyro::get_sensor_keep_alive()) {
dusk::gyro::set_sensor_keep_alive(false);
}
#endif
@@ -753,7 +755,7 @@ static void koro2_game(fshop_class* i_this) {
old_stick_x = mDoCPd_c::getSubStickX(PAD_1);
cLib_addCalcAngleS2(&i_this->field_0x4060, i_this->field_0x4062, 4, 0x1000);
#if TARGET_PC
if (dusk::getSettings().game.enableGyroRollgoal) {
if (dusk::gyro::rollgoal_gyro_enabled()) {
dusk::gyro::rollgoalTick(true, i_this->field_0x4060);
}
#endif
@@ -791,7 +793,7 @@ static void koro2_game(fshop_class* i_this) {
s16 gyro_ax = 0;
s16 gyro_az = 0;
#if TARGET_PC
if (dusk::getSettings().game.enableGyroRollgoal) {
if (dusk::gyro::rollgoal_gyro_enabled()) {
dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az);
}
#endif
+9
View File
@@ -12,6 +12,10 @@
#include "SSystem/SComponent/c_counter.h"
#include <cstring>
#if TARGET_PC
#include "dusk/settings.h"
#endif
#define DRAW_TYPE_YELLOW 0
#define DRAW_TYPE_RED 1
@@ -1422,6 +1426,11 @@ int dAttention_c::Run() {
}
void dAttention_c::Draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
if (mAttParam.CheckFlag(dAttParam_c::EFlag_ARROW_OFF)) {
draw[0].field_0x173 = 3;
draw[1].field_0x173 = 3;
+114 -23
View File
@@ -31,6 +31,7 @@
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#include "dusk/logging.h"
#include "imgui.h"
#endif
namespace {
@@ -7483,6 +7484,8 @@ 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;
static constexpr s16 FLYCAM_ROLL_SPEED = 256;
static ImVec2 sFlyCamLastMousePos = {-1.f, -1.f};
#if TARGET_PC
bool dCamera_c::executeDebugFlyCam() {
@@ -7490,6 +7493,7 @@ bool dCamera_c::executeDebugFlyCam() {
if (mDebugFlyCam.initialized) {
deactivateDebugFlyCam();
}
sFlyCamLastMousePos = {-1.f, -1.f};
return false;
}
@@ -7519,16 +7523,63 @@ bool dCamera_c::executeDebugFlyCam() {
mDebugFlyCam.initialized = true;
}
event->mEventStatus = 1;
dComIfGp_getEventManager().setCameraPlay(1);
if (dusk::getSettings().game.debugFlyCamLockEvents) {
event->mEventStatus = 1;
dComIfGp_getEventManager().setCameraPlay(1);
} else {
if (event->mEventStatus != 0) {
event->mEventStatus = 0;
}
dComIfGp_getEventManager().setCameraPlay(0);
}
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 stickY = 0.f;
f32 stickX = 0.f;
f32 cStickY = 0.f;
f32 cStickX = 0.f;
f32 trigL = 0.f;
f32 trigR = 0.f;
f32 rollInput = 0.f;
bool fast = false;
if (dusk::getSettings().game.debugFlyCamLockEvents) {
interface_of_controller_pad& pad = mDoCPd_c::getCpadInfo(0);
stickY = pad.mMainStickPosY * 72.0f;
stickX = pad.mMainStickPosX * 72.0f;
cStickY = pad.mCStickPosY * 59.0f;
cStickX = pad.mCStickPosX * 59.0f;
trigL = pad.mTriggerLeft * 150.0f;
trigR = pad.mTriggerRight * 150.0f;
fast = mDoCPd_c::getHoldZ(PAD_1);
if (mDoCPd_c::getHoldY(PAD_1)) rollInput -= 1.f;
if (mDoCPd_c::getHoldX(PAD_1)) rollInput += 1.f;
}
{
ImGuiIO& io = ImGui::GetIO();
if (!io.WantCaptureKeyboard) {
f32 kbX = 0.0f, kbY = 0.0f;
if (ImGui::IsKeyDown(ImGuiKey_W) || ImGui::IsKeyDown(ImGuiKey_UpArrow)) kbY += 1.f;
if (ImGui::IsKeyDown(ImGuiKey_S) || ImGui::IsKeyDown(ImGuiKey_DownArrow)) kbY -= 1.f;
if (ImGui::IsKeyDown(ImGuiKey_D) || ImGui::IsKeyDown(ImGuiKey_RightArrow)) kbX += 1.f;
if (ImGui::IsKeyDown(ImGuiKey_A) || ImGui::IsKeyDown(ImGuiKey_LeftArrow)) kbX -= 1.f;
f32 len = sqrtf(kbX * kbX + kbY * kbY);
if (len > 1.f) { kbX /= len; kbY /= len; }
stickX += kbX * 72.0f;
stickY += kbY * 72.0f;
if (ImGui::IsKeyDown(ImGuiKey_Space)) trigR += 150.0f;
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) trigL += 150.0f;
if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) fast = true;
if (ImGui::IsKeyDown(ImGuiKey_Q)) rollInput -= 1.0f;
if (ImGui::IsKeyDown(ImGuiKey_E)) rollInput += 1.0f;
}
bool mouseValid = !io.WantCaptureMouse && io.MousePos.x >= 0.0f && io.MousePos.y >= 0.0f;
if (mouseValid && sFlyCamLastMousePos.x >= 0.0f) {
cStickX -= (io.MousePos.x - sFlyCamLastMousePos.x) * 2.0f;
cStickY -= (io.MousePos.y - sFlyCamLastMousePos.y) * 2.0f;
}
sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f};
}
f32 verticalDisp = 0.0f;
if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
@@ -7542,7 +7593,7 @@ bool dCamera_c::executeDebugFlyCam() {
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;
f32 speed = fast ? FLYCAM_FAST_SPEED : FLYCAM_SPEED;
mEye.x += speed * moveDx;
mEye.y += speed * moveDy;
@@ -7553,6 +7604,7 @@ bool dCamera_c::executeDebugFlyCam() {
mCenter.z = mEye.z + sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
mCenter.y = mEye.y + sinf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
mBank = mBank + static_cast<s16>(rollInput * FLYCAM_ROLL_SPEED * (fast ? FLYCAM_FAST_SPEED / FLYCAM_SPEED : 1.f));
Reset(mCenter, mEye);
f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? cStickX : -cStickX;
@@ -7570,7 +7622,7 @@ void dCamera_c::deactivateDebugFlyCam() {
Reset(mDebugFlyCam.savedCenter, mDebugFlyCam.savedEye, mDebugFlyCam.savedFovy, mDebugFlyCam.savedBank.Val());
dEvt_control_c* event = dComIfGp_getEvent();
if (event != nullptr) {
if (event != nullptr && event->mEventStatus != 0) {
event->mEventStatus = 0;
}
dComIfGp_getEventManager().setCameraPlay(0);
@@ -11210,6 +11262,26 @@ static int camera_execute(camera_process_class* i_this) {
return 1;
}
#ifdef TARGET_PC
void set_ar_corrected_trim(dDlst_window_c* window, float trim_height) {
const auto viewport = window->getViewPort();
if (mDoGph_gInf_c::isWideZoom()) {
const auto target_ar = FB_WIDTH / (FB_HEIGHT - trim_height * 2.0f);
const auto current_ar = mDoGph_gInf_c::m_safeWidthF / mDoGph_gInf_c::m_safeHeightF;
if (current_ar < target_ar) {
trim_height = FB_HEIGHT / 2.0f * (1.0f - current_ar / target_ar);
} else {
trim_height = 0.0f;
}
}
trim_height *= viewport->height / FB_HEIGHT;
window->setScissor(0.0f, trim_height, viewport->width, viewport->height - trim_height * 2.0f);
}
#endif
static int camera_draw(camera_process_class* i_this) {
camera_class* a_this = (camera_class*)i_this;
dCamera_c* body = &i_this->mCamera;
@@ -11263,21 +11335,40 @@ static int camera_draw(camera_process_class* i_this) {
#endif
#if TARGET_PC
auto trim_height = body->TrimHeight();
set_ar_corrected_trim(window, body->TrimHeight());
if (mDoGph_gInf_c::isWideZoom()) {
const auto target_ar = FB_WIDTH / (FB_HEIGHT - trim_height * 2.0f);
const auto current_ar = mDoGph_gInf_c::m_safeWidthF / mDoGph_gInf_c::m_safeHeightF;
if (dusk::getSettings().game.enableFrameInterpolation) {
dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) {
const auto i_this = static_cast<camera_process_class*>(pUserWork);
const auto camera = &i_this->mCamera;
if (current_ar < target_ar) {
trim_height = FB_HEIGHT / 2.0f * (1.0f - current_ar / target_ar);
} else {
trim_height = 0.0f;
}
const auto trim_size = camera->mTrimSize;
if (camera->mCurState != 2 && trim_size >= 0 && trim_size <= 3) {
// derive trim height at previous tick using current camera state
f32 target;
switch (trim_size) {
case 0:
target = 0.0f;
break;
case 1:
target = camera->mCamSetup.VistaTrimHeight();
break;
case 2:
case 3:
target = camera->mCamSetup.CinemaScopeTrimHeight();
break;
}
const auto step = dusk::frame_interp::get_interpolation_step();
const auto cur = camera->TrimHeight();
const auto prev = (4.0f * cur - target) / 3.0f;
const auto trim_height = prev + (cur - prev) * step;
set_ar_corrected_trim(get_window((camera_class*)i_this), trim_height);
}
}, i_this);
}
trim_height *= viewport->height / FB_HEIGHT;
window->setScissor(0.0f, trim_height, viewport->width, viewport->height - trim_height * 2.0f);
#else
int trim_height = body->TrimHeight();
+8
View File
@@ -13,6 +13,9 @@
#include "d/d_s_play.h"
#include "d/d_com_inf_game.h"
#include "f_op/f_op_actor_mng.h"
#if TARGET_PC
#include "dusk/achievements.h"
#endif
static int plCutLRC[58] = {
0, //
@@ -434,6 +437,11 @@ fopAc_ac_c* cc_at_check(fopAc_ac_c* i_enemy, dCcU_AtInfo* i_AtInfo) {
if (i_AtInfo->mAttackPower != 0 && i_enemy->health <= 0) {
i_AtInfo->mHitStatus = 2;
i_enemy->health = 0;
#if TARGET_PC
if (fopAcM_GetGroup(i_enemy) == fopAc_ENEMY_e) {
dusk::AchievementSystem::get().signal("enemy_killed");
}
#endif
}
int uvar8;
+2 -1
View File
@@ -965,7 +965,8 @@ void dMenu_Letter_c::screenSetBase() {
}
if (field_0x374 > 1) {
J2DPane* pJVar6 = mpBaseScreen->search('pi_n');
f32 dVar18 = field_0x1f0[1]->getBounds().i.x - field_0x1f0[0]->getBounds().i.x;
f32 x1 = field_0x1f0[1]->getBounds().i.x;
f32 dVar18 = x1 - field_0x1f0[0]->getBounds().i.x;
f32 dVar17 = dVar18 * (field_0x374 - 1);
f32 dVar16 = (pJVar6->getWidth() / 2) - (dVar17 / 2);
for (int i = 0; i < 9; i++) {
+6 -3
View File
@@ -24,9 +24,10 @@
#include "d/actor/d_a_horse.h"
#include <cstring>
#if TARGET_PC
#include "dusk/memory.h"
#include "dusk/memory.h"
#include "dusk/settings.h"
#endif
int dMeter2_c::_create() {
stage_stag_info_class* stag_info = dComIfGp_getStageStagInfo();
@@ -317,7 +318,9 @@ int dMeter2_c::_execute() {
int dMeter2_c::_draw() {
#if TARGET_PC
if (dusk::getSettings().game.disableMainHUD) {
if (dusk::getSettings().game.recordingMode || dusk::getSettings().game.minimalHUD ||
dusk::getSettings().game.debugFlyCam)
{
return 1;
}
#endif
+9
View File
@@ -6,6 +6,10 @@
#include "d/d_com_inf_game.h"
#include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnArrow_c::dMsgScrnArrow_c() {
mpScreen = JKR_NEW J2DScreen();
JUT_ASSERT(0, mpScreen != NULL);
@@ -65,6 +69,11 @@ dMsgScrnArrow_c::~dMsgScrnArrow_c() {
}
void dMsgScrnArrow_c::draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort();
mpScreen->draw(0.0f, 0.0f, graf_ctx);
}
+24
View File
@@ -8,6 +8,10 @@
#include "d/d_pane_class.h"
#include <cstring>
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnBase_c::dMsgScrnBase_c() {
init();
}
@@ -57,12 +61,22 @@ void dMsgScrnBase_c::init() {
}
void dMsgScrnBase_c::multiDraw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
if (field_0x48 != NULL) {
dComIfGd_set2DOpa(field_0x48);
}
}
void dMsgScrnBase_c::draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort();
ctx->setup2D();
@@ -72,10 +86,20 @@ void dMsgScrnBase_c::draw() {
}
void dMsgScrnBase_c::drawSelf() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
drawOutFont(0.0f, 0.0f, 1.0f);
}
void dMsgScrnBase_c::drawOutFont(f32 param_0, f32 param_1, f32 param_2) {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
mpOutFont->draw(NULL, param_0, param_1, param_2);
}
+9
View File
@@ -6,6 +6,10 @@
#include "d/d_msg_object.h"
#include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnBoss_c::dMsgScrnBoss_c() {
static u64 t_tag[7] = {
MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'),
@@ -91,6 +95,11 @@ void dMsgScrnBoss_c::exec() {
}
void dMsgScrnBoss_c::drawSelf() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort();
ctx->setup2D();
drawOutFont(0.0f, 0.0f, 1.0f);
+9
View File
@@ -13,6 +13,10 @@
#include "d/d_msg_out_font.h"
#include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnKanban_c::dMsgScrnKanban_c(JKRExpHeap* param_0) {
if (param_0 != NULL) {
field_0xd4 = param_0;
@@ -176,6 +180,11 @@ void dMsgScrnKanban_c::exec() {
}
void dMsgScrnKanban_c::draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
grafContext->setup2D();
mpScreen->draw(0.0f, 0.0f, grafContext);
+9
View File
@@ -12,6 +12,10 @@
#include "d/d_msg_object.h"
#include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnPlace_c::dMsgScrnPlace_c() {
static u64 t_tag[7] = {
MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'),
@@ -127,6 +131,11 @@ void dMsgScrnPlace_c::exec() {
}
void dMsgScrnPlace_c::drawSelf() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
grafContext->setup2D();
drawOutFont(0.0f, 0.0f, 1.0f);
+9
View File
@@ -20,6 +20,10 @@
#include "JSystem/J2DGraph/J2DScreen.h"
#include <cstring>
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnTalk_c::dMsgScrnTalk_c(u8 param_1, u8 param_2, JKRExpHeap* param_3) {
if (param_3 != NULL) {
field_0xe4 = param_3;
@@ -303,6 +307,11 @@ void dMsgScrnTalk_c::exec() {
}
void dMsgScrnTalk_c::drawSelf() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* grafContext[1];
grafContext[0] = dComIfGp_getCurrentGrafPort();
grafContext[0]->setup2D();
+9
View File
@@ -9,6 +9,10 @@
#include "d/d_msg_out_font.h"
#include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnTree_c::dMsgScrnTree_c(JUTFont* param_0, JKRExpHeap* param_1) {
if (param_1 != NULL) {
field_0xd8 = param_1;
@@ -187,6 +191,11 @@ void dMsgScrnTree_c::exec() {
}
void dMsgScrnTree_c::draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
grafContext->setup2D();
mpScreen->draw(0.0f, 0.0f, grafContext);
+31 -8
View File
@@ -21,16 +21,39 @@ static bool checkEnabled() {
return !__OSReport_disable || dusk::OSReportReallyForceEnable;
}
#ifndef va_copy
#define va_copy(d, s) ((d) = (s))
#endif
static std::string FormatToString(const char* msg, va_list list) {
int ret = vsnprintf(nullptr, 0, msg, list);
if (ret <= 0) {
return {};
size_t size = (strlen(msg) * 2) + 50;
std::string str;
va_list ap;
int attempts = 0;
while (true) {
str.resize(size);
va_copy(ap, list);
int n = vsnprintf(str.data(), size, msg, ap);
va_end(ap);
if (n > -1 && n < size) {
str.resize(n);
break;
}
++attempts;
if (attempts >= 3) {
if (n == -1) {
str.clear();
}
break;
}
if (n > -1) {
size = n + 1;
} else {
size *= 2;
}
}
++ret;
std::unique_ptr<char[]> buf(new char[ret]);
vsnprintf(buf.get(), ret, msg, list);
buf[ret - 1] = '\0';
return {buf.get()};
return str;
}
void OSReport(const char* fmt, ...) {
+695 -136
View File
@@ -1,13 +1,21 @@
#include "dusk/achievements.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 "d/d_meter2_info.h"
#include "dusk/io.hpp"
#include "dusk/main.h"
#include "d/d_com_inf_game.h"
#include "d/d_item_data.h"
#include "d/d_map_path_fmap.h"
#include "d/d_stage.h"
#include "d/d_menu_fmap.h"
#include "JSystem/JKernel/JKRArchive.h"
#include "d/d_meter2_info.h"
#include "d/actor/d_a_alink.h"
#include "d/actor/d_a_ni.h"
#include "d/actor/d_a_npc4.h"
#include "d/actor/d_a_b_ob.h"
#include "d/actor/d_a_player.h"
#include "d/d_demo.h"
#include "dusk/ui/ui.hpp"
#include "f_pc/f_pc_name.h"
#include "f_op/f_op_actor_mng.h"
#include "f_pc/f_pc_name.h"
@@ -18,6 +26,14 @@ namespace dusk {
using json = nlohmann::json;
static void* s_cucco_play_search(void* i_actor, void*) {
if (!fopAcM_IsActor(i_actor) || fopAcM_GetName((fopAc_ac_c*)i_actor) != fpcNm_NI_e) {
return nullptr;
}
auto* ni = static_cast<ni_class*>(i_actor);
return ni->mAction == ACTION_PLAY_e ? i_actor : nullptr;
}
static void checkGoatHerding(Achievement& a, int32_t threshMs) {
if (dMeter2Info_getMaxCount() != 20 || dMeter2Info_getNowCount() != 20) {
return;
@@ -32,12 +48,13 @@ static constexpr auto ACHIEVEMENTS_FILENAME = "achievements.json";
std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
return {
// Challenge
{
{
"hero_of_twilight",
"Hero of Twilight",
"Deliver the finishing blow to Ganondorf.",
AchievementCategory::Story,
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
@@ -48,6 +65,445 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
},
{}
},
{
{
"completionist",
"Completionist",
"Complete the game after collecting all equipment, heart containers, portals, bugs, poes, and hidden skills.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
if (dComIfGs_getMaxLife() < 100) {
return;
}
for (int i = 0; i < 4; ++i) {
if (!dComIfGs_isCollectMirror(i)) {
return;
}
}
for (int i = 0; i < 3; ++i) {
if (!dComIfGs_isCollectCrystal(i)) {
return;
}
}
static const u16 skillBits[] = {
dSv_event_flag_c::F_0338, dSv_event_flag_c::F_0339,
dSv_event_flag_c::F_0340, dSv_event_flag_c::F_0341,
dSv_event_flag_c::F_0342, dSv_event_flag_c::F_0343,
dSv_event_flag_c::F_0344
};
for (u16 bit : skillBits) {
if (!dComIfGs_isEventBit(bit)) {
return;
}
}
if (dComIfGs_checkGetInsectNum() < 24) {
return;
}
if (dComIfGs_getPohSpiritNum() < 60) {
return;
}
if (dComIfGs_getWalletSize() < 2) {
return;
}
if (dComIfGs_getArrowMax() < 100) {
return;
}
if (!dComIfGs_isCollectSword(COLLECT_MASTER_SWORD)) {
return;
}
if (!dComIfGs_isCollectShield(COLLECT_HYLIAN_SHIELD)) {
return;
}
if (!dComIfGs_isCollectClothes(KOKIRI_CLOTHES_FLAG)) {
return;
}
if (!dComIfGs_isItemFirstBit(dItemNo_WEAR_ZORA_e)) {
return;
}
if (!dComIfGs_isItemFirstBit(dItemNo_ARMOR_e)) {
return;
}
static const struct { int stage; int sw; } warpPortals[] = {
{ dStage_SaveTbl_ORDON, 52 }, // Ordon Spring
{ dStage_SaveTbl_FARON, 71 }, // South Faron Woods
{ dStage_SaveTbl_FARON, 2 }, // North Faron Woods
{ dStage_SaveTbl_GROVE, 100 }, // Sacred Grove
{ dStage_SaveTbl_FIELD, 21 }, // Gorge
{ dStage_SaveTbl_ELDIN, 31 }, // Kakariko Village
{ dStage_SaveTbl_ELDIN, 21 }, // Death Mountain
{ dStage_SaveTbl_FIELD, 99 }, // Bridge of Eldin
{ dStage_SaveTbl_FIELD, 3 }, // Castle Town
{ dStage_SaveTbl_LANAYRU, 10 }, // Lake Hylia
{ dStage_SaveTbl_LANAYRU, 2 }, // Zora's Domain
{ dStage_SaveTbl_LANAYRU, 21 }, // Upper Zora's River
{ dStage_SaveTbl_SNOWPEAK, 21 }, // Snowpeak
{ dStage_SaveTbl_DESERT, 21 }, // Gerudo Mesa
{ dStage_SaveTbl_DESERT, 40 }, // Mirror Chamber
};
for (const auto& p : warpPortals) {
if (!g_dComIfG_gameInfo.info.getSavedata().getSave(p.stage).getBit().isSwitch(p.sw)) {
return;
}
}
if (dComIfGs_getCollectSmell() == dItemNo_NONE_e) {
return;
}
if (dMeter2Info_getRecieveLetterNum() == 0) {
return;
}
bool hasJournal = false;
for (int fi = 0; fi < 6; ++fi) {
if (dComIfGs_getFishNum(fi) != 0) {
hasJournal = true;
break;
}
}
if (!hasJournal) {
return;
}
int bottleCount = 0;
for (int i = 0; i < dSv_player_item_c::BOTTLE_MAX; ++i) {
if (dComIfGs_getItem(SLOT_11 + i, false) != dItemNo_NONE_e) {
bottleCount++;
}
}
if (bottleCount < 4) {
return;
}
int bombBagCount = 0;
for (int i = 0; i < dSv_player_item_c::BOMB_BAG_MAX; ++i) {
if (dComIfGs_getItem(SLOT_15 + i, false) != dItemNo_NONE_e) {
bombBagCount++;
}
}
if (bombBagCount < 3) {
return;
}
bool hasJewelRod = false;
for (int slot = 0; slot < 24 && !hasJewelRod; ++slot) {
const u8 item = dComIfGs_getItem(slot, false);
if (item == dItemNo_JEWEL_ROD_e || item == dItemNo_JEWEL_BEE_ROD_e || item == dItemNo_JEWEL_WORM_ROD_e) {
hasJewelRod = true;
}
}
if (!hasJewelRod) {
return;
}
static const u8 requiredWheelItems[] = {
dItemNo_BOOMERANG_e,
dItemNo_BOW_e,
dItemNo_W_HOOKSHOT_e,
dItemNo_SPINNER_e,
dItemNo_IRONBALL_e,
dItemNo_COPY_ROD_e,
dItemNo_HVY_BOOTS_e,
dItemNo_KANTERA_e,
dItemNo_PACHINKO_e,
dItemNo_HAWK_EYE_e,
dItemNo_ANCIENT_DOCUMENT_e,
dItemNo_HORSE_FLUTE_e,
};
for (u8 required : requiredWheelItems) {
bool found = false;
for (int slot = 0; slot < 24; ++slot) {
if (dComIfGs_getItem(slot, false) == required) {
found = true;
break;
}
}
if (!found) {
return;
}
}
a.progress = 1;
},
{}
},
// Collection
{
{
"princess_of_bugs",
"The Princess of Bugs",
"Deliver all 24 golden bugs to Agitha.",
AchievementCategory::Collection,
true, 24, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_checkGetInsectNum();
},
{}
},
{
{
"all_poes",
"Poe Collector",
"Collect all 60 Poe Souls.",
AchievementCategory::Collection,
true, 60, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_getPohSpiritNum();
},
{}
},
{
{
"hylian_loach",
"Legendary Catch",
"Catch a Hylian Loach.",
AchievementCategory::Collection,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (dComIfGs_getFishNum(1) > 0) {
a.progress = 1;
}
},
{}
},
{
{
"all_fish",
"Gone Fishin'",
"Catch all 6 species of fish.",
AchievementCategory::Collection,
true, 6, 0, false
},
[](Achievement& a, json&) {
int nUniqueFish = 0;
for (int i = 0; i < 6; ++i) {
if (dComIfGs_getFishNum(i) != 0) {
nUniqueFish++;
}
}
a.progress = nUniqueFish;
},
{}
},
{
{
"a_big_heart",
"A Big Heart",
"Reach maximum health with all 20 heart containers.",
AchievementCategory::Collection,
true, 20, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_getMaxLife() / 5;
},
{}
},
{
{
"all_bottles",
"Glassware Guardian",
"Obtain all 4 bottles.",
AchievementCategory::Collection,
true, 4, 0, false
},
[](Achievement& a, json&) {
if (daPy_getPlayerActorClass() == nullptr) {
return;
}
int count = 0;
for (int i = 0; i < dSv_player_item_c::BOTTLE_MAX; ++i) {
if (dComIfGs_getItem(SLOT_11 + i, false) != dItemNo_NONE_e) {
count++;
}
}
a.progress = count;
},
{}
},
{
{
"all_hidden_skills",
"Master of Secrets",
"Learn all 7 Hidden Skills.",
AchievementCategory::Collection,
true, 7, 0, false
},
[](Achievement& a, json&) {
static const u16 skillBits[] = {
dSv_event_flag_c::F_0338, dSv_event_flag_c::F_0339,
dSv_event_flag_c::F_0340, dSv_event_flag_c::F_0341,
dSv_event_flag_c::F_0342, dSv_event_flag_c::F_0343,
dSv_event_flag_c::F_0344
};
int count = 0;
for (u16 bit : skillBits) {
if (dComIfGs_isEventBit(bit)) {
count++;
}
}
a.progress = count;
},
{}
},
{
{
"all_letters",
"We Deliver!",
"Collect all 16 postman letters.",
AchievementCategory::Collection,
true, 16, 0, false
},
[](Achievement& a, json&) {
a.progress = dMeter2Info_getRecieveLetterNum();
},
{}
},
{
{
"cave_of_ordeals",
"Conqueror of Ordeals",
"Clear all 50 floors of the Cave of Ordeals.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daNpcF_chkEvtBit(0x1F9)) {
a.progress = 1;
}
},
{}
},
{
{
"cave_of_ordeals_heartless",
"Indomitable",
"Clear all 50 floors of the Cave of Ordeals with only 3 heart containers.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) {
a.progress = 1;
}
},
{}
},
{
{
"speedrun_12h",
"Been There Done That",
"Defeat Ganondorf with a total save file play time under 12 hours.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
const int64_t ticks = (static_cast<int64_t>(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime();
if (ticks / OS_TIMER_CLOCK < 12 * 3600) {
a.progress = 1;
}
},
{}
},
{
{
"speedrun_8h",
"Swift Blade",
"Defeat Ganondorf with a total save file play time under 6 hours.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
const int64_t ticks = (static_cast<int64_t>(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime();
if (ticks / OS_TIMER_CLOCK < 8 * 3600) {
a.progress = 1;
}
},
{}
},
{
{
"dark_hammer_one_hit",
"Mortal Edge",
"Defeat Dark Hammer in a single hit.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (AchievementSystem::get().hasSignal("dark_hammer_one_hit")) {
a.progress = 1;
}
},
{}
},
{
{
"no_deaths_clear",
"Deathless",
"Defeat Ganondorf with 0 deaths on your save file.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
if (dComIfGs_getDeathCount() == 0) {
a.progress = 1;
}
},
{}
},
{
{
"untouchable",
"Untouchable",
"Kill 25 enemies in a row without taking damage.",
AchievementCategory::Challenge,
true, 25, 0, false
},
[](Achievement& a, json&) {
auto& sys = AchievementSystem::get();
if (sys.hasSignal("player_damaged")) {
a.progress = 0;
}
if (sys.hasSignal("enemy_killed")) {
a.progress++;
}
},
{}
},
{
{
"bow_100m_hit",
"Long Shot",
"Hit an enemy from over 100 meters away with the bow.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (AchievementSystem::get().hasSignal("arrow_hit_100m")) {
a.progress = 1;
}
},
{}
},
// Minigame
{
{
"plumm_max",
@@ -134,14 +590,16 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
},
{
{
"cave_of_ordeals",
"Conqueror of Ordeals",
"Clear all 50 floors of the Cave of Ordeals.",
AchievementCategory::Challenge,
"snowboard_70s",
"Downhill Dash",
"Finish the snowboarding minigame in under 70 seconds.",
AchievementCategory::Minigame,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daNpcF_chkEvtBit(0x1F9)) {
const int32_t bestMs = dComIfGs_getRaceGameTime();
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0481) &&
bestMs > 0 && bestMs <= 70000) {
a.progress = 1;
}
},
@@ -149,54 +607,37 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
},
{
{
"cave_of_ordeals_heartless",
"Indomitable",
"Clear all 50 floors of the Cave of Ordeals with only 3 heart containers.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) {
a.progress = 1;
}
},
{}
},
{
{
"speedrun_12h",
"Been There Done That",
"Defeat Ganondorf with a total save file play time under 12 hours.",
AchievementCategory::Challenge,
"canoe_perfect",
"River Raider",
"Achieve a perfect score in the canoe minigame.",
AchievementCategory::Minigame,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
if (link == nullptr) {
return;
}
const int64_t ticks = (static_cast<int64_t>(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime();
if (ticks / OS_TIMER_CLOCK < 12 * 3600) {
static bool wasInCanoe = false;
bool inCanoe = link->mProcID >= daAlink_c::PROC_CANOE_RIDE &&
link->mProcID <= daAlink_c::PROC_CANOE_KANDELAAR_POUR;
if (wasInCanoe && !inCanoe && dMeter2Info_getNowCount() >= 30) {
a.progress = 1;
}
wasInCanoe = inCanoe;
},
{}
},
{
{
"speedrun_8h",
"Swift Blade",
"Defeat Ganondorf with a total save file play time under 6 hours.",
AchievementCategory::Challenge,
"star_2_under_40s",
"Rising Star",
"Complete the STAR Prize 2 minigame in under 40 seconds.",
AchievementCategory::Minigame,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
const int64_t ticks = (static_cast<int64_t>(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime();
if (ticks / OS_TIMER_CLOCK < 8 * 3600) {
if(dComIfGs_getHookGameTime() > 0 && dComIfGs_getHookGameTime() <= 40000) {
a.progress = 1;
}
},
@@ -204,77 +645,20 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
},
{
{
"princess_of_bugs",
"The Princess of Bugs",
"Deliver all 24 golden bugs to Agitha.",
AchievementCategory::Collection,
true, 24, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_checkGetInsectNum();
},
{}
},
{
{
"all_poes",
"Poe Collector",
"Collect all 60 Poe Souls.",
AchievementCategory::Collection,
true, 60, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_getPohSpiritNum();
},
{}
},
{
{
"hylian_loach",
"Legendary Catch",
"Catch a Hylian Loach.",
AchievementCategory::Collection,
"star_2_under_30s",
"Shooting Star",
"Complete the STAR Prize 2 minigame in under 30 seconds.",
AchievementCategory::Minigame,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (dComIfGs_getFishNum(1) > 0) {
if(dComIfGs_getHookGameTime() > 0 && dComIfGs_getHookGameTime() <= 30000) {
a.progress = 1;
}
},
{}
},
{
{
"all_fish",
"Gone Fishin'",
"Catch all 6 species of fish.",
AchievementCategory::Collection,
true, 6, 0, false
},
[](Achievement& a, json&) {
int nUniqueFish = 0;
for (int i = 0; i < 6; ++i) {
if (dComIfGs_getFishNum(i) != 0) {
nUniqueFish++;
}
}
a.progress = nUniqueFish;
},
{}
},
{
{
"a_big_heart",
"A Big Heart",
"Reach maximum health with all 20 heart containers.",
AchievementCategory::Collection,
true, 20, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_getMaxLife() / 5;
},
{}
},
// Misc
{
{
"friendly_fire",
@@ -327,6 +711,87 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
},
{}
},
{
{
"email_me",
"Email Me",
"Read a letter during the Dark Beast Ganon fight.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e);
if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) {
a.progress = 1;
}
},
{}
},
{
{
"heavy_hitter",
"Heavy Hitter",
"Wear the Iron Boots during the end credits.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
if (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) {
a.progress = 1;
}
},
{}
},
{
{
"fishing_rod_ganondorf",
"Here Fishy Fishy",
"Confuse Ganondorf with the fishing rod.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (AchievementSystem::get().hasSignal("ganondorf_fishing_rod")) {
a.progress = 1;
}
},
{}
},
{
{
"steal_from_trill",
"Petty Theft",
"Steal from Trill.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0758)) {
a.progress = 1;
}
},
{}
},
{
{
"cucco_control",
"Cucco Whisperer",
"Take control of a cucco.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (fopAcM_Search(s_cucco_play_search, nullptr) != nullptr) {
a.progress = 1;
}
},
{}
},
// Glitched
{
{
"back_in_time",
@@ -377,19 +842,6 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
},
{}
},
{
{
"ultimate_delivery",
"The Ultimate Delivery",
"Have all 16 postman letters at the same time.",
AchievementCategory::Glitched,
true, 16, 0, false
},
[](Achievement& a, json&) {
a.progress = dMeter2Info_getRecieveLetterNum();
},
{}
},
{
{
"speedrun_4h",
@@ -412,15 +864,85 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
},
{
{
"email_me",
"Email Me",
"Read a letter during the Dark Beast Ganon fight.",
AchievementCategory::Misc,
"no_fish_suit",
"No Fish Suit No Problem",
"Defeat Morpheel without equipping Zora Armor.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e);
if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) {
static bool prevMorpheelAlive = false;
static bool inArena = false;
static bool zoraWorn = false;
const auto* morpheel = static_cast<const b_ob_class*>(fopAcM_SearchByName(fpcNm_B_OB_e));
const bool morpheelAlive = morpheel != nullptr && morpheel->mAnmID != 0x14;
const bool morpheelDead = morpheel != nullptr && morpheel->mAnmID == 0x14;
const bool lakebedCleared = dComIfGs_isEventBit(dSv_event_flag_c::M_045);
if (inArena && morpheel == nullptr) {
zoraWorn = false;
}
if (morpheelAlive && !lakebedCleared) {
inArena = true;
if (daPy_py_c::checkZoraWearFlg()) {
zoraWorn = true;
}
}
if (prevMorpheelAlive && morpheelDead && inArena && !zoraWorn) {
a.progress = 1;
}
prevMorpheelAlive = morpheelAlive;
},
{}
},
{
{
"null_item",
"Null Item",
"Obtain the mysterious black rupee in the item wheel.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daPy_getPlayerActorClass() == nullptr) {
return;
}
for (int i = 0; i < 24; ++i) {
if (dComIfGs_getItem(i, false) == 0x00) {
a.progress = 1;
break;
}
}
},
{}
},
{
{
"stallord_skip",
"Stallord Skip",
"Leave Stallord's arena through the exit without defeating Stallord.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
static bool seenStallord = false;
if (strcmp(dComIfGp_getStartStageName(), "D_MN10A") != 0) {
seenStallord = false;
return;
}
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0265)) {
seenStallord = false;
return;
}
if (fopAcM_SearchByName(fpcNm_B_DS_e) != nullptr) {
seenStallord = true;
}
if (seenStallord &&
dComIfGp_isEnableNextStage() &&
strcmp(dComIfGp_getNextStageName(), "F_SP125") == 0) {
a.progress = 1;
}
},
@@ -428,22 +950,59 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
},
{
{
"heavy-hitter",
"Heavy Hitter",
"Wear the Iron Boots during the end credits.",
AchievementCategory::Misc,
"lakebed_before_lanayru",
"White Midna Glitch",
"Clear the Lakebed Temple before clearing Lanayru's Twilight.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
if (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) {
if (dComIfGs_isEventBit(dSv_event_flag_c::M_045) &&
!dComIfGs_isDarkClearLV(2)) {
a.progress = 1;
}
},
{}
},
{
{
"early_hidden_village",
"Quick Detour",
"Rescue the Hidden Village before clearing Goron Mines.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0278) &&
!dComIfGs_isEventBit(dSv_event_flag_c::M_031)) {
a.progress = 1;
}
},
{}
},
{
{
"forest_temple_no_boomerang",
"Must Have Been The Wind",
"Complete the Forest Temple without obtaining the Gale Boomerang.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (!dComIfGs_isEventBit(dSv_event_flag_c::M_022)) {
return;
}
if (daPy_getPlayerActorClass() == nullptr) {
return;
}
for (int i = 0; i < 24; ++i) {
if (dComIfGs_getItem(i, false) == dItemNo_BOOMERANG_e) {
return;
}
}
a.progress = 1;
},
{}
}
};
}
@@ -554,7 +1113,7 @@ void AchievementSystem::processEntry(Entry& e) {
if (nowUnlocked) {
e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1;
e.achievement.unlocked = true;
if (getSettings().game.enableAchievementNotifications) {
if (getSettings().game.enableAchievementToasts) {
ui::push_toast({
.type = "achievement",
.title = "Achievement Unlocked!",
+1
View File
@@ -156,6 +156,7 @@ namespace dusk::config {
template class ConfigImpl<dusk::BloomMode>;
template class ConfigImpl<dusk::DiscVerificationState>;
template class ConfigImpl<dusk::GameLanguage>;
template class ConfigImpl<dusk::GyroMode>;
}
void dusk::config::Register(ConfigVarBase& configVar) {
+105
View File
@@ -1,9 +1,16 @@
#include "file_select.hpp"
#include <memory>
#include <string_view>
#include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_stdinc.h>
#if defined(__ANDROID__) || defined(ANDROID)
#include <SDL3/SDL_system.h>
#include <jni.h>
#endif
#if defined(__APPLE__)
#include <TargetConditionals.h>
@@ -19,6 +26,92 @@
namespace dusk {
namespace {
std::string fallback_display_name(std::string_view path) {
if (path.empty()) {
return {};
}
std::string pathString(path);
const std::size_t slash = pathString.find_last_of("/\\");
if (slash == std::string::npos || slash + 1 >= pathString.size()) {
return pathString;
}
return pathString.substr(slash + 1);
}
#if defined(__ANDROID__) || defined(ANDROID)
bool clear_pending_exception(JNIEnv* env) {
if (env == nullptr || !env->ExceptionCheck()) {
return false;
}
env->ExceptionClear();
return true;
}
std::string to_string(JNIEnv* env, jstring value) {
if (env == nullptr || value == nullptr) {
return {};
}
const char* utf8 = env->GetStringUTFChars(value, nullptr);
if (utf8 == nullptr) {
clear_pending_exception(env);
return {};
}
std::string result(utf8);
env->ReleaseStringUTFChars(value, utf8);
return result;
}
std::string android_display_name(std::string_view path) {
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
if (env == nullptr) {
return {};
}
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (activity == nullptr || clear_pending_exception(env)) {
if (activity != nullptr) {
env->DeleteLocalRef(activity);
}
return {};
}
jclass activityClass = env->GetObjectClass(activity);
if (activityClass == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return {};
}
jmethodID getDisplayName = env->GetMethodID(
activityClass, "getDisplayNameForUri", "(Ljava/lang/String;)Ljava/lang/String;");
env->DeleteLocalRef(activityClass);
if (getDisplayName == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return {};
}
jstring uri = env->NewStringUTF(std::string(path).c_str());
if (uri == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return {};
}
auto* displayName =
static_cast<jstring>(env->CallObjectMethod(activity, getDisplayName, uri));
env->DeleteLocalRef(uri);
env->DeleteLocalRef(activity);
if (displayName == nullptr || clear_pending_exception(env)) {
return {};
}
std::string result = to_string(env, displayName);
env->DeleteLocalRef(displayName);
return result;
}
#endif
#if USE_IOS_DIALOG
struct IOSDialogCallbackState {
FileCallback callback;
@@ -88,4 +181,16 @@ void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
default_location, allow_many);
#endif
}
std::string display_name_for_path(std::string_view path) {
#if defined(__ANDROID__) || defined(ANDROID)
if (path.starts_with("content:") || path.starts_with("file:")) {
std::string displayName = android_display_name(path);
if (!displayName.empty()) {
return displayName;
}
}
#endif
return fallback_display_name(path);
}
} // namespace dusk
+7 -2
View File
@@ -2,6 +2,9 @@
#include <SDL3/SDL_dialog.h>
#include <string>
#include <string_view>
struct SDL_Window;
namespace dusk {
@@ -9,7 +12,9 @@ namespace dusk {
using FileCallback = void (*)(void* userdata, const char* path, const char* error);
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
bool allow_many);
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
bool allow_many);
std::string display_name_for_path(std::string_view path);
} // namespace dusk
+81 -20
View File
@@ -1,5 +1,9 @@
#include "dusk/gyro.h"
#include "dusk/ui/ui.hpp"
#include "d/actor/d_a_alink.h"
#include <aurora/lib/window.hpp>
#include <SDL3/SDL_mouse.h>
#include <cmath>
namespace dusk::gyro {
@@ -12,11 +16,14 @@ constexpr float kGravityEmaAlpha = 0.1f;
constexpr float kMinGravityProjection = 0.2f;
// Let roll contribute more strongly as the pad approaches an upright posture.
constexpr float kRollAimBoostMax = 2.0f;
constexpr float kMousePixelToRad = 0.0025f;
bool s_sensor_enabled = false;
bool s_accel_enabled = false;
bool s_was_aiming = false;
bool s_have_gravity_baseline = false;
bool s_mouse_enabled = false;
bool s_mouse_relative = false;
float s_smooth_gx = 0.0f;
float s_smooth_gy = 0.0f;
float s_smooth_gz = 0.0f;
@@ -36,6 +43,7 @@ void reset_filter_state() {
s_baseline_gravity_y = s_baseline_gravity_z = 0.0f;
s_was_aiming = false;
s_have_gravity_baseline = false;
s_mouse_enabled = false;
s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f;
s_rollgoal_ax = s_rollgoal_az = 0;
}
@@ -46,14 +54,29 @@ float apply_deadband(float v, float deadband_rad_s) {
}
return v;
}
void disable_pad_sensors() {
if (s_sensor_enabled) {
PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE);
s_sensor_enabled = false;
}
if (s_accel_enabled) {
PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE);
s_accel_enabled = false;
}
}
} // namespace
bool s_sensor_keep_alive = false;
bool get_sensor_keep_alive() { return s_sensor_keep_alive; }
void set_sensor_keep_alive(bool value) { s_sensor_keep_alive = value; }
bool rollgoal_gyro_enabled() {
return getSettings().game.enableGyroRollgoal && getSettings().game.gyroMode.getValue() != GyroMode::Mouse;
}
bool queryGyroAimContext() {
if (!static_cast<bool>(dusk::getSettings().game.enableGyroAim)) {
if (!static_cast<bool>(getSettings().game.enableGyroAim)) {
return false;
}
@@ -71,15 +94,28 @@ void read(float dt) {
const bool aim_just_ended = !aim_active && s_was_aiming;
s_was_aiming = aim_active;
const bool mouse_mode = getSettings().game.gyroMode.getValue() == GyroMode::Mouse;
const bool mouse_gyro_active = !ui::any_document_visible() && mouse_mode && (aim_active || s_sensor_keep_alive);
SDL_Window* window = aurora::window::get_sdl_window();
if (window != nullptr && mouse_gyro_active != s_mouse_relative &&
SDL_SetWindowRelativeMouseMode(window, mouse_gyro_active))
{
s_mouse_relative = mouse_gyro_active;
}
if (mouse_gyro_active && !s_mouse_enabled && window != nullptr) {
const AuroraWindowSize sz = aurora::window::get_window_size();
const float cx = static_cast<float>(sz.width) * 0.5f;
const float cy = static_cast<float>(sz.height) * 0.5f;
SDL_WarpMouseInWindow(window, cx, cy);
float discard_x = 0.0f;
float discard_y = 0.0f;
SDL_GetRelativeMouseState(&discard_x, &discard_y);
}
s_mouse_enabled = mouse_gyro_active;
if (!s_sensor_keep_alive && !aim_active) {
if (s_sensor_enabled) {
PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE);
s_sensor_enabled = false;
}
if (s_accel_enabled) {
PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE);
s_accel_enabled = false;
}
disable_pad_sensors();
reset_filter_state();
return;
}
@@ -90,6 +126,31 @@ void read(float dt) {
s_have_gravity_baseline = false;
}
if (mouse_mode && !mouse_gyro_active) {
s_pitch_rad = 0.0f;
s_yaw_rad = 0.0f;
s_roll_rad = 0.0f;
return;
}
if (mouse_mode) {
disable_pad_sensors();
float mx_rel = 0.0f;
float my_rel = 0.0f;
SDL_GetRelativeMouseState(&mx_rel, &my_rel);
// Convert pixels to radians
s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX;
s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY;
s_roll_rad = 0.0f;
s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;
s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad;
return;
}
if (!s_sensor_enabled) {
if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) {
return;
@@ -112,8 +173,8 @@ void read(float dt) {
return;
}
const float smooth_alpha = kGyroEmaAlphaMax + dusk::getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax);
const float deadband = dusk::getSettings().game.gyroDeadband;
const float smooth_alpha = kGyroEmaAlphaMax + getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax);
const float deadband = getSettings().game.gyroDeadband;
s_smooth_gx += smooth_alpha * (gyro[0] - s_smooth_gx);
s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy);
@@ -123,8 +184,8 @@ void read(float dt) {
const float yaw_rate = apply_deadband(s_smooth_gy, deadband);
const float roll_rate = apply_deadband(s_smooth_gz, deadband);
s_pitch_rad = -pitch_rate * dt * dusk::getSettings().game.gyroSensitivityX;
s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X
s_pitch_rad = -pitch_rate * dt * getSettings().game.gyroSensitivityX;
s_roll_rad = roll_rate * dt * getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X
float horizontal_rate = yaw_rate;
if (aim_active && s_accel_enabled) {
@@ -162,11 +223,11 @@ void read(float dt) {
}
}
s_yaw_rad = horizontal_rate * dt * dusk::getSettings().game.gyroSensitivityY;
s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityY;
s_pitch_rad = dusk::getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
s_yaw_rad = dusk::getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;
s_yaw_rad = dusk::getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad;
s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;
s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad;
}
void getAimDeltas(float& out_yaw, float& out_pitch) {
@@ -180,9 +241,9 @@ void rollgoalTick(bool play_active, s16 camera_yaw) {
return;
}
float pitch_rad = -s_pitch_rad * dusk::getSettings().game.gyroSensitivityRollgoal;
float roll_rad = s_roll_rad * dusk::getSettings().game.gyroSensitivityRollgoal;
roll_rad = dusk::getSettings().game.enableMirrorMode ? -roll_rad : roll_rad;
float pitch_rad = -s_pitch_rad * getSettings().game.gyroSensitivityRollgoal;
float roll_rad = s_roll_rad * getSettings().game.gyroSensitivityRollgoal;
roll_rad = getSettings().game.enableMirrorMode ? -roll_rad : roll_rad;
s_rollgoal_az += cM_rad2s(roll_rad);
cXyz in(roll_rad, 0.0f, pitch_rad);
+402
View File
@@ -0,0 +1,402 @@
#include "http.hpp"
#include <SDL3/SDL_system.h>
#include <jni.h>
#include <algorithm>
#include <limits>
#include <string_view>
#include <utility>
namespace dusk::http {
namespace {
constexpr int JavaErrorNone = 0;
constexpr int JavaErrorInvalidUrl = 1;
constexpr int JavaErrorUnsupportedScheme = 2;
constexpr int JavaErrorTimeout = 3;
constexpr int JavaErrorTooLarge = 4;
int timeout_ms(std::chrono::milliseconds timeout) {
const auto count = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
return static_cast<int>(
std::min<std::chrono::milliseconds::rep>(count, std::numeric_limits<int>::max()));
}
jlong max_body_bytes(size_t maxBodyBytes) {
return static_cast<jlong>(std::min<size_t>(
maxBodyBytes, static_cast<size_t>(std::numeric_limits<jlong>::max())));
}
bool clear_pending_exception(JNIEnv* env) {
if (env == nullptr || !env->ExceptionCheck()) {
return false;
}
env->ExceptionClear();
return true;
}
std::string to_string(JNIEnv* env, jstring value) {
if (env == nullptr || value == nullptr) {
return {};
}
const char* utf8 = env->GetStringUTFChars(value, nullptr);
if (utf8 == nullptr) {
clear_pending_exception(env);
return {};
}
std::string result(utf8);
env->ReleaseStringUTFChars(value, utf8);
return result;
}
jstring to_jstring(JNIEnv* env, std::string_view value) {
if (env == nullptr) {
return nullptr;
}
return env->NewStringUTF(std::string(value).c_str());
}
Error map_java_error(int error) {
switch (error) {
case JavaErrorNone:
return Error::None;
case JavaErrorInvalidUrl:
return Error::InvalidUrl;
case JavaErrorUnsupportedScheme:
return Error::UnsupportedScheme;
case JavaErrorTimeout:
return Error::Timeout;
case JavaErrorTooLarge:
return Error::TooLarge;
default:
return Error::Network;
}
}
jclass load_dusk_class(JNIEnv* env, jobject activity, const char* className) {
jclass activityClass = env->GetObjectClass(activity);
if (activityClass == nullptr || clear_pending_exception(env)) {
return nullptr;
}
jmethodID getClassLoader =
env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
env->DeleteLocalRef(activityClass);
if (getClassLoader == nullptr || clear_pending_exception(env)) {
return nullptr;
}
jobject classLoader = env->CallObjectMethod(activity, getClassLoader);
if (classLoader == nullptr || clear_pending_exception(env)) {
return nullptr;
}
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
if (classLoaderClass == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(classLoader);
return nullptr;
}
jmethodID loadClass = env->GetMethodID(
classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
env->DeleteLocalRef(classLoaderClass);
if (loadClass == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(classLoader);
return nullptr;
}
jstring javaClassName = env->NewStringUTF(className);
if (javaClassName == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(classLoader);
return nullptr;
}
auto* loadedClass =
static_cast<jclass>(env->CallObjectMethod(classLoader, loadClass, javaClassName));
env->DeleteLocalRef(javaClassName);
env->DeleteLocalRef(classLoader);
if (loadedClass == nullptr || clear_pending_exception(env)) {
return nullptr;
}
return loadedClass;
}
jobjectArray make_string_array(JNIEnv* env, const std::vector<Header>& headers, bool names) {
jclass stringClass = env->FindClass("java/lang/String");
if (stringClass == nullptr || clear_pending_exception(env)) {
return nullptr;
}
jobjectArray array =
env->NewObjectArray(static_cast<jsize>(headers.size()), stringClass, nullptr);
env->DeleteLocalRef(stringClass);
if (array == nullptr || clear_pending_exception(env)) {
return nullptr;
}
for (jsize i = 0; i < static_cast<jsize>(headers.size()); ++i) {
const std::string& value = names ? headers[static_cast<size_t>(i)].name :
headers[static_cast<size_t>(i)].value;
jstring javaValue = to_jstring(env, value);
if (javaValue == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(array);
return nullptr;
}
env->SetObjectArrayElement(array, i, javaValue);
env->DeleteLocalRef(javaValue);
if (clear_pending_exception(env)) {
env->DeleteLocalRef(array);
return nullptr;
}
}
return array;
}
std::vector<Header> read_headers(JNIEnv* env, jobjectArray names, jobjectArray values) {
std::vector<Header> headers;
if (names == nullptr || values == nullptr) {
return headers;
}
const jsize count = std::min(env->GetArrayLength(names), env->GetArrayLength(values));
headers.reserve(static_cast<size_t>(count));
for (jsize i = 0; i < count; ++i) {
auto* name = static_cast<jstring>(env->GetObjectArrayElement(names, i));
auto* value = static_cast<jstring>(env->GetObjectArrayElement(values, i));
if (clear_pending_exception(env)) {
if (name != nullptr) {
env->DeleteLocalRef(name);
}
if (value != nullptr) {
env->DeleteLocalRef(value);
}
headers.clear();
return headers;
}
if (name != nullptr) {
headers.push_back({
.name = to_string(env, name),
.value = to_string(env, value),
});
}
if (name != nullptr) {
env->DeleteLocalRef(name);
}
if (value != nullptr) {
env->DeleteLocalRef(value);
}
}
return headers;
}
std::string read_body(JNIEnv* env, jbyteArray body) {
if (body == nullptr) {
return {};
}
const jsize bodySize = env->GetArrayLength(body);
std::string result(static_cast<size_t>(bodySize), '\0');
if (bodySize > 0) {
env->GetByteArrayRegion(body, 0, bodySize, reinterpret_cast<jbyte*>(result.data()));
if (clear_pending_exception(env)) {
return {};
}
}
return result;
}
Result result_from_response(JNIEnv* env, jobject response) {
if (response == nullptr) {
return {
.error = Error::Network,
.message = "Android HTTP request did not return a response",
};
}
jclass responseClass = env->GetObjectClass(response);
if (responseClass == nullptr || clear_pending_exception(env)) {
return {
.error = Error::Network,
.message = "Failed to inspect Android HTTP response",
};
}
jfieldID errorField = env->GetFieldID(responseClass, "error", "I");
jfieldID messageField = env->GetFieldID(responseClass, "message", "Ljava/lang/String;");
jfieldID statusField = env->GetFieldID(responseClass, "statusCode", "I");
jfieldID headerNamesField =
env->GetFieldID(responseClass, "headerNames", "[Ljava/lang/String;");
jfieldID headerValuesField =
env->GetFieldID(responseClass, "headerValues", "[Ljava/lang/String;");
jfieldID bodyField = env->GetFieldID(responseClass, "body", "[B");
env->DeleteLocalRef(responseClass);
if (errorField == nullptr || messageField == nullptr || statusField == nullptr ||
headerNamesField == nullptr || headerValuesField == nullptr || bodyField == nullptr ||
clear_pending_exception(env))
{
return {
.error = Error::Network,
.message = "Android HTTP response shape was not recognized",
};
}
const int javaError = env->GetIntField(response, errorField);
auto* message = static_cast<jstring>(env->GetObjectField(response, messageField));
auto* headerNames = static_cast<jobjectArray>(env->GetObjectField(response, headerNamesField));
auto* headerValues =
static_cast<jobjectArray>(env->GetObjectField(response, headerValuesField));
auto* body = static_cast<jbyteArray>(env->GetObjectField(response, bodyField));
if (clear_pending_exception(env)) {
return {
.error = Error::Network,
.message = "Failed to read Android HTTP response",
};
}
Response httpResponse{
.statusCode = static_cast<int>(env->GetIntField(response, statusField)),
.headers = read_headers(env, headerNames, headerValues),
.body = read_body(env, body),
};
std::string messageString = to_string(env, message);
if (message != nullptr) {
env->DeleteLocalRef(message);
}
if (headerNames != nullptr) {
env->DeleteLocalRef(headerNames);
}
if (headerValues != nullptr) {
env->DeleteLocalRef(headerValues);
}
if (body != nullptr) {
env->DeleteLocalRef(body);
}
return {
.error = map_java_error(javaError),
.message = std::move(messageString),
.response = std::move(httpResponse),
};
}
} // namespace
bool available() noexcept {
return true;
}
Backend backend() noexcept {
return Backend::Android;
}
const char* backend_name() noexcept {
return "Android";
}
Result get(const Request& request) {
if (request.url.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is empty",
};
}
if (!request.url.starts_with("https://")) {
return {
.error = Error::UnsupportedScheme,
.message = "Only https:// URLs are supported",
};
}
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
if (env == nullptr) {
return {
.error = Error::Network,
.message = "Failed to access Android JNI environment",
};
}
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (activity == nullptr || clear_pending_exception(env)) {
if (activity != nullptr) {
env->DeleteLocalRef(activity);
}
return {
.error = Error::Network,
.message = "Failed to access Android activity",
};
}
jclass clientClass =
load_dusk_class(env, activity, "com.twilitrealm.dusk.DuskHttpClient");
env->DeleteLocalRef(activity);
if (clientClass == nullptr) {
return {
.error = Error::Network,
.message = "Failed to load Android HTTP helper",
};
}
jmethodID getMethod = env->GetStaticMethodID(clientClass, "get",
"(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;IJ)"
"Lcom/twilitrealm/dusk/DuskHttpClient$Response;");
if (getMethod == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(clientClass);
return {
.error = Error::Network,
.message = "Failed to find Android HTTP helper method",
};
}
jstring url = to_jstring(env, request.url);
jobjectArray headerNames = make_string_array(env, request.headers, true);
jobjectArray headerValues = make_string_array(env, request.headers, false);
if (url == nullptr || headerNames == nullptr || headerValues == nullptr ||
clear_pending_exception(env))
{
if (url != nullptr) {
env->DeleteLocalRef(url);
}
if (headerNames != nullptr) {
env->DeleteLocalRef(headerNames);
}
if (headerValues != nullptr) {
env->DeleteLocalRef(headerValues);
}
env->DeleteLocalRef(clientClass);
return {
.error = Error::Network,
.message = "Failed to prepare Android HTTP request",
};
}
jobject response = env->CallStaticObjectMethod(clientClass, getMethod, url, headerNames,
headerValues, timeout_ms(request.timeout), max_body_bytes(request.maxBodyBytes));
env->DeleteLocalRef(url);
env->DeleteLocalRef(headerNames);
env->DeleteLocalRef(headerValues);
env->DeleteLocalRef(clientClass);
if (clear_pending_exception(env)) {
return {
.error = Error::Network,
.message = "Android HTTP request failed with a Java exception",
};
}
Result result = result_from_response(env, response);
if (response != nullptr) {
env->DeleteLocalRef(response);
}
return result;
}
} // namespace dusk::http
+206
View File
@@ -0,0 +1,206 @@
#include "http.hpp"
#include <curl/curl.h>
#include <algorithm>
#include <mutex>
#include <string_view>
#include <utility>
namespace dusk::http {
namespace {
struct CurlHeaders {
curl_slist* list = nullptr;
~CurlHeaders() {
if (list != nullptr) {
curl_slist_free_all(list);
}
}
bool append(const std::string& header) {
curl_slist* next = curl_slist_append(list, header.c_str());
if (next == nullptr) {
return false;
}
list = next;
return true;
}
};
struct CurlContext {
Response response;
size_t maxBodyBytes = 0;
bool tooLarge = false;
};
void initialize_curl() {
curl_global_init(CURL_GLOBAL_DEFAULT);
}
std::string trim_header_value(std::string_view value) {
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
value.remove_prefix(1);
}
while (!value.empty() &&
(value.back() == '\r' || value.back() == '\n' || value.back() == ' ' ||
value.back() == '\t')) {
value.remove_suffix(1);
}
return std::string(value);
}
size_t write_body(char* ptr, size_t size, size_t nmemb, void* userdata) {
auto* context = static_cast<CurlContext*>(userdata);
const size_t bytes = size * nmemb;
if (bytes > context->maxBodyBytes ||
context->response.body.size() > context->maxBodyBytes - bytes) {
context->tooLarge = true;
return 0;
}
context->response.body.append(ptr, bytes);
return bytes;
}
size_t write_header(char* ptr, size_t size, size_t nmemb, void* userdata) {
auto* context = static_cast<CurlContext*>(userdata);
const std::string_view line(ptr, size * nmemb);
if (line.starts_with("HTTP/")) {
context->response.headers.clear();
return size * nmemb;
}
const size_t colon = line.find(':');
if (colon == std::string_view::npos) {
return size * nmemb;
}
context->response.headers.push_back({
.name = std::string(line.substr(0, colon)),
.value = trim_header_value(line.substr(colon + 1)),
});
return size * nmemb;
}
Error map_curl_error(CURLcode code, bool tooLarge) {
if (tooLarge) {
return Error::TooLarge;
}
switch (code) {
case CURLE_OK:
return Error::None;
case CURLE_URL_MALFORMAT:
return Error::InvalidUrl;
case CURLE_UNSUPPORTED_PROTOCOL:
return Error::UnsupportedScheme;
case CURLE_OPERATION_TIMEDOUT:
return Error::Timeout;
default:
return Error::Network;
}
}
long timeout_ms(std::chrono::milliseconds timeout) {
return std::max<std::chrono::milliseconds::rep>(1, timeout.count());
}
} // namespace
bool available() noexcept {
return true;
}
Backend backend() noexcept {
return Backend::LibCurl;
}
const char* backend_name() noexcept {
return "libcurl";
}
Result get(const Request& request) {
if (request.url.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is empty",
};
}
if (!request.url.starts_with("https://")) {
return {
.error = Error::UnsupportedScheme,
.message = "Only https:// URLs are supported",
};
}
static std::once_flag initFlag;
std::call_once(initFlag, initialize_curl);
CURL* curl = curl_easy_init();
if (curl == nullptr) {
return {
.error = Error::Network,
.message = "Failed to create libcurl request",
};
}
CurlHeaders headers;
for (const Header& header : request.headers) {
if (!headers.append(header.name + ": " + header.value)) {
curl_easy_cleanup(curl);
return {
.error = Error::Network,
.message = "Failed to allocate libcurl headers",
};
}
}
CurlContext context{
.maxBodyBytes = request.maxBodyBytes,
};
curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers.list);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms(request.timeout));
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms(request.timeout));
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &context);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_header);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &context);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
#if CURL_AT_LEAST_VERSION(7, 85, 0)
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
#else
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
#endif
const CURLcode code = curl_easy_perform(curl);
long statusCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
curl_easy_cleanup(curl);
context.response.statusCode = static_cast<int>(statusCode);
if (code == CURLE_OK) {
return {
.response = std::move(context.response),
};
}
const Error error = map_curl_error(code, context.tooLarge);
return {
.error = error,
.message = error == Error::TooLarge ? "Response body exceeded the configured limit"
: curl_easy_strerror(code),
.response = std::move(context.response),
};
}
} // namespace dusk::http
+60
View File
@@ -0,0 +1,60 @@
#ifndef DUSK_HTTP_HTTP_HPP
#define DUSK_HTTP_HTTP_HPP
#include <chrono>
#include <cstddef>
#include <string>
#include <vector>
namespace dusk::http {
enum class Backend {
None,
WinHttp,
UrlSession,
LibCurl,
Android,
};
enum class Error {
None,
NoBackend,
InvalidUrl,
UnsupportedScheme,
Timeout,
TooLarge,
Network,
};
struct Header {
std::string name;
std::string value;
};
struct Request {
std::string url;
std::vector<Header> headers;
std::chrono::milliseconds timeout{10000};
size_t maxBodyBytes = 1024 * 1024;
};
struct Response {
int statusCode = 0;
std::vector<Header> headers;
std::string body;
};
struct Result {
Error error = Error::None;
std::string message;
Response response;
};
bool available() noexcept;
Backend backend() noexcept;
const char* backend_name() noexcept;
Result get(const Request& request);
} // namespace dusk::http
#endif // DUSK_HTTP_HTTP_HPP
+24
View File
@@ -0,0 +1,24 @@
#include "http.hpp"
namespace dusk::http {
bool available() noexcept {
return false;
}
Backend backend() noexcept {
return Backend::None;
}
const char* backend_name() noexcept {
return "none";
}
Result get(const Request&) {
return {
.error = Error::NoBackend,
.message = "No HTTP backend is available",
};
}
} // namespace dusk::http
+238
View File
@@ -0,0 +1,238 @@
#include "http.hpp"
#import <Foundation/Foundation.h>
#include <algorithm>
#include <string_view>
#include <utility>
@interface DuskHttpRequestDelegate : NSObject <NSURLSessionDataDelegate, NSURLSessionTaskDelegate>
@property(nonatomic) dispatch_semaphore_t semaphore;
@property(nonatomic) size_t maxBodyBytes;
@property(nonatomic, strong) NSMutableData* data;
@property(nonatomic, strong) NSURLResponse* response;
@property(nonatomic, strong) NSError* error;
@property(nonatomic) BOOL tooLarge;
- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes;
@end
@implementation DuskHttpRequestDelegate
- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes {
self = [super init];
if (self != nil) {
_semaphore = dispatch_semaphore_create(0);
_maxBodyBytes = maxBodyBytes;
_data = [NSMutableData data];
}
return self;
}
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
willPerformHTTPRedirection:(NSHTTPURLResponse*)response
newRequest:(NSURLRequest*)request
completionHandler:(void (^)(NSURLRequest*))completionHandler {
if ([[request.URL.scheme lowercaseString] isEqualToString:@"https"]) {
completionHandler(request);
} else {
completionHandler(nil);
}
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveResponse:(NSURLResponse*)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
self.response = response;
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveData:(NSData*)data {
if (data.length > self.maxBodyBytes ||
self.data.length > self.maxBodyBytes - data.length) {
self.tooLarge = YES;
[dataTask cancel];
return;
}
[self.data appendData:data];
}
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didCompleteWithError:(NSError*)error {
if (error != nil && !self.tooLarge) {
self.error = error;
}
dispatch_semaphore_signal(self.semaphore);
}
@end
namespace dusk::http {
namespace {
NSString* to_nsstring(std::string_view value) {
return [[NSString alloc] initWithBytes:value.data()
length:value.size()
encoding:NSUTF8StringEncoding];
}
std::string to_string(NSString* value) {
if (value == nil) {
return {};
}
const char* utf8 = [value UTF8String];
return utf8 == nullptr ? std::string() : std::string(utf8);
}
Error map_nsurl_error(NSError* error) {
if (error == nil || ![error.domain isEqualToString:NSURLErrorDomain]) {
return Error::Network;
}
switch (error.code) {
case NSURLErrorTimedOut:
return Error::Timeout;
case NSURLErrorBadURL:
case NSURLErrorUnsupportedURL:
return Error::InvalidUrl;
default:
return Error::Network;
}
}
dispatch_time_t timeout_deadline(std::chrono::milliseconds timeout) {
const auto milliseconds = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
return dispatch_time(DISPATCH_TIME_NOW,
static_cast<int64_t>(milliseconds) * static_cast<int64_t>(NSEC_PER_MSEC));
}
} // namespace
bool available() noexcept {
return true;
}
Backend backend() noexcept {
return Backend::UrlSession;
}
const char* backend_name() noexcept {
return "NSURLSession";
}
Result get(const Request& request) {
@autoreleasepool {
if (request.url.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is empty",
};
}
if (!request.url.starts_with("https://")) {
return {
.error = Error::UnsupportedScheme,
.message = "Only https:// URLs are supported",
};
}
NSString* urlString = to_nsstring(request.url);
if (urlString == nil) {
return {
.error = Error::InvalidUrl,
.message = "URL is not valid UTF-8",
};
}
NSURL* url = [NSURL URLWithString:urlString];
if (url == nil || ![[url.scheme lowercaseString] isEqualToString:@"https"]) {
return {
.error = Error::InvalidUrl,
.message = "Failed to parse URL",
};
}
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
urlRequest.HTTPMethod = @"GET";
urlRequest.timeoutInterval = request.timeout.count() / 1000.0;
urlRequest.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
for (const Header& header : request.headers) {
NSString* name = to_nsstring(header.name);
NSString* value = to_nsstring(header.value);
if (name == nil || value == nil) {
return {
.error = Error::InvalidUrl,
.message = "Request header is not valid UTF-8",
};
}
[urlRequest setValue:value forHTTPHeaderField:name];
}
NSURLSessionConfiguration* configuration =
[NSURLSessionConfiguration ephemeralSessionConfiguration];
configuration.timeoutIntervalForRequest = request.timeout.count() / 1000.0;
configuration.timeoutIntervalForResource = request.timeout.count() / 1000.0;
DuskHttpRequestDelegate* delegate =
[[DuskHttpRequestDelegate alloc] initWithMaxBodyBytes:request.maxBodyBytes];
NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration
delegate:delegate
delegateQueue:nil];
NSURLSessionDataTask* task = [session dataTaskWithRequest:urlRequest];
[task resume];
if (dispatch_semaphore_wait(delegate.semaphore, timeout_deadline(request.timeout)) != 0) {
[task cancel];
[session invalidateAndCancel];
return {
.error = Error::Timeout,
.message = "Request timed out",
};
}
[session finishTasksAndInvalidate];
Response response;
if ([delegate.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)delegate.response;
response.statusCode = static_cast<int>(httpResponse.statusCode);
NSDictionary* headers = httpResponse.allHeaderFields;
for (id key in headers) {
id value = headers[key];
response.headers.push_back({
.name = to_string([key description]),
.value = to_string([value description]),
});
}
}
if (delegate.data != nil && delegate.data.length > 0) {
response.body.assign(static_cast<const char*>(delegate.data.bytes),
static_cast<size_t>(delegate.data.length));
}
if (delegate.tooLarge) {
return {
.error = Error::TooLarge,
.message = "Response body exceeded the configured limit",
.response = std::move(response),
};
}
if (delegate.error != nil) {
return {
.error = map_nsurl_error(delegate.error),
.message = to_string(delegate.error.localizedDescription),
.response = std::move(response),
};
}
return {
.response = std::move(response),
};
}
}
} // namespace dusk::http
+320
View File
@@ -0,0 +1,320 @@
#include "http.hpp"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <winhttp.h>
#include <algorithm>
#include <limits>
#include <string_view>
#include <utility>
#include <vector>
namespace dusk::http {
namespace {
struct WinHttpHandle {
HINTERNET handle = nullptr;
WinHttpHandle() = default;
explicit WinHttpHandle(HINTERNET handle) : handle(handle) {}
WinHttpHandle(const WinHttpHandle&) = delete;
WinHttpHandle& operator=(const WinHttpHandle&) = delete;
~WinHttpHandle() {
if (handle != nullptr) {
WinHttpCloseHandle(handle);
}
}
operator HINTERNET() const { return handle; }
};
std::wstring utf8_to_wide(std::string_view value) {
if (value.empty()) {
return {};
}
const int required = MultiByteToWideChar(
CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()), nullptr, 0);
if (required <= 0) {
return {};
}
std::wstring result(static_cast<size_t>(required), L'\0');
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()),
result.data(), required);
return result;
}
std::string wide_to_utf8(std::wstring_view value) {
if (value.empty()) {
return {};
}
const int required = WideCharToMultiByte(
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
if (required <= 0) {
return {};
}
std::string result(static_cast<size_t>(required), '\0');
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(),
required, nullptr, nullptr);
return result;
}
DWORD timeout_ms(std::chrono::milliseconds timeout) {
const auto count = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
return static_cast<DWORD>(
std::min<std::chrono::milliseconds::rep>(count, std::numeric_limits<int>::max()));
}
Error map_winhttp_error(DWORD error) {
switch (error) {
case ERROR_WINHTTP_TIMEOUT:
return Error::Timeout;
case ERROR_WINHTTP_INVALID_URL:
case ERROR_WINHTTP_UNRECOGNIZED_SCHEME:
return Error::InvalidUrl;
case ERROR_WINHTTP_SECURE_FAILURE:
case ERROR_WINHTTP_CANNOT_CONNECT:
case ERROR_WINHTTP_CONNECTION_ERROR:
default:
return Error::Network;
}
}
Result fail_from_last_error(const char* message) {
const DWORD error = GetLastError();
return {
.error = map_winhttp_error(error),
.message = std::string(message) + " (" + std::to_string(error) + ")",
};
}
std::string trim_header_value(std::string_view value) {
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
value.remove_prefix(1);
}
while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || value.back() == ' ' ||
value.back() == '\t'))
{
value.remove_suffix(1);
}
return std::string(value);
}
void parse_headers(std::wstring_view rawHeaders, Response& response) {
size_t start = 0;
bool firstLine = true;
while (start < rawHeaders.size()) {
size_t end = rawHeaders.find(L"\r\n", start);
if (end == std::wstring_view::npos) {
end = rawHeaders.size();
}
const std::wstring_view line = rawHeaders.substr(start, end - start);
if (!line.empty() && !firstLine) {
const size_t colon = line.find(L':');
if (colon != std::wstring_view::npos) {
response.headers.push_back({
.name = wide_to_utf8(line.substr(0, colon)),
.value = trim_header_value(wide_to_utf8(line.substr(colon + 1))),
});
}
}
firstLine = false;
if (end == rawHeaders.size()) {
break;
}
start = end + 2;
}
}
bool read_status(HINTERNET request, Response& response) {
DWORD statusCode = 0;
DWORD statusCodeSize = sizeof(statusCode);
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX))
{
return false;
}
response.statusCode = static_cast<int>(statusCode);
return true;
}
bool read_headers(HINTERNET request, Response& response) {
DWORD headerBytes = 0;
WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
WINHTTP_NO_OUTPUT_BUFFER, &headerBytes, WINHTTP_NO_HEADER_INDEX);
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
return false;
}
std::wstring rawHeaders(headerBytes / sizeof(wchar_t), L'\0');
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
rawHeaders.data(), &headerBytes, WINHTTP_NO_HEADER_INDEX))
{
return false;
}
if (!rawHeaders.empty() && rawHeaders.back() == L'\0') {
rawHeaders.pop_back();
}
parse_headers(rawHeaders, response);
return true;
}
} // namespace
bool available() noexcept {
return true;
}
Backend backend() noexcept {
return Backend::WinHttp;
}
const char* backend_name() noexcept {
return "WinHTTP";
}
Result get(const Request& request) {
if (request.url.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is empty",
};
}
std::wstring wideUrl = utf8_to_wide(request.url);
if (wideUrl.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is not valid UTF-8",
};
}
URL_COMPONENTS components{};
components.dwStructSize = sizeof(components);
components.dwSchemeLength = static_cast<DWORD>(-1);
components.dwHostNameLength = static_cast<DWORD>(-1);
components.dwUrlPathLength = static_cast<DWORD>(-1);
components.dwExtraInfoLength = static_cast<DWORD>(-1);
if (!WinHttpCrackUrl(wideUrl.c_str(), static_cast<DWORD>(wideUrl.size()), 0, &components)) {
return fail_from_last_error("Failed to parse URL");
}
if (components.nScheme != INTERNET_SCHEME_HTTPS) {
return {
.error = Error::UnsupportedScheme,
.message = "Only https:// URLs are supported",
};
}
const std::wstring host(components.lpszHostName, components.dwHostNameLength);
std::wstring path;
if (components.lpszUrlPath != nullptr && components.dwUrlPathLength > 0) {
path.assign(components.lpszUrlPath, components.dwUrlPathLength);
}
if (components.lpszExtraInfo != nullptr && components.dwExtraInfoLength > 0) {
path.append(components.lpszExtraInfo, components.dwExtraInfoLength);
}
if (path.empty()) {
path = L"/";
}
WinHttpHandle session(WinHttpOpen(L"Dusk", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
if (session.handle == nullptr) {
return fail_from_last_error("Failed to create WinHTTP session");
}
const DWORD timeout = timeout_ms(request.timeout);
WinHttpSetTimeouts(session, timeout, timeout, timeout, timeout);
WinHttpHandle connection(WinHttpConnect(session, host.c_str(), components.nPort, 0));
if (connection.handle == nullptr) {
return fail_from_last_error("Failed to connect");
}
WinHttpHandle httpRequest(WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr,
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
if (httpRequest.handle == nullptr) {
return fail_from_last_error("Failed to create request");
}
DWORD redirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP;
WinHttpSetOption(
httpRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy));
DWORD maxRedirects = 5;
WinHttpSetOption(httpRequest, WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, &maxRedirects,
sizeof(maxRedirects));
for (const Header& header : request.headers) {
const std::wstring wideHeader = utf8_to_wide(header.name + ": " + header.value);
if (wideHeader.empty()) {
return {
.error = Error::InvalidUrl,
.message = "Request header is not valid UTF-8",
};
}
if (!WinHttpAddRequestHeaders(httpRequest, wideHeader.c_str(),
static_cast<DWORD>(wideHeader.size()), WINHTTP_ADDREQ_FLAG_ADD))
{
return fail_from_last_error("Failed to add request header");
}
}
if (!WinHttpSendRequest(
httpRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0))
{
return fail_from_last_error("Failed to send request");
}
if (!WinHttpReceiveResponse(httpRequest, nullptr)) {
return fail_from_last_error("Failed to receive response");
}
Response response;
if (!read_status(httpRequest, response)) {
return fail_from_last_error("Failed to read response status");
}
read_headers(httpRequest, response);
for (;;) {
DWORD availableBytes = 0;
if (!WinHttpQueryDataAvailable(httpRequest, &availableBytes)) {
return fail_from_last_error("Failed to query response body");
}
if (availableBytes == 0) {
break;
}
if (availableBytes > request.maxBodyBytes ||
response.body.size() > request.maxBodyBytes - availableBytes)
{
return {
.error = Error::TooLarge,
.message = "Response body exceeded the configured limit",
.response = std::move(response),
};
}
std::vector<char> buffer(availableBytes);
DWORD bytesRead = 0;
if (!WinHttpReadData(httpRequest, buffer.data(), availableBytes, &bytesRead)) {
return fail_from_last_error("Failed to read response body");
}
response.body.append(buffer.data(), bytesRead);
}
return {
.response = std::move(response),
};
}
} // namespace dusk::http
+6 -2
View File
@@ -282,7 +282,9 @@ static void ShowAllJAISeqs() {
}
void dusk::ImGuiMenuTools::ShowAudioDebug() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug))
{
return;
}
@@ -328,7 +330,9 @@ void dusk::ImGuiMenuTools::ShowAudioDebug() {
}
void dusk::ImGuiMenuTools::ShowSaveEditor() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor))
{
return;
}
m_saveEditor.draw(m_showSaveEditor);
+22 -4
View File
@@ -10,7 +10,9 @@
namespace dusk {
void ImGuiMenuTools::ShowCameraOverlay() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay))
{
return;
}
@@ -61,16 +63,32 @@ namespace dusk {
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");
"WASD/Arrows/Left stick: move, Mouse/C-stick: look\n"
"Ctrl/L: down, Space/R: up, Shift/Z: fast\n"
"Q Key/Y: roll left, R Key/X: roll right");
}
}
if (eventRunning) {
ImGui::EndDisabled();
}
if (!getSettings().game.debugFlyCam) {
ImGui::BeginDisabled();
}
config::ImGuiCheckbox("Lock Events", getSettings().game.debugFlyCamLockEvents);
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (!getSettings().game.debugFlyCam) {
ImGui::SetTooltip("Enable Fly Mode first.");
} else {
ImGui::SetTooltip("Freeze game events while flying.");
}
}
if (!getSettings().game.debugFlyCam) {
ImGui::EndDisabled();
}
ShowCornerContextMenu(m_cameraOverlayCorner, 0);
ImGui::End();
}
}
}
+10 -21
View File
@@ -253,20 +253,20 @@ namespace dusk {
UpdateSettings();
if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) &&
(ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
ImGui::IsKeyPressed(ImGuiKey_R))
{
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
}
if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
ImGuiMenuGame::ToggleFullscreen();
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen);
config::Save();
}
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
m_isHidden = !m_isHidden;
if (getSettings().backend.enableAdvancedSettings) {
m_isHidden = !m_isHidden;
} else {
m_isHidden = true;
}
}
bool showMenu = !m_isHidden;
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
@@ -276,16 +276,6 @@ namespace dusk {
m_menuGame.draw();
m_menuTools.draw();
const auto fpsLabel =
fmt::format(FMT_STRING("FPS: {:.2f}\n"), ImGui::GetIO().Framerate);
const auto fpsSize =
ImGui::CalcTextSize(fpsLabel.data(), fpsLabel.data() + fpsLabel.size());
ImGui::SetCursorPosX(
ImMax(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() -
ImGui::GetStyle().DisplaySafeAreaPadding.x -
fpsSize.x - ImGui::GetStyle().ItemSpacing.x));
ImGuiStringViewText(fpsLabel);
ImGui::EndMainMenuBar();
}
ImGui::PopStyleColor();
@@ -360,8 +350,7 @@ namespace dusk {
ImGui::End();
}
m_menuGame.windowControllerConfig();
m_menuGame.windowInputViewer();
m_menuTools.ShowInputViewer();
m_menuGame.drawSpeedrunTimerOverlay();
if (getSettings().game.liveSplitEnabled) {
+1 -2
View File
@@ -3,12 +3,11 @@
#include "imgui.h"
#include <imgui_internal.h>
#include "ImGuiConsole.hpp"
#include "ImGuiMenuGame.hpp"
#include <dolphin/pad.h>
namespace dusk {
void ImGuiMenuGame::windowInputViewer() {
void ImGuiMenuTools::ShowInputViewer() {
if (!m_showInputViewer) {
return;
}
+3 -1
View File
@@ -22,7 +22,9 @@ namespace dusk {
void ShowHeapDetailed(JKRHeap* heap, OpenHeapData& data, bool& open);
void ImGuiMenuTools::ShowHeapOverlay() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F4, m_showHeapOverlay)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F4, m_showHeapOverlay))
{
return;
}
+3 -1
View File
@@ -9,7 +9,9 @@
namespace dusk {
void ImGuiMenuTools::ShowMapLoader() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader))
{
return;
}
+2 -558
View File
@@ -1,573 +1,16 @@
#include "fmt/format.h"
#include "imgui.h"
#include "ImGuiEngine.hpp"
#include "ImGuiConsole.hpp"
#include "ImGuiConfig.hpp"
#include "dusk/main.h"
#include "dusk/hotkeys.h"
#include "m_Do/m_Do_main.h"
#include <SDL3/SDL_gamepad.h>
namespace dusk {
void ImGuiMenuGame::ToggleFullscreen() {
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen);
config::Save();
}
ImGuiMenuGame::ImGuiMenuGame() {}
void ImGuiMenuGame::draw() {
if (ImGui::BeginMenu("Settings")) {
// TODO: Remove this once Controller Config exists in RmlUi
if (ImGui::Button("Configure Controller")){
m_showControllerConfig = !m_showControllerConfig;
}
ImGui::Checkbox("Show Input Viewer", &m_showInputViewer);
ImGui::EndMenu();
}
}
static void drawVirtualStick(const char* id, const ImVec2& stick) {
float scale = ImGuiScale();
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 45 * scale, ImGui::GetCursorPos().y + 10));
ImGui::BeginChild(id, ImVec2(80 * scale, 80 * scale), 0, ImGuiWindowFlags_NoBackground);
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 p = ImGui::GetCursorScreenPos();
float radius = 30.0f * scale;
ImVec2 pos = ImVec2(p.x + radius, p.y + radius);
constexpr ImU32 stickGray = IM_COL32(150, 150, 150, 255);
constexpr ImU32 white = IM_COL32(255, 255, 255, 255);
constexpr ImU32 red = IM_COL32(230, 0, 0, 255);
dl->AddCircleFilled(pos, radius, stickGray, 8);
dl->AddCircleFilled(ImVec2(pos.x + stick.x * (radius), pos.y + -stick.y * (radius)), 3 * scale, red);
ImGui::EndChild();
}
struct SpecificButtonName {
SDL_GamepadType Type;
const char* Name;
};
struct ButtonNames {
SDL_GamepadButton Button;
std::vector<SpecificButtonName> Names;
};
// clang-format off
static const std::vector<ButtonNames> GamepadButtonNames = {
{ 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
static const char* GetNameForGamepadButton(SDL_Gamepad* gamepad, u32 buttonUntyped) {
if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) {
return "Not bound";
}
auto button = static_cast<SDL_GamepadButton>(buttonUntyped);
auto label = SDL_GetGamepadButtonLabel(gamepad, button);
switch (label) {
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:; // Fall through
}
auto padType = SDL_GetGamepadType(gamepad);
for (const auto& buttonNames : GamepadButtonNames) {
if (buttonNames.Button != button) {
continue;
}
for (const auto& name : buttonNames.Names) {
if (name.Type == padType) {
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:
return PADGetNativeButtonName(buttonUntyped);
}
}
void ImGuiMenuGame::windowControllerConfig() {
if (!m_showControllerConfig) {
return;
}
// if pending for a button mapping, check to set new input
if (m_controllerConfig.m_pendingButtonMapping != nullptr) {
s32 nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort);
if (nativeButton != -1) {
m_controllerConfig.m_pendingButtonMapping->nativeButton = nativeButton;
m_controllerConfig.m_pendingButtonMapping = nullptr;
m_controllerConfig.m_pendingPort = -1;
PADBlockInput(false);
PADSerializeMappings();
}
}
// if pending for an axis mapping, check to set new input
if (m_controllerConfig.m_pendingAxisMapping != nullptr) {
auto nativeAxis = PADGetNativeAxisPulled(m_controllerConfig.m_pendingPort);
if (nativeAxis.nativeAxis != -1) {
m_controllerConfig.m_pendingAxisMapping->nativeAxis = nativeAxis;
m_controllerConfig.m_pendingAxisMapping->nativeButton = -1;
m_controllerConfig.m_pendingAxisMapping = nullptr;
m_controllerConfig.m_pendingPort = -1;
PADBlockInput(false);
PADSerializeMappings();
} else {
auto nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort);
if (nativeButton != -1) {
m_controllerConfig.m_pendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
m_controllerConfig.m_pendingAxisMapping->nativeButton = nativeButton;
m_controllerConfig.m_pendingAxisMapping = nullptr;
m_controllerConfig.m_pendingPort = -1;
PADBlockInput(false);
PADSerializeMappings();
}
}
}
float scale = ImGuiScale();
ImGuiWindowFlags windowFlags =
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_AlwaysAutoResize;
// ImGui::SetNextWindowBgAlpha(0.65f);
if (!ImGui::Begin("Controller Config", &m_showControllerConfig, windowFlags)) {
ImGui::End();
return;
}
// port tabs
ImGui::BeginTabBar("##ControllerTabs");
for (int i = PAD_1; i <= PAD_4; i++) {
if (ImGui::BeginTabItem(fmt::format("Port {}", i + 1).c_str())) {
m_controllerConfig.m_selectedPort = i;
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
// if tab is changed while waiting for input, cancel pending
if ((m_controllerConfig.m_pendingButtonMapping != nullptr ||
m_controllerConfig.m_pendingAxisMapping != nullptr) &&
m_controllerConfig.m_pendingPort != m_controllerConfig.m_selectedPort)
{
m_controllerConfig.m_pendingButtonMapping = nullptr;
m_controllerConfig.m_pendingAxisMapping = nullptr;
m_controllerConfig.m_pendingPort = -1;
PADBlockInput(false);
}
// get a list of all available controller's names
std::vector<std::string> controllerList;
controllerList.push_back("None");
for (int i = 0; i < PADCount(); i++) {
// attach index to name for unique name
controllerList.push_back(fmt::format("{}-{}", PADGetNameForControllerIndex(i), i));
}
// get current controller name
const char* tmpName = PADGetName(m_controllerConfig.m_selectedPort);
std::string currentName = "None";
if (tmpName != nullptr) {
currentName = fmt::format("{}-{}", tmpName, PADGetIndexForPort(m_controllerConfig.m_selectedPort));
}
// controller selection combo box
bool changedController = false;
int changedControllerIndex = 0;
ImGui::SetNextItemWidth(400.0f * scale);
if (ImGui::BeginCombo("##ControllerDeviceList", currentName.c_str())) {
for (int i = 0; const auto& name : controllerList) {
if (ImGui::Selectable(name.c_str(), currentName == name)) {
changedControllerIndex = i;
changedController = true;
}
i++;
}
ImGui::EndCombo();
}
// handle controller change
if (changedController) {
if (changedControllerIndex > 0) {
PADSetPortForIndex(changedControllerIndex - 1, m_controllerConfig.m_selectedPort);
}
else if (changedControllerIndex == 0) {
// if "None" selected
PADClearPort(m_controllerConfig.m_selectedPort);
}
PADSerializeMappings();
}
// restore defaults button
ImGui::SameLine();
if (ImGui::Button("Default")) {
PADRestoreDefaultMapping(m_controllerConfig.m_selectedPort);
PADSerializeMappings();
}
// buttons panel
const float uiButtonSize = 40 * scale;
ImVec2 btnSize(110.0f * scale, 30.0f * scale);
ImGuiBeginGroupPanel("Buttons", ImVec2(150 * scale, 20 * scale));
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(PADGetIndexForPort(m_controllerConfig.m_selectedPort));
u32 buttonCount;
PADButtonMapping* btnMappingList = PADGetButtonMappings(m_controllerConfig.m_selectedPort, &buttonCount);
if (btnMappingList != nullptr) {
for (int i = 0; i < buttonCount; i++) {
const char* btnName = PADGetButtonName(btnMappingList[i].padButton);
ImVec2 len = ImGui::CalcTextSize(btnName);
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SetCursorPosY(pos.y + len.y / 4);
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
ImGui::Text("%s", btnName);
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
std::string dispName;
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingButtonMapping == &btnMappingList[i]) {
dispName = fmt::format("Press a Key...##{}", btnName);
} else {
const char* nativeName = GetNameForGamepadButton(gamepad, btnMappingList[i].nativeButton);
if (nativeName == nullptr) {
nativeName = "[unbound]";
}
dispName = fmt::format("{0}##-{1}", nativeName, i);
}
bool pressed = ImGui::Button(dispName.c_str(),
btnSize);
if (pressed) {
m_controllerConfig.m_isReading = true;
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
m_controllerConfig.m_pendingButtonMapping = &btnMappingList[i];
PADBlockInput(true);
}
}
}
ImGuiEndGroupPanel();
ImGui::SameLine();
uint32_t axisCount;
PADAxisMapping* axisMappingList = PADGetAxisMappings(m_controllerConfig.m_selectedPort, &axisCount);
ImGuiBeginGroupPanel("Analog Triggers", ImVec2(150 * scale, 20 * scale));
PADAxis triggers[] = {PAD_AXIS_TRIGGER_L, PAD_AXIS_TRIGGER_R};
if (axisMappingList != nullptr) {
for (PADAxis trigger : triggers) {
const char* axisName = PADGetAxisName(axisMappingList[trigger].padAxis);
ImVec2 len = ImGui::CalcTextSize(axisName);
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SetCursorPosY(pos.y + len.y / 4);
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
ImGui::Text("%s", axisName);
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
std::string dispName;
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[trigger]) {
dispName = fmt::format("Press a Key...##{}", axisName);
} else {
dispName = fmt::format("{0}##-{1}", PADGetNativeAxisName(axisMappingList[trigger].nativeAxis), trigger);
}
bool pressed = ImGui::Button(dispName.c_str(),
btnSize);
if (pressed) {
m_controllerConfig.m_isReading = true;
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[trigger];
PADBlockInput(true);
}
}
}
int port = m_controllerConfig.m_selectedPort;
PADDeadZones* deadZones = PADGetDeadZones(port);
if (deadZones != nullptr) {
ImGui::Text("L Threshold");
ImGui::SameLine();
{
float tmp = static_cast<float>(deadZones->leftTriggerActivationZone * 100.f) / 32767.f;
if (ImGui::DragFloat("##LThreshold", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
deadZones->leftTriggerActivationZone = static_cast<u16>((tmp / 100.f) * 32767);
PADSerializeMappings();
}
}
}
if (deadZones != nullptr) {
ImGui::Text("R Threshold");
ImGui::SameLine();
{
float tmp = static_cast<float>(deadZones->rightTriggerActivationZone * 100.f) / 32767.f;
if (ImGui::DragFloat("##RThreshold", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
deadZones->rightTriggerActivationZone = static_cast<u16>((tmp / 100.f) * 32767);
PADSerializeMappings();
}
}
}
ImGuiEndGroupPanel();
ImGui::SameLine();
// main stick panel
ImGuiBeginGroupPanel("Control Stick", ImVec2(150 * scale, 20 * scale));
drawVirtualStick("##mainStick", ImVec2{ mDoCPd_c::getStickX(port), mDoCPd_c::getStickY(port) });
if (axisMappingList != nullptr) {
const PADAxis lStickAxes[] = {PAD_AXIS_LEFT_Y_POS, PAD_AXIS_LEFT_Y_NEG, PAD_AXIS_LEFT_X_NEG, PAD_AXIS_LEFT_X_POS};
for (auto axis : lStickAxes) {
const char* label = PADGetAxisDirectionLabel(axis);
ImVec2 len = ImGui::CalcTextSize(label);
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SetCursorPosY(pos.y + len.y / 4);
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
ImGui::Text("%s", label);
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
std::string dispName;
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) {
dispName = fmt::format("Press a Key...##{}", label);
} else {
if (axisMappingList[axis].nativeAxis.nativeAxis != -1) {
const char* signStr;
if (axis == PAD_AXIS_TRIGGER_L || axis == PAD_AXIS_TRIGGER_R) {
signStr = "";
} else if (axisMappingList[axis].nativeAxis.sign == AXIS_SIGN_POSITIVE) {
signStr = "+";
} else {
signStr = "-";
}
dispName = fmt::format("{0}{1}##-{2}", PADGetNativeAxisName(axisMappingList[axis].nativeAxis), signStr, axis);
} else {
assert(axisMappingList[axis].nativeButton != -1);
dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis);
}
}
bool pressed = ImGui::Button(dispName.c_str(), btnSize);
if (pressed) {
m_controllerConfig.m_isReading = true;
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis];
PADBlockInput(true);
}
}
}
if (deadZones != nullptr) {
ImGui::Text("Dead Zone");
{
float tmp = static_cast<float>(deadZones->stickDeadZone * 100.f) / 32767.f;
if (ImGui::DragFloat("##mainDeadZone", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
deadZones->stickDeadZone = static_cast<u16>((tmp / 100.f) * 32767);
PADSerializeMappings();
}
}
}
ImGuiEndGroupPanel();
ImGui::SameLine();
// sub stick panel
ImGuiBeginGroupPanel("C Stick", ImVec2(150 * scale, 20 * scale));
drawVirtualStick("##subStick", ImVec2{ mDoCPd_c::getSubStickX(port), mDoCPd_c::getSubStickY(port) });
if (axisMappingList != nullptr) {
const PADAxis rStickAxes[] = {PAD_AXIS_RIGHT_Y_POS, PAD_AXIS_RIGHT_Y_NEG, PAD_AXIS_RIGHT_X_NEG, PAD_AXIS_RIGHT_X_POS};
for (auto axis : rStickAxes) {
const char* label = PADGetAxisDirectionLabel(axisMappingList[axis].padAxis);
ImVec2 len = ImGui::CalcTextSize(label);
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SetCursorPosY(pos.y + len.y / 4);
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
ImGui::Text("%s", label);
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
std::string dispName;
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) {
dispName = fmt::format("Press a Key...##sub{}", label);
} else {
if (axisMappingList[axis].nativeAxis.nativeAxis != -1) {
const char* signStr;
if (axis == PAD_AXIS_TRIGGER_L || axis == PAD_AXIS_TRIGGER_R) {
signStr = "";
} else if (axisMappingList[axis].nativeAxis.sign == AXIS_SIGN_POSITIVE) {
signStr = "+";
} else {
signStr = "-";
}
dispName = fmt::format("{0}{1}##-{2}", PADGetNativeAxisName(axisMappingList[axis].nativeAxis), signStr, axis);
} else {
assert(axisMappingList[axis].nativeButton != -1);
dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis);
}
}
bool pressed = ImGui::Button(fmt::format("{0}##sub{1}", dispName, label).c_str(), btnSize);
if (pressed) {
m_controllerConfig.m_isReading = true;
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis];
PADBlockInput(true);
}
}
}
if (deadZones != nullptr) {
ImGui::Text("Dead Zone");
{
float tmp = static_cast<float>(deadZones->substickDeadZone * 100.f) / 32767.f;
if (ImGui::DragFloat("##subDeadZone", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
deadZones->substickDeadZone = static_cast<u16>((tmp / 100.f) * 32767);
PADSerializeMappings();
}
}
}
ImGuiEndGroupPanel();
ImGui::SameLine();
// Options panel
ImGuiBeginGroupPanel("Options", ImVec2(150 * scale, -1));
if (deadZones != nullptr) {
if (ImGui::Checkbox("Enable Dead Zones", &deadZones->useDeadzones)) {
PADSerializeMappings();
}
if (ImGui::Checkbox("Emulate Triggers", &deadZones->emulateTriggers)) {
PADSerializeMappings();
}
}
if (PADSupportsRumbleIntensity(m_controllerConfig.m_selectedPort)) {
ImGuiBeginGroupPanel("Rumble Intensity", ImVec2(150 * scale, -1));
u16 low;
u16 high;
(void)PADGetRumbleIntensity(m_controllerConfig.m_selectedPort, &low, &high);
float fLow = (static_cast<float>(low) / 32767.f) * 100.f;
bool changed = ImGui::SliderFloat("Low", &fLow, 0.f, 100.f, "%.0f%%");
float fHigh = (static_cast<float>(high) / 32767.f) * 100.f;
changed |= ImGui::SliderFloat("High", &fHigh, 0.f, 100.f, "%.0f%%");
if (changed) {
PADSetRumbleIntensity(m_controllerConfig.m_selectedPort,
static_cast<u16>((fLow / 100) * 32767),
static_cast<u16>((fHigh / 100) * 32767));
PADSerializeMappings();
}
if (ImGui::Button(fmt::format("{0}...##rumbleTest", m_controllerConfig.m_isRumbling ? "Stop": "Test").c_str(), {-1, 0})) {
PADControlMotor(m_controllerConfig.m_selectedPort, !m_controllerConfig.m_isRumbling ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
m_controllerConfig.m_isRumbling ^= 1;
}
ImGuiEndGroupPanel();
}
ImGuiEndGroupPanel();
ImGui::End();
}
void ImGuiMenuGame::draw() {}
static std::string GetFormattedTime(OSTime ticks) {
OSCalendarTime time;
@@ -602,6 +45,7 @@ namespace dusk {
getSettings().game.enableTurboKeybind.setValue(false);
getSettings().game.debugFlyCam.setValue(false);
getSettings().game.autoSave.setValue(false);
}
SpeedrunInfo m_speedrunInfo;
-20
View File
@@ -46,31 +46,11 @@ namespace dusk {
ImGuiMenuGame();
void draw();
void windowInputViewer();
void windowControllerConfig();
void drawSpeedrunTimerOverlay();
static void ToggleFullscreen();
static void resetForSpeedrunMode();
private:
struct {
int m_selectedPort = 0;
bool m_isReading = false;
PADButtonMapping* m_pendingButtonMapping = nullptr;
PADAxisMapping* m_pendingAxisMapping = nullptr;
int m_pendingPort = -1;
bool m_isRumbling = false;
} m_controllerConfig;
bool m_showControllerConfig = false;
bool m_showInputViewer = false;
bool m_showInputViewerGyro = false;
int m_inputOverlayCorner = 3;
std::string m_controllerName;
bool m_showTimerWindow = false;
};
}
+8 -2
View File
@@ -48,6 +48,8 @@ namespace dusk {
ImGui::EndDisabled();
}
ImGui::Separator();
ImGui::Checkbox("Show Input Viewer", &m_showInputViewer);
#if DUSK_CAN_OPEN_DATA_FOLDER
ImGui::Separator();
@@ -119,7 +121,9 @@ namespace dusk {
}
void ImGuiMenuTools::ShowDebugOverlay() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F3, m_showDebugOverlay)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F3, m_showDebugOverlay))
{
return;
}
@@ -183,7 +187,9 @@ namespace dusk {
}
void ImGuiMenuTools::ShowPlayerInfo() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo))
{
return;
}
+6
View File
@@ -27,6 +27,7 @@ namespace dusk {
void ShowAudioDebug();
void ShowSaveEditor();
void ShowStateShare();
void ShowInputViewer();
private:
bool m_showDebugOverlay = false;
@@ -66,6 +67,11 @@ namespace dusk {
bool m_showStateShare = false;
ImGuiStateShare m_stateShare;
bool m_showInputViewer = false;
bool m_showInputViewerGyro = false;
int m_inputOverlayCorner = 3;
std::string m_controllerName;
};
}
+3 -1
View File
@@ -126,7 +126,9 @@ namespace dusk {
}
void ImGuiMenuTools::ShowProcessManager() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F2, m_showProcessManagement)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F2, m_showProcessManagement))
{
return;
}
+3 -1
View File
@@ -417,7 +417,9 @@ void ImGuiStateShare::draw(bool& open) {
}
void ImGuiMenuTools::ShowStateShare() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) {
if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare))
{
return;
}
m_stateShare.draw(m_showStateShare);
+33 -15
View File
@@ -8,6 +8,8 @@ UserSettings g_userSettings = {
.enableFullscreen {"video.enableFullscreen", false},
.enableVsync {"video.enableVsync", true},
.lockAspectRatio {"video.lockAspectRatio", false},
.enableFpsOverlay {"game.enableFpsOverlay", false},
.fpsOverlayCorner {"game.fpsOverlayCorner", 0},
},
.audio = {
@@ -45,10 +47,11 @@ UserSettings g_userSettings = {
// Preferences
.enableMirrorMode {"game.enableMirrorMode", false},
.disableMainHUD {"game.disableMainHUD", false},
.minimalHUD {"game.minimalHUD", false},
.pauseOnFocusLost {"game.pauseOnFocusLost", false},
.enableLinkDollRotation = {"game.enableLinkDollRotation", false},
.enableAchievementNotifications {"game.enableAchievementNotifications", true},
.enableLinkDollRotation {"game.enableLinkDollRotation", false},
.enableAchievementToasts {"game.enableAchievementToasts", true},
.enableControllerToasts {"game.enableControllerToasts", true},
// Graphics
.bloomMode {"game.bloomMode", BloomMode::Dusk},
@@ -65,6 +68,7 @@ UserSettings g_userSettings = {
.midnasLamentNonStop {"game.midnasLamentNonStop", false},
// Input
.gyroMode {"game.gyroMode", GyroMode::Sensor},
.enableGyroAim {"game.enableGyroAim", false},
.enableGyroRollgoal {"game.enableGyroRollgoal", false},
.gyroSensitivityX {"game.gyroSensitivityX", 1.0f},
@@ -79,18 +83,20 @@ UserSettings g_userSettings = {
.invertCameraYAxis {"game.invertCameraYAxis", false},
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
.debugFlyCam {"game.debugFlyCam", false},
.debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true},
.allowBackgroundInput {"game.allowBackgroundInput", true},
// Cheats
.infiniteHearts {"game.infiniteHearts", false},
.infiniteArrows{"game.infiniteArrows", false},
.infiniteBombs{"game.infiniteBombs", false},
.infiniteOil{"game.infiniteOil", false},
.infiniteOxygen{"game.infiniteOxygen", false},
.infiniteRupees{"game.infiniteRupees", false},
.infiniteArrows {"game.infiniteArrows", false},
.infiniteBombs {"game.infiniteBombs", false},
.infiniteOil {"game.infiniteOil", false},
.infiniteOxygen {"game.infiniteOxygen", false},
.infiniteRupees {"game.infiniteRupees", false},
.enableIndefiniteItemDrops {"game.enableIndefiniteItemDrops", false},
.moonJump{"game.moonJump", false},
.superClawshot{"game.superClawshot", false},
.alwaysGreatspin{"game.alwaysGreatspin", false},
.moonJump {"game.moonJump", false},
.superClawshot {"game.superClawshot", false},
.alwaysGreatspin {"game.alwaysGreatspin", false},
.enableFastIronBoots {"game.enableFastIronBoots", false},
.canTransformAnywhere {"game.canTransformAnywhere", false},
.fastSpinner {"game.fastSpinner", false},
@@ -104,7 +110,8 @@ UserSettings g_userSettings = {
// Tools
.speedrunMode {"game.speedrunMode", false},
.liveSplitEnabled {"game.liveSplitEnabled", false}
.liveSplitEnabled {"game.liveSplitEnabled", false},
.recordingMode {"game.recordingMode", false}
},
.backend = {
@@ -115,7 +122,9 @@ UserSettings g_userSettings = {
.showPipelineCompilation {"backend.showPipelineCompilation", false},
.wasPresetChosen {"backend.wasPresetChosen", false},
.enableCrashReporting {"backend.enableCrashReporting", true},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)}
.checkForUpdates {"backend.checkForUpdates", true},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)},
.enableAdvancedSettings {"backend.enableAdvancedSettings", false},
}
};
@@ -128,6 +137,8 @@ void registerSettings() {
Register(g_userSettings.video.enableFullscreen);
Register(g_userSettings.video.enableVsync);
Register(g_userSettings.video.lockAspectRatio);
Register(g_userSettings.video.enableFpsOverlay);
Register(g_userSettings.video.fpsOverlayCorner);
// Audio
Register(g_userSettings.audio.masterVolume);
@@ -161,7 +172,7 @@ void registerSettings() {
Register(g_userSettings.game.invertCameraXAxis);
Register(g_userSettings.game.invertCameraYAxis);
Register(g_userSettings.game.freeCameraSensitivity);
Register(g_userSettings.game.disableMainHUD);
Register(g_userSettings.game.minimalHUD);
Register(g_userSettings.game.pauseOnFocusLost);
Register(g_userSettings.game.bloomMode);
Register(g_userSettings.game.bloomMultiplier);
@@ -175,13 +186,15 @@ void registerSettings() {
Register(g_userSettings.game.freeMagicArmor);
Register(g_userSettings.game.restoreWiiGlitches);
Register(g_userSettings.game.enableLinkDollRotation);
Register(g_userSettings.game.enableAchievementNotifications);
Register(g_userSettings.game.enableAchievementToasts);
Register(g_userSettings.game.enableControllerToasts);
Register(g_userSettings.game.noMissClimbing);
Register(g_userSettings.game.noLowHpSound);
Register(g_userSettings.game.midnasLamentNonStop);
Register(g_userSettings.game.enableTurboKeybind);
Register(g_userSettings.game.speedrunMode);
Register(g_userSettings.game.liveSplitEnabled);
Register(g_userSettings.game.recordingMode);
Register(g_userSettings.game.fastSpinner);
Register(g_userSettings.game.infiniteHearts);
Register(g_userSettings.game.infiniteArrows);
@@ -194,6 +207,7 @@ void registerSettings() {
Register(g_userSettings.game.superClawshot);
Register(g_userSettings.game.alwaysGreatspin);
Register(g_userSettings.game.enableFrameInterpolation);
Register(g_userSettings.game.gyroMode);
Register(g_userSettings.game.enableGyroAim);
Register(g_userSettings.game.enableGyroRollgoal);
Register(g_userSettings.game.gyroSensitivityX);
@@ -205,6 +219,8 @@ void registerSettings() {
Register(g_userSettings.game.gyroInvertYaw);
Register(g_userSettings.game.freeCamera);
Register(g_userSettings.game.debugFlyCam);
Register(g_userSettings.game.debugFlyCamLockEvents);
Register(g_userSettings.game.allowBackgroundInput);
Register(g_userSettings.backend.isoPath);
Register(g_userSettings.backend.isoVerification);
@@ -213,7 +229,9 @@ void registerSettings() {
Register(g_userSettings.backend.showPipelineCompilation);
Register(g_userSettings.backend.wasPresetChosen);
Register(g_userSettings.backend.enableCrashReporting);
Register(g_userSettings.backend.checkForUpdates);
Register(g_userSettings.backend.cardFileType);
Register(g_userSettings.backend.enableAdvancedSettings);
}
// Transient settings
+26 -3
View File
@@ -16,9 +16,8 @@ struct CategoryInfo {
};
constexpr CategoryInfo kCategories[] = {
{AchievementCategory::Story, "Story"},
{AchievementCategory::Collection, "Collection"},
{AchievementCategory::Challenge, "Challenge"},
{AchievementCategory::Collection, "Collection"},
{AchievementCategory::Minigame, "Minigame"},
{AchievementCategory::Misc, "Misc"},
{AchievementCategory::Glitched, "Glitched"},
@@ -41,7 +40,7 @@ Rml::String build_achievement_info_rml(const Achievement& a) {
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"(<progress value="{:.3f}" class="{}"/>)"
R"(<span class="achievement-progress">{} / {}</span>)",
fraction,
a.unlocked ? "progress-done" : "progress-ongoing",
@@ -114,6 +113,13 @@ private:
AchievementsWindow::AchievementsWindow() {
const auto all = AchievementSystem::get().getAchievements();
{
auto elem = mDocument->CreateElement("div");
elem->SetClass("achievement-total", true);
mTotalEl = mRoot->AppendChild(std::move(elem));
updateTotal();
}
for (const auto& catInfo : kCategories) {
int catTotal = 0;
for (const auto& a : all) {
@@ -201,8 +207,25 @@ void AchievementsWindow::update() {
if (dirty) {
mSnapshot = current;
refresh_active_tab();
updateTotal();
}
Window::update();
}
void AchievementsWindow::updateTotal() {
if (mTotalEl == nullptr) {
return;
}
const auto all = AchievementSystem::get().getAchievements();
int total = static_cast<int>(all.size());
int unlocked = 0;
for (const auto& a : all) {
if (a.unlocked) {
++unlocked;
}
}
const int pct = total > 0 ? (unlocked * 100 / total) : 0;
mTotalEl->SetInnerRML(fmt::format("{}%", pct));
}
} // namespace dusk::ui
+2
View File
@@ -13,7 +13,9 @@ public:
void update() override;
private:
void updateTotal();
std::vector<Achievement> mSnapshot;
Rml::Element* mTotalEl = nullptr;
};
} // namespace dusk::ui
+174 -6
View File
@@ -3,7 +3,7 @@
#include "bool_button.hpp"
#include "button.hpp"
#include "pane.hpp"
#include "select_button.hpp"
#include "number_button.hpp"
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_keyboard.h>
@@ -279,6 +279,18 @@ s32 keyboard_key_pressed() {
return PAD_KEY_INVALID;
}
u16 percent_to_raw(int percent) {
return static_cast<u16>((static_cast<float>(percent) / 100.f) * 32767.f);
}
int deadzone_raw_to_percent(u16 raw) {
return static_cast<int>((static_cast<float>(raw) * 100.f) / 32767.f + 0.5f);
}
int rumble_raw_to_percent(u16 raw) {
return static_cast<int>((static_cast<float>(raw) / 32767.f) * 100.f + 0.5f);
}
} // namespace
ControllerConfigWindow::ControllerConfigWindow() {
@@ -308,6 +320,7 @@ ControllerConfigWindow::ControllerConfigWindow() {
}
void ControllerConfigWindow::hide(bool close) {
stop_rumble_test();
cancel_pending_binding();
Window::hide(close);
}
@@ -318,16 +331,18 @@ void ControllerConfigWindow::update() {
}
void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
stop_rumble_test();
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) {
Page page, Rml::String key, auto getValue, auto isDisabled) {
leftPane.register_control(leftPane.add_select_button({
.key = std::move(key),
.getValue = std::move(getValue),
.isDisabled = std::move(isDisabled),
}),
rightPane, [this, port, page](Pane& pane) {
mPage = page;
@@ -335,10 +350,11 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
});
};
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(">"); });
addPageButton(Page::Controller, "Controller", [port] { return current_controller_name(port); }, [] { return false; });
addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); }, [] { return false; });
addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); }, [] { return false; });
addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); }, [] { return false; });
addPageButton(Page::Rumble, "Rumble", [] { return Rml::String(">"); }, [port] { return !PADSupportsRumbleIntensity(static_cast<u32>(port)); });
leftPane.add_section("Options");
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
@@ -684,6 +700,38 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
});
}
}
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
pane.add_section("Emulated Trigger Thresholds");
pane.add_child<NumberButton>(NumberButton::Props{
.key = "L Threshold",
.getValue = [deadZones] { return deadzone_raw_to_percent(deadZones->leftTriggerActivationZone); },
.setValue =
[deadZones](int value) {
deadZones->leftTriggerActivationZone = percent_to_raw(value);
PADSerializeMappings();
},
.isDisabled = [deadZones] { return !deadZones->emulateTriggers; },
.min = 0,
.max = 100,
.step = 1,
.suffix = "%",
});
pane.add_child<NumberButton>(NumberButton::Props{
.key = "R Threshold",
.getValue = [deadZones] { return deadzone_raw_to_percent(deadZones->rightTriggerActivationZone); },
.setValue =
[deadZones](int value) {
deadZones->rightTriggerActivationZone = percent_to_raw(value);
PADSerializeMappings();
},
.isDisabled = [deadZones] { return !deadZones->emulateTriggers; },
.min = 0,
.max = 100,
.step = 1,
.suffix = "%",
});
}
break;
}
case Page::Sticks: {
@@ -769,12 +817,121 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
addAxis(PAD_AXIS_LEFT_Y_NEG);
addAxis(PAD_AXIS_LEFT_X_NEG);
addAxis(PAD_AXIS_LEFT_X_POS);
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
pane.add_child<NumberButton>(NumberButton::Props{
.key = "Deadzone",
.getValue = [deadZones] { return deadzone_raw_to_percent(deadZones->stickDeadZone); },
.setValue =
[deadZones](int value) {
deadZones->stickDeadZone = percent_to_raw(value);
PADSerializeMappings();
},
.isDisabled = [deadZones] { return !deadZones->useDeadzones; },
.min = 0,
.max = 100,
.step = 1,
.suffix = "%",
});
}
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);
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
pane.add_child<NumberButton>(NumberButton::Props{
.key = "Deadzone",
.getValue = [deadZones] { return deadzone_raw_to_percent(deadZones->substickDeadZone); },
.setValue =
[deadZones](int value) {
deadZones->substickDeadZone = percent_to_raw(value);
PADSerializeMappings();
},
.isDisabled = [deadZones] { return !deadZones->useDeadzones; },
.min = 0,
.max = 100,
.step = 1,
.suffix = "%",
});
}
break;
}
case Page::Rumble: {
auto& rumbleTest = pane.add_select_button({
.key = "Test Rumble",
.getValue =
[this, port] {
return (mRumbleTestActive && mRumbleTestPort == port) ? Rml::String("Stop")
: Rml::String("Start");
},
});
rumbleTest.on_pressed([this, port] {
if (!PADSupportsRumbleIntensity(static_cast<u32>(port))) {
return;
}
mDoAud_seStartMenu(kSoundItemChange);
if (mRumbleTestActive && mRumbleTestPort == port) {
PADControlMotor(port, PAD_MOTOR_STOP_HARD);
mRumbleTestActive = false;
mRumbleTestPort = -1;
} else {
if (mRumbleTestActive) {
PADControlMotor(mRumbleTestPort, PAD_MOTOR_STOP_HARD);
}
PADControlMotor(port, PAD_MOTOR_RUMBLE);
mRumbleTestActive = true;
mRumbleTestPort = port;
}
});
pane.add_child<NumberButton>(NumberButton::Props{
.key = "Low Rumble Frequency",
.getValue =
[port] {
u16 low = 0;
u16 high = 0;
PADGetRumbleIntensity(static_cast<u32>(port), &low, &high);
return rumble_raw_to_percent(low);
},
.setValue =
[port](int value) {
u16 low = 0;
u16 high = 0;
PADGetRumbleIntensity(static_cast<u32>(port), &low, &high);
PADSetRumbleIntensity(static_cast<u32>(port), percent_to_raw(value), high);
PADSerializeMappings();
},
.isDisabled = [this] { return mRumbleTestActive; },
.min = 0,
.max = 100,
.step = 1,
.suffix = "%",
});
pane.add_child<NumberButton>(NumberButton::Props{
.key = "High Rumble Frequency",
.getValue =
[port] {
u16 low = 0;
u16 high = 0;
PADGetRumbleIntensity(static_cast<u32>(port), &low, &high);
return rumble_raw_to_percent(high);
},
.setValue =
[port](int value) {
u16 low = 0;
u16 high = 0;
PADGetRumbleIntensity(static_cast<u32>(port), &low, &high);
PADSetRumbleIntensity(static_cast<u32>(port), low, percent_to_raw(value));
PADSerializeMappings();
},
.isDisabled = [this] { return mRumbleTestActive; },
.min = 0,
.max = 100,
.step = 1,
.suffix = "%",
});
pane.add_text("Configure your desired rumble intensities, then run a test to check how they feel.");
break;
}
}
@@ -938,4 +1095,15 @@ Rml::String ControllerConfigWindow::pending_key_label() const {
return mPendingBindingArmed ? "Press a key or mouse button..." : "Waiting...";
}
void ControllerConfigWindow::stop_rumble_test() {
if (!mRumbleTestActive) {
return;
}
if (mRumbleTestPort >= PAD_CHAN0 && mRumbleTestPort < PAD_CHANMAX) {
PADControlMotor(mRumbleTestPort, PAD_MOTOR_STOP_HARD);
}
mRumbleTestActive = false;
mRumbleTestPort = -1;
}
} // namespace dusk::ui
+4
View File
@@ -19,6 +19,7 @@ private:
Buttons,
Triggers,
Sticks,
Rumble,
};
void build_port_tab(Rml::Element* content, int port);
@@ -34,6 +35,7 @@ private:
void cancel_pending_binding();
void finish_pending_key_binding();
Rml::String pending_key_label() const;
void stop_rumble_test();
Page mPage = Page::Controller;
Pane* mRightPane = nullptr;
@@ -46,6 +48,8 @@ private:
PADAxisMapping* mPendingAxisMapping = nullptr;
int mPendingKeyButton = -1;
int mPendingKeyAxis = -1;
bool mRumbleTestActive = false;
int mRumbleTestPort = -1;
};
} // namespace dusk::ui
+13 -5
View File
@@ -91,18 +91,23 @@ Rml::Element* create_stepped_carousel_arrow(
return parent->AppendChild(std::move(button));
}
void update_carousel_arrow_color(Rml::Element* arrow, bool dim) {
const Rml::Colourb& color = Rml::Colourb(255, 255, 255, dim ? 128 : 255);
arrow->SetProperty(Rml::PropertyId::Color, Rml::Property(color, Rml::Unit::COLOUR));
}
} // namespace
SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
: Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "&#xe5cb;");
mPrevElem = create_stepped_carousel_arrow(mRoot, "prev", "&#xe5cb;");
mValueElem = append(mRoot, "div");
mValueElem->SetClass("stepped-carousel-value", true);
Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", "&#xe5cc;");
mNextElem = create_stepped_carousel_arrow(mRoot, "next", "&#xe5cc;");
listen(prevElem, Rml::EventId::Click,
listen(mPrevElem, Rml::EventId::Click,
[this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
listen(nextElem, Rml::EventId::Click,
listen(mNextElem, 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);
@@ -126,6 +131,9 @@ void SteppedCarousel::update() {
} else {
mValueElem->SetInnerRML(std::to_string(value));
}
update_carousel_arrow_color(mPrevElem, value == mProps.min);
update_carousel_arrow_color(mNextElem, value == mProps.max);
}
bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
@@ -280,4 +288,4 @@ void GraphicsTuner::reset_default() {
set_value(mOption, mDefaultValue);
}
} // namespace dusk::ui
} // namespace dusk::ui
+2
View File
@@ -34,6 +34,8 @@ private:
void apply(int value);
Props mProps;
Rml::Element* mPrevElem = nullptr;
Rml::Element* mNextElem = nullptr;
Rml::Element* mValueElem = nullptr;
};
+69 -7
View File
@@ -13,6 +13,7 @@
#include "f_pc/f_pc_manager.h"
#include "f_pc/f_pc_name.h"
#include "imgui.h"
#include "modal.hpp"
#include "settings.hpp"
#include "ui.hpp"
#include "window.hpp"
@@ -50,17 +51,78 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
// mTabBar->add_tab("Warp", [] {
// // TODO
// });
mTabBar->add_tab("Editor", [this] { push(std::make_unique<EditorWindow>()); });
if (getSettings().backend.enableAdvancedSettings) {
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] {
mTabBar->set_active_tab(-1);
if (fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) {
return;
}
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
hide(false);
const auto dismiss = [](Modal& modal) { modal.pop(); };
push(std::make_unique<Modal>(Modal::Props{
.title = "Reset Game",
.bodyRml = "Unsaved progress will be lost.<br/>"
"<span class=\"tip\">Tip: You can also reset by holding Start+X+B</span>",
.actions =
{
ModalAction{
.label = "Cancel",
.onPressed =
[this, dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundWindowClose);
dismiss(modal);
},
},
ModalAction{
.label = "Reset",
.onPressed =
[this, dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundClick);
if (fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) {
dismiss(modal);
return;
}
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
dismiss(modal);
hide(false);
},
},
},
.onDismiss = dismiss,
.icon = "question-mark",
}));
});
mTabBar->add_tab("Quit", [this] {
mTabBar->set_active_tab(-1);
const auto dismiss = [](Modal& modal) { modal.pop(); };
push(std::make_unique<Modal>(Modal::Props{
.title = "Quit Dusk",
.bodyRml = "Unsaved progress will be lost.",
.actions =
{
ModalAction{
.label = "Cancel",
.onPressed =
[dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundWindowClose);
dismiss(modal);
},
},
ModalAction{
.label = "Quit",
.onPressed =
[dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundClick);
dismiss(modal);
IsRunning = false;
},
},
},
.onDismiss = dismiss,
.icon = "question-mark",
}));
});
mTabBar->add_tab("Quit", [] { IsRunning = false; });
// Hide document after transition completion
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
+4 -5
View File
@@ -14,12 +14,9 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo
title->SetClass("modal-title", true);
title->SetInnerRML(mProps.title);
if (mProps.isWarning) {
if (!mProps.icon.empty()) {
auto* icon = append(header, "icon");
icon->SetClass("warning", true);
} else if (mProps.isError) {
auto* icon = append(header, "icon");
icon->SetClass("error", true);
icon->SetClass(mProps.icon, true);
}
auto* body = append(mDialog, "div");
@@ -39,6 +36,8 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo
});
mButtons.push_back(std::move(btn));
}
mDoAud_seStartMenu(kSoundWindowOpen);
}
bool Modal::focus() {
+1 -2
View File
@@ -19,8 +19,7 @@ public:
std::vector<ModalAction> actions;
std::function<void(Modal&)> onDismiss;
Rml::String variant;
bool isWarning = false;
bool isError = false;
Rml::String icon = "";
};
explicit Modal(Props props);
+2
View File
@@ -2,6 +2,8 @@
#include "string_button.hpp"
#include <climits>
namespace dusk::ui {
class NumberButton : public BaseStringButton {
+70
View File
@@ -6,6 +6,7 @@
#include "window.hpp"
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_timer.h>
#include <algorithm>
#include <dolphin/pad.h>
@@ -19,6 +20,7 @@ const Rml::String kDocumentSource = R"RML(
<link type="text/rcss" href="res/rml/overlay.rcss" />
</head>
<body>
<fps id="fps" />
</body>
</rml>
)RML";
@@ -31,6 +33,8 @@ constexpr std::array<std::pair<const char*, const char*>, 3> kAutoSaveLayers{{
constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500);
constexpr std::array<const char*, 4> kFpsCorners = { "tl", "tr", "bl", "br" };
Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
if (toast.type == "autosave") {
auto* logo = append(parent, "logo");
@@ -153,7 +157,46 @@ void remove_element(Rml::Element*& elem) noexcept {
} // namespace
// https://vplesko.com/posts/how_to_implement_an_fps_counter.html
void Overlay::advance_fps_counter(float& outFps, Uint64 perfFreq) {
if (perfFreq == 0) {
outFps = 0.f;
return;
}
const Uint64 curr = SDL_GetPerformanceCounter();
if (!mFpsHavePrevCounter) {
mFpsPrevCounter = curr;
mFpsHavePrevCounter = true;
outFps = 0.f;
return;
}
const Uint64 processingTicks = curr - mFpsPrevCounter;
mFpsPrevCounter = curr;
mFpsFrameEvents.push_back({curr, processingTicks});
mFpsSumTicks += processingTicks;
while (!mFpsFrameEvents.empty() && mFpsFrameEvents.front().endCounter + perfFreq < curr) {
mFpsSumTicks -= mFpsFrameEvents.front().processingTicks;
mFpsFrameEvents.pop_front();
}
const auto n = mFpsFrameEvents.size();
if (n == 0 || mFpsSumTicks == 0) {
outFps = 0.f;
return;
}
const double avgSeconds =
static_cast<double>(mFpsSumTicks) / static_cast<double>(n) / static_cast<double>(perfFreq);
outFps = static_cast<float>(1.0 / avgSeconds);
}
Overlay::Overlay() : Document(kDocumentSource) {
mFpsCounter = mDocument->GetElementById("fps");
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) {
@@ -187,6 +230,33 @@ void Overlay::update() {
return;
}
if (mFpsCounter != nullptr) {
if (getSettings().video.enableFpsOverlay.getValue()) {
const int idx = getSettings().video.fpsOverlayCorner.getValue();
mFpsCounter->SetAttribute("open", "");
mFpsCounter->SetAttribute("corner", kFpsCorners[idx]);
const Uint64 perfFreq = SDL_GetPerformanceFrequency();
float fps = 0.f;
advance_fps_counter(fps, perfFreq);
const Uint64 now = SDL_GetPerformanceCounter();
// Limit updates to twice per second
const bool refreshLabel = perfFreq == 0 || mFpsLastUpdate == 0 ||
static_cast<double>(now - mFpsLastUpdate) >= 0.5 * static_cast<double>(perfFreq);
if (refreshLabel) {
mFpsLastUpdate = now;
mFpsCounter->SetInnerRML(escape(fmt::format("{:.0f} FPS", fps)));
}
} else {
mFpsCounter->RemoveAttribute("open");
mFpsFrameEvents.clear();
mFpsSumTicks = 0;
mFpsHavePrevCounter = false;
mFpsLastUpdate = 0;
}
}
const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 &&
PADGetKeyButtonBindings(PAD_CHAN0, nullptr) == nullptr &&
dynamic_cast<Window*>(top_document()) == nullptr &&
+15
View File
@@ -3,6 +3,7 @@
#include "document.hpp"
#include <chrono>
#include <deque>
namespace dusk::ui {
@@ -16,11 +17,25 @@ public:
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
Rml::Element* mFpsCounter = nullptr;
Rml::Element* mCurrentToast = nullptr;
Rml::Element* mControllerWarning = nullptr;
Rml::Element* mMenuNotification = nullptr;
clock::time_point mCurrentToastStartTime;
clock::time_point mMenuNotificationStartTime;
struct FpsFrameEvent {
Uint64 endCounter;
Uint64 processingTicks;
};
std::deque<FpsFrameEvent> mFpsFrameEvents;
Uint64 mFpsSumTicks = 0;
bool mFpsHavePrevCounter = false;
Uint64 mFpsPrevCounter = 0;
Uint64 mFpsLastUpdate = 0;
void advance_fps_counter(float& outFps, Uint64 perfFreq);
};
} // namespace dusk::ui
+10 -1
View File
@@ -126,11 +126,20 @@ Component& Pane::register_control(
}
});
component.listen(component.root(), Rml::EventId::Focus,
[&component, &nextPane, callback = std::move(callback)](Rml::Event&) {
[this, &component, &nextPane, callback = std::move(callback)](Rml::Event&) {
if (component.disabled()) {
return;
}
nextPane.clear();
// If an item is already selected, deselect
for (const auto& child : mChildren) {
if (child->selected()) {
set_selected_item(-1);
break;
}
}
if (callback) {
callback(nextPane);
}
+145 -13
View File
@@ -5,12 +5,15 @@
#include "dusk/iso_validate.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "dusk/update_check.hpp"
#include "modal.hpp"
#include "preset.hpp"
#include "settings.hpp"
#include "version.h"
#include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_misc.h>
#include <aurora/lib/logging.hpp>
#include <aurora/lib/window.hpp>
#include <fmt/format.h>
@@ -54,7 +57,13 @@ const Rml::String kDocumentSource = R"RML(
</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>
<div id="update-status" class="update">
<span id="update-message"></span>
<button id="update-download">
<span id="update-download-label"></span>
&nbsp;<icon />
</button>
</div>
</version-info>
</content>
</body>
@@ -114,6 +123,44 @@ struct DiscVerificationTask {
std::unique_ptr<DiscVerificationTask> sDiscVerificationTask;
bool sDiscVerificationModalPushed = false;
struct UpdateCheckTask {
UpdateCheckTask() {
worker = std::thread([this] {
try {
result = update_check::check_latest_github_release("TwilitRealm", "dusk");
} catch (const std::exception& e) {
result = {
.status = update_check::Status::Failed,
.message = fmt::format("Update check failed with exception: {}", e.what()),
};
} catch (...) {
result = {
.status = update_check::Status::Failed,
.message = "Update check failed with an unknown exception",
};
}
done.store(true, std::memory_order_release);
});
}
~UpdateCheckTask() { join(); }
void join() {
if (worker.joinable()) {
worker.join();
}
}
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
update_check::Result result;
std::atomic_bool done = false;
std::thread worker;
};
std::unique_ptr<UpdateCheckTask> sUpdateCheckTask;
std::optional<update_check::Result> sUpdateCheckResult;
bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
return validation == iso::ValidationError::Unknown ||
validation == iso::ValidationError::Success ||
@@ -185,6 +232,52 @@ std::optional<DiscVerificationResult> take_finished_disc_verification() {
return result;
}
void begin_update_check() {
if (!getSettings().backend.checkForUpdates.getValue()) {
return;
}
if (sUpdateCheckTask != nullptr || sUpdateCheckResult.has_value()) {
return;
}
sUpdateCheckTask = std::make_unique<UpdateCheckTask>();
}
std::optional<update_check::Result> take_finished_update_check() {
if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) {
return std::nullopt;
}
sUpdateCheckTask->join();
auto result = std::move(sUpdateCheckTask->result);
sUpdateCheckTask.reset();
return result;
}
std::string update_release_label(const update_check::Release& release) {
std::string_view tagName = release.tagName;
if (!tagName.empty() && tagName.front() == 'v') {
tagName.remove_prefix(1);
}
return std::string(tagName);
}
void open_update_release() {
if (!sUpdateCheckResult.has_value() ||
sUpdateCheckResult->status != update_check::Status::UpdateAvailable)
{
return;
}
const std::string url = sUpdateCheckResult->latest.htmlUrl;
if (url.empty()) {
PrelaunchLog.warn("Update is available, but the release did not include a download URL");
return;
}
if (!SDL_OpenURL(url.c_str())) {
PrelaunchLog.warn("Failed to open update URL '{}': {}", url, SDL_GetError());
}
}
std::string get_error_msg(iso::ValidationError error) {
switch (error) {
default:
@@ -278,7 +371,7 @@ public:
mFileName = append(content, "div");
mFileName->SetClass("verification-file", true);
mProgress = append(content, "progressbar");
mProgress = append(content, "progress");
mProgress->SetClass("progress-ongoing", true);
mProgress->SetClass("verification-progress-bar", true);
mProgress->SetAttribute("value", 0.f);
@@ -352,8 +445,7 @@ private:
}
if (mFileName != nullptr) {
std::string fileName =
std::filesystem::path(sDiscVerificationTask->path).filename().string();
std::string fileName = display_name_for_path(sDiscVerificationTask->path);
if (fileName.empty()) {
fileName = sDiscVerificationTask->path;
}
@@ -507,7 +599,7 @@ void try_push_verification_modal(Document& host) {
},
.onDismiss = dismiss,
.variant = "danger",
.isWarning = true,
.icon = "warning",
}));
return;
}
@@ -523,7 +615,7 @@ void try_push_verification_modal(Document& host) {
},
},
.onDismiss = dismiss,
.isError = true,
.icon = "error",
}));
}
@@ -582,6 +674,7 @@ void try_apply_mirrored_layout(Rml::Element* body) {
Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) {
ensure_initialized();
begin_update_check();
if (auto* menuList = mDocument->GetElementById("menu-list")) {
auto& state = prelaunch_state();
@@ -610,9 +703,6 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
}
IsGameLaunched = true;
if (!getSettings().backend.wasPresetChosen) {
push_document(std::make_unique<dusk::ui::PresetWindow>());
}
hide(true);
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
@@ -624,7 +714,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit To Desktop"));
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit"));
mMenuButtons.back()->on_pressed([] { IsRunning = false; });
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
}
@@ -632,6 +722,23 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
mDiscStatus = mDocument->GetElementById("disc-status");
mDiscDetail = mDocument->GetElementById("disc-version");
mVersion = mDocument->GetElementById("version-text");
mUpdateStatus = mDocument->GetElementById("update-status");
mUpdateMessage = mDocument->GetElementById("update-message");
mUpdateDownload = mDocument->GetElementById("update-download");
mUpdateDownloadLabel = mDocument->GetElementById("update-download-label");
if (mUpdateDownload != nullptr) {
listen(mUpdateDownload, Rml::EventId::Click, [](Rml::Event& event) {
open_update_release();
event.StopPropagation();
});
listen(mUpdateDownload, Rml::EventId::Keydown, [](Rml::Event& event) {
if (map_nav_event(event) == NavCommand::Confirm) {
open_update_release();
event.StopPropagation();
}
});
}
try_apply_mirrored_layout(mDocument);
@@ -718,9 +825,6 @@ void Prelaunch::update() {
activeDiscLoaded && state.configuredDiscPath != state.activeDiscPath;
mDocument->SetClass("disc-ready", IsGameLaunched);
if (canLaunchConfiguredDisc) {
if (getSettings().backend.skipPreLaunchUI) {
hide(true);
}
IsGameLaunched = true;
}
@@ -773,6 +877,34 @@ void Prelaunch::update() {
}
mVersion->SetInnerRML(escape(versionStr));
}
if (mUpdateStatus != nullptr && mUpdateMessage != nullptr) {
if (auto result = take_finished_update_check()) {
if (result->status == update_check::Status::Failed) {
PrelaunchLog.error("Failed to check for updates: {}", result->message);
}
sUpdateCheckResult = std::move(*result);
}
if (sUpdateCheckTask != nullptr) {
mUpdateStatus->SetAttribute("state", "checking");
mUpdateMessage->SetInnerRML("Checking for updates...");
} else if (!sUpdateCheckResult.has_value() ||
sUpdateCheckResult->status == update_check::Status::UpToDate)
{
mUpdateStatus->RemoveAttribute("state");
mUpdateMessage->SetInnerRML("");
} else if (sUpdateCheckResult->status == update_check::Status::UpdateAvailable) {
mUpdateStatus->SetAttribute("state", "available");
mUpdateMessage->SetInnerRML("Update available!");
if (mUpdateDownloadLabel != nullptr) {
mUpdateDownloadLabel->SetInnerRML(escape(
fmt::format("Download {}", update_release_label(sUpdateCheckResult->latest))));
}
} else {
mUpdateStatus->SetAttribute("state", "failed");
mUpdateMessage->SetInnerRML("Failed to check for updates");
}
}
Document::update();
}
+4
View File
@@ -31,6 +31,10 @@ private:
Rml::Element* mDiscStatus = nullptr;
Rml::Element* mDiscDetail = nullptr;
Rml::Element* mVersion = nullptr;
Rml::Element* mUpdateStatus = nullptr;
Rml::Element* mUpdateMessage = nullptr;
Rml::Element* mUpdateDownload = nullptr;
Rml::Element* mUpdateDownloadLabel = nullptr;
};
class PrelaunchOptions;
+5 -2
View File
@@ -14,7 +14,8 @@ void applyPresetClassic() {
auto& s = getSettings();
s.video.lockAspectRatio.setValue(true);
s.game.bloomMode.setValue(BloomMode::Classic);
s.game.enableAchievementNotifications.setValue(false);
s.game.enableAchievementToasts.setValue(false);
s.game.enableControllerToasts.setValue(false);
s.game.internalResolutionScale.setValue(1);
s.game.shadowResolutionMultiplier.setValue(1);
s.game.hideTvSettingsScreen.setValue(false);
@@ -33,7 +34,8 @@ void applyPresetDusk() {
s.game.biggerWallets.setValue(true);
s.game.invertCameraXAxis.setValue(true);
s.game.no2ndFishForCat.setValue(true);
s.game.enableAchievementNotifications.setValue(true);
s.game.enableAchievementToasts.setValue(true);
s.game.enableControllerToasts.setValue(true);
s.game.enableQuickTransform.setValue(true);
s.game.instantSaves.setValue(true);
s.game.midnasLamentNonStop.setValue(true);
@@ -43,6 +45,7 @@ void applyPresetDusk() {
s.game.internalResolutionScale.setValue(0);
s.game.shadowResolutionMultiplier.setValue(4);
s.game.enableGyroAim.setValue(true);
s.game.autoSave.setValue(true);
}
} // namespace
+236 -27
View File
@@ -6,11 +6,14 @@
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/config.hpp"
#include "dusk/file_select.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/livesplit.h"
#include "graphics_tuner.hpp"
#include "m_Do/m_Do_main.h"
#include "menu_bar.hpp"
#include "number_button.hpp"
#include "menu_bar.hpp"
#include "pane.hpp"
#include "prelaunch.hpp"
#include "ui.hpp"
@@ -33,6 +36,18 @@ constexpr std::array kCardFileTypes = {
"GCI Folder",
};
constexpr std::array kFpsOverlayCornerNames = {
"Top Left",
"Top Right",
"Bottom Left",
"Bottom Right",
};
constexpr std::array kGyroInputModeLabels = {
"Sensor",
"Mouse",
};
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
if (backend == "auto") {
outBackend = BACKEND_AUTO;
@@ -167,6 +182,8 @@ void reset_for_speedrun_mode() {
getSettings().game.freeMagicArmor.setValue(false);
getSettings().game.enableTurboKeybind.setValue(false);
getSettings().game.debugFlyCam.setValue(false);
getSettings().game.autoSave.setValue(false);
}
const Rml::String kInternalResolutionHelpText =
@@ -189,7 +206,9 @@ int float_setting_percent(ConfigVar<float>& var) {
}
bool gyro_enabled() {
return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal;
return getSettings().game.enableGyroAim ||
(getSettings().game.enableGyroRollgoal &&
getSettings().game.gyroMode.getValue() != GyroMode::Mouse);
}
struct ConfigBoolProps {
@@ -307,7 +326,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
if (path.empty()) {
display = "(none)";
} else {
display = std::filesystem::path(path).filename().string();
display = display_name_for_path(path);
if (display.empty()) {
display = path;
}
@@ -332,9 +351,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.getValue =
[] {
const auto& state = prelaunch_state();
if (!state.configuredDiscCanLaunch ||
!state.configuredDiscInfo.isPal)
{
if (!state.configuredDiscCanLaunch || !state.configuredDiscInfo.isPal) {
return kLanguageNames[0];
}
const u8 idx = static_cast<u8>(getSettings().game.language.getValue());
@@ -430,7 +447,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
});
}
add_tab("Graphics", [this](Rml::Element* content) {
add_tab("Video", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
@@ -472,6 +489,57 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.key = "Pause on Focus Lost",
.isDisabled = [] { return IsMobile; },
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Show FPS Counter",
.getValue =
[] {
if (!getSettings().video.enableFpsOverlay.getValue()) {
return Rml::String{"Off"};
}
const int idx = getSettings().video.fpsOverlayCorner.getValue();
return Rml::String{kFpsOverlayCornerNames[idx]};
},
.isModified =
[] {
const auto& enable = getSettings().video.enableFpsOverlay;
const auto& corner = getSettings().video.fpsOverlayCorner;
return enable.getValue() != enable.getDefaultValue() ||
(enable.getValue() && corner.getValue() != corner.getDefaultValue());
},
}),
rightPane, [](Pane& pane) {
pane.add_button(
{
.text = "Off",
.isSelected =
[] { return !getSettings().video.enableFpsOverlay.getValue(); },
})
.on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().video.enableFpsOverlay.setValue(false);
config::Save();
});
for (int i = 0; i < static_cast<int>(kFpsOverlayCornerNames.size()); ++i) {
pane.add_button(
{
.text = kFpsOverlayCornerNames[i],
.isSelected =
[i] {
return getSettings().video.enableFpsOverlay.getValue() &&
getSettings().video.fpsOverlayCorner.getValue() == i;
},
})
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().video.enableFpsOverlay.setValue(true);
getSettings().video.fpsOverlayCorner.setValue(i);
config::Save();
});
}
pane.add_rml(
"<br/>Display the current framerate in a corner of the screen while playing.");
});
leftPane.add_section("Resolution");
graphics_tuner_control(*this, leftPane, rightPane,
@@ -553,6 +621,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
pane.clear();
pane.add_text("Open controller binding configuration.");
});
config_bool_select(leftPane, rightPane, getSettings().game.allowBackgroundInput,
{
.key = "Allow Background Input",
.helpText = "Allow controller input even when the game window is not focused.",
.onChange = [](bool value) { aurora_set_background_input(value); },
});
leftPane.add_section("Camera");
addOption("Free Camera", getSettings().game.freeCamera,
@@ -568,12 +642,50 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
[] { return !getSettings().game.freeCamera; });
leftPane.add_section("Gyro");
leftPane.register_control(
leftPane.add_select_button({
.key = "Gyro Input Method",
.getValue =
[] {
const auto mode = getSettings().game.gyroMode.getValue();
const auto idx = static_cast<size_t>(mode);
return Rml::String{kGyroInputModeLabels[idx]};
},
.isModified =
[] {
return getSettings().game.gyroMode.getValue() !=
getSettings().game.gyroMode.getDefaultValue();
},
}),
rightPane, [](Pane& pane) {
for (size_t i = 0; i < kGyroInputModeLabels.size(); i++) {
pane
.add_button({
.text = Rml::String{kGyroInputModeLabels[i]},
.isSelected =
[i] {
return getSettings().game.gyroMode.getValue() == static_cast<GyroMode>(i);
},
})
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
const GyroMode mode = static_cast<GyroMode>(i);
getSettings().game.gyroMode.setValue(mode);
config::Save();
});
}
pane.add_rml(
"<br/><b>Sensor</b> reads motion directly from a supported controller's gyro via SDL.<br/>"
"<br/><b>Mouse</b> treats mouse input as gyro, intended for use with the Steam Deck.<br/>"
"<br/>Mouse input cannot currently be used with Gyro Rollgoal.");
});
addOption("Gyro Aim", getSettings().game.enableGyroAim,
"Enables gyro controls while in look mode, aiming a hawk, and aiming "
"supported items.<br/><br/>Supported items include the Slingshot, Gale Boomerang, "
"Hero's Bow, Clawshot(s), Ball and Chain, and Dominion Rod.");
addOption("Gyro Rollgoal", getSettings().game.enableGyroRollgoal,
"Enables gyro controls for Rollgoal in Hena's Cabin.");
"Enables gyro controls for Rollgoal in Hena's Cabin.",
[] { return getSettings().game.gyroMode.getValue() == GyroMode::Mouse; });
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityY,
"Gyro Pitch Sensitivity", "Controls vertical gyro aiming sensitivity.", 25, 400, 5,
[] { return !gyro_enabled(); });
@@ -582,7 +694,11 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
[] { return !gyro_enabled(); });
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityRollgoal,
"Rollgoal Sensitivity", "Controls how strongly gyro input tilts the Rollgoal table.",
25, 400, 5, [] { return !getSettings().game.enableGyroRollgoal; });
25, 400, 5,
[] {
return !getSettings().game.enableGyroRollgoal ||
getSettings().game.gyroMode.getValue() == GyroMode::Mouse;
});
config_percent_select(leftPane, rightPane, getSettings().game.gyroDeadband, "Gyro Deadband",
"Ignores small gyro movement to reduce drift and jitter.", 0, 50, 1,
[] { return !gyro_enabled(); });
@@ -687,8 +803,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
leftPane.add_section("General");
addOption("Mirror Mode", getSettings().game.enableMirrorMode,
"Mirrors the world horizontally, matching the Wii version of the game.");
addOption("Disable Main HUD", getSettings().game.disableMainHUD,
"Disables the main HUD of the game.<br/>Useful for recording or a more immersive "
addOption("Minimal HUD", getSettings().game.minimalHUD,
"Disables the elements of the main HUD of the game.<br/>Useful for a more immersive "
"experience.");
addOption("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches,
"Restores patched glitches from Wii USA 1.0, the first released version.");
@@ -736,11 +852,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
config_bool_select(leftPane, rightPane, getSettings().game.autoSave,
{
.key = "Autosave",
.icon = "warning",
.helpText =
"Autosaves the game when going to a new area, opening a dungeon door, "
"or getting a new item.<br/><br/><icon class=\"warning\"/> Experimental "
"feature: Use at your own risk.",
.helpText = "Autosaves the game when going to a new area, opening a dungeon door, "
"or getting a new item.",
});
addOption("Instant Saves", getSettings().game.instantSaves,
"Skips the delay when writing to the Memory Card.");
@@ -815,7 +928,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
addCheat(
"Moon Jump (R+A)", getSettings().game.moonJump, "Hold R and A to rise into the air.");
addCheat("Super Clawshot", getSettings().game.superClawshot,
"Extends clawshot behavior beyond the normal game rules.");
"Extends Clawshot behavior beyond the normal game rules.");
addCheat("Always Greatspin", getSettings().game.alwaysGreatspin,
"Allows the Great Spin attack without requiring full health.");
addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots,
@@ -828,15 +941,74 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
"Lets the magic armor work without consuming rupees.");
});
// TODO: Reorganize all of this?
add_tab("Interface", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
config_bool_select(leftPane, rightPane, getSettings().game.enableAchievementNotifications,
{
.key = "Achievement Notifications",
.helpText = "Display a toast when an achievement is unlocked.",
leftPane.add_section("Dusk");
leftPane.register_control(
leftPane.add_select_button({
.key = "Notifications",
.getValue = [] {
const bool ach = getSettings().game.enableAchievementToasts.getValue();
const bool ctl = getSettings().game.enableControllerToasts.getValue();
if (!ach && !ctl) {
return Rml::String{"Off"};
}
if (ach && ctl) {
return Rml::String{"All"};
}
return Rml::String{"Some"};
},
.isModified = [] {
const auto& ach = getSettings().game.enableAchievementToasts;
const auto& ctl = getSettings().game.enableControllerToasts;
return ach.getValue() != ach.getDefaultValue() || ctl.getValue() != ctl.getDefaultValue();
},
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_button("Select All").on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().game.enableAchievementToasts.setValue(true);
getSettings().game.enableControllerToasts.setValue(true);
config::Save();
});
pane.add_button("Select None").on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().game.enableAchievementToasts.setValue(false);
getSettings().game.enableControllerToasts.setValue(false);
config::Save();
});
pane.add_section("Types");
pane.add_button(
{
.text = "Achievements",
.isSelected =
[] {
return getSettings().game.enableAchievementToasts.getValue();
},
})
.on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
auto& v = getSettings().game.enableAchievementToasts;
v.setValue(!v.getValue());
config::Save();
});
pane.add_button(
{
.text = "Controller",
.isSelected =
[] { return getSettings().game.enableControllerToasts.getValue(); },
})
.on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
auto& v = getSettings().game.enableControllerToasts;
v.setValue(!v.getValue());
config::Save();
});
pane.add_rml("<br/>Choose which notifications can be displayed.");
});
#if DUSK_ENABLE_SENTRY_NATIVE
config_bool_select(leftPane, rightPane, getSettings().backend.enableCrashReporting,
@@ -853,19 +1025,56 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
config_bool_select(leftPane, rightPane, getSettings().backend.skipPreLaunchUI,
{
.key = "Skip Dusk Main Menu",
.helpText = "When starting Dusk, skips the main menu and boots straight into the "
.helpText = "When starting Dusk, skip the main menu and boot straight into the "
"game if a disc image is available.",
});
config_bool_select(leftPane, rightPane, getSettings().game.hideTvSettingsScreen,
{
.key = "Skip TV Settings Screen",
.helpText = "Skips the TV calibration screen shown when loading a save.",
});
config_bool_select(leftPane, rightPane, getSettings().backend.showPipelineCompilation,
{
.key = "Show Pipeline Compilation",
.helpText = "Show an overlay when shaders are being compiled for your hardware.",
});
config_bool_select(leftPane, rightPane, getSettings().backend.checkForUpdates,
{
.key = "Check for Updates",
.helpText = "Checks GitHub releases for a new Dusk version on startup.<br/><br/>"
"No personal information is transmitted or collected.",
});
config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
{
.key = "Pause On Focus Lost",
.helpText = "Pause the game when window focus is lost.",
.onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); },
});
config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings,
{
.key = "Enable Advanced Settings",
.icon = "warning",
.helpText = "Show advanced settings and debugging tools with "
"Shift+F1.<br/><br/><icon class=\"warning\"/> WARNING: Debugging tools "
"can easily break your game. Do not use on a regular save!",
.onChange =
[](bool) {
for (auto& doc : get_document_stack()) {
if (dynamic_cast<MenuBar*>(doc.get())) {
doc = std::make_unique<MenuBar>();
break;
}
}
},
});
leftPane.add_section("Game");
config_bool_select(leftPane, rightPane, getSettings().game.hideTvSettingsScreen,
{
.key = "Skip TV Settings Screen",
.helpText = "Skips the TV calibration screen shown when loading a save.",
});
config_bool_select(leftPane, rightPane, getSettings().game.recordingMode,
{
.key = "Recording Mode",
.helpText = "Disables the game HUD and all background music.<br/><br/>Useful for "
"recording footage.",
});
});
}
+11 -1
View File
@@ -212,6 +212,15 @@ bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (activeTab != -1) {
currentComponent = activeTab;
}
} else {
int activeTab = tab_containing(event.GetTargetElement());
if (activeTab != -1) {
currentComponent = activeTab;
} else if (mLastFocusedTabIndex >= 0 &&
mLastFocusedTabIndex < static_cast<int>(mTabs.size()))
{
currentComponent = mLastFocusedTabIndex;
}
}
int direction = isNext ? 1 : -1;
if (currentComponent == -1) {
@@ -221,8 +230,9 @@ bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
return false;
}
currentComponent = -1;
} else if (cmd == NavCommand::Next) {
currentComponent = -1;
} else {
// Next/Previous require a currently selected tab to navigate from
return false;
}
}
+39 -35
View File
@@ -125,43 +125,47 @@ void handle_event(const SDL_Event& event) noexcept {
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
auto* gamepad = SDL_GetGamepadFromID(event.gdevice.which);
if (SDL_GamepadConnected(gamepad)) {
const char* name = SDL_GetGamepadName(gamepad);
Rml::String content = fmt::format("<span>{}</span>", name ? name : "[Unknown]");
Rml::String title = "Controller connected";
if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
title = fmt::format(
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>", title,
icon);
}
int batteryLevel = -1;
const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
if (powerState != SDL_POWERSTATE_UNKNOWN) {
content = fmt::format(
"<row>{}</row><row class=\"muted\"><icon class=\"battery\">&#x{};</icon>",
content, battery_icon(powerState, batteryLevel));
if (batteryLevel > -1) {
content = fmt::format("{}&nbsp;<span>{}%</span>", content, batteryLevel);
if (getSettings().game.enableControllerToasts) {
const char* name = SDL_GetGamepadName(gamepad);
Rml::String content = fmt::format("<span>{}</span>", name ? name : "[Unknown]");
Rml::String title = "Controller connected";
if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
title = fmt::format(
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>", title,
icon);
}
content += "</row>";
int batteryLevel = -1;
const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
if (powerState != SDL_POWERSTATE_UNKNOWN) {
content = fmt::format(
"<row>{}</row><row class=\"muted\"><icon class=\"battery\">&#x{};</icon>",
content, battery_icon(powerState, batteryLevel));
if (batteryLevel > -1) {
content = fmt::format("{}&nbsp;<span>{}%</span>", content, batteryLevel);
}
content += "</row>";
}
push_toast({
.type = "controller",
.title = title,
.content = content,
.duration = std::chrono::seconds(4),
});
}
push_toast({
.type = "controller",
.title = title,
.content = content,
.duration = std::chrono::seconds(4),
});
sConnectedGamepads.insert(event.gdevice.which);
}
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED &&
sConnectedGamepads.contains(event.gdevice.which))
{
const char* name = SDL_GetGamepadNameForID(event.gdevice.which);
push_toast({
.type = "controller",
.title = "Controller disconnected",
.content = name ? name : "[Unknown]",
.duration = std::chrono::seconds(4),
});
if (getSettings().game.enableControllerToasts) {
const char* name = SDL_GetGamepadNameForID(event.gdevice.which);
push_toast({
.type = "controller",
.title = "Controller disconnected",
.content = name ? name : "[Unknown]",
.duration = std::chrono::seconds(4),
});
}
sConnectedGamepads.erase(event.gdevice.which);
}
input::handle_event(event);
@@ -255,11 +259,7 @@ void update() noexcept {
}
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {
const char* basePath = SDL_GetBasePath();
if (basePath == nullptr) {
return std::filesystem::path("res") / filename;
}
return std::filesystem::path(basePath) / "res" / filename;
return std::filesystem::path("res") / filename;
}
std::string escape(std::string_view str) noexcept {
@@ -363,6 +363,10 @@ void push_toast(Toast toast) noexcept {
sToasts.push_back(std::move(toast));
}
std::vector<std::unique_ptr<Document> >& get_document_stack() noexcept {
return sDocumentStack;
}
std::deque<Toast>& get_toasts() noexcept {
return sToasts;
}
+2
View File
@@ -82,6 +82,8 @@ Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept;
NavCommand map_nav_event(const Rml::Event& event) noexcept;
Insets safe_area_insets(Rml::Context* context) noexcept;
std::vector<std::unique_ptr<Document> >& get_document_stack() noexcept;
void push_toast(Toast toast) noexcept;
std::deque<Toast>& get_toasts() noexcept;
void show_menu_notification() noexcept;
+1 -4
View File
@@ -16,10 +16,7 @@ namespace dusk::ui {
namespace {
float base_body_padding(Rml::Context* context) noexcept {
if (context == nullptr) {
return 64.0f;
}
const float dpRatio = std::max(context->GetDensityIndependentPixelRatio(), 0.001f);
const float dpRatio = context->GetDensityIndependentPixelRatio();
const float heightDp = static_cast<float>(context->GetDimensions().y) / dpRatio;
if (heightDp <= 640.0f) {
return 16.0f * dpRatio;
+351
View File
@@ -0,0 +1,351 @@
#include "update_check.hpp"
#include "dusk/http/http.hpp"
#include "fmt/format.h"
#include "nlohmann/json.hpp"
#include "version.h"
#include <algorithm>
#include <charconv>
#include <optional>
#include <string_view>
#include <utility>
#include <vector>
namespace dusk::update_check {
namespace {
using json = nlohmann::json;
constexpr std::string_view GitHubApiVersion = "2026-03-10";
struct Version {
int major = 0;
int minor = 0;
int patch = 0;
std::vector<std::string_view> prerelease;
};
std::string json_string(const json& value, const char* key) {
const auto iter = value.find(key);
if (iter == value.end() || !iter->is_string()) {
return {};
}
return iter->get<std::string>();
}
std::optional<int> parse_component(std::string_view& value) {
if (value.empty() || value.front() < '0' || value.front() > '9') {
return std::nullopt;
}
int parsed = 0;
const char* begin = value.data();
const char* end = value.data() + value.size();
const auto [ptr, ec] = std::from_chars(begin, end, parsed);
if (ec != std::errc()) {
return std::nullopt;
}
value.remove_prefix(static_cast<size_t>(ptr - begin));
return parsed;
}
bool consume(std::string_view& value, char expected) {
if (value.empty() || value.front() != expected) {
return false;
}
value.remove_prefix(1);
return true;
}
bool is_digit(char value) {
return value >= '0' && value <= '9';
}
bool is_identifier_char(char value) {
return is_digit(value) || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || value == '-';
}
bool is_numeric_identifier(std::string_view value) {
if (value.empty()) {
return false;
}
for (const char c : value) {
if (!is_digit(c)) {
return false;
}
}
return true;
}
bool is_identifier_list(std::string_view value) {
if (value.empty()) {
return false;
}
bool expectingIdentifier = true;
for (const char c : value) {
if (c == '.') {
if (expectingIdentifier) {
return false;
}
expectingIdentifier = true;
continue;
}
if (!is_identifier_char(c)) {
return false;
}
expectingIdentifier = false;
}
return !expectingIdentifier;
}
std::string_view trim_git_describe_suffix(std::string_view value) {
if (value.ends_with("-dirty")) {
value.remove_suffix(6);
}
if (is_numeric_identifier(value)) {
return {};
}
const size_t suffixStart = value.rfind('-');
if (suffixStart != std::string_view::npos && value.substr(0, suffixStart).find('.') != std::string_view::npos
&& is_numeric_identifier(value.substr(suffixStart + 1))) {
value.remove_suffix(value.size() - suffixStart);
}
return value;
}
void split_identifiers(std::string_view value, std::vector<std::string_view>& identifiers) {
while (!value.empty()) {
const size_t separator = value.find('.');
if (separator == std::string_view::npos) {
identifiers.push_back(value);
return;
}
identifiers.push_back(value.substr(0, separator));
value.remove_prefix(separator + 1);
}
}
std::string_view trim_leading_zeroes(std::string_view value) {
while (value.size() > 1 && value.front() == '0') {
value.remove_prefix(1);
}
return value;
}
int compare_identifier(std::string_view lhs, std::string_view rhs) {
const bool lhsNumeric = is_numeric_identifier(lhs);
const bool rhsNumeric = is_numeric_identifier(rhs);
if (lhsNumeric && rhsNumeric) {
lhs = trim_leading_zeroes(lhs);
rhs = trim_leading_zeroes(rhs);
if (lhs.size() != rhs.size()) {
return lhs.size() < rhs.size() ? -1 : 1;
}
} else if (lhsNumeric != rhsNumeric) {
return lhsNumeric ? -1 : 1;
}
const int result = lhs.compare(rhs);
if (result < 0) {
return -1;
}
if (result > 0) {
return 1;
}
return 0;
}
int compare_version(const Version& lhs, const Version& rhs) {
if (lhs.major != rhs.major) {
return lhs.major < rhs.major ? -1 : 1;
}
if (lhs.minor != rhs.minor) {
return lhs.minor < rhs.minor ? -1 : 1;
}
if (lhs.patch != rhs.patch) {
return lhs.patch < rhs.patch ? -1 : 1;
}
if (lhs.prerelease.empty() != rhs.prerelease.empty()) {
return lhs.prerelease.empty() ? 1 : -1;
}
const size_t commonSize = std::min(lhs.prerelease.size(), rhs.prerelease.size());
for (size_t i = 0; i < commonSize; ++i) {
const int result = compare_identifier(lhs.prerelease[i], rhs.prerelease[i]);
if (result != 0) {
return result;
}
}
if (lhs.prerelease.size() != rhs.prerelease.size()) {
return lhs.prerelease.size() < rhs.prerelease.size() ? -1 : 1;
}
return 0;
}
std::optional<Version> parse_version(std::string_view value) {
if (!value.empty() && value.front() == 'v') {
value.remove_prefix(1);
}
Version version;
auto major = parse_component(value);
if (!major || !consume(value, '.')) {
return std::nullopt;
}
auto minor = parse_component(value);
if (!minor || !consume(value, '.')) {
return std::nullopt;
}
auto patch = parse_component(value);
if (!patch) {
return std::nullopt;
}
version.major = *major;
version.minor = *minor;
version.patch = *patch;
if (value.empty()) {
return version;
}
if (value.front() == '+') {
value.remove_prefix(1);
if (!is_identifier_list(value)) {
return std::nullopt;
}
return version;
}
if (!consume(value, '-')) {
return std::nullopt;
}
const size_t buildStart = value.find('+');
std::string_view prerelease = value.substr(0, buildStart);
if (!is_identifier_list(prerelease)) {
return std::nullopt;
}
if (buildStart != std::string_view::npos && !is_identifier_list(value.substr(buildStart + 1))) {
return std::nullopt;
}
prerelease = trim_git_describe_suffix(prerelease);
if (!prerelease.empty()) {
split_identifiers(prerelease, version.prerelease);
}
return version;
}
Release parse_release(const json& value) {
Release release{
.tagName = json_string(value, "tag_name"),
.name = json_string(value, "name"),
.htmlUrl = json_string(value, "html_url"),
.body = json_string(value, "body"),
};
const auto assets = value.find("assets");
if (assets != value.end() && assets->is_array()) {
for (const auto& asset : *assets) {
if (!asset.is_object()) {
continue;
}
release.assets.push_back({
.name = json_string(asset, "name"),
.browserDownloadUrl = json_string(asset, "browser_download_url"),
.digest = json_string(asset, "digest"),
});
}
}
return release;
}
std::string release_url(std::string_view owner, std::string_view repo) {
return fmt::format("https://api.github.com/repos/{}/{}/releases/latest", owner, repo);
}
std::string user_agent() {
return fmt::format("Dusk/{}", DUSK_WC_DESCRIBE);
}
} // namespace
Result check_latest_github_release(std::string_view owner, std::string_view repo) {
if (!http::available()) {
return {
.status = Status::Disabled,
.message = "No HTTP backend is available",
};
}
if (owner.empty() || repo.empty()) {
return {
.status = Status::Failed,
.message = "GitHub owner and repo are required",
};
}
http::Request request{
.url = release_url(owner, repo),
.headers =
{
{.name = "User-Agent", .value = user_agent()},
{.name = "Accept", .value = "application/vnd.github+json"},
{.name = "X-GitHub-Api-Version", .value = std::string(GitHubApiVersion)},
},
};
http::Result result = http::get(request);
if (result.error != http::Error::None) {
return {
.status = Status::Failed,
.message = result.message,
};
}
if (result.response.statusCode != 200) {
return {
.status = Status::Failed,
.message = fmt::format("GitHub returned HTTP {}", result.response.statusCode),
};
}
Release latest;
try {
latest = parse_release(json::parse(result.response.body));
} catch (const std::exception& e) {
return {
.status = Status::Failed,
.message = fmt::format("Failed to parse GitHub release JSON: {}", e.what()),
};
}
const std::optional<Version> latestVersion = parse_version(latest.tagName);
const std::optional<Version> currentVersion = parse_version(DUSK_WC_DESCRIBE);
if (!latestVersion) {
return {
.status = Status::Failed,
.message = fmt::format("Failed to parse release tag '{}'", latest.tagName),
.latest = std::move(latest),
};
}
if (!currentVersion) {
return {
.status = Status::Failed,
.message = fmt::format("Failed to parse Dusk version '{}'", DUSK_WC_DESCRIBE),
.latest = std::move(latest),
};
}
const bool updateAvailable = compare_version(*latestVersion, *currentVersion) > 0;
return {
.status = updateAvailable ? Status::UpdateAvailable : Status::UpToDate,
.message = updateAvailable ? "Update available" : "Dusk is up to date",
.latest = std::move(latest),
};
}
} // namespace dusk::update_check

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