Compare commits

..

25 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
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
73 changed files with 3788 additions and 346 deletions
+45 -7
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
@@ -109,6 +111,7 @@ 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)
@@ -284,7 +287,7 @@ 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 MTX_USE_PS=1)
@@ -314,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;
+16 -2
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
@@ -102,8 +113,8 @@ struct UserSettings {
ConfigVar<bool> minimalHUD;
ConfigVar<bool> pauseOnFocusLost;
ConfigVar<bool> enableLinkDollRotation;
ConfigVar<bool> enableAchievementNotifications;
ConfigVar<bool> enableAchievementToasts;
ConfigVar<bool> enableControllerToasts;
// Graphics
ConfigVar<BloomMode> bloomMode;
@@ -120,6 +131,7 @@ struct UserSettings {
ConfigVar<bool> midnasLamentNonStop;
// Input
ConfigVar<GyroMode> gyroMode;
ConfigVar<bool> enableGyroAim;
ConfigVar<bool> enableGyroRollgoal;
ConfigVar<float> gyroSensitivityX;
@@ -135,6 +147,7 @@ struct UserSettings {
ConfigVar<float> freeCameraSensitivity;
ConfigVar<bool> debugFlyCam;
ConfigVar<bool> debugFlyCamLockEvents;
ConfigVar<bool> allowBackgroundInput;
// Cheats
ConfigVar<bool> infiniteHearts;
@@ -172,6 +185,7 @@ 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;
+27 -12
View File
@@ -163,30 +163,30 @@ 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);
}
@@ -274,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;
}
}
+107 -15
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;
@@ -334,8 +374,8 @@ body.animate-in .intro-item {
}
menu {
left: 20dp;
right: 20dp;
left: 32dp;
right: 32dp;
width: auto;
min-width: 0;
max-width: none;
@@ -346,8 +386,8 @@ body.animate-in .intro-item {
}
body.mirrored menu {
left: 20dp;
right: 20dp;
left: 32dp;
right: 32dp;
flex-direction: row-reverse;
}
@@ -355,7 +395,7 @@ body.animate-in .intro-item {
flex: 1 1 0;
min-width: 0;
max-width: 48%;
margin-left: 32dp;
}
body.mirrored hero {
@@ -397,9 +437,61 @@ body.animate-in .intro-item {
decorator: horizontal-gradient(#FEE685FF #FEE68500);
}
.eyebrow,
disc-info,
version-info {
.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;
}
}
+14
View File
@@ -17,6 +17,7 @@ body {
window {
display: flex;
flex-flow: column;
position: relative;
height: 100%;
width: 100%;
max-width: 1088dp;
@@ -291,6 +292,19 @@ 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;
+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
+51 -12
View File
@@ -11262,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;
@@ -11315,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++) {
+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
-7
View File
@@ -253,13 +253,6 @@ 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)) {
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen);
+1
View File
@@ -45,6 +45,7 @@ namespace dusk {
getSettings().game.enableTurboKeybind.setValue(false);
getSettings().game.debugFlyCam.setValue(false);
getSettings().game.autoSave.setValue(false);
}
SpeedrunInfo m_speedrunInfo;
+10 -2
View File
@@ -50,7 +50,8 @@ UserSettings g_userSettings = {
.minimalHUD {"game.minimalHUD", false},
.pauseOnFocusLost {"game.pauseOnFocusLost", false},
.enableLinkDollRotation {"game.enableLinkDollRotation", false},
.enableAchievementNotifications {"game.enableAchievementNotifications", true},
.enableAchievementToasts {"game.enableAchievementToasts", true},
.enableControllerToasts {"game.enableControllerToasts", true},
// Graphics
.bloomMode {"game.bloomMode", BloomMode::Dusk},
@@ -67,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},
@@ -82,6 +84,7 @@ UserSettings g_userSettings = {
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
.debugFlyCam {"game.debugFlyCam", false},
.debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true},
.allowBackgroundInput {"game.allowBackgroundInput", true},
// Cheats
.infiniteHearts {"game.infiniteHearts", false},
@@ -119,6 +122,7 @@ UserSettings g_userSettings = {
.showPipelineCompilation {"backend.showPipelineCompilation", false},
.wasPresetChosen {"backend.wasPresetChosen", false},
.enableCrashReporting {"backend.enableCrashReporting", true},
.checkForUpdates {"backend.checkForUpdates", true},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)},
.enableAdvancedSettings {"backend.enableAdvancedSettings", false},
}
@@ -182,7 +186,8 @@ 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);
@@ -202,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);
@@ -214,6 +220,7 @@ void registerSettings() {
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);
@@ -222,6 +229,7 @@ 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);
}
+25 -2
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"},
@@ -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
+12 -2
View File
@@ -68,12 +68,17 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
{
ModalAction{
.label = "Cancel",
.onPressed = dismiss,
.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;
@@ -98,12 +103,17 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
{
ModalAction{
.label = "Cancel",
.onPressed = dismiss,
.onPressed =
[dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundWindowClose);
dismiss(modal);
},
},
ModalAction{
.label = "Quit",
.onPressed =
[dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundClick);
dismiss(modal);
IsRunning = false;
},
+2
View File
@@ -36,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() {
+141 -3
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:
@@ -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;
}
@@ -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();
@@ -629,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);
@@ -767,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;
+4 -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);
+139 -9
View File
@@ -6,12 +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"
@@ -41,6 +43,11 @@ constexpr std::array kFpsOverlayCornerNames = {
"Bottom Right",
};
constexpr std::array kGyroInputModeLabels = {
"Sensor",
"Mouse",
};
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
if (backend == "auto") {
outBackend = BACKEND_AUTO;
@@ -175,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 =
@@ -197,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 {
@@ -315,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;
}
@@ -610,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,
@@ -625,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(); });
@@ -639,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(); });
@@ -869,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,
@@ -887,10 +946,69 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.add_section("Dusk");
config_bool_select(leftPane, rightPane, getSettings().game.enableAchievementNotifications,
{
.key = "Achievement Notifications",
.helpText = "Display a toast when an achievement is unlocked.",
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,
@@ -915,6 +1033,18 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.key = "Show Pipeline Compilation",
.helpText = "Show an overlay when shaders are being compiled for your hardware.",
});
config_bool_select(leftPane, rightPane, getSettings().backend.checkForUpdates,
{
.key = "Check for Updates",
.helpText = "Checks GitHub releases for a new Dusk version on startup.<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",
+35 -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 {
+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
+41
View File
@@ -0,0 +1,41 @@
#ifndef DUSK_UPDATE_CHECK_HPP
#define DUSK_UPDATE_CHECK_HPP
#include <string>
#include <string_view>
#include <vector>
namespace dusk::update_check {
enum class Status {
Disabled,
UpToDate,
UpdateAvailable,
Failed,
};
struct Asset {
std::string name;
std::string browserDownloadUrl;
std::string digest;
};
struct Release {
std::string tagName;
std::string name;
std::string htmlUrl;
std::string body;
std::vector<Asset> assets;
};
struct Result {
Status status = Status::Failed;
std::string message;
Release latest;
};
Result check_latest_github_release(std::string_view owner, std::string_view repo);
} // namespace dusk::update_check
#endif // DUSK_UPDATE_CHECK_HPP
+2
View File
@@ -14,6 +14,7 @@
#include "d/actor/d_a_midna.h"
#include "d/d_model.h"
#include "d/d_tresure.h"
#include "dusk/achievements.h"
#include "dusk/frame_interpolation.h"
#include "dusk/livesplit.h"
#include "dusk/logging.h"
@@ -828,6 +829,7 @@ void fapGm_Execute() {
cCt_Counter(0);
#ifdef TARGET_PC
dusk::speedrun::onGameFrame();
dusk::AchievementSystem::get().tick();
#endif
}
+8
View File
@@ -502,6 +502,14 @@ void mDoGph_gInf_c::calcFade() {
}
if (mFadeColor.a != 0) {
#ifdef TARGET_PC
if (dusk::frame_interp::is_enabled() && mFade != 0) {
const auto step = dusk::frame_interp::get_interpolation_step();
const auto progress = mFadeSpeed < 0.0f ? 1.0f - mFadeRate : mFadeRate;
const auto fade_amt = mFadeRate + mFadeSpeed * (step - 1.0f + progress);
mFadeColor.a = 255.0f * std::clamp(fade_amt, 0.0f, 1.0f);
}
#endif
darwFilter(mFadeColor);
}
}
+31 -21
View File
@@ -46,7 +46,6 @@
#include <system_error>
#include <thread>
#include "SSystem/SComponent/c_API.h"
#include "dusk/achievements.h"
#include "dusk/app_info.hpp"
#include "dusk/crash_reporting.h"
#include "dusk/dusk.h"
@@ -106,7 +105,6 @@ const int audioHeapSize = 0x14D800;
bool dusk::IsRunning = true;
bool dusk::IsShuttingDown = false;
bool dusk::IsGameLaunched = false;
bool dusk::IsFocusPaused = false;
bool dusk::RestartRequested = false;
std::filesystem::path dusk::ConfigPath;
#endif
@@ -229,19 +227,16 @@ void main01(void) {
switch (event->type) {
case AURORA_NONE:
goto eventsDone;
case AURORA_PAUSED:
dusk::audio::SetPaused(true);
break;
case AURORA_UNPAUSED:
dusk::audio::SetPaused(false);
dusk::game_clock::reset_frame_timer();
break;
case AURORA_SDL_EVENT:
dusk::ui::handle_event(event->sdl);
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST &&
dusk::getSettings().game.pauseOnFocusLost) {
dusk::IsFocusPaused = true;
dusk::audio::SetPaused(true);
} else if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_GAINED &&
dusk::IsFocusPaused) {
dusk::IsFocusPaused = false;
dusk::audio::SetPaused(false);
dusk::game_clock::reset_frame_timer();
}
break;
case AURORA_DISPLAY_SCALE_CHANGED:
dusk::ImGuiEngine_Initialize(event->windowSize.scale);
@@ -255,19 +250,14 @@ void main01(void) {
eventsDone:;
if (dusk::IsFocusPaused) {
std::this_thread::sleep_for(std::chrono::milliseconds(16));
if (!aurora_begin_frame()) {
DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame");
continue;
}
VIWaitForRetrace();
dusk::lastFrameAuroraStats = *aurora_get_stats();
if (!aurora_begin_frame()) {
DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame");
continue;
}
mDoGph_gInf_c::updateRenderSize();
dusk::ui::update();
@@ -283,7 +273,6 @@ void main01(void) {
dusk::gyro::read(pacing.sim_pace);
fapGm_Execute();
mDoAud_Execute();
dusk::AchievementSystem::get().tick();
dusk::game_clock::commit_sim_tick();
}
}
@@ -573,7 +562,8 @@ int game_main(int argc, char* argv[]) {
config.logLevel = startupLogLevel;
config.mem1Size = 256 * 1024 * 1024;
config.mem2Size = 24 * 1024 * 1024;
config.allowJoystickBackgroundEvents = true;
config.allowJoystickBackgroundEvents = dusk::getSettings().game.allowBackgroundInput;
config.pauseOnFocusLost = dusk::getSettings().game.pauseOnFocusLost;
config.imGuiInitCallback = &aurora_imgui_init_callback;
config.allowTextureReplacements = true;
config.allowTextureDumps = false;
@@ -620,13 +610,19 @@ int game_main(int argc, char* argv[]) {
// Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up.
// This is only a metadata check; full hash verification is handled by the prelaunch UI.
bool forcePreLaunchUI = false;
bool saveConfigBeforePrelaunch = false;
const std::string p = dusk::getSettings().backend.isoPath;
dusk::iso::DiscInfo discInfo{};
if (!p.empty() &&
dusk::iso::inspect(p.c_str(), discInfo) != dusk::iso::ValidationError::Success)
{
DuskLog.warn("Saved DVD image path failed validation, clearing configured path: {}", p);
dusk::getSettings().backend.isoPath.setValue("");
dusk::getSettings().backend.isoVerification.setValue(dusk::DiscVerificationState::Unknown);
forcePreLaunchUI = true;
saveConfigBeforePrelaunch = true;
}
std::string dvd_path;
@@ -638,6 +634,7 @@ int game_main(int argc, char* argv[]) {
dvd_opened = aurora_dvd_open(dvd_path.c_str());
if (!dvd_opened) {
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
forcePreLaunchUI = true;
} else {
dusk::getSettings().backend.isoPath.setValue(dvd_path);
dusk::getSettings().backend.isoVerification.setValue(
@@ -647,10 +644,23 @@ int game_main(int argc, char* argv[]) {
}
} else {
DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path);
forcePreLaunchUI = true;
}
}
if (!dvd_opened) {
if (dusk::getSettings().backend.isoPath.getValue().empty()) {
forcePreLaunchUI = true;
}
if (forcePreLaunchUI && dusk::getSettings().backend.skipPreLaunchUI.getValue()) {
DuskLog.warn("Prelaunch UI was disabled with no usable DVD image, enabling prelaunch UI");
dusk::getSettings().backend.skipPreLaunchUI.setValue(false);
saveConfigBeforePrelaunch = true;
}
if (saveConfigBeforePrelaunch) {
dusk::config::Save();
}
if (!dusk::getSettings().backend.skipPreLaunchUI) {
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);