Merge remote-tracking branch 'upstream/main' into mods
@@ -67,9 +67,9 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
|
||||
path: |
|
||||
build/install/Dusk-*.AppImage
|
||||
build/install/Dusklight-*.AppImage
|
||||
build/install/debug.tar.*
|
||||
|
||||
build-apple:
|
||||
@@ -135,9 +135,9 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
|
||||
name: dusklight-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
|
||||
path: |
|
||||
build/install/Dusk.app
|
||||
build/install/Dusklight.app
|
||||
build/install/debug.tar.*
|
||||
|
||||
build-android:
|
||||
@@ -192,7 +192,7 @@ jobs:
|
||||
run: cmake --preset ${{matrix.preset}}
|
||||
|
||||
- name: Build native library
|
||||
run: cmake --build --preset ${{matrix.preset}} --target dusk
|
||||
run: cmake --build --preset ${{matrix.preset}} --target dusklight
|
||||
|
||||
- name: Stage stripped JNI library
|
||||
run: ANDROID_STAGE_ABIS="${{matrix.abi}}" platforms/android/scripts/stage-jni-libs.sh
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusk-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}}
|
||||
path: platforms/android/app/build/outputs/apk/release/app-${{matrix.abi}}-release-unsigned.apk
|
||||
|
||||
build-windows:
|
||||
@@ -266,7 +266,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
|
||||
path: |
|
||||
build/install/*.exe
|
||||
build/install/*.dll
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "(gdb) Launch Dusk MSVC",
|
||||
"name": "(gdb) Launch Dusklight MSVC",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${command:cmake.launchTargetPath}",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"cmake.buildDirectory": "${workspaceFolder}/build/dusk/${buildType}/${variant:tp_version}",
|
||||
"cmake.buildDirectory": "${workspaceFolder}/build/dusklight/${buildType}/${variant:tp_version}",
|
||||
"cmake.generator": "Ninja",
|
||||
"cmake.configureSettings": {
|
||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
|
||||
|
||||
@@ -67,9 +67,9 @@ endif ()
|
||||
if(DEFINED ENV{GITHUB_ENV})
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
|
||||
endif()
|
||||
message(STATUS "Dusk version set to ${DUSK_WC_DESCRIBE}")
|
||||
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
|
||||
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
||||
project(dusk LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
|
||||
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
|
||||
if (APPLE)
|
||||
enable_language(OBJC OBJCXX)
|
||||
endif ()
|
||||
@@ -141,9 +141,9 @@ endif ()
|
||||
if (DUSK_MOVIE_SUPPORT)
|
||||
find_package(libjpeg-turbo 3.0 CONFIG QUIET)
|
||||
if (libjpeg-turbo_FOUND)
|
||||
message(STATUS "dusk: Using system libjpeg-turbo")
|
||||
message(STATUS "dusklight: Using system libjpeg-turbo")
|
||||
else ()
|
||||
message(STATUS "dusk: Fetching libjpeg-turbo")
|
||||
message(STATUS "dusklight: Fetching libjpeg-turbo")
|
||||
include(ExternalProject)
|
||||
set(_jpeg_install_dir ${CMAKE_BINARY_DIR}/libjpeg-turbo-install)
|
||||
if (WIN32)
|
||||
@@ -233,15 +233,14 @@ endif ()
|
||||
include(FetchContent)
|
||||
|
||||
# Declare all dependencies first so CMake can download them in parallel
|
||||
message(STATUS "dusk: Fetching cxxopts")
|
||||
message(STATUS "dusklight: Fetching cxxopts")
|
||||
FetchContent_Declare(cxxopts
|
||||
URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.3.1.tar.gz
|
||||
URL_HASH SHA256=3bfc70542c521d4b55a46429d808178916a579b28d048bd8c727ee76c39e2072
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
|
||||
message(STATUS "dusk: Fetching nlohmann/json")
|
||||
message(STATUS "dusklight: Fetching nlohmann/json")
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz
|
||||
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
|
||||
@@ -277,7 +276,7 @@ set(FUNCHOOK_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(cxxopts json miniz funchook)
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
message(STATUS "dusk: Fetching sentry-native")
|
||||
message(STATUS "dusklight: Fetching sentry-native")
|
||||
set(SENTRY_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(SENTRY_BACKEND crashpad CACHE STRING "" FORCE)
|
||||
if (WIN32)
|
||||
@@ -322,15 +321,15 @@ include(files.cmake)
|
||||
|
||||
# TODO: version handling for res includes
|
||||
|
||||
set(DUSK_BUNDLE_NAME Dusk)
|
||||
set(DUSK_BUNDLE_NAME Dusklight)
|
||||
set(DUSK_BUNDLE_IDENTIFIER dev.twilitrealm.dusk)
|
||||
set(DUSK_COMPANY_NAME "Twilit Realm")
|
||||
set(DUSK_FILE_DESCRIPTION "Dusk")
|
||||
set(DUSK_PRODUCT_NAME "Dusk")
|
||||
set(DUSK_FILE_DESCRIPTION "Dusklight")
|
||||
set(DUSK_PRODUCT_NAME "Dusklight")
|
||||
set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
|
||||
|
||||
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
|
||||
source_group("dusk" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
|
||||
source_group("dusklight" 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)
|
||||
|
||||
@@ -375,30 +374,30 @@ if (DUSK_ENABLE_UPDATE_CHECKER)
|
||||
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)")
|
||||
message(STATUS "dusklight: 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)")
|
||||
message(STATUS "dusklight: 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)")
|
||||
message(STATUS "dusklight: 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)")
|
||||
message(STATUS "dusklight: Enabled update checker (libcurl)")
|
||||
else ()
|
||||
message(STATUS "dusk: Disabled update checker (libcurl + HTTPS/SSL not found)")
|
||||
message(STATUS "dusklight: Disabled update checker (libcurl + HTTPS/SSL not found)")
|
||||
endif ()
|
||||
else ()
|
||||
message(STATUS "dusk: Disabled update checker (unsupported platform)")
|
||||
message(STATUS "dusklight: Disabled update checker (unsupported platform)")
|
||||
endif ()
|
||||
endif ()
|
||||
list(APPEND DUSK_FILES ${DUSK_HTTP_BACKEND_SOURCE})
|
||||
@@ -476,23 +475,23 @@ endif ()
|
||||
|
||||
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_SOURCE_DIR}/miniz.c)
|
||||
if(ANDROID)
|
||||
add_library(dusk SHARED ${DUSK_FILES})
|
||||
set_target_properties(dusk PROPERTIES OUTPUT_NAME main)
|
||||
set(DUSK_MAIN_TARGET dusk)
|
||||
add_library(dusklight SHARED ${DUSK_FILES})
|
||||
set_target_properties(dusklight PROPERTIES OUTPUT_NAME main)
|
||||
set(DUSK_MAIN_TARGET dusklight)
|
||||
elseif(WIN32)
|
||||
add_library(dusk_game SHARED ${DUSK_FILES})
|
||||
set_target_properties(dusk_game PROPERTIES
|
||||
add_library(dusklight_game SHARED ${DUSK_FILES})
|
||||
set_target_properties(dusklight_game PROPERTIES
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS ON
|
||||
OUTPUT_NAME dusk
|
||||
PDB_NAME dusk_game)
|
||||
OUTPUT_NAME dusklight
|
||||
PDB_NAME dusklight_game)
|
||||
|
||||
add_executable(dusk WIN32 src/dusk/launcher_win32.cpp)
|
||||
target_link_libraries(dusk PRIVATE dusk_game)
|
||||
target_include_directories(dusk PRIVATE include)
|
||||
set(DUSK_MAIN_TARGET dusk_game)
|
||||
add_executable(dusklight WIN32 src/dusk/launcher_win32.cpp)
|
||||
target_link_libraries(dusklight PRIVATE dusklight_game)
|
||||
target_include_directories(dusklight PRIVATE include)
|
||||
set(DUSK_MAIN_TARGET dusklight_game)
|
||||
else ()
|
||||
add_executable(dusk ${DUSK_FILES})
|
||||
set(DUSK_MAIN_TARGET dusk)
|
||||
add_executable(dusklight ${DUSK_FILES})
|
||||
set(DUSK_MAIN_TARGET dusklight)
|
||||
endif ()
|
||||
|
||||
if (WIN32 AND TARGET imgui)
|
||||
@@ -519,6 +518,12 @@ endif()
|
||||
|
||||
if (TARGET crashpad_handler)
|
||||
add_dependencies(${DUSK_MAIN_TARGET} crashpad_handler)
|
||||
add_custom_command(TARGET ${DUSK_MAIN_TARGET} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:crashpad_handler>"
|
||||
"$<TARGET_FILE_DIR:dusklight>"
|
||||
COMMENT "Copying crashpad handler"
|
||||
)
|
||||
endif ()
|
||||
|
||||
if (ANDROID)
|
||||
@@ -528,10 +533,10 @@ if (ANDROID)
|
||||
endif ()
|
||||
|
||||
if (NOT APPLE)
|
||||
add_custom_command(TARGET dusk POST_BUILD
|
||||
add_custom_command(TARGET dusklight POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/res"
|
||||
"$<TARGET_FILE_DIR:dusk>/res"
|
||||
"$<TARGET_FILE_DIR:dusklight>/res"
|
||||
COMMENT "Copying resources"
|
||||
)
|
||||
endif ()
|
||||
@@ -539,9 +544,9 @@ endif ()
|
||||
if (WIN32)
|
||||
set(DUSK_WINDOWS_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/windows)
|
||||
set(DUSK_WINDOWS_ICON_PNG ${CMAKE_CURRENT_SOURCE_DIR}/res/icon.png)
|
||||
set(DUSK_WINDOWS_ICON_ICO ${CMAKE_CURRENT_BINARY_DIR}/dusk.ico)
|
||||
set(DUSK_WINDOWS_RC ${CMAKE_CURRENT_BINARY_DIR}/dusk.rc)
|
||||
set(DUSK_WINDOWS_MANIFEST ${CMAKE_CURRENT_BINARY_DIR}/dusk.manifest)
|
||||
set(DUSK_WINDOWS_ICON_ICO ${CMAKE_CURRENT_BINARY_DIR}/dusklight.ico)
|
||||
set(DUSK_WINDOWS_RC ${CMAKE_CURRENT_BINARY_DIR}/dusklight.rc)
|
||||
set(DUSK_WINDOWS_MANIFEST ${CMAKE_CURRENT_BINARY_DIR}/dusklight.manifest)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${DUSK_WINDOWS_ICON_ICO}
|
||||
@@ -554,12 +559,12 @@ if (WIN32)
|
||||
COMMENT "Generating Windows icon"
|
||||
)
|
||||
|
||||
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.manifest.in ${DUSK_WINDOWS_MANIFEST} @ONLY)
|
||||
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.rc.in ${DUSK_WINDOWS_RC} @ONLY)
|
||||
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusklight.manifest.in ${DUSK_WINDOWS_MANIFEST} @ONLY)
|
||||
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusklight.rc.in ${DUSK_WINDOWS_RC} @ONLY)
|
||||
|
||||
target_sources(dusk PRIVATE ${DUSK_WINDOWS_ICON_ICO} ${DUSK_WINDOWS_RC})
|
||||
target_sources(dusklight PRIVATE ${DUSK_WINDOWS_ICON_ICO} ${DUSK_WINDOWS_RC})
|
||||
if (MSVC)
|
||||
target_link_options(dusk PRIVATE /MANIFEST:NO)
|
||||
target_link_options(dusklight PRIVATE /MANIFEST:NO)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
@@ -578,10 +583,10 @@ if (APPLE)
|
||||
file(GLOB_RECURSE DUSK_RESOURCE_FILES
|
||||
"${DUSK_RESOURCE_DIR}/Assets.car"
|
||||
"${DUSK_RESOURCE_DIR}/Base.lproj/*"
|
||||
"${DUSK_RESOURCE_DIR}/Dusk.icns")
|
||||
"${DUSK_RESOURCE_DIR}/Dusklight.icns")
|
||||
file(GLOB_RECURSE DUSK_APP_RESOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/*")
|
||||
target_sources(dusk PRIVATE ${DUSK_RESOURCE_FILES})
|
||||
target_sources(dusk PRIVATE ${DUSK_APP_RESOURCE_FILES})
|
||||
target_sources(dusklight PRIVATE ${DUSK_RESOURCE_FILES})
|
||||
target_sources(dusklight PRIVATE ${DUSK_APP_RESOURCE_FILES})
|
||||
foreach (FILE ${DUSK_RESOURCE_FILES})
|
||||
file(RELATIVE_PATH NEW_FILE "${DUSK_RESOURCE_DIR}" ${FILE})
|
||||
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
|
||||
@@ -593,14 +598,14 @@ if (APPLE)
|
||||
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
|
||||
endforeach ()
|
||||
set_target_properties(
|
||||
dusk PROPERTIES
|
||||
dusklight PROPERTIES
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING}
|
||||
MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST}
|
||||
OUTPUT_NAME Dusk
|
||||
OUTPUT_NAME Dusklight
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${DUSK_ENTITLEMENTS}
|
||||
@@ -608,19 +613,26 @@ if (APPLE)
|
||||
)
|
||||
endif ()
|
||||
|
||||
if (APPLE AND NOT IOS AND NOT TVOS)
|
||||
find_library(APPKIT_FRAMEWORK AppKit REQUIRED)
|
||||
target_sources(dusklight PRIVATE src/dusk/file_select_macos.mm)
|
||||
set_source_files_properties(src/dusk/file_select_macos.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_libraries(dusklight PRIVATE ${APPKIT_FRAMEWORK})
|
||||
endif ()
|
||||
|
||||
if (IOS)
|
||||
find_library(UIKIT_FRAMEWORK UIKit REQUIRED)
|
||||
find_library(UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED)
|
||||
target_sources(dusk PRIVATE src/dusk/ios/FileSelectDialog.m)
|
||||
target_sources(dusklight PRIVATE src/dusk/ios/FileSelectDialog.m)
|
||||
set_source_files_properties(src/dusk/ios/FileSelectDialog.m PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_libraries(dusk PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK})
|
||||
target_link_libraries(dusklight PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK})
|
||||
endif ()
|
||||
|
||||
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
|
||||
if(WIN32)
|
||||
aurora_copy_runtime_dlls(dusk dusk_game)
|
||||
aurora_copy_runtime_dlls(dusklight dusklight_game)
|
||||
else()
|
||||
aurora_copy_runtime_dlls(dusk)
|
||||
aurora_copy_runtime_dlls(dusklight)
|
||||
endif()
|
||||
|
||||
if (DUSK_SELECTED_OPT)
|
||||
@@ -659,9 +671,9 @@ function(get_target_prefix target result_var)
|
||||
endif ()
|
||||
endif ()
|
||||
endfunction()
|
||||
list(APPEND BINARY_TARGETS dusk)
|
||||
list(APPEND BINARY_TARGETS dusklight)
|
||||
if(WIN32)
|
||||
list(APPEND BINARY_TARGETS dusk_game)
|
||||
list(APPEND BINARY_TARGETS dusklight_game)
|
||||
endif()
|
||||
set(EXTRA_TARGETS "")
|
||||
if (TARGET crashpad_handler)
|
||||
@@ -677,7 +689,7 @@ if (WIN32)
|
||||
else ()
|
||||
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_PREFIX})
|
||||
endif ()
|
||||
aurora_install_runtime_dlls(dusk ${CMAKE_INSTALL_PREFIX})
|
||||
aurora_install_runtime_dlls(dusklight ${CMAKE_INSTALL_PREFIX})
|
||||
if (NOT APPLE)
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/res DESTINATION ${CMAKE_INSTALL_PREFIX})
|
||||
endif ()
|
||||
|
||||
@@ -551,7 +551,7 @@
|
||||
"description": "iOS release build with debug info",
|
||||
"displayName": "iOS RelWithDebInfo",
|
||||
"targets": [
|
||||
"dusk"
|
||||
"dusklight"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -560,7 +560,7 @@
|
||||
"description": "tvOS release build with debug info",
|
||||
"displayName": "tvOS RelWithDebInfo",
|
||||
"targets": [
|
||||
"dusk"
|
||||
"dusklight"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -569,7 +569,7 @@
|
||||
"description": "Android arm64-v8a release build with debug info",
|
||||
"displayName": "Android arm64-v8a RelWithDebInfo",
|
||||
"targets": [
|
||||
"dusk"
|
||||
"dusklight"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -578,7 +578,7 @@
|
||||
"description": "Android x86_64 release build with debug info",
|
||||
"displayName": "Android x86_64 RelWithDebInfo",
|
||||
"targets": [
|
||||
"dusk"
|
||||
"dusklight"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -587,7 +587,7 @@
|
||||
"description": "(Internal) Android CI arm64-v8a",
|
||||
"displayName": "(Internal) Android CI arm64-v8a",
|
||||
"targets": [
|
||||
"dusk"
|
||||
"dusklight"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<div align="center">
|
||||
<img src="res/logo-mascot.png" alt="Logo" width="640">
|
||||
<img src="res/logo.png" alt="Logo" width="640">
|
||||
|
||||
<p align="center">
|
||||
<a href="https://twilitrealm.dev">Official Website</a>
|
||||
•
|
||||
<a href="https://discord.gg/dusktp">Discord</a>
|
||||
<a href="https://discord.gg/6NpMhefCK9">Discord</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
# Overview
|
||||
|
||||
Dusk is a reverse-engineered reimplementation of Twilight Princess.
|
||||
Dusklight is a reverse-engineered reimplementation of Twilight Princess.
|
||||
|
||||
It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience.
|
||||
|
||||
# Setup
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Dusk does *not* provide any copyrighted assets. You must provide your own copy of the original game.
|
||||
> Dusklight does *not* provide any copyrighted assets. You must provide your own copy of the original game.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> At a minimum, Dusk requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelyhood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them.
|
||||
> At a minimum, Dusklight requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelihood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them.
|
||||
|
||||
### 1. Verify your dump
|
||||
|
||||
First, make sure your dump of the game is clean and supported by Dusk. You can do this by checking the SHA-1 hash of your dump against this list of supported versions:
|
||||
First, make sure your dump of the game is clean and supported by Dusklight. You can do this by checking the SHA-1 hash of your dump against this list of supported versions:
|
||||
|
||||
| Version | SHA-1 hash |
|
||||
|--------------| ------------------------------------------ |
|
||||
@@ -33,12 +33,12 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d
|
||||
|
||||
*Support for other versions of the game is planned in the future.
|
||||
|
||||
### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases)
|
||||
### 2. Download [Dusklight](https://github.com/TwilitRealm/dusklight/releases)
|
||||
|
||||
### 3. Setup the game
|
||||
**Windows / macOS / Linux**
|
||||
- Extract the .zip file
|
||||
- Launch Dusk
|
||||
- Launch Dusklight
|
||||
- Press **Select Disc Image** and provide the path to your supported game dump
|
||||
- Press **Play**!
|
||||
|
||||
@@ -46,20 +46,20 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d
|
||||
- Follow the [iOS setup guide](docs/ios-install-altstore.md)
|
||||
|
||||
**Android**
|
||||
- Install the Dusk apk
|
||||
- Launch Dusk
|
||||
- Install the Dusklight APK
|
||||
- Launch Dusklight
|
||||
- Press **Select Disc Image** and provide the path to your supported game dump
|
||||
- Press **Play**!
|
||||
|
||||
# Building
|
||||
|
||||
If you'd like to build Dusk from source, please read the [build instructions](docs/building.md).
|
||||
If you'd like to build Dusklight from source, please read the [build instructions](docs/building.md).
|
||||
|
||||
Pull requests are welcomed! Note that we do not accept contributions that are primarily AI-generated and will close your PR if we suspect as much. Please also see the [code conventions](docs/code-conventions.md).
|
||||
|
||||
# Credits
|
||||
|
||||
Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusk/graphs/contributors).
|
||||
Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusklight/graphs/contributors).
|
||||
|
||||
<br/>
|
||||
<div align="center">
|
||||
|
||||
@@ -19,7 +19,7 @@ for install_path in build/install/*; do
|
||||
cp -r "$install_path" build/appdir/usr/bin
|
||||
done
|
||||
cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,1024x1024} build/appdir/usr/share/icons/hicolor
|
||||
cp platforms/freedesktop/dusk.desktop build/appdir/usr/share/applications
|
||||
cp platforms/freedesktop/dusklight.desktop build/appdir/usr/share/applications
|
||||
|
||||
cd build/install
|
||||
VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
sudo dnf groupinstall "Development Tools" "Development Libraries"
|
||||
```
|
||||
#### Setup
|
||||
Clone and initialize the Dusk repository
|
||||
Clone and initialize the Dusklight repository
|
||||
```sh
|
||||
git clone --recursive https://github.com/TwilitRealm/dusk.git
|
||||
cd dusk
|
||||
git clone --recursive https://github.com/TwilitRealm/dusklight.git
|
||||
cd dusklight
|
||||
git pull
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
@@ -93,6 +93,6 @@ Alternate presets available:
|
||||
#### Running
|
||||
Pass the disc image as a positional argument. Supported formats: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ
|
||||
```sh
|
||||
build/{preset}/dusk /path/to/game.rvz
|
||||
build/{preset}/dusklight/path/to/game.rvz
|
||||
```
|
||||
If no path is specified, Dusk defaults to `game.iso` in the current working directory.
|
||||
If no path is specified, Dusklight defaults to `game.iso` in the current working directory.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Installing Dusk on iOS via AltStore
|
||||
# Installing Dusklight on iOS via AltStore
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Mac with Homebrew installed
|
||||
- iPhone connected via USB
|
||||
- Dusk IPA file (download the latest `Dusk-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusk/releases))
|
||||
- Dusklight IPA file (download the latest `Dusklight-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusk/releases))
|
||||
- Game disc - `GZ2E01` (Gamecube USA) or `GZ2PE01` (Gamecube PAL)
|
||||
|
||||
## 1. Install AltServer
|
||||
|
||||
@@ -1420,6 +1420,8 @@ set(DUSK_FILES
|
||||
src/dusk/asserts.cpp
|
||||
src/dusk/config.cpp
|
||||
src/dusk/crash_reporting.cpp
|
||||
src/dusk/data.cpp
|
||||
src/dusk/data.hpp
|
||||
src/dusk/endian.cpp
|
||||
src/dusk/extras.c
|
||||
src/dusk/file_select.cpp
|
||||
@@ -1435,6 +1437,7 @@ set(DUSK_FILES
|
||||
src/dusk/layout.cpp
|
||||
src/dusk/logging.cpp
|
||||
src/dusk/settings.cpp
|
||||
src/dusk/speedrun.cpp
|
||||
src/dusk/stubs.cpp
|
||||
src/dusk/update_check.cpp
|
||||
src/dusk/update_check.hpp
|
||||
@@ -1444,8 +1447,6 @@ set(DUSK_FILES
|
||||
src/dusk/imgui/ImGuiConsole.cpp
|
||||
src/dusk/imgui/ImGuiEngine.cpp
|
||||
src/dusk/imgui/ImGuiEngine.hpp
|
||||
src/dusk/imgui/ImGuiMenuGame.cpp
|
||||
src/dusk/imgui/ImGuiMenuGame.hpp
|
||||
src/dusk/imgui/ImGuiBloomWindow.cpp
|
||||
src/dusk/imgui/ImGuiBloomWindow.hpp
|
||||
src/dusk/imgui/ImGuiMenuTools.cpp
|
||||
@@ -1497,6 +1498,8 @@ set(DUSK_FILES
|
||||
src/dusk/ui/prelaunch.hpp
|
||||
src/dusk/ui/preset.cpp
|
||||
src/dusk/ui/preset.hpp
|
||||
src/dusk/ui/reporting.cpp
|
||||
src/dusk/ui/reporting.hpp
|
||||
src/dusk/ui/select_button.cpp
|
||||
src/dusk/ui/select_button.hpp
|
||||
src/dusk/ui/settings.cpp
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
url = "https://github.com/mikke89/RmlUi/archive/f9b8c9e2935d5df2c7dff2c190d3968e99b0c3dc.tar.gz";
|
||||
hash = "sha256-g4O/JZUrrcseOz8o2QJRt+2CeuiLnVeuDJc906xvuIg=";
|
||||
};
|
||||
# Dusk Actual
|
||||
dusk = pkgs.stdenv.mkDerivation {
|
||||
name = "dusk";
|
||||
# Dusklight Actual
|
||||
dusklight = pkgs.stdenv.mkDerivation {
|
||||
name = "dusklight";
|
||||
src = ./.;
|
||||
postUnpack = ''
|
||||
mkdir -p $sourceRoot/extern/aurora
|
||||
@@ -69,7 +69,7 @@
|
||||
];
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp dusk $out/bin/dusk
|
||||
cp dusklight $out/bin/dusklight
|
||||
cp -r ./res $out/bin/res
|
||||
'';
|
||||
nativeBuildInputs = [
|
||||
@@ -99,6 +99,6 @@
|
||||
];
|
||||
};
|
||||
in {
|
||||
packages.x86_64-linux.default = dusk;
|
||||
packages.x86_64-linux.default = dusklight;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,7 +196,11 @@ public:
|
||||
/* 0x108 */ int mSkipTimer;
|
||||
/* 0x10C */ int mSkipParameter;
|
||||
/* 0x110 */ BOOL mIsSkipFade;
|
||||
#if TARGET_PC
|
||||
/* 0x114 */ char mSkipEventName[21];
|
||||
#else
|
||||
/* 0x114 */ char mSkipEventName[20];
|
||||
#endif
|
||||
/* 0x128 */ u8 mCompulsory;
|
||||
/* 0x129 */ bool mRoomInfoSet;
|
||||
/* 0x12C */ int mRoomNo;
|
||||
|
||||
@@ -223,6 +223,9 @@ private:
|
||||
/* 0x8F */ u8 field_0x8f;
|
||||
/* 0x90 */ u8 field_0x90;
|
||||
/* 0x91 */ u8 field_0x91;
|
||||
#if TARGET_PC
|
||||
bool previousMirror;
|
||||
#endif
|
||||
}; // Size: 0x94
|
||||
|
||||
class dMap_HIO_list_c : public dMpath_HIO_n::hioList_c {
|
||||
|
||||
@@ -7,7 +7,12 @@ namespace dusk {
|
||||
*
|
||||
* This gets used for file paths and such, and cannot be changed!
|
||||
*/
|
||||
constexpr auto AppName = "Dusk";
|
||||
constexpr auto AppName = "Dusklight";
|
||||
|
||||
/**
|
||||
* Previous AppName to migrate data from.
|
||||
*/
|
||||
constexpr auto LegacyAppName = "Dusk";
|
||||
|
||||
/**
|
||||
* \brief The internal organization name for the game.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef DUSK_CONFIG_HPP
|
||||
#define DUSK_CONFIG_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "config_var.hpp"
|
||||
@@ -111,6 +112,11 @@ void Save();
|
||||
*/
|
||||
ConfigVarBase* GetConfigVar(std::string_view name);
|
||||
|
||||
/**
|
||||
* \brief Call a function on every registered CVar.
|
||||
*/
|
||||
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
|
||||
|
||||
template <ConfigValue T>
|
||||
const ConfigImplBase* GetConfigImpl() {
|
||||
static ConfigImpl<T> config;
|
||||
|
||||
@@ -48,6 +48,13 @@ enum class ConfigVarLayer : u8 {
|
||||
* Will not get saved to config.
|
||||
*/
|
||||
Override,
|
||||
|
||||
/**
|
||||
* The CVar is temporarily overridden by speedrun mode.
|
||||
* Will not get saved to config. Cleared when speedrun mode is disabled.
|
||||
* Lower priority than Override, so launch args still win.
|
||||
*/
|
||||
Speedrun,
|
||||
};
|
||||
|
||||
class ConfigImplBase;
|
||||
@@ -113,6 +120,12 @@ public:
|
||||
* This is necessary to make it legal to access.
|
||||
*/
|
||||
void markRegistered();
|
||||
|
||||
/**
|
||||
* Clear a speedrun-mode override if one is active on this CVar.
|
||||
* Safe to call on any CVar, no-op if not at the Speedrun layer.
|
||||
*/
|
||||
virtual void clearSpeedrunOverride() {}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
@@ -189,6 +202,7 @@ public:
|
||||
case ConfigVarLayer::Value:
|
||||
return value;
|
||||
case ConfigVarLayer::Override:
|
||||
case ConfigVarLayer::Speedrun:
|
||||
return overrideValue;
|
||||
default:
|
||||
abort();
|
||||
@@ -239,6 +253,38 @@ public:
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Override;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Give a CVar a speedrun-mode override value.
|
||||
*
|
||||
* Lower priority than a launch-arg override. Cleared when speedrun mode is disabled.
|
||||
* The overridden value will not get saved to config.
|
||||
*
|
||||
* @param newValue The new value the CVar will get.
|
||||
*/
|
||||
void setSpeedrunValue(T newValue) {
|
||||
checkRegistered();
|
||||
if (layer != ConfigVarLayer::Override) {
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Speedrun;
|
||||
}
|
||||
}
|
||||
|
||||
void clearOverride() {
|
||||
checkRegistered();
|
||||
if (layer == ConfigVarLayer::Override) {
|
||||
overrideValue = {};
|
||||
layer = ConfigVarLayer::Value;
|
||||
}
|
||||
}
|
||||
|
||||
void clearSpeedrunOverride() override {
|
||||
checkRegistered();
|
||||
if (layer == ConfigVarLayer::Speedrun) {
|
||||
overrideValue = {};
|
||||
layer = ConfigVarLayer::Value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
namespace dusk::crash_reporting {
|
||||
|
||||
void InitializeCrashReporting();
|
||||
void ShutdownCrashReporting();
|
||||
enum class Consent {
|
||||
Unavailable,
|
||||
Unknown,
|
||||
Given,
|
||||
Revoked,
|
||||
};
|
||||
|
||||
} // namespace dusk
|
||||
void initialize();
|
||||
void shutdown();
|
||||
Consent get_consent();
|
||||
void set_consent(bool enabled);
|
||||
|
||||
} // namespace dusk::crash_reporting
|
||||
|
||||
@@ -1,36 +1,25 @@
|
||||
#ifndef DUSK_MAIN_H
|
||||
#define DUSK_MAIN_H
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#if defined(_WIN32) || \
|
||||
(defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \
|
||||
(defined(__linux__) && !defined(__ANDROID__))
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 1
|
||||
#else
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 0
|
||||
#endif
|
||||
|
||||
namespace dusk {
|
||||
extern bool IsRunning;
|
||||
extern bool IsShuttingDown;
|
||||
extern bool IsGameLaunched;
|
||||
extern bool RestartRequested;
|
||||
extern std::filesystem::path ConfigPath;
|
||||
|
||||
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
|
||||
extern bool IsRunning;
|
||||
extern bool IsShuttingDown;
|
||||
extern bool IsGameLaunched;
|
||||
extern bool RestartRequested;
|
||||
extern std::filesystem::path ConfigPath;
|
||||
|
||||
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
|
||||
(defined(TARGET_OS_TV) && TARGET_OS_TV)
|
||||
inline constexpr bool SupportsProcessRestart = false;
|
||||
inline constexpr bool SupportsProcessRestart = false;
|
||||
#else
|
||||
inline constexpr bool SupportsProcessRestart = true;
|
||||
inline constexpr bool SupportsProcessRestart = true;
|
||||
#endif
|
||||
|
||||
void RequestRestart() noexcept;
|
||||
bool OpenDataFolder();
|
||||
}
|
||||
void RequestRestart() noexcept;
|
||||
|
||||
} // namespace dusk
|
||||
|
||||
#endif // DUSK_MAIN_H
|
||||
|
||||
@@ -125,6 +125,7 @@ struct UserSettings {
|
||||
ConfigVar<int> shadowResolutionMultiplier;
|
||||
ConfigVar<bool> enableDepthOfField;
|
||||
ConfigVar<bool> enableMapBackground;
|
||||
ConfigVar<bool> disableCutscenePillarboxing;
|
||||
|
||||
// Audio
|
||||
ConfigVar<bool> noLowHpSound;
|
||||
@@ -144,6 +145,8 @@ struct UserSettings {
|
||||
ConfigVar<bool> freeCamera;
|
||||
ConfigVar<bool> invertCameraXAxis;
|
||||
ConfigVar<bool> invertCameraYAxis;
|
||||
ConfigVar<bool> invertFirstPersonXAxis;
|
||||
ConfigVar<bool> invertFirstPersonYAxis;
|
||||
ConfigVar<float> freeCameraSensitivity;
|
||||
ConfigVar<bool> debugFlyCam;
|
||||
ConfigVar<bool> debugFlyCamLockEvents;
|
||||
@@ -162,6 +165,7 @@ struct UserSettings {
|
||||
ConfigVar<bool> alwaysGreatspin;
|
||||
ConfigVar<bool> enableFastIronBoots;
|
||||
ConfigVar<bool> canTransformAnywhere;
|
||||
ConfigVar<bool> fastRoll;
|
||||
ConfigVar<bool> fastSpinner;
|
||||
ConfigVar<bool> freeMagicArmor;
|
||||
|
||||
@@ -174,6 +178,7 @@ struct UserSettings {
|
||||
// Tools
|
||||
ConfigVar<bool> speedrunMode;
|
||||
ConfigVar<bool> liveSplitEnabled;
|
||||
ConfigVar<bool> showSpeedrunRTATimer;
|
||||
ConfigVar<bool> recordingMode;
|
||||
} game;
|
||||
|
||||
@@ -184,7 +189,6 @@ struct UserSettings {
|
||||
ConfigVar<bool> skipPreLaunchUI;
|
||||
ConfigVar<bool> showPipelineCompilation;
|
||||
ConfigVar<bool> wasPresetChosen;
|
||||
ConfigVar<bool> enableCrashReporting;
|
||||
ConfigVar<bool> checkForUpdates;
|
||||
ConfigVar<int> cardFileType;
|
||||
ConfigVar<bool> enableAdvancedSettings;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
struct SpeedrunInfo {
|
||||
void startRun() {
|
||||
m_isRunStarted = true;
|
||||
m_startTimestamp = OSGetTime();
|
||||
}
|
||||
|
||||
void stopRun() {
|
||||
m_isRunStarted = false;
|
||||
m_endTimestamp = OSGetTime() - m_startTimestamp;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
m_isRunStarted = false;
|
||||
m_startTimestamp = 0;
|
||||
m_endTimestamp = 0;
|
||||
m_isPauseIGT = false;
|
||||
m_loadStartTimestamp = 0;
|
||||
m_totalLoadTime = 0;
|
||||
m_igtTimer = 0;
|
||||
}
|
||||
|
||||
bool m_isRunStarted = false;
|
||||
OSTime m_startTimestamp = 0;
|
||||
OSTime m_endTimestamp = 0;
|
||||
|
||||
bool m_isPauseIGT = false;
|
||||
OSTime m_loadStartTimestamp = 0;
|
||||
OSTime m_totalLoadTime = 0;
|
||||
OSTime m_igtTimer = 0;
|
||||
};
|
||||
|
||||
extern SpeedrunInfo m_speedrunInfo;
|
||||
|
||||
void resetForSpeedrunMode();
|
||||
|
||||
} // namespace dusk
|
||||
@@ -1,6 +1,6 @@
|
||||
# Android Shell
|
||||
|
||||
This directory contains a minimal SDLActivity-based Android app wrapper for Dusk.
|
||||
This directory contains a minimal SDLActivity-based Android app wrapper for Dusklight.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
|
||||
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusk').get().asFile
|
||||
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight').get().asFile
|
||||
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
|
||||
from(new File(duskRepoDir, 'res')) {
|
||||
into 'res'
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
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.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
@@ -22,6 +26,13 @@ import java.util.List;
|
||||
|
||||
public class DuskActivity extends SDLActivity {
|
||||
private static final String TAG = "DuskActivity";
|
||||
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
|
||||
private static final String EXTERNAL_STORAGE_AUTHORITY =
|
||||
"com.android.externalstorage.documents";
|
||||
|
||||
private long folderDialogUserdata = 0;
|
||||
|
||||
private static native void nativeFolderDialogResult(long userdata, String path, String error);
|
||||
|
||||
private static String[] splitArgs(String raw) {
|
||||
List<String> out = new ArrayList<>();
|
||||
@@ -147,9 +158,154 @@ public class DuskActivity extends SDLActivity {
|
||||
if (resultCode == RESULT_OK) {
|
||||
persistUriPermissions(data);
|
||||
}
|
||||
if (requestCode == FOLDER_DIALOG_REQUEST_CODE) {
|
||||
finishFolderDialog(resultCode, data);
|
||||
return;
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
public boolean showFolderDialog(long userdata) {
|
||||
if (userdata == 0 || folderDialogUserdata != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
folderDialogUserdata = userdata;
|
||||
runOnUiThread(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Unable to open folder dialog.", e);
|
||||
finishFolderDialog(Activity.RESULT_CANCELED, null);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private void finishFolderDialog(int resultCode, Intent data) {
|
||||
long userdata = folderDialogUserdata;
|
||||
folderDialogUserdata = 0;
|
||||
if (userdata == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
String path = getRealPathForUri(data.getData());
|
||||
if (path != null && !path.isEmpty()) {
|
||||
nativeFolderDialogResult(userdata, path, null);
|
||||
} else {
|
||||
nativeFolderDialogResult(
|
||||
userdata, null, "Selected folder is not available as a filesystem path");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nativeFolderDialogResult(userdata, null, null);
|
||||
}
|
||||
|
||||
private String getRealPathForUri(Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if ("file".equals(scheme)) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
if (!"content".equals(scheme) ||
|
||||
!EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to resolve URI: " + uri, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getExternalStorageDocumentId(Uri uri) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) {
|
||||
return DocumentsContract.getTreeDocumentId(uri);
|
||||
}
|
||||
|
||||
return DocumentsContract.getDocumentId(uri);
|
||||
}
|
||||
|
||||
private static boolean isTreeDocumentUri(Uri uri) {
|
||||
List<String> segments = uri.getPathSegments();
|
||||
return segments.size() >= 2 && "tree".equals(segments.get(0));
|
||||
}
|
||||
|
||||
private String getExternalStoragePathForDocumentId(String documentId) {
|
||||
if (documentId == null || documentId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (documentId.startsWith("raw:")) {
|
||||
return documentId.substring("raw:".length());
|
||||
}
|
||||
|
||||
String[] parts = documentId.split(":", 2);
|
||||
String volumeId = parts[0];
|
||||
String relativePath = parts.length > 1 ? parts[1] : "";
|
||||
|
||||
File root = getExternalStorageRoot(volumeId);
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return relativePath.isEmpty()
|
||||
? root.getAbsolutePath()
|
||||
: new File(root, relativePath).getAbsolutePath();
|
||||
}
|
||||
|
||||
private File getExternalStorageRoot(String volumeId) {
|
||||
if ("primary".equalsIgnoreCase(volumeId)) {
|
||||
return Environment.getExternalStorageDirectory();
|
||||
}
|
||||
if ("home".equalsIgnoreCase(volumeId)) {
|
||||
return new File(
|
||||
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS);
|
||||
}
|
||||
|
||||
File[] externalFilesDirs = getExternalFilesDirs(null);
|
||||
if (externalFilesDirs != null) {
|
||||
for (File externalFilesDir : externalFilesDirs) {
|
||||
File root = getStorageRootForExternalFilesDir(externalFilesDir);
|
||||
if (root != null && volumeId.equalsIgnoreCase(root.getName())) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File fallback = new File("/storage", volumeId);
|
||||
return fallback.exists() ? fallback : null;
|
||||
}
|
||||
|
||||
private File getStorageRootForExternalFilesDir(File externalFilesDir) {
|
||||
if (externalFilesDir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String path = externalFilesDir.getAbsolutePath();
|
||||
int androidDir = path.indexOf("/Android/");
|
||||
if (androidDir <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File(path.substring(0, androidDir));
|
||||
}
|
||||
|
||||
private void persistUriPermissions(Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
|
||||
@@ -14,15 +14,22 @@ import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
|
||||
|
||||
private static final String ROOT_ID = "dusk";
|
||||
private static final String ROOT_DOCUMENT_ID = "root";
|
||||
private static final String LOCATION_DESCRIPTOR_NAME = "data_location.json";
|
||||
private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
|
||||
|
||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
||||
@@ -46,13 +53,19 @@ public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
ensureUserDirectories();
|
||||
if (!isCustomDataPathEnabled()) {
|
||||
ensureUserDirectories();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
||||
if (isCustomDataPathEnabled()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final File root = getRootDirectory();
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
|
||||
@@ -222,9 +235,14 @@ public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
}
|
||||
|
||||
private File getRootDirectory() throws FileNotFoundException {
|
||||
if (isCustomDataPathEnabled()) {
|
||||
throw new FileNotFoundException(
|
||||
"Dusk DocumentsProvider is disabled while a custom data path is configured");
|
||||
}
|
||||
|
||||
final File root = getContext().getFilesDir();
|
||||
if (root == null) {
|
||||
throw new FileNotFoundException("Dusk files directory is unavailable");
|
||||
throw new FileNotFoundException("Dusklight files directory is unavailable");
|
||||
}
|
||||
return root;
|
||||
}
|
||||
@@ -241,7 +259,7 @@ public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1);
|
||||
final File file = new File(root, relativePath);
|
||||
if (!isInside(root, file)) {
|
||||
throw new FileNotFoundException("Document escapes Dusk files directory: " + documentId);
|
||||
throw new FileNotFoundException("Document escapes Dusklight files directory: " + documentId);
|
||||
}
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("Document does not exist: " + documentId);
|
||||
@@ -255,7 +273,7 @@ public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
return ROOT_DOCUMENT_ID;
|
||||
}
|
||||
if (!isInside(root, file)) {
|
||||
throw new FileNotFoundException("File escapes Dusk files directory: " + file);
|
||||
throw new FileNotFoundException("File escapes Dusklight files directory: " + file);
|
||||
}
|
||||
|
||||
final String rootPath = canonicalPath(root);
|
||||
@@ -273,6 +291,42 @@ public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
new File(root, "EUR/Card A").mkdirs();
|
||||
}
|
||||
|
||||
private boolean isCustomDataPathEnabled() {
|
||||
if (getContext() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File filesDir = getContext().getFilesDir();
|
||||
if (filesDir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME);
|
||||
if (!descriptor.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final JSONObject json = new JSONObject(readText(descriptor));
|
||||
return "custom".equals(json.optString("mode", "default"));
|
||||
} catch (IOException | JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String readText(File file) throws IOException {
|
||||
try (FileInputStream input = new FileInputStream(file);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream())
|
||||
{
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return output.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] resolveRootProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dusk</string>
|
||||
<string name="documents_provider_root_name">Dusk Data</string>
|
||||
<string name="app_name">Dusklight</string>
|
||||
<string name="documents_provider_root_name">Dusklight Data</string>
|
||||
<string name="documents_provider_summary">Saves, texture packs, settings, and logs</string>
|
||||
</resources>
|
||||
|
||||
@@ -14,5 +14,5 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "dusk-android"
|
||||
rootProject.name = "dusklight-android"
|
||||
include ':app'
|
||||
|
||||
|
Before Width: | Height: | Size: 928 KiB After Width: | Height: | Size: 928 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1014 B After Width: | Height: | Size: 1014 B |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
@@ -1,9 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Name=Dusk
|
||||
GenericName=Dusk
|
||||
Comment=The Legend of Zelda: Twilight Princess
|
||||
Exec=dusk
|
||||
Icon=dusk
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Game;
|
||||
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=Dusklight
|
||||
GenericName=Dusklight
|
||||
Comment=PC port of a classic adventure game
|
||||
Exec=dusklight
|
||||
Icon=dusklight
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Game;
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Dusk</string>
|
||||
<string>Dusklight</string>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>Dusk</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
||||
@@ -24,9 +24,9 @@ BEGIN
|
||||
VALUE "CompanyName", "@DUSK_COMPANY_NAME@\0"
|
||||
VALUE "FileDescription", "@DUSK_FILE_DESCRIPTION@\0"
|
||||
VALUE "FileVersion", "@DUSK_VERSION_STRING@\0"
|
||||
VALUE "InternalName", "dusk\0"
|
||||
VALUE "InternalName", "dusklight\0"
|
||||
VALUE "LegalCopyright", "@DUSK_COPYRIGHT@\0"
|
||||
VALUE "OriginalFilename", "dusk.exe\0"
|
||||
VALUE "OriginalFilename", "dusklight.exe\0"
|
||||
VALUE "ProductName", "@DUSK_PRODUCT_NAME@\0"
|
||||
VALUE "ProductVersion", "@DUSK_VERSION_STRING@\0"
|
||||
END
|
||||
|
Before Width: | Height: | Size: 457 KiB |
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 642 KiB |
@@ -201,6 +201,37 @@ fps {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
speedrun-timer {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
background-color: rgba(0, 0, 0, 65%);
|
||||
padding: 2dp 4dp;
|
||||
pointer-events: none;
|
||||
font-family: "Noto Mono";
|
||||
font-size: 16dp;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
speedrun-timer[open] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
speedrun-rta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
speedrun-rta[open] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
speedrun-igt {
|
||||
display: block;
|
||||
}
|
||||
|
||||
fps[open] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -65,10 +65,10 @@ menu {
|
||||
right: auto;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
/* Scale based on a reference screen width, 428/1216 */
|
||||
width: 35.230264vw;
|
||||
/* Scale based on a reference screen width, 856/1216 */
|
||||
width: 70.394736vw;
|
||||
min-width: 428dp;
|
||||
max-width: 856dp;
|
||||
max-width: 50vw;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -83,9 +83,8 @@ body.mirrored menu {
|
||||
hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 8dp;
|
||||
gap: 4dp;
|
||||
}
|
||||
|
||||
body.mirrored hero {
|
||||
@@ -96,19 +95,19 @@ hero img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
eyebrow {
|
||||
font-family: "Alegreya SC";
|
||||
font-size: 32dp;
|
||||
}
|
||||
|
||||
@media (min-width: 1216dp) {
|
||||
.eyebrow {
|
||||
eyebrow {
|
||||
/* Same logic as .menu, 32/1216 */
|
||||
font-size: 2.631579vw;
|
||||
}
|
||||
}
|
||||
|
||||
.eyebrow span {
|
||||
eyebrow span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -437,7 +436,7 @@ body.animate-in .intro-item {
|
||||
decorator: horizontal-gradient(#FEE685FF #FEE68500);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
eyebrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,12 @@ window content pane:last-of-type > div {
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
.data-folder-current {
|
||||
display: block;
|
||||
font-size: 16dp;
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
}
|
||||
|
||||
window content pane > spacer {
|
||||
display: block;
|
||||
/* Completes the 24dp bottom inset after the pane's 8dp gap. */
|
||||
@@ -199,6 +205,11 @@ button:not(:disabled):active {
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
button.modal-btn {
|
||||
flex: 1 1 0;
|
||||
text-align: center;
|
||||
|
||||
@@ -16114,6 +16114,9 @@ int daAlink_c::procSlideLand() {
|
||||
|
||||
int daAlink_c::procFrontRollInit() {
|
||||
BOOL is_guard_anime = checkUpperGuardAnime();
|
||||
#ifdef TARGET_PC
|
||||
const f32 fastRollMultiplier = dusk::getSettings().game.fastRoll ? 2.0f : 1.0f;
|
||||
#endif
|
||||
|
||||
if (mProcID == PROC_FRONT_ROLL && mDemo.getDemoMode() == daPy_demo_c::DEMO_FRONT_ROLL_e) {
|
||||
return 0;
|
||||
@@ -16129,10 +16132,16 @@ int daAlink_c::procFrontRollInit() {
|
||||
roll_anm_speed = mpHIO->mFrontRoll.m.mRollAnm.mStartFrame;
|
||||
}
|
||||
|
||||
setSingleAnime(ANM_FRONT_ROLL, mpHIO->mFrontRoll.m.mRollAnm.mSpeed, roll_anm_speed,
|
||||
setSingleAnime(ANM_FRONT_ROLL,
|
||||
#ifdef TARGET_PC
|
||||
mpHIO->mFrontRoll.m.mRollAnm.mSpeed * fastRollMultiplier,
|
||||
#else
|
||||
mpHIO->mFrontRoll.m.mRollAnm.mSpeed,
|
||||
#endif
|
||||
roll_anm_speed,
|
||||
mpHIO->mFrontRoll.m.mRollAnm.mEndFrame,
|
||||
mpHIO->mFrontRoll.m.mRollAnm.mInterpolation);
|
||||
|
||||
|
||||
mNormalSpeed = speedF * mpHIO->mFrontRoll.m.mSpeedRate + mpHIO->mFrontRoll.m.mInitSpeed;
|
||||
|
||||
f32 max_speed = mpHIO->mFrontRoll.m.mInitSpeed + mpHIO->mMove.m.mMaxSpeed * mpHIO->mFrontRoll.m.mSpeedRate;
|
||||
@@ -16145,11 +16154,20 @@ int daAlink_c::procFrontRollInit() {
|
||||
}
|
||||
|
||||
if (checkNoResetFlg0(FLG0_WATER_IN_MOVE)) {
|
||||
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
|
||||
#if TARGET_PC
|
||||
if (!(dusk::getSettings().game.enableFastIronBoots))
|
||||
#endif
|
||||
{
|
||||
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
|
||||
}
|
||||
} else if (checkHeavyStateOn(TRUE, TRUE)) {
|
||||
mNormalSpeed *= mHeavySpeedMultiplier;
|
||||
}
|
||||
|
||||
#ifdef TARGET_PC
|
||||
mNormalSpeed *= fastRollMultiplier;
|
||||
#endif
|
||||
|
||||
current.angle.y = shape_angle.y;
|
||||
voiceStart(Z2SE_AL_V_BACKTEN);
|
||||
mProcVar2.field_0x300c = 0;
|
||||
@@ -16280,8 +16298,13 @@ int daAlink_c::procFrontRollCrashInit() {
|
||||
speed.y = mpHIO->mFrontRoll.m.mCrashSpeedV;
|
||||
|
||||
if (checkNoResetFlg0(FLG0_WATER_IN_MOVE)) {
|
||||
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
|
||||
speed.y *= mpHIO->mItem.mIronBoots.m.mWaterVelocityY;
|
||||
#if TARGET_PC
|
||||
if (!(dusk::getSettings().game.enableFastIronBoots))
|
||||
#endif
|
||||
{
|
||||
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
|
||||
speed.y *= mpHIO->mItem.mIronBoots.m.mWaterVelocityY;
|
||||
}
|
||||
}
|
||||
|
||||
ANGLE_ADD_2(current.angle.y, 0x8000);
|
||||
@@ -16375,6 +16398,9 @@ int daAlink_c::procFrontRollSuccess() {
|
||||
|
||||
int daAlink_c::procSideRollInit(int param_0) {
|
||||
BOOL is_prev_guardAnm = checkUpperGuardAnime();
|
||||
#ifdef TARGET_PC
|
||||
const f32 fastRollMultiplier = dusk::getSettings().game.fastRoll ? 2.0f : 1.0f;
|
||||
#endif
|
||||
|
||||
if (!commonProcInitNotSameProc(PROC_SIDE_ROLL)) {
|
||||
return 0;
|
||||
@@ -16391,17 +16417,30 @@ int daAlink_c::procSideRollInit(int param_0) {
|
||||
current.angle.y = shape_angle.y + -0x4000;
|
||||
}
|
||||
|
||||
setSingleAnime(anmID, mpHIO->mGuard.mTurnMove.m.mSideRollAnmSpeed,
|
||||
setSingleAnime(anmID,
|
||||
#ifdef TARGET_PC
|
||||
mpHIO->mGuard.mTurnMove.m.mSideRollAnmSpeed * fastRollMultiplier,
|
||||
#else
|
||||
mpHIO->mGuard.mTurnMove.m.mSideRollAnmSpeed,
|
||||
#endif
|
||||
mpHIO->mGuard.mTurnMove.m.mTurnAnm.mStartFrame,
|
||||
mpHIO->mGuard.mTurnMove.m.mTurnAnm.mEndFrame,
|
||||
mpHIO->mGuard.mTurnMove.m.mTurnAnm.mInterpolation);
|
||||
mNormalSpeed = mpHIO->mGuard.mTurnMove.m.mSideRollSpeed;
|
||||
|
||||
if (checkNoResetFlg0(FLG0_WATER_IN_MOVE)) {
|
||||
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
|
||||
#if TARGET_PC
|
||||
if (!(dusk::getSettings().game.enableFastIronBoots))
|
||||
#endif
|
||||
{
|
||||
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
|
||||
}
|
||||
} else if (checkHeavyStateOn(TRUE, TRUE)) {
|
||||
mNormalSpeed *= mHeavySpeedMultiplier;
|
||||
}
|
||||
#ifdef TARGET_PC
|
||||
mNormalSpeed *= fastRollMultiplier;
|
||||
#endif
|
||||
|
||||
setFootEffectProcType(0);
|
||||
field_0x2f9d = 4;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/speedrun.h"
|
||||
|
||||
BOOL daAlink_c::checkEventRun() const {
|
||||
return dComIfGp_event_runCheck() || checkPlayerDemoMode();
|
||||
|
||||
@@ -119,8 +119,8 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
||||
var_f31 /= dComIfGp_getCameraZoomScale(field_0x317c);
|
||||
}
|
||||
|
||||
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle));
|
||||
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle));
|
||||
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
|
||||
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
|
||||
|
||||
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
|
||||
sp8 = mBodyAngle.x;
|
||||
@@ -144,8 +144,8 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
||||
f32 gy_pitch = 0.f;
|
||||
dusk::gyro::getAimDeltas(gy_yaw, gy_pitch);
|
||||
|
||||
shape_angle.y = shape_angle.y + cM_rad2s(gy_yaw * gyro_scale);
|
||||
sp8 = sp8 + cM_rad2s(gy_pitch * gyro_scale);
|
||||
shape_angle.y = shape_angle.y + cM_rad2s(gy_yaw * gyro_scale * (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f));
|
||||
sp8 = sp8 + cM_rad2s(gy_pitch * gyro_scale * (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f));
|
||||
|
||||
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
|
||||
sp8 = mBodyAngle.x;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "d/d_msg_string.h"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/speedrun.h"
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
|
||||
dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) {
|
||||
@@ -146,7 +147,7 @@ void dBrightCheck_c::modeMove() {
|
||||
if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) {
|
||||
// start a new run if a run isn't already in progress
|
||||
if (!dusk::m_speedrunInfo.m_isRunStarted) {
|
||||
dusk::ImGuiMenuGame::resetForSpeedrunMode();
|
||||
dusk::resetForSpeedrunMode();
|
||||
dusk::m_speedrunInfo.startRun();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7648,7 +7648,10 @@ bool dCamera_c::freeCamera() {
|
||||
cXyz camMovement = {mPadInfo.mCStick.mLastPosX, mPadInfo.mCStick.mLastPosY, 0.0f};
|
||||
f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY);
|
||||
|
||||
if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY != 0) {
|
||||
// If we aren't in manual cam mode, don't trigger it if the player tries to hit C-up
|
||||
// for first person
|
||||
if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY < 0 ||
|
||||
(mCamParam.mManualMode == 1 && mPadInfo.mCStick.mLastPosY != 0)) {
|
||||
mCamParam.mManualMode = 1;
|
||||
camMovement = camMovement.normalize();
|
||||
camMovement.y *= dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f;
|
||||
@@ -11231,6 +11234,62 @@ cXyz dCamera_c::Center() {
|
||||
return mCenter + mShake.field_0x24;
|
||||
}
|
||||
|
||||
#ifdef TARGET_PC
|
||||
f32 get_target_trim_height(camera_process_class* i_this) {
|
||||
const auto camera = &i_this->mCamera;
|
||||
if (camera->mCurState != 2) {
|
||||
switch (camera->mTrimSize) {
|
||||
case 0:
|
||||
case 4:
|
||||
return 0.0f;
|
||||
case 1:
|
||||
return camera->mCamSetup.VistaTrimHeight();
|
||||
case 2:
|
||||
case 3:
|
||||
return camera->mCamSetup.CinemaScopeTrimHeight();
|
||||
default:
|
||||
return camera->mTrimHeight;
|
||||
}
|
||||
}
|
||||
return camera->mTrimHeight;
|
||||
}
|
||||
|
||||
void widezoom_correction(camera_process_class* i_this, float trim_height) {
|
||||
camera_class* camera = (camera_class*)i_this;
|
||||
dDlst_window_c* window = get_window(camera);
|
||||
view_port_class* viewport = window->getViewPort();
|
||||
|
||||
auto trim_width = 0.0f;
|
||||
|
||||
if (mDoGph_gInf_c::isWideZoom()) {
|
||||
const auto target_ar = FB_WIDTH_BASE / (FB_HEIGHT_BASE - trim_height * 2.0f);
|
||||
const auto target_ar_real =
|
||||
FB_WIDTH_BASE / (FB_HEIGHT_BASE - get_target_trim_height(i_this) * 2.0f);
|
||||
const auto current_ar = camera->view.aspect;
|
||||
|
||||
if (current_ar < target_ar) {
|
||||
trim_height = FB_HEIGHT_BASE / 2.0f * (1.0f - current_ar / target_ar);
|
||||
} else {
|
||||
trim_height = 0.0f;
|
||||
trim_width = FB_WIDTH_BASE / 2.0f * (1.0f - target_ar_real / current_ar);
|
||||
}
|
||||
|
||||
if (dusk::frame_interp::is_sim_frame()) {
|
||||
constexpr auto base_ar =
|
||||
static_cast<f32>(FB_WIDTH_BASE) / static_cast<f32>(FB_HEIGHT_BASE);
|
||||
const auto ar_corr = base_ar / std::min(current_ar, target_ar_real);
|
||||
camera->view.fovy =
|
||||
MTXRadToDeg(2.0f * atanf(tanf(MTXDegToRad(camera->view.fovy) * 0.5f) * ar_corr));
|
||||
}
|
||||
}
|
||||
|
||||
trim_width *= viewport->width / FB_WIDTH_BASE;
|
||||
trim_height *= viewport->height / FB_HEIGHT_BASE;
|
||||
window->setScissor(trim_width, trim_height, viewport->width - trim_width * 2.0f,
|
||||
viewport->height - trim_height * 2.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
static int camera_execute(camera_process_class* i_this) {
|
||||
preparation(i_this);
|
||||
|
||||
@@ -11251,6 +11310,28 @@ static int camera_execute(camera_process_class* i_this) {
|
||||
store(i_this);
|
||||
|
||||
#ifdef TARGET_PC
|
||||
widezoom_correction(i_this, i_this->mCamera.TrimHeight());
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
const auto target = get_target_trim_height(i_this);
|
||||
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;
|
||||
|
||||
widezoom_correction(i_this, trim_height);
|
||||
}
|
||||
}, i_this);
|
||||
}
|
||||
|
||||
// record new camera for our sim frame
|
||||
dusk::frame_interp::record_camera(i_this, get_camera_id(i_this));
|
||||
// interpolate the view now so that this sim frame's view matrix matches what
|
||||
@@ -11262,26 +11343,6 @@ 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;
|
||||
@@ -11334,42 +11395,8 @@ static int camera_draw(camera_process_class* i_this) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
set_ar_corrected_trim(window, body->TrimHeight());
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
#else
|
||||
#if !TARGET_PC
|
||||
// trim handling moved to camera_execute for PC
|
||||
int trim_height = body->TrimHeight();
|
||||
|
||||
window->setScissor(0.0f, trim_height, FB_WIDTH, FB_HEIGHT - trim_height * 2.0f);
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "dusk/imgui/ImGuiBloomWindow.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/game_clock.h"
|
||||
#endif
|
||||
|
||||
static void GxXFog_set();
|
||||
@@ -2268,6 +2269,7 @@ void dKy_calc_color_set(GXColorS10* out_color_p, color_RGB_class* color_a_start_
|
||||
color_b_start_p->b, color_b_end_p->b, blend_ratio, add_col.b, scale);
|
||||
}
|
||||
|
||||
|
||||
void dScnKy_env_light_c::setLight() {
|
||||
f32 color_ratio;
|
||||
|
||||
@@ -2513,7 +2515,14 @@ void dScnKy_env_light_c::setLight() {
|
||||
static s16 S_fuwan_sin;
|
||||
|
||||
f32 sin = cM_ssin(S_fuwan_sin);
|
||||
S_fuwan_sin += (s16)cM_rndF(2000.0f) + 500;
|
||||
|
||||
#if TARGET_PC
|
||||
const f32 deltaTime = dusk::game_clock::consume_interval(this);
|
||||
const f32 timeScale = deltaTime / dusk::game_clock::period_for_original_frames(1.0f);
|
||||
S_fuwan_sin += (s16)((cM_rndF(2000.0f) + 500) * timeScale);
|
||||
#else
|
||||
S_fuwan_sin += (s16)cM_rndF(2000.0f) + 500;
|
||||
#endif
|
||||
|
||||
blure_size += (u8)(sin * (0.2f * blure_size));
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ u8 STControl::checkTrigger() {
|
||||
field_0x22 = -field_0x24;
|
||||
}
|
||||
}
|
||||
|
||||
#if !TARGET_PC
|
||||
if (!(mDirectionTrig & 3)) {
|
||||
Xinit();
|
||||
}
|
||||
@@ -160,10 +160,35 @@ u8 STControl::checkTrigger() {
|
||||
if (!(mDirectionTrig & 0xC)) {
|
||||
Yinit();
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
mDirectionTrig = 0;
|
||||
#if !TARGET_PC
|
||||
Xinit();
|
||||
Yinit();
|
||||
#endif
|
||||
#if TARGET_PC
|
||||
if (mDoCPd_c::getHoldLeft(PAD_1)) {
|
||||
mDirectionTrig |= TRIG_LEFT;
|
||||
}
|
||||
if (mDoCPd_c::getHoldRight(PAD_1)) {
|
||||
mDirectionTrig |= TRIG_RIGHT;
|
||||
}
|
||||
if (mDoCPd_c::getHoldUp(PAD_1)) {
|
||||
mDirectionTrig |= TRIG_UP;
|
||||
}
|
||||
if (mDoCPd_c::getHoldDown(PAD_1)) {
|
||||
mDirectionTrig |= TRIG_DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(mDirectionTrig & 3)) {
|
||||
Xinit();
|
||||
}
|
||||
|
||||
if (!(mDirectionTrig & 0xC)) {
|
||||
Yinit();
|
||||
#endif
|
||||
}
|
||||
|
||||
if ((field_0x0d & mDirectionTrig & 3) && field_0x0e > 0) {
|
||||
|
||||
@@ -1141,6 +1141,9 @@ dMap_c::dMap_c(int width, int height, int param_2, int param_3) {
|
||||
field_0x91 = 0;
|
||||
m_mySelfPointer = this;
|
||||
#endif
|
||||
#if TARGET_PC
|
||||
previousMirror = dusk::getSettings().game.enableMirrorMode;
|
||||
#endif
|
||||
|
||||
m_res = JKR_NEW_ARGS (0x20) dMap_prm_res_s;
|
||||
JUT_ASSERT(2559, m_res != NULL);
|
||||
@@ -1579,6 +1582,17 @@ bool dMap_c::isDrawRoomIcon(int param_0, int param_1) const {
|
||||
}
|
||||
|
||||
void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
#if TARGET_PC
|
||||
bool currentMirror = dusk::getSettings().game.enableMirrorMode;
|
||||
if (currentMirror != previousMirror) {
|
||||
previousMirror = currentMirror;
|
||||
if (currentMirror) {
|
||||
mCenterX -= 2.0f * mPackX;
|
||||
} else {
|
||||
mCenterX += 2.0f * mPackX;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (mStayRoomNo == -1) {
|
||||
mStayRoomNo = i_roomNo;
|
||||
field_0x80 = mStayRoomNo;
|
||||
|
||||
@@ -139,7 +139,7 @@ bool dMenu_Fishing_c::isSync() {
|
||||
void dMenu_Fishing_c::init() {
|
||||
#if TARGET_PC || VERSION == VERSION_GCN_PAL
|
||||
BOOL isEnglish = FALSE;
|
||||
if (dusk::version::isRegionUsa() || (dusk::version::isRegionPal() && dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH)) {
|
||||
if (dusk::version::isRegionPal() && dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) {
|
||||
isEnglish = TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "d/d_s_name.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/memory.h"
|
||||
#include "dusk/speedrun.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "f_op/f_op_overlap_mng.h"
|
||||
#include "f_op/f_op_scene_mng.h"
|
||||
@@ -418,7 +419,7 @@ void dScnName_c::changeGameScene() {
|
||||
if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) {
|
||||
// start a new run on file load if a run isn't already in progress
|
||||
if (!dusk::m_speedrunInfo.m_isRunStarted) {
|
||||
dusk::ImGuiMenuGame::resetForSpeedrunMode();
|
||||
dusk::resetForSpeedrunMode();
|
||||
dusk::m_speedrunInfo.startRun();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,3 +264,9 @@ ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void dusk::config::EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
|
||||
for (auto& pair : RegisteredConfigVars) {
|
||||
callback(*pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -13,114 +12,83 @@
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#include "SDL3/SDL_filesystem.h"
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#include <sentry.h>
|
||||
#endif
|
||||
|
||||
namespace dusk {
|
||||
namespace dusk::crash_reporting {
|
||||
|
||||
namespace {
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
bool g_sentryInitialized = false;
|
||||
|
||||
bool IsTruthy(std::string_view value) {
|
||||
return value == "1" || value == "true" || value == "TRUE" || value == "yes"
|
||||
|| value == "YES" || value == "on" || value == "ON";
|
||||
bool truthy(std::string_view value) {
|
||||
return value == "1" || value == "true" || value == "TRUE" || value == "yes" || value == "YES" ||
|
||||
value == "on" || value == "ON";
|
||||
}
|
||||
|
||||
std::string GetEnvOrEmpty(const char* name) {
|
||||
std::string env_or_empty(const char* name) {
|
||||
if (const char* value = std::getenv(name)) {
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool GetEffectiveEnabled() {
|
||||
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_ENABLED");
|
||||
if (!env.empty()) {
|
||||
return IsTruthy(env);
|
||||
}
|
||||
return getSettings().backend.enableCrashReporting;
|
||||
bool disabled_by_env() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_ENABLED");
|
||||
return !env.empty() && !truthy(env);
|
||||
}
|
||||
|
||||
std::string GetEffectiveDsn() {
|
||||
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_DSN");
|
||||
std::string effective_dsn() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_DSN");
|
||||
if (!env.empty()) {
|
||||
return env;
|
||||
}
|
||||
return DUSK_SENTRY_DSN;
|
||||
}
|
||||
|
||||
bool GetEffectiveDebug() {
|
||||
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_DEBUG");
|
||||
bool effective_debug() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_DEBUG");
|
||||
if (!env.empty()) {
|
||||
return IsTruthy(env);
|
||||
return truthy(env);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GetReleaseName() {
|
||||
std::string release_name() {
|
||||
return std::string(AppName) + "@" DUSK_WC_DESCRIBE;
|
||||
}
|
||||
|
||||
std::filesystem::path GetSentryDatabasePath() {
|
||||
std::filesystem::path sentry_database_path() {
|
||||
return dusk::ConfigPath / "sentry";
|
||||
}
|
||||
|
||||
std::filesystem::path GetLogAttachmentPath() {
|
||||
std::filesystem::path log_attachment_path() {
|
||||
if (const char* logPath = GetLogFilePath()) {
|
||||
return logPath;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path GetCrashpadHandlerPath() {
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
if (!basePath) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::filesystem::path handlerDir(basePath);
|
||||
#if _WIN32
|
||||
return handlerDir / "crashpad_handler.exe";
|
||||
#else
|
||||
return handlerDir / "crashpad_handler";
|
||||
#endif
|
||||
}
|
||||
|
||||
void ConfigurePathOptions(sentry_options_t* options) {
|
||||
const auto databasePath = GetSentryDatabasePath();
|
||||
void configure_path_options(sentry_options_t* options) {
|
||||
const auto databasePath = sentry_database_path();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(databasePath, ec);
|
||||
if (ec) {
|
||||
DuskLog.warn("Unable to create Sentry database path '{}': {}",
|
||||
databasePath.string(), ec.message());
|
||||
DuskLog.warn(
|
||||
"Unable to create Sentry database path '{}': {}", databasePath.string(), ec.message());
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const std::wstring databasePathWide = databasePath.wstring();
|
||||
sentry_options_set_database_pathw(options, databasePathWide.c_str());
|
||||
|
||||
const auto handlerPath = GetCrashpadHandlerPath();
|
||||
if (!handlerPath.empty()) {
|
||||
const std::wstring handlerPathWide = handlerPath.wstring();
|
||||
sentry_options_set_handler_pathw(options, handlerPathWide.c_str());
|
||||
}
|
||||
#else
|
||||
const std::string databasePathUtf8 = databasePath.string();
|
||||
sentry_options_set_database_path(options, databasePathUtf8.c_str());
|
||||
|
||||
const auto handlerPath = GetCrashpadHandlerPath();
|
||||
if (!handlerPath.empty()) {
|
||||
const std::string handlerPathUtf8 = handlerPath.string();
|
||||
sentry_options_set_handler_path(options, handlerPathUtf8.c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
const auto logPath = GetLogAttachmentPath();
|
||||
const auto logPath = log_attachment_path();
|
||||
if (!logPath.empty()) {
|
||||
#if _WIN32
|
||||
sentry_options_add_attachmentw(options, logPath.wstring().c_str());
|
||||
@@ -133,32 +101,29 @@ void ConfigurePathOptions(sentry_options_t* options) {
|
||||
|
||||
} // namespace
|
||||
|
||||
void InitializeCrashReporting() {
|
||||
void initialize() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (g_sentryInitialized) {
|
||||
if (g_sentryInitialized || disabled_by_env()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GetEffectiveEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string dsn = GetEffectiveDsn();
|
||||
const std::string dsn = effective_dsn();
|
||||
if (dsn.empty()) {
|
||||
DuskLog.warn("Crash reporting is enabled but no Sentry DSN is configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string release = GetReleaseName();
|
||||
const std::string release = release_name();
|
||||
|
||||
sentry_options_t* options = sentry_options_new();
|
||||
sentry_options_set_dsn(options, dsn.c_str());
|
||||
sentry_options_set_release(options, release.c_str());
|
||||
sentry_options_set_environment(options, DUSK_SENTRY_ENVIRONMENT);
|
||||
sentry_options_set_debug(options, GetEffectiveDebug() ? 1 : 0);
|
||||
sentry_options_set_debug(options, effective_debug() ? 1 : 0);
|
||||
sentry_options_set_require_user_consent(options, 1);
|
||||
sentry_options_set_cache_keep(options, 1);
|
||||
sentry_options_set_max_breadcrumbs(options, 100);
|
||||
ConfigurePathOptions(options);
|
||||
configure_path_options(options);
|
||||
|
||||
if (sentry_init(options) != 0) {
|
||||
DuskLog.warn("Failed to initialize Sentry crash reporting");
|
||||
@@ -173,7 +138,7 @@ void InitializeCrashReporting() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShutdownCrashReporting() {
|
||||
void shutdown() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return;
|
||||
@@ -184,4 +149,40 @@ void ShutdownCrashReporting() {
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
Consent get_consent() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return Consent::Unavailable;
|
||||
}
|
||||
|
||||
switch (sentry_user_consent_get()) {
|
||||
case SENTRY_USER_CONSENT_GIVEN:
|
||||
return Consent::Given;
|
||||
case SENTRY_USER_CONSENT_REVOKED:
|
||||
return Consent::Revoked;
|
||||
case SENTRY_USER_CONSENT_UNKNOWN:
|
||||
default:
|
||||
return Consent::Unknown;
|
||||
}
|
||||
#else
|
||||
return Consent::Unavailable;
|
||||
#endif
|
||||
}
|
||||
|
||||
void set_consent(bool enabled) {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
sentry_user_consent_give();
|
||||
} else {
|
||||
sentry_user_consent_revoke();
|
||||
}
|
||||
#else
|
||||
(void)enabled;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk::crash_reporting
|
||||
|
||||
@@ -0,0 +1,971 @@
|
||||
#include "data.hpp"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <SDL3/SDL_misc.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
namespace dusk::data {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log{"dusk::data"};
|
||||
|
||||
constexpr auto kLocationDescriptorName = "data_location.json";
|
||||
constexpr auto kPipelineCacheName = "pipeline_cache.db";
|
||||
constexpr auto kInitialPipelineCacheName = "initial_pipeline_cache.db";
|
||||
|
||||
enum class LocationMode {
|
||||
Default,
|
||||
Portable,
|
||||
Custom,
|
||||
};
|
||||
|
||||
struct LocationDescriptor {
|
||||
LocationMode mode = LocationMode::Default;
|
||||
std::filesystem::path customPath;
|
||||
std::filesystem::path previousPath;
|
||||
};
|
||||
|
||||
struct LocatedDescriptor {
|
||||
LocationDescriptor descriptor;
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
struct MigrationStats {
|
||||
std::uintmax_t directoriesCreated = 0;
|
||||
std::uintmax_t filesCopied = 0;
|
||||
std::uintmax_t symlinksCopied = 0;
|
||||
std::uintmax_t sourcesRemoved = 0;
|
||||
std::uintmax_t emptyDirectoriesRemoved = 0;
|
||||
std::uintmax_t skippedExistingTargets = 0;
|
||||
std::uintmax_t skippedDescriptorFiles = 0;
|
||||
std::uintmax_t skippedNestedTargets = 0;
|
||||
std::uintmax_t skippedUnsupportedEntries = 0;
|
||||
std::uintmax_t failures = 0;
|
||||
};
|
||||
|
||||
std::optional<std::filesystem::path> sConfiguredDataPath;
|
||||
std::optional<std::filesystem::path> sActiveDescriptorPath;
|
||||
|
||||
std::filesystem::path path_from_utf8(std::string_view value) {
|
||||
return std::filesystem::path{
|
||||
reinterpret_cast<const char8_t*>(value.data()),
|
||||
reinterpret_cast<const char8_t*>(value.data() + value.size()),
|
||||
};
|
||||
}
|
||||
|
||||
std::filesystem::path get_legacy_path() {
|
||||
if (std::string_view{LegacyAppName}.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
char* prefPath = SDL_GetPrefPath(OrgName, LegacyAppName);
|
||||
if (!prefPath) {
|
||||
Log.fatal("Unable to get PrefPath: {}", SDL_GetError());
|
||||
}
|
||||
|
||||
std::filesystem::path result{reinterpret_cast<const char8_t*>(prefPath)};
|
||||
SDL_free(prefPath);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::filesystem::path get_pref_path() {
|
||||
char* prefPath = SDL_GetPrefPath(OrgName, AppName);
|
||||
if (!prefPath) {
|
||||
Log.fatal("Unable to get PrefPath: {}", SDL_GetError());
|
||||
}
|
||||
|
||||
std::filesystem::path result{reinterpret_cast<const char8_t*>(prefPath)};
|
||||
SDL_free(prefPath);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
|
||||
const auto* basePath = SDL_GetBasePath();
|
||||
if (!basePath) {
|
||||
return path;
|
||||
}
|
||||
return std::filesystem::path{basePath} / path;
|
||||
}
|
||||
|
||||
std::filesystem::path default_data_path(const std::filesystem::path& prefPath) {
|
||||
#ifdef __APPLE__
|
||||
#if TARGET_OS_IOS && !TARGET_OS_TV
|
||||
const char* documentsPath = SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS);
|
||||
if (!documentsPath) {
|
||||
Log.fatal("Unable to get iOS Documents path: {}", SDL_GetError());
|
||||
}
|
||||
|
||||
return reinterpret_cast<const char8_t*>(documentsPath);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return prefPath;
|
||||
}
|
||||
|
||||
std::filesystem::path portable_data_path() {
|
||||
return base_path_relative("data");
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> descriptor_paths(const std::filesystem::path& prefPath) {
|
||||
std::vector<std::filesystem::path> paths;
|
||||
if (const auto basePath = base_path_relative(kLocationDescriptorName); !basePath.empty()) {
|
||||
paths.push_back(basePath);
|
||||
}
|
||||
paths.push_back(prefPath / kLocationDescriptorName);
|
||||
return paths;
|
||||
}
|
||||
|
||||
std::optional<LocationDescriptor> read_location_descriptor_file(const std::filesystem::path& path) {
|
||||
if (path.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (std::error_code ec; !std::filesystem::exists(path, ec)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto bytes = io::FileStream::ReadAllBytes(path);
|
||||
const auto json = nlohmann::json::parse(bytes);
|
||||
if (!json.is_object()) {
|
||||
Log.warn("Ignoring data location descriptor '{}': root is not an object",
|
||||
io::fs_path_to_string(path));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
LocationDescriptor descriptor;
|
||||
const auto mode = json.value<std::string>("mode", "default");
|
||||
if (mode == "portable") {
|
||||
descriptor.mode = LocationMode::Portable;
|
||||
} else if (mode == "custom") {
|
||||
descriptor.mode = LocationMode::Custom;
|
||||
} else if (mode != "default") {
|
||||
Log.warn("Ignoring unknown data location mode '{}'", mode);
|
||||
}
|
||||
|
||||
if (const auto customPath = json.find("customPath");
|
||||
customPath != json.end() && customPath->is_string())
|
||||
{
|
||||
descriptor.customPath = path_from_utf8(customPath->get<std::string>());
|
||||
}
|
||||
if (const auto previousPath = json.find("previousPath");
|
||||
previousPath != json.end() && previousPath->is_string())
|
||||
{
|
||||
descriptor.previousPath = path_from_utf8(previousPath->get<std::string>());
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
} catch (const std::exception& e) {
|
||||
Log.warn(
|
||||
"Ignoring data location descriptor '{}': {}", io::fs_path_to_string(path), e.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<LocatedDescriptor> read_location_descriptor(const std::filesystem::path& prefPath) {
|
||||
for (const auto& path : descriptor_paths(prefPath)) {
|
||||
if (auto descriptor = read_location_descriptor_file(path)) {
|
||||
return LocatedDescriptor{
|
||||
.descriptor = *descriptor,
|
||||
.path = path,
|
||||
};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::filesystem::path resolve_data_path(
|
||||
const std::filesystem::path& prefPath, const LocationDescriptor* descriptor) {
|
||||
if (!descriptor) {
|
||||
return default_data_path(prefPath);
|
||||
}
|
||||
|
||||
switch (descriptor->mode) {
|
||||
case LocationMode::Default:
|
||||
return default_data_path(prefPath);
|
||||
case LocationMode::Portable:
|
||||
return portable_data_path();
|
||||
case LocationMode::Custom:
|
||||
if (!descriptor->customPath.empty()) {
|
||||
return descriptor->customPath;
|
||||
}
|
||||
Log.warn("Data location descriptor requested custom mode without a path");
|
||||
return default_data_path(prefPath);
|
||||
}
|
||||
|
||||
return default_data_path(prefPath);
|
||||
}
|
||||
|
||||
const char* location_mode_id(LocationMode mode) {
|
||||
switch (mode) {
|
||||
case LocationMode::Default:
|
||||
return "default";
|
||||
case LocationMode::Portable:
|
||||
return "portable";
|
||||
case LocationMode::Custom:
|
||||
return "custom";
|
||||
}
|
||||
|
||||
return "default";
|
||||
}
|
||||
|
||||
std::filesystem::path normalized_path(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
auto normalized = std::filesystem::weakly_canonical(path, ec);
|
||||
if (!ec) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
normalized = std::filesystem::absolute(path, ec);
|
||||
if (!ec) {
|
||||
return normalized.lexically_normal();
|
||||
}
|
||||
|
||||
return path.lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path absolute_path(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
const auto absolute = std::filesystem::absolute(path, ec);
|
||||
if (ec) {
|
||||
return path;
|
||||
}
|
||||
return absolute.lexically_normal();
|
||||
}
|
||||
|
||||
bool is_same_or_inside(const std::filesystem::path& root, const std::filesystem::path& path) {
|
||||
const auto normalizedRoot = normalized_path(root);
|
||||
const auto normalizedPath = normalized_path(path);
|
||||
const auto relativePath = normalizedPath.lexically_relative(normalizedRoot);
|
||||
if (relativePath.empty()) {
|
||||
return normalizedPath == normalizedRoot;
|
||||
}
|
||||
if (relativePath == ".") {
|
||||
return true;
|
||||
}
|
||||
if (relativePath.is_absolute()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto it = relativePath.begin();
|
||||
return it == relativePath.end() || *it != "..";
|
||||
}
|
||||
|
||||
bool should_skip_migration_path(const std::filesystem::path& path,
|
||||
const std::filesystem::path& from, const std::filesystem::path& to, MigrationStats& stats) {
|
||||
if (is_same_or_inside(to, path)) {
|
||||
++stats.skippedNestedTargets;
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto relativePath = path.lexically_relative(from);
|
||||
if (relativePath == kLocationDescriptorName) {
|
||||
++stats.skippedDescriptorFiles;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool has_location_descriptor(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
return std::filesystem::exists(path / kLocationDescriptorName, ec);
|
||||
}
|
||||
|
||||
bool remove_empty_destination_for_rename(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
const bool exists = std::filesystem::exists(path, ec);
|
||||
if (ec) {
|
||||
Log.debug("Could not inspect migration destination '{}': {}", io::fs_path_to_string(path),
|
||||
ec.message());
|
||||
return false;
|
||||
}
|
||||
if (!exists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool canRemove = std::filesystem::is_directory(path, ec) &&
|
||||
std::filesystem::is_empty(path, ec) && !has_location_descriptor(path);
|
||||
if (ec || !canRemove) {
|
||||
if (ec) {
|
||||
Log.debug("Could not inspect migration destination '{}': {}",
|
||||
io::fs_path_to_string(path), ec.message());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::remove(path, ec);
|
||||
if (ec) {
|
||||
Log.debug("Could not remove empty migration destination '{}': {}",
|
||||
io::fs_path_to_string(path), ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool try_rename_directory_migration(
|
||||
const std::filesystem::path& from, const std::filesystem::path& to) {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_directory(from, ec)) {
|
||||
return false;
|
||||
}
|
||||
if (ec) {
|
||||
Log.debug("Could not inspect migration source '{}': {}", io::fs_path_to_string(from),
|
||||
ec.message());
|
||||
return false;
|
||||
}
|
||||
if (has_location_descriptor(from)) {
|
||||
return false;
|
||||
}
|
||||
if (!remove_empty_destination_for_rename(to)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(to.parent_path(), ec);
|
||||
if (ec) {
|
||||
Log.debug("Could not create migration destination parent '{}': {}",
|
||||
io::fs_path_to_string(to.parent_path()), ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::rename(from, to, ec);
|
||||
if (ec) {
|
||||
Log.debug("Could not rename data directory '{}' to '{}': {}", io::fs_path_to_string(from),
|
||||
io::fs_path_to_string(to), ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.info("Renamed data directory '{}' to '{}'", io::fs_path_to_string(from),
|
||||
io::fs_path_to_string(to));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::filesystem::path current_data_path() {
|
||||
if (!ConfigPath.empty()) {
|
||||
return ConfigPath;
|
||||
}
|
||||
const auto prefPath = get_pref_path();
|
||||
const auto descriptor = read_location_descriptor(prefPath);
|
||||
if (descriptor) {
|
||||
sActiveDescriptorPath = descriptor->path;
|
||||
}
|
||||
return resolve_data_path(prefPath, descriptor ? &descriptor->descriptor : nullptr);
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> descriptor_write_paths(const std::filesystem::path& prefPath) {
|
||||
if (sActiveDescriptorPath && !sActiveDescriptorPath->empty()) {
|
||||
return {*sActiveDescriptorPath};
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> paths;
|
||||
#if defined(_WIN32)
|
||||
if (const auto basePath = base_path_relative(kLocationDescriptorName); !basePath.empty()) {
|
||||
paths.push_back(basePath);
|
||||
}
|
||||
#endif
|
||||
paths.push_back(prefPath / kLocationDescriptorName);
|
||||
return paths;
|
||||
}
|
||||
|
||||
bool write_descriptor_json(const std::filesystem::path& path, const nlohmann::json& json) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
Log.warn("Failed to create data location descriptor directory '{}': {}",
|
||||
io::fs_path_to_string(path.parent_path()), ec.message());
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
io::FileStream::WriteAllText(path, json.dump(4));
|
||||
} catch (const std::exception& e) {
|
||||
Log.warn("Failed to write data location descriptor '{}': {}", io::fs_path_to_string(path),
|
||||
e.what());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool write_location_descriptor(LocationMode mode, const std::filesystem::path& targetPath) {
|
||||
LocationDescriptor descriptor;
|
||||
descriptor.mode = mode;
|
||||
if (mode == LocationMode::Custom) {
|
||||
descriptor.customPath = absolute_path(targetPath);
|
||||
}
|
||||
|
||||
const auto currentPath = current_data_path();
|
||||
const auto resolvedTargetPath =
|
||||
mode == LocationMode::Custom ? descriptor.customPath : targetPath;
|
||||
if (!currentPath.empty() && normalized_path(currentPath) != normalized_path(resolvedTargetPath))
|
||||
{
|
||||
descriptor.previousPath = currentPath;
|
||||
}
|
||||
|
||||
nlohmann::json json;
|
||||
json["version"] = 1;
|
||||
json["mode"] = location_mode_id(descriptor.mode);
|
||||
if (descriptor.mode == LocationMode::Custom && !descriptor.customPath.empty()) {
|
||||
json["customPath"] = io::fs_path_to_string(descriptor.customPath);
|
||||
}
|
||||
if (!descriptor.previousPath.empty()) {
|
||||
json["previousPath"] = io::fs_path_to_string(descriptor.previousPath);
|
||||
}
|
||||
|
||||
const auto prefPath = get_pref_path();
|
||||
for (const auto& path : descriptor_write_paths(prefPath)) {
|
||||
if (write_descriptor_json(path, json)) {
|
||||
sActiveDescriptorPath = path;
|
||||
sConfiguredDataPath = resolvedTargetPath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uintmax_t remove_empty_directories(const std::filesystem::path& root, bool includeRoot) {
|
||||
std::error_code ec;
|
||||
std::vector<std::filesystem::path> directories;
|
||||
for (std::filesystem::recursive_directory_iterator it(
|
||||
root, std::filesystem::directory_options::skip_permission_denied, ec);
|
||||
it != std::filesystem::recursive_directory_iterator(); it.increment(ec))
|
||||
{
|
||||
if (ec) {
|
||||
Log.warn("Failed to scan empty directories under '{}': {}", io::fs_path_to_string(root),
|
||||
ec.message());
|
||||
return 0;
|
||||
}
|
||||
const auto status = it->symlink_status(ec);
|
||||
if (ec) {
|
||||
Log.warn("Failed to inspect '{}' while pruning empty directories: {}",
|
||||
io::fs_path_to_string(it->path()), ec.message());
|
||||
ec.clear();
|
||||
continue;
|
||||
}
|
||||
if (std::filesystem::is_directory(status)) {
|
||||
directories.push_back(it->path());
|
||||
}
|
||||
}
|
||||
|
||||
std::uintmax_t removed = 0;
|
||||
for (auto& dir : std::views::reverse(directories)) {
|
||||
if (!std::filesystem::is_empty(dir, ec)) {
|
||||
ec.clear();
|
||||
continue;
|
||||
}
|
||||
if (std::filesystem::remove(dir, ec)) {
|
||||
++removed;
|
||||
} else if (ec) {
|
||||
Log.warn("Failed to remove empty migrated source directory '{}': {}",
|
||||
io::fs_path_to_string(dir), ec.message());
|
||||
}
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
if (includeRoot) {
|
||||
if (std::filesystem::is_empty(root, ec)) {
|
||||
if (std::filesystem::remove(root, ec)) {
|
||||
++removed;
|
||||
} else if (ec) {
|
||||
Log.warn("Failed to remove empty migrated source root '{}': {}",
|
||||
io::fs_path_to_string(root), ec.message());
|
||||
}
|
||||
}
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
bool ensure_parent_directory(const std::filesystem::path& targetPath, MigrationStats& stats) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(targetPath.parent_path(), ec);
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn("Failed to create migration target parent '{}': {}",
|
||||
io::fs_path_to_string(targetPath.parent_path()), ec.message());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool remove_migrated_source(const std::filesystem::path& sourcePath, MigrationStats& stats) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(sourcePath, ec);
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn("Migrated '{}' but failed to remove source: {}", io::fs_path_to_string(sourcePath),
|
||||
ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
++stats.sourcesRemoved;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool try_rename_migration_entry(
|
||||
const std::filesystem::path& sourcePath, const std::filesystem::path& targetPath) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(targetPath, ec) || std::filesystem::is_symlink(targetPath, ec)) {
|
||||
return false;
|
||||
}
|
||||
ec.clear();
|
||||
|
||||
if (!std::filesystem::exists(sourcePath, ec)) {
|
||||
return false;
|
||||
}
|
||||
ec.clear();
|
||||
|
||||
std::filesystem::create_directories(targetPath.parent_path(), ec);
|
||||
if (ec) {
|
||||
Log.debug("Could not create migration target parent '{}' before rename: {}",
|
||||
io::fs_path_to_string(targetPath.parent_path()), ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::rename(sourcePath, targetPath, ec);
|
||||
if (ec) {
|
||||
Log.debug("Could not rename migration entry '{}' to '{}': {}",
|
||||
io::fs_path_to_string(sourcePath), io::fs_path_to_string(targetPath), ec.message());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void migrate_symlink(const std::filesystem::path& sourcePath,
|
||||
const std::filesystem::path& targetPath, MigrationStats& stats) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(targetPath, ec) || std::filesystem::is_symlink(targetPath, ec)) {
|
||||
++stats.skippedExistingTargets;
|
||||
return;
|
||||
}
|
||||
ec.clear();
|
||||
|
||||
const auto linkTarget = std::filesystem::read_symlink(sourcePath, ec);
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn("Failed to read migration symlink '{}': {}", io::fs_path_to_string(sourcePath),
|
||||
ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_parent_directory(targetPath, stats)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool targetIsDirectory = std::filesystem::is_directory(sourcePath, ec);
|
||||
if (ec) {
|
||||
Log.debug("Could not resolve symlink target type for '{}': {}",
|
||||
io::fs_path_to_string(sourcePath), ec.message());
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
if (targetIsDirectory) {
|
||||
std::filesystem::create_directory_symlink(linkTarget, targetPath, ec);
|
||||
} else {
|
||||
std::filesystem::create_symlink(linkTarget, targetPath, ec);
|
||||
}
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn("Failed to migrate symlink '{}' -> '{}' to '{}': {}",
|
||||
io::fs_path_to_string(sourcePath), io::fs_path_to_string(linkTarget),
|
||||
io::fs_path_to_string(targetPath), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
++stats.symlinksCopied;
|
||||
remove_migrated_source(sourcePath, stats);
|
||||
}
|
||||
|
||||
void migrate_regular_file(const std::filesystem::path& sourcePath,
|
||||
const std::filesystem::path& targetPath, MigrationStats& stats) {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(targetPath, ec)) {
|
||||
++stats.skippedExistingTargets;
|
||||
return;
|
||||
}
|
||||
ec.clear();
|
||||
|
||||
if (try_rename_migration_entry(sourcePath, targetPath)) {
|
||||
++stats.filesCopied;
|
||||
++stats.sourcesRemoved;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensure_parent_directory(targetPath, stats)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::copy_file(
|
||||
sourcePath, targetPath, std::filesystem::copy_options::skip_existing, ec);
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn("Failed to migrate file '{}' to '{}': {}", io::fs_path_to_string(sourcePath),
|
||||
io::fs_path_to_string(targetPath), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
++stats.filesCopied;
|
||||
remove_migrated_source(sourcePath, stats);
|
||||
}
|
||||
|
||||
void migrate_directory(const std::filesystem::path& from, const std::filesystem::path& to,
|
||||
const std::filesystem::path& prefPath) {
|
||||
if (from.empty() || to.empty() || normalized_path(from) == normalized_path(to)) {
|
||||
Log.debug("Skipping data migration from '{}' to '{}'", io::fs_path_to_string(from),
|
||||
io::fs_path_to_string(to));
|
||||
return;
|
||||
}
|
||||
|
||||
MigrationStats stats;
|
||||
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(from, ec)) {
|
||||
if (ec) {
|
||||
Log.warn("Failed to inspect migration source '{}': {}", io::fs_path_to_string(from),
|
||||
ec.message());
|
||||
} else {
|
||||
Log.debug("Migration source '{}' does not exist", io::fs_path_to_string(from));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (try_rename_directory_migration(from, to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (try_rename_directory_migration(from, to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::create_directories(to, ec);
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn("Failed to create data directory '{}' for migration: {}",
|
||||
io::fs_path_to_string(to), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::recursive_directory_iterator it(
|
||||
from, std::filesystem::directory_options::skip_permission_denied, ec);
|
||||
if (ec) {
|
||||
Log.warn("Failed to begin migration scan for '{}': {}", io::fs_path_to_string(from),
|
||||
ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
const std::filesystem::recursive_directory_iterator end;
|
||||
while (it != end) {
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn(
|
||||
"Migration scan error under '{}': {}", io::fs_path_to_string(from), ec.message());
|
||||
ec.clear();
|
||||
}
|
||||
|
||||
const auto sourcePath = it->path();
|
||||
const auto status = it->symlink_status(ec);
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn("Failed to inspect migration source '{}': {}",
|
||||
io::fs_path_to_string(sourcePath), ec.message());
|
||||
ec.clear();
|
||||
it.increment(ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (should_skip_migration_path(sourcePath, from, to, stats)) {
|
||||
if (std::filesystem::is_directory(status)) {
|
||||
it.disable_recursion_pending();
|
||||
}
|
||||
ec.clear();
|
||||
it.increment(ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto relativePath = sourcePath.lexically_relative(from);
|
||||
if (relativePath.empty() || relativePath.is_absolute()) {
|
||||
++stats.failures;
|
||||
Log.warn("Failed to calculate migration relative path for '{}'",
|
||||
io::fs_path_to_string(sourcePath));
|
||||
it.increment(ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto targetPath = to / relativePath;
|
||||
if (std::filesystem::is_symlink(status)) {
|
||||
migrate_symlink(sourcePath, targetPath, stats);
|
||||
} else if (std::filesystem::is_directory(status)) {
|
||||
if (try_rename_migration_entry(sourcePath, targetPath)) {
|
||||
++stats.directoriesCreated;
|
||||
++stats.sourcesRemoved;
|
||||
it.disable_recursion_pending();
|
||||
} else {
|
||||
std::filesystem::create_directories(targetPath, ec);
|
||||
if (ec) {
|
||||
++stats.failures;
|
||||
Log.warn("Failed to create migration target directory '{}': {}",
|
||||
io::fs_path_to_string(targetPath), ec.message());
|
||||
ec.clear();
|
||||
it.disable_recursion_pending();
|
||||
} else {
|
||||
++stats.directoriesCreated;
|
||||
}
|
||||
}
|
||||
} else if (std::filesystem::is_regular_file(status)) {
|
||||
migrate_regular_file(sourcePath, targetPath, stats);
|
||||
} else {
|
||||
++stats.skippedUnsupportedEntries;
|
||||
}
|
||||
|
||||
it.increment(ec);
|
||||
}
|
||||
|
||||
const bool includeRoot = normalized_path(from) != normalized_path(prefPath);
|
||||
stats.emptyDirectoriesRemoved = remove_empty_directories(from, includeRoot);
|
||||
|
||||
const bool migratedAnything = stats.filesCopied > 0 || stats.symlinksCopied > 0 ||
|
||||
stats.sourcesRemoved > 0 || stats.emptyDirectoriesRemoved > 0 ||
|
||||
stats.failures > 0;
|
||||
if (migratedAnything) {
|
||||
Log.info(
|
||||
"Finished data migration from '{}' to '{}': {} files copied, {} symlinks copied, {} "
|
||||
"sources removed, {} empty directories removed, {} existing targets skipped, {} "
|
||||
"descriptor files skipped, {} nested destination paths skipped, {} unsupported entries "
|
||||
"skipped, {} failures",
|
||||
io::fs_path_to_string(from), io::fs_path_to_string(to), stats.filesCopied,
|
||||
stats.symlinksCopied, stats.sourcesRemoved, stats.emptyDirectoriesRemoved,
|
||||
stats.skippedExistingTargets, stats.skippedDescriptorFiles, stats.skippedNestedTargets,
|
||||
stats.skippedUnsupportedEntries, stats.failures);
|
||||
}
|
||||
}
|
||||
|
||||
void migrate_data(const std::filesystem::path& prefPath, const std::filesystem::path& dataPath,
|
||||
const LocationDescriptor* descriptor) {
|
||||
if (descriptor && !descriptor->previousPath.empty()) {
|
||||
migrate_directory(descriptor->previousPath, dataPath, prefPath);
|
||||
} else if (const auto legacyPath = get_legacy_path(); !legacyPath.empty()) {
|
||||
migrate_directory(legacyPath, dataPath, prefPath);
|
||||
}
|
||||
}
|
||||
|
||||
void ensure_data_directory(const std::filesystem::path& dataPath) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dataPath, ec);
|
||||
if (ec) {
|
||||
Log.fatal("Failed to create data directory '{}': {}", io::fs_path_to_string(dataPath),
|
||||
ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
SDL_IOStream* open_initial_pipeline_cache_source(std::string& sourcePathString) {
|
||||
const auto basePath = base_path_relative(kInitialPipelineCacheName);
|
||||
sourcePathString = io::fs_path_to_string(basePath);
|
||||
auto* source = SDL_IOFromFile(sourcePathString.c_str(), "rb");
|
||||
if (source != nullptr) {
|
||||
return source;
|
||||
}
|
||||
|
||||
sourcePathString = std::string{kInitialPipelineCacheName};
|
||||
return SDL_IOFromFile(sourcePathString.c_str(), "rb");
|
||||
}
|
||||
|
||||
void ensure_initial_pipeline_cache(const std::filesystem::path& configDir) {
|
||||
if (configDir.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(configDir, ec);
|
||||
if (ec) {
|
||||
Log.warn("Failed to create config directory '{}' for pipeline cache: {}",
|
||||
io::fs_path_to_string(configDir), ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pipelineCachePath = configDir / kPipelineCacheName;
|
||||
if (std::filesystem::exists(pipelineCachePath, ec)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string sourcePathString;
|
||||
SDL_IOStream* source = open_initial_pipeline_cache_source(sourcePathString);
|
||||
if (source == nullptr) {
|
||||
Log.info("No bundled initial pipeline cache found");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pipelineCacheString = io::fs_path_to_string(pipelineCachePath);
|
||||
SDL_IOStream* destination = SDL_IOFromFile(pipelineCacheString.c_str(), "wb");
|
||||
if (destination == nullptr) {
|
||||
Log.warn("Failed to open '{}' for seeded pipeline cache: {}", pipelineCacheString,
|
||||
SDL_GetError());
|
||||
SDL_CloseIO(source);
|
||||
return;
|
||||
}
|
||||
|
||||
bool copied = true;
|
||||
std::array<char, 64 * 1024> buffer{};
|
||||
while (true) {
|
||||
const size_t bytesRead = SDL_ReadIO(source, buffer.data(), buffer.size());
|
||||
if (bytesRead > 0) {
|
||||
size_t bytesWritten = 0;
|
||||
while (bytesWritten < bytesRead) {
|
||||
const size_t written = SDL_WriteIO(
|
||||
destination, buffer.data() + bytesWritten, bytesRead - bytesWritten);
|
||||
if (written == 0) {
|
||||
Log.warn("Failed to write seeded pipeline cache '{}': {}", pipelineCacheString,
|
||||
SDL_GetError());
|
||||
copied = false;
|
||||
break;
|
||||
}
|
||||
bytesWritten += written;
|
||||
}
|
||||
}
|
||||
|
||||
if (!copied) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (bytesRead < buffer.size()) {
|
||||
if (SDL_GetIOStatus(source) == SDL_IO_STATUS_EOF) {
|
||||
break;
|
||||
}
|
||||
|
||||
Log.warn(
|
||||
"Failed to read bundled pipeline cache '{}': {}", sourcePathString, SDL_GetError());
|
||||
copied = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!SDL_CloseIO(destination)) {
|
||||
Log.warn(
|
||||
"Failed to close seeded pipeline cache '{}': {}", pipelineCacheString, SDL_GetError());
|
||||
copied = false;
|
||||
}
|
||||
SDL_CloseIO(source);
|
||||
|
||||
if (!copied) {
|
||||
std::filesystem::remove(pipelineCachePath, ec);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info("Seeded pipeline cache from '{}'", sourcePathString);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool open_data_path() {
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
std::error_code ec;
|
||||
std::filesystem::path path = std::filesystem::absolute(ConfigPath, ec);
|
||||
if (ec) {
|
||||
Log.warn("Failed to resolve absolute data folder path '{}': {}",
|
||||
io::fs_path_to_string(ConfigPath), ec.message());
|
||||
path = ConfigPath;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
const std::string url = "file:///" + path.generic_string();
|
||||
#else
|
||||
const std::string url = "file://" + path.generic_string();
|
||||
#endif
|
||||
if (!SDL_OpenURL(url.c_str())) {
|
||||
Log.warn(
|
||||
"Failed to open data folder '{}': {}", io::fs_path_to_string(path), SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool set_custom_data_path(const std::filesystem::path& path) {
|
||||
if (path.empty()) {
|
||||
Log.warn("Ignoring empty custom data path");
|
||||
return false;
|
||||
}
|
||||
|
||||
return write_location_descriptor(LocationMode::Custom, path);
|
||||
}
|
||||
|
||||
bool set_custom_data_path(const char* path) {
|
||||
return set_custom_data_path(path_from_utf8(path));
|
||||
}
|
||||
|
||||
bool set_portable_data_path() {
|
||||
return write_location_descriptor(LocationMode::Portable, portable_data_path());
|
||||
}
|
||||
|
||||
bool reset_data_path() {
|
||||
const auto prefPath = get_pref_path();
|
||||
return write_location_descriptor(LocationMode::Default, default_data_path(prefPath));
|
||||
}
|
||||
|
||||
bool is_default_data_path() {
|
||||
const auto prefPath = get_pref_path();
|
||||
return normalized_path(configured_data_path()) == normalized_path(default_data_path(prefPath));
|
||||
}
|
||||
|
||||
std::filesystem::path configured_data_path() {
|
||||
if (sConfiguredDataPath) {
|
||||
return *sConfiguredDataPath;
|
||||
}
|
||||
|
||||
const auto prefPath = get_pref_path();
|
||||
const auto descriptor = read_location_descriptor(prefPath);
|
||||
if (descriptor) {
|
||||
sActiveDescriptorPath = descriptor->path;
|
||||
}
|
||||
sConfiguredDataPath =
|
||||
resolve_data_path(prefPath, descriptor ? &descriptor->descriptor : nullptr);
|
||||
return *sConfiguredDataPath;
|
||||
}
|
||||
|
||||
bool is_data_path_restart_pending() {
|
||||
if (ConfigPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalized_path(ConfigPath) != normalized_path(configured_data_path());
|
||||
}
|
||||
|
||||
std::filesystem::path initialize_data() {
|
||||
const auto prefPath = get_pref_path();
|
||||
const auto descriptor = read_location_descriptor(prefPath);
|
||||
if (descriptor) {
|
||||
sActiveDescriptorPath = descriptor->path;
|
||||
} else {
|
||||
sActiveDescriptorPath.reset();
|
||||
}
|
||||
const auto dataPath =
|
||||
resolve_data_path(prefPath, descriptor ? &descriptor->descriptor : nullptr);
|
||||
sConfiguredDataPath = dataPath;
|
||||
|
||||
migrate_data(prefPath, dataPath, descriptor ? &descriptor->descriptor : nullptr);
|
||||
ensure_data_directory(dataPath);
|
||||
ensure_initial_pipeline_cache(dataPath);
|
||||
|
||||
return dataPath;
|
||||
}
|
||||
|
||||
} // namespace dusk::data
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) || \
|
||||
(defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \
|
||||
(defined(__linux__) && !defined(__ANDROID__))
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 1
|
||||
#else
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 0
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
|
||||
#define DUSK_CAN_CHANGE_DATA_FOLDER 0
|
||||
#else
|
||||
#define DUSK_CAN_CHANGE_DATA_FOLDER 1
|
||||
#endif
|
||||
|
||||
namespace dusk::data {
|
||||
|
||||
std::filesystem::path initialize_data();
|
||||
std::filesystem::path configured_data_path();
|
||||
bool open_data_path();
|
||||
bool set_custom_data_path(const char* path);
|
||||
bool set_custom_data_path(const std::filesystem::path& path);
|
||||
bool set_portable_data_path();
|
||||
bool reset_data_path();
|
||||
bool is_default_data_path();
|
||||
bool is_data_path_restart_pending();
|
||||
|
||||
} // namespace dusk::data
|
||||
@@ -81,7 +81,7 @@ void update_presence() {
|
||||
rpc::Presence presence{};
|
||||
presence.startTimestamp = g_startTime;
|
||||
presence.largeImageKey = "icon";
|
||||
presence.largeImageText = "Dusk";
|
||||
presence.largeImageText = "Dusklight";
|
||||
|
||||
if (IsGameLaunched) {
|
||||
const char* stageName = dComIfGp_getLastPlayStageName();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_init.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
#if defined(__ANDROID__) || defined(ANDROID)
|
||||
@@ -16,6 +17,12 @@
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST
|
||||
#define USE_MACOS_FOLDER_DIALOG 1
|
||||
#else
|
||||
#define USE_MACOS_FOLDER_DIALOG 0
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
|
||||
#define USE_IOS_DIALOG 1
|
||||
#include "ios/FileSelectDialog.h"
|
||||
@@ -23,6 +30,13 @@
|
||||
#define USE_IOS_DIALOG 0
|
||||
#endif
|
||||
|
||||
#if USE_MACOS_FOLDER_DIALOG
|
||||
namespace dusk {
|
||||
bool ShowMacOSFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location);
|
||||
} // namespace dusk
|
||||
#endif
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
@@ -32,6 +46,10 @@ std::string fallback_display_name(std::string_view path) {
|
||||
}
|
||||
|
||||
std::string pathString(path);
|
||||
while (pathString.size() > 1 && (pathString.back() == '/' || pathString.back() == '\\')) {
|
||||
pathString.pop_back();
|
||||
}
|
||||
|
||||
const std::size_t slash = pathString.find_last_of("/\\");
|
||||
if (slash == std::string::npos || slash + 1 >= pathString.size()) {
|
||||
return pathString;
|
||||
@@ -98,8 +116,7 @@ std::string android_display_name(std::string_view path) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto* displayName =
|
||||
static_cast<jstring>(env->CallObjectMethod(activity, getDisplayName, uri));
|
||||
auto* displayName = static_cast<jstring>(env->CallObjectMethod(activity, getDisplayName, uri));
|
||||
env->DeleteLocalRef(uri);
|
||||
env->DeleteLocalRef(activity);
|
||||
if (displayName == nullptr || clear_pending_exception(env)) {
|
||||
@@ -110,6 +127,76 @@ std::string android_display_name(std::string_view path) {
|
||||
env->DeleteLocalRef(displayName);
|
||||
return result;
|
||||
}
|
||||
|
||||
struct AndroidFolderDialogState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
std::string path;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
void onAndroidFolderDialogFinished(void* userdata) {
|
||||
std::unique_ptr<AndroidFolderDialogState> state(
|
||||
static_cast<AndroidFolderDialogState*>(userdata));
|
||||
|
||||
const char* path = state->path.empty() ? nullptr : state->path.c_str();
|
||||
const char* error = state->error.empty() ? nullptr : state->error.c_str();
|
||||
state->callback(state->userdata, path, error);
|
||||
}
|
||||
|
||||
bool show_android_folder_select(AndroidFolderDialogState* state) {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (activity == nullptr || clear_pending_exception(env)) {
|
||||
if (activity != nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (activityClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return false;
|
||||
}
|
||||
|
||||
jmethodID showFolderDialog =
|
||||
env->GetMethodID(activityClass, "showFolderDialog", "(J)Z");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (showFolderDialog == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return false;
|
||||
}
|
||||
|
||||
const jboolean shown = env->CallBooleanMethod(
|
||||
activity, showFolderDialog, reinterpret_cast<jlong>(state));
|
||||
env->DeleteLocalRef(activity);
|
||||
if (clear_pending_exception(env)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shown == JNI_TRUE;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_dev_twilitrealm_dusk_DuskActivity_nativeFolderDialogResult(
|
||||
JNIEnv* env, jclass, jlong userdata, jstring path, jstring error) {
|
||||
auto* state = reinterpret_cast<AndroidFolderDialogState*>(userdata);
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->path = to_string(env, path);
|
||||
state->error = to_string(env, error);
|
||||
|
||||
if (!SDL_RunOnMainThread(&onAndroidFolderDialogFinished, state, false)) {
|
||||
onAndroidFolderDialogFinished(state);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
@@ -159,8 +246,8 @@ void onSDLDialogFinished(void* userdata, const char* const* filelist, [[maybe_un
|
||||
} // namespace
|
||||
|
||||
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) {
|
||||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -171,14 +258,45 @@ void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
|
||||
state->userdata = userdata;
|
||||
|
||||
Dusk_iOS_ShowFileSelect(&onIOSDialogFinished, state.release(), window, filters, nfilters,
|
||||
default_location, allow_many);
|
||||
default_location, allow_many);
|
||||
#else
|
||||
auto state = std::make_unique<SDLDialogCallbackState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
SDL_ShowOpenFileDialog(&onSDLDialogFinished, state.release(), window, filters, nfilters,
|
||||
default_location, allow_many);
|
||||
default_location, allow_many);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShowFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location) {
|
||||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
callback(userdata, nullptr, "Folder selection is not supported on this platform");
|
||||
#elif USE_MACOS_FOLDER_DIALOG
|
||||
ShowMacOSFolderSelect(callback, userdata, window, default_location);
|
||||
#elif defined(__ANDROID__) || defined(ANDROID)
|
||||
auto state = std::make_unique<AndroidFolderDialogState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
if (show_android_folder_select(state.get())) {
|
||||
state.release();
|
||||
return;
|
||||
}
|
||||
|
||||
callback(userdata, nullptr, "Folder selection is not supported on this platform");
|
||||
#else
|
||||
auto state = std::make_unique<SDLDialogCallbackState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
SDL_ShowOpenFolderDialog(
|
||||
&onSDLDialogFinished, state.release(), window, default_location, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ using FileCallback = void (*)(void* userdata, const char* path, const char* erro
|
||||
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
|
||||
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
|
||||
bool allow_many);
|
||||
void ShowFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location);
|
||||
|
||||
std::string display_name_for_path(std::string_view path);
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#include "file_select.hpp"
|
||||
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
struct MacOSFolderDialogState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
};
|
||||
|
||||
void finish_folder_dialog(MacOSFolderDialogState* state, NSURL* url, const char* error) {
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error != nullptr) {
|
||||
state->callback(state->userdata, nullptr, error);
|
||||
delete state;
|
||||
return;
|
||||
}
|
||||
|
||||
if (url == nil) {
|
||||
state->callback(state->userdata, nullptr, nullptr);
|
||||
delete state;
|
||||
return;
|
||||
}
|
||||
|
||||
state->callback(state->userdata, [[url path] UTF8String], nullptr);
|
||||
delete state;
|
||||
}
|
||||
|
||||
void configure_default_location(NSOpenPanel* panel, const char* defaultLocation) {
|
||||
if (panel == nil || defaultLocation == nullptr || defaultLocation[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* path = [NSString stringWithUTF8String:defaultLocation];
|
||||
if (path == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
NSFileManager* fileManager = [NSFileManager defaultManager];
|
||||
NSURL* url = [NSURL fileURLWithPath:path];
|
||||
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory] && isDirectory) {
|
||||
[panel setDirectoryURL:url];
|
||||
} else {
|
||||
[panel setDirectoryURL:[url URLByDeletingLastPathComponent]];
|
||||
}
|
||||
}
|
||||
|
||||
NSWindow* window_for_sdl_window(SDL_Window* window) {
|
||||
if (window == nullptr) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
auto props = SDL_GetWindowProperties(window);
|
||||
return (__bridge NSWindow*)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ShowMacOSFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* defaultLocation) {
|
||||
if (callback == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* state = new MacOSFolderDialogState{
|
||||
.callback = callback,
|
||||
.userdata = userdata,
|
||||
};
|
||||
|
||||
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||||
[panel setCanChooseFiles:NO];
|
||||
[panel setCanChooseDirectories:YES];
|
||||
[panel setAllowsMultipleSelection:NO];
|
||||
[panel setCanCreateDirectories:YES];
|
||||
configure_default_location(panel, defaultLocation);
|
||||
|
||||
NSWindow* modalWindow = window_for_sdl_window(window);
|
||||
if (modalWindow != nil) {
|
||||
[panel beginSheetModalForWindow:modalWindow
|
||||
completionHandler:^(NSModalResponse result) {
|
||||
finish_folder_dialog(
|
||||
state, result == NSModalResponseOK ? [panel URL] : nil, nullptr);
|
||||
}];
|
||||
return true;
|
||||
}
|
||||
|
||||
const NSModalResponse result = [panel runModal];
|
||||
finish_folder_dialog(state, result == NSModalResponseOK ? [panel URL] : nil, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "SDL3/SDL_mouse.h"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/livesplit.h"
|
||||
@@ -274,7 +275,6 @@ namespace dusk {
|
||||
// so make the window bg fully transparent temporarily
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
||||
if (showMenu && ImGui::BeginMainMenuBar()) {
|
||||
m_menuGame.draw();
|
||||
m_menuTools.draw();
|
||||
|
||||
ImGui::EndMainMenuBar();
|
||||
@@ -283,7 +283,7 @@ namespace dusk {
|
||||
|
||||
if (dusk::IsGameLaunched && !m_isLaunchInitialized) {
|
||||
m_isLaunchInitialized = true;
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
if (getSettings().game.speedrunMode && getSettings().game.liveSplitEnabled) {
|
||||
dusk::speedrun::connectLiveSplit();
|
||||
}
|
||||
}
|
||||
@@ -310,13 +310,13 @@ namespace dusk {
|
||||
ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize});
|
||||
} else {
|
||||
ImGui::PushFont(ImGuiEngine::fontExtraLarge);
|
||||
ImGuiTextCenter("Dusk");
|
||||
ImGuiTextCenter("Dusklight");
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::PushFont(ImGuiEngine::fontLarge);
|
||||
ImGuiTextCenter("Failed to initialize any graphics backend.");
|
||||
ImGuiTextCenter("\nYour system may be misconfigured, or your hardware may not support the required versions of any of the available backends.");
|
||||
ImGuiTextCenter("\nA clean reinstall of Dusk may help. For further assistance, please visit #tech-support on the Twilit Realm Discord server.");
|
||||
ImGuiTextCenter("\nA clean reinstall of Dusklight may help. For further assistance, please visit #tech-support on the Twilit Realm Discord server.");
|
||||
const auto& style = ImGui::GetStyle();
|
||||
const auto retrySize = ImGui::CalcTextSize("Retry (Auto backend)");
|
||||
const auto quitSize = ImGui::CalcTextSize("Quit");
|
||||
@@ -342,7 +342,7 @@ namespace dusk {
|
||||
}
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
if (ImGui::Button("Open Data Folder")) {
|
||||
OpenDataFolder();
|
||||
data::open_data_path();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
#endif
|
||||
@@ -354,15 +354,6 @@ namespace dusk {
|
||||
}
|
||||
|
||||
m_menuTools.ShowInputViewer();
|
||||
m_menuGame.drawSpeedrunTimerOverlay();
|
||||
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
dusk::speedrun::updateLiveSplit();
|
||||
if (dusk::speedrun::consumeConnectedEvent())
|
||||
AddToast("LiveSplit connected");
|
||||
else if (dusk::speedrun::consumeDisconnectedEvent())
|
||||
AddToast("LiveSplit disconnected");
|
||||
}
|
||||
|
||||
if (dusk::IsGameLaunched && !dusk::getSettings().game.speedrunMode) {
|
||||
m_menuTools.ShowDebugOverlay();
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
#include "ImGuiMenuGame.hpp"
|
||||
#include "ImGuiMenuTools.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "imgui.h"
|
||||
@@ -44,8 +43,6 @@ private:
|
||||
ImVec2 m_dragScrollLastMousePos = {};
|
||||
std::deque<Toast> m_toasts;
|
||||
|
||||
ImGuiMenuGame m_menuGame;
|
||||
|
||||
// Keep always last
|
||||
ImGuiMenuTools m_menuTools;
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ void ImGuiEngine_AddTextures() {
|
||||
ImGuiEngine::orgIcon = AddTexture("org-icon.png");
|
||||
}
|
||||
if (ImGuiEngine::duskLogo == 0) {
|
||||
ImGuiEngine::duskLogo = AddTexture("logo-mascot.png");
|
||||
ImGuiEngine::duskLogo = AddTexture("logo.png");
|
||||
}
|
||||
}
|
||||
} // namespace dusk
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
#include "fmt/format.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "ImGuiConfig.hpp"
|
||||
|
||||
#include "dusk/main.h"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
|
||||
namespace dusk {
|
||||
ImGuiMenuGame::ImGuiMenuGame() {}
|
||||
|
||||
void ImGuiMenuGame::draw() {}
|
||||
|
||||
static std::string GetFormattedTime(OSTime ticks) {
|
||||
OSCalendarTime time;
|
||||
OSTicksToCalendarTime(ticks, &time);
|
||||
|
||||
return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", time.hour, time.min, time.sec, time.msec);
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::resetForSpeedrunMode() {
|
||||
// reset settings that should be off for speedrun mode
|
||||
mDoMain::developmentMode = -1;
|
||||
|
||||
getSettings().game.damageMultiplier.setValue(1);
|
||||
getSettings().game.instantDeath.setValue(false);
|
||||
getSettings().game.noHeartDrops.setValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setValue(false);
|
||||
getSettings().game.infiniteArrows.setValue(false);
|
||||
getSettings().game.infiniteBombs.setValue(false);
|
||||
getSettings().game.infiniteOil.setValue(false);
|
||||
getSettings().game.infiniteOxygen.setValue(false);
|
||||
getSettings().game.infiniteRupees.setValue(false);
|
||||
getSettings().game.enableIndefiniteItemDrops.setValue(false);
|
||||
|
||||
getSettings().game.moonJump.setValue(false);
|
||||
getSettings().game.superClawshot.setValue(false);
|
||||
getSettings().game.alwaysGreatspin.setValue(false);
|
||||
getSettings().game.enableFastIronBoots.setValue(false);
|
||||
getSettings().game.canTransformAnywhere.setValue(false);
|
||||
getSettings().game.fastSpinner.setValue(false);
|
||||
getSettings().game.freeMagicArmor.setValue(false);
|
||||
|
||||
getSettings().game.enableTurboKeybind.setValue(false);
|
||||
getSettings().game.debugFlyCam.setValue(false);
|
||||
getSettings().game.autoSave.setValue(false);
|
||||
}
|
||||
|
||||
SpeedrunInfo m_speedrunInfo;
|
||||
|
||||
void ImGuiMenuGame::drawSpeedrunTimerOverlay() {
|
||||
if (!getSettings().game.speedrunMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// L+R+A+Start to reset timer
|
||||
if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) {
|
||||
m_speedrunInfo.reset();
|
||||
}
|
||||
|
||||
// L+R+A+Z to manually stop timer
|
||||
if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) {
|
||||
if (m_speedrunInfo.m_isRunStarted) {
|
||||
m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp;
|
||||
m_speedrunInfo.m_isRunStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowBgAlpha(0.65f);
|
||||
ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_NoResize
|
||||
| ImGuiWindowFlags_NoDocking
|
||||
| ImGuiWindowFlags_NoTitleBar
|
||||
| ImGuiWindowFlags_NoScrollbar;
|
||||
|
||||
if (ImGui::Begin("##SpeedrunTimerWindow", nullptr, flags)) {
|
||||
OSTime elapsedTime = 0;
|
||||
if (m_speedrunInfo.m_isRunStarted) {
|
||||
elapsedTime = OSGetTime() - m_speedrunInfo.m_startTimestamp;
|
||||
} else if (m_speedrunInfo.m_endTimestamp != 0) {
|
||||
elapsedTime = m_speedrunInfo.m_endTimestamp;
|
||||
}
|
||||
|
||||
ImGui::Text("RTA");
|
||||
ImGui::SameLine(60.0f);
|
||||
ImGuiStringViewText(GetFormattedTime(elapsedTime));
|
||||
|
||||
if (!m_speedrunInfo.m_isPauseIGT) {
|
||||
m_speedrunInfo.m_igtTimer = elapsedTime - m_speedrunInfo.m_totalLoadTime;
|
||||
}
|
||||
|
||||
ImGui::Text("IGT");
|
||||
ImGui::SameLine(60.0f);
|
||||
ImGuiStringViewText(GetFormattedTime(m_speedrunInfo.m_igtTimer));
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#ifndef DUSK_IMGUI_MENUGAME_HPP
|
||||
#define DUSK_IMGUI_MENUGAME_HPP
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <pad.h>
|
||||
#include <string>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dusk {
|
||||
struct SpeedrunInfo {
|
||||
void startRun() {
|
||||
m_isRunStarted = true;
|
||||
m_startTimestamp = OSGetTime();
|
||||
}
|
||||
|
||||
void stopRun() {
|
||||
m_isRunStarted = false;
|
||||
m_endTimestamp = OSGetTime() - m_startTimestamp;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
m_isRunStarted = false;
|
||||
m_startTimestamp = 0;
|
||||
m_endTimestamp = 0;
|
||||
m_isPauseIGT = false;
|
||||
m_loadStartTimestamp = 0;
|
||||
m_totalLoadTime = 0;
|
||||
m_igtTimer = 0;
|
||||
}
|
||||
|
||||
bool m_isRunStarted = false;
|
||||
OSTime m_startTimestamp = 0;
|
||||
OSTime m_endTimestamp = 0;
|
||||
|
||||
bool m_isPauseIGT = false;
|
||||
OSTime m_loadStartTimestamp = 0;
|
||||
OSTime m_totalLoadTime = 0;
|
||||
OSTime m_igtTimer = 0;
|
||||
};
|
||||
|
||||
extern SpeedrunInfo m_speedrunInfo;
|
||||
|
||||
class ImGuiMenuGame {
|
||||
public:
|
||||
ImGuiMenuGame();
|
||||
void draw();
|
||||
|
||||
void drawSpeedrunTimerOverlay();
|
||||
|
||||
static void resetForSpeedrunMode();
|
||||
|
||||
private:
|
||||
bool m_showTimerWindow = false;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // DUSK_IMGUI_MENUGAME_HPP
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/actor/d_a_horse.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/main.h"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
@@ -54,7 +55,7 @@ namespace dusk {
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Open Data Folder")) {
|
||||
OpenDataFolder();
|
||||
data::open_data_path();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ static void RunOnMainThread(void (^block)(void))
|
||||
|
||||
static NSError *MakeError(NSString *message)
|
||||
{
|
||||
return [NSError errorWithDomain:@"org.twilitrealm.dusk.file-select"
|
||||
return [NSError errorWithDomain:@"dev.twilitrealm.dusk.file-select"
|
||||
code:1
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
}
|
||||
|
||||
@@ -2,19 +2,45 @@
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
using socket_t = SOCKET;
|
||||
static void closeSocket(socket_t s) { closesocket(s); }
|
||||
static void closeSocket(socket_t s) {
|
||||
LINGER li{1, 0};
|
||||
setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast<const char*>(&li), sizeof(li));
|
||||
closesocket(s);
|
||||
}
|
||||
static int socketError(socket_t s) {
|
||||
int err = 0; int len = sizeof(err);
|
||||
getsockopt(s, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&err), &len);
|
||||
return err;
|
||||
}
|
||||
static constexpr int kSendFlags = 0;
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <sys/select.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
using socket_t = int;
|
||||
static void closeSocket(socket_t s) { close(s); }
|
||||
static void closeSocket(socket_t s) {
|
||||
struct linger li{1, 0};
|
||||
setsockopt(s, SOL_SOCKET, SO_LINGER, &li, sizeof(li));
|
||||
close(s);
|
||||
}
|
||||
static int socketError(socket_t s) {
|
||||
int err = 0; socklen_t len = sizeof(err);
|
||||
getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len);
|
||||
return err;
|
||||
}
|
||||
#ifndef INVALID_SOCKET
|
||||
#define INVALID_SOCKET -1
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
static constexpr int kSendFlags = 0;
|
||||
#else
|
||||
static constexpr int kSendFlags = MSG_NOSIGNAL;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <cstdio>
|
||||
@@ -24,12 +50,17 @@
|
||||
namespace dusk::speedrun {
|
||||
|
||||
static bool running = false;
|
||||
static bool startPending = false;
|
||||
static uint64_t frameCount = 0;
|
||||
static socket_t sock = INVALID_SOCKET;
|
||||
static bool wasLoading = false;
|
||||
static bool connected = false;
|
||||
static bool connectPending = false;
|
||||
static bool disconnectPending = false;
|
||||
static uint32_t idleProbeCounter = 0;
|
||||
static uint32_t reconnectCounter = 0;
|
||||
static char storedHost[64] = "127.0.0.1";
|
||||
static int storedPort = 16834;
|
||||
|
||||
static void sendCmd(const char* cmd) {
|
||||
if (sock == INVALID_SOCKET) {
|
||||
@@ -37,18 +68,20 @@ static void sendCmd(const char* cmd) {
|
||||
}
|
||||
|
||||
char msg[64];
|
||||
int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd);
|
||||
const int len = snprintf(msg, sizeof(msg), "%s\r\n", cmd);
|
||||
if (len <= 0 || len >= static_cast<int>(sizeof(msg))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (send(sock, msg, len, 0) >= 0) {
|
||||
if (send(sock, msg, len, kSendFlags) >= 0) {
|
||||
if (!connected) {
|
||||
connected = connectPending = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
int err = WSAGetLastError();
|
||||
const int err = WSAGetLastError();
|
||||
if (err == WSAEWOULDBLOCK || err == WSAENOTCONN) {
|
||||
return;
|
||||
}
|
||||
@@ -58,10 +91,13 @@ static void sendCmd(const char* cmd) {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (connected) disconnectPending = true;
|
||||
if (connected) {
|
||||
disconnectPending = true;
|
||||
}
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
connected = connectPending = false;
|
||||
reconnectCounter = 0;
|
||||
}
|
||||
|
||||
uint64_t getFrameCount() {
|
||||
@@ -89,57 +125,93 @@ void start() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
running = true;
|
||||
startPending = true;
|
||||
frameCount = 0;
|
||||
wasLoading = false;
|
||||
sendCmd("initgametime");
|
||||
sendCmd("reset");
|
||||
sendCmd("starttimer");
|
||||
}
|
||||
|
||||
void reset() {
|
||||
running = false;
|
||||
startPending = false;
|
||||
frameCount = 0;
|
||||
wasLoading = false;
|
||||
sendCmd("reset");
|
||||
}
|
||||
|
||||
void connectLiveSplit(const char* host, int port) {
|
||||
#if _WIN32
|
||||
WSADATA wd{}; WSAStartup(MAKEWORD(2, 2), &wd);
|
||||
#endif
|
||||
|
||||
static void reconnect() {
|
||||
if (sock != INVALID_SOCKET) {
|
||||
closeSocket(sock); sock = INVALID_SOCKET;
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
}
|
||||
connected = connectPending = false;
|
||||
|
||||
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
|
||||
if (sock == INVALID_SOCKET) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
u_long nb = 1;
|
||||
ioctlsocket(sock, FIONBIO, &nb);
|
||||
if (ioctlsocket(sock, FIONBIO, &nb) != 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
fcntl(sock, F_SETFL, fcntl(sock, F_GETFL, 0) | O_NONBLOCK);
|
||||
const int fl = fcntl(sock, F_GETFL, 0);
|
||||
if (fl < 0 || fcntl(sock, F_SETFL, fl | O_NONBLOCK) < 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
sockaddr_in addr{}; addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons((uint16_t)port);
|
||||
inet_pton(AF_INET, host, &addr.sin_addr);
|
||||
connect(sock, (sockaddr*)&addr, sizeof(addr));
|
||||
sendCmd("initgametime");
|
||||
#if defined(__APPLE__)
|
||||
{
|
||||
int opt = 1;
|
||||
setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt));
|
||||
}
|
||||
#endif
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(storedPort));
|
||||
if (inet_pton(AF_INET, storedHost, &addr.sin_addr) != 1) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
return;
|
||||
}
|
||||
|
||||
const int cr = connect(sock, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
|
||||
#if _WIN32
|
||||
const bool connectPending_ = cr < 0 && WSAGetLastError() == WSAEWOULDBLOCK;
|
||||
#else
|
||||
const bool connectPending_ = cr < 0 && errno == EINPROGRESS;
|
||||
#endif
|
||||
if (cr != 0 && !connectPending_) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
}
|
||||
}
|
||||
|
||||
void connectLiveSplit(const char* host, int port) {
|
||||
#if _WIN32
|
||||
WSADATA wd{};
|
||||
WSAStartup(MAKEWORD(2, 2), &wd);
|
||||
#endif
|
||||
snprintf(storedHost, sizeof(storedHost), "%s", host);
|
||||
storedPort = port;
|
||||
reconnect();
|
||||
}
|
||||
|
||||
void disconnectLiveSplit() {
|
||||
if (sock != INVALID_SOCKET) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
connected = false;
|
||||
}
|
||||
connected = connectPending = disconnectPending = false;
|
||||
}
|
||||
|
||||
bool consumeConnectedEvent() { bool v = connectPending; connectPending = false; return v; }
|
||||
@@ -147,29 +219,76 @@ bool consumeDisconnectedEvent() { bool v = disconnectPending; disconnectPending
|
||||
|
||||
void updateLiveSplit() {
|
||||
if (sock == INVALID_SOCKET) {
|
||||
if ((reconnectCounter++ % 30) == 0) {
|
||||
reconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
fd_set writefds, errorfds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_ZERO(&errorfds);
|
||||
FD_SET(sock, &writefds);
|
||||
FD_SET(sock, &errorfds);
|
||||
timeval tv{0, 0};
|
||||
#if _WIN32
|
||||
const int r = select(0, nullptr, &writefds, &errorfds, &tv);
|
||||
#else
|
||||
const int r = select(sock + 1, nullptr, &writefds, &errorfds, &tv);
|
||||
#endif
|
||||
if (r < 0 || FD_ISSET(sock, &errorfds) || socketError(sock) != 0) {
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
reconnectCounter = 0;
|
||||
return;
|
||||
}
|
||||
if (!FD_ISSET(sock, &writefds)) {
|
||||
return;
|
||||
}
|
||||
sendCmd("initgametime");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startPending) {
|
||||
startPending = false;
|
||||
sendCmd("initgametime");
|
||||
sendCmd("reset");
|
||||
sendCmd("starttimer");
|
||||
}
|
||||
|
||||
if (!running) {
|
||||
if ((idleProbeCounter++ % 60) == 0) {
|
||||
char buf;
|
||||
const int r = recv(sock, &buf, 1, 0);
|
||||
if (r == 0
|
||||
#if _WIN32
|
||||
|| (r < 0 && WSAGetLastError() != WSAEWOULDBLOCK)
|
||||
#else
|
||||
|| (r < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
#endif
|
||||
) {
|
||||
if (connected) {
|
||||
disconnectPending = true;
|
||||
}
|
||||
closeSocket(sock);
|
||||
sock = INVALID_SOCKET;
|
||||
connected = connectPending = false;
|
||||
reconnectCounter = 0;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t totalMs = frameCount * 1000 / 30;
|
||||
const uint64_t totalSec = totalMs / 1000;
|
||||
char cmd[32];
|
||||
|
||||
snprintf(cmd, sizeof(cmd), "setgametime %u:%02u:%02u.%03u",
|
||||
(uint32_t)(totalSec / 3600),
|
||||
(uint32_t)((totalSec / 60) % 60),
|
||||
(uint32_t)(totalSec % 60),
|
||||
(uint32_t)(totalMs % 1000)
|
||||
static_cast<uint32_t>(totalSec / 3600),
|
||||
static_cast<uint32_t>((totalSec / 60) % 60),
|
||||
static_cast<uint32_t>(totalSec % 60),
|
||||
static_cast<uint32_t>(totalMs % 1000)
|
||||
);
|
||||
|
||||
sendCmd(cmd);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ std::string MakeTimestampedLogName() {
|
||||
#endif
|
||||
|
||||
std::array<char, 32> buffer{};
|
||||
std::strftime(buffer.data(), buffer.size(), "dusk-%Y%m%d-%H%M%S.log", &localTime);
|
||||
std::strftime(buffer.data(), buffer.size(), "dusklight-%Y%m%d-%H%M%S.log", &localTime);
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
@@ -109,6 +109,16 @@ void WriteLogLine(FILE* out, const char* levelStr, const char* module, const cha
|
||||
std::fputc('\n', out);
|
||||
std::fflush(out);
|
||||
}
|
||||
|
||||
void WriteLogLineToFile(
|
||||
const char* levelStr, const char* module, const char* message, unsigned int len) {
|
||||
if (g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
if (g_logState.file != nullptr) {
|
||||
WriteLogLine(g_logState.file, levelStr, module, message, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
static bool IsForStubLog(const char* message) {
|
||||
@@ -132,6 +142,11 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m
|
||||
return;
|
||||
}
|
||||
|
||||
if (module == nullptr) {
|
||||
module = "";
|
||||
}
|
||||
|
||||
const char* levelStr = LogLevelString(level);
|
||||
int android_log_level = 0;
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
@@ -151,12 +166,14 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m
|
||||
break;
|
||||
}
|
||||
|
||||
std::stringstream msgStream(message);
|
||||
std::stringstream msgStream(std::string(message, len));
|
||||
std::string segment;
|
||||
while(std::getline(msgStream, segment)) {
|
||||
__android_log_print(android_log_level, module, "%s\n", segment.c_str());
|
||||
}
|
||||
|
||||
WriteLogLineToFile(levelStr, module, message, len);
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
abort();
|
||||
}
|
||||
@@ -177,13 +194,7 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m
|
||||
const char* levelStr = LogLevelString(level);
|
||||
FILE* out = LogStreamForLevel(level);
|
||||
WriteLogLine(out, levelStr, module, message, len);
|
||||
|
||||
if (g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
if (g_logState.file != nullptr) {
|
||||
WriteLogLine(g_logState.file, levelStr, module, message, len);
|
||||
}
|
||||
}
|
||||
WriteLogLineToFile(levelStr, module, message, len);
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
abort();
|
||||
|
||||
@@ -44,7 +44,7 @@ bool RestartProcess(int argc, char* argv[]) {
|
||||
if (!CreateProcessW(nullptr, commandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr,
|
||||
&startupInfo, &processInfo))
|
||||
{
|
||||
fprintf(stderr, "Failed to restart Dusk: CreateProcessW error %lu\n", GetLastError());
|
||||
fprintf(stderr, "Failed to restart Dusklight: CreateProcessW error %lu\n", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ bool RestartProcess(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
if (executablePath.empty()) {
|
||||
fprintf(stderr, "Failed to restart Dusk: unable to resolve executable path\n");
|
||||
fprintf(stderr, "Failed to restart Dusklight: unable to resolve executable path\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ bool RestartProcess(int argc, char* argv[]) {
|
||||
execArgv.push_back(nullptr);
|
||||
|
||||
execv(executablePath.c_str(), execArgv.data());
|
||||
fprintf(stderr, "Failed to restart Dusk: execv failed: %s\n", std::strerror(errno));
|
||||
fprintf(stderr, "Failed to restart Dusklight: execv failed: %s\n", std::strerror(errno));
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ UserSettings g_userSettings = {
|
||||
.shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1},
|
||||
.enableDepthOfField {"game.enableDepthOfField", true},
|
||||
.enableMapBackground {"game.enableMapBackground", true},
|
||||
.disableCutscenePillarboxing {"game.disableCutscenePillarboxing", false},
|
||||
|
||||
// Audio
|
||||
.noLowHpSound {"game.noLowHpSound", false},
|
||||
@@ -81,6 +82,8 @@ UserSettings g_userSettings = {
|
||||
.freeCamera {"game.freeCamera", false},
|
||||
.invertCameraXAxis {"game.invertCameraXAxis", false},
|
||||
.invertCameraYAxis {"game.invertCameraYAxis", false},
|
||||
.invertFirstPersonXAxis {"game.invertFirstPersonXAxis", false},
|
||||
.invertFirstPersonYAxis {"game.invertFirstPersonYAxis", false},
|
||||
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
|
||||
.debugFlyCam {"game.debugFlyCam", false},
|
||||
.debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true},
|
||||
@@ -99,6 +102,7 @@ UserSettings g_userSettings = {
|
||||
.alwaysGreatspin {"game.alwaysGreatspin", false},
|
||||
.enableFastIronBoots {"game.enableFastIronBoots", false},
|
||||
.canTransformAnywhere {"game.canTransformAnywhere", false},
|
||||
.fastRoll {"game.fastRoll", false},
|
||||
.fastSpinner {"game.fastSpinner", false},
|
||||
.freeMagicArmor {"game.freeMagicArmor", false},
|
||||
|
||||
@@ -111,6 +115,7 @@ UserSettings g_userSettings = {
|
||||
// Tools
|
||||
.speedrunMode {"game.speedrunMode", false},
|
||||
.liveSplitEnabled {"game.liveSplitEnabled", false},
|
||||
.showSpeedrunRTATimer {"game.showSpeedrunRTATimer", true},
|
||||
.recordingMode {"game.recordingMode", false}
|
||||
},
|
||||
|
||||
@@ -121,7 +126,6 @@ UserSettings g_userSettings = {
|
||||
.skipPreLaunchUI {"backend.skipPreLaunchUI", false},
|
||||
.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},
|
||||
@@ -171,6 +175,8 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.enableMirrorMode);
|
||||
Register(g_userSettings.game.invertCameraXAxis);
|
||||
Register(g_userSettings.game.invertCameraYAxis);
|
||||
Register(g_userSettings.game.invertFirstPersonXAxis);
|
||||
Register(g_userSettings.game.invertFirstPersonYAxis);
|
||||
Register(g_userSettings.game.freeCameraSensitivity);
|
||||
Register(g_userSettings.game.minimalHUD);
|
||||
Register(g_userSettings.game.pauseOnFocusLost);
|
||||
@@ -181,8 +187,10 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.shadowResolutionMultiplier);
|
||||
Register(g_userSettings.game.enableDepthOfField);
|
||||
Register(g_userSettings.game.enableMapBackground);
|
||||
Register(g_userSettings.game.disableCutscenePillarboxing);
|
||||
Register(g_userSettings.game.enableFastIronBoots);
|
||||
Register(g_userSettings.game.canTransformAnywhere);
|
||||
Register(g_userSettings.game.fastRoll);
|
||||
Register(g_userSettings.game.freeMagicArmor);
|
||||
Register(g_userSettings.game.restoreWiiGlitches);
|
||||
Register(g_userSettings.game.enableLinkDollRotation);
|
||||
@@ -194,6 +202,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.enableTurboKeybind);
|
||||
Register(g_userSettings.game.speedrunMode);
|
||||
Register(g_userSettings.game.liveSplitEnabled);
|
||||
Register(g_userSettings.game.showSpeedrunRTATimer);
|
||||
Register(g_userSettings.game.recordingMode);
|
||||
Register(g_userSettings.game.fastSpinner);
|
||||
Register(g_userSettings.game.infiniteHearts);
|
||||
@@ -228,7 +237,6 @@ void registerSettings() {
|
||||
Register(g_userSettings.backend.skipPreLaunchUI);
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "dusk/speedrun.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
SpeedrunInfo m_speedrunInfo;
|
||||
|
||||
void resetForSpeedrunMode() {
|
||||
mDoMain::developmentMode = -1;
|
||||
|
||||
getSettings().game.enableTurboKeybind.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.damageMultiplier.setSpeedrunValue(1);
|
||||
getSettings().game.instantDeath.setSpeedrunValue(false);
|
||||
getSettings().game.noHeartDrops.setSpeedrunValue(false);
|
||||
getSettings().game.autoSave.setSpeedrunValue(false);
|
||||
getSettings().game.sunsSong.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteArrows.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteBombs.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteOil.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteOxygen.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteRupees.setSpeedrunValue(false);
|
||||
getSettings().game.enableIndefiniteItemDrops.setSpeedrunValue(false);
|
||||
getSettings().game.moonJump.setSpeedrunValue(false);
|
||||
getSettings().game.superClawshot.setSpeedrunValue(false);
|
||||
getSettings().game.alwaysGreatspin.setSpeedrunValue(false);
|
||||
getSettings().game.enableFastIronBoots.setSpeedrunValue(false);
|
||||
getSettings().game.canTransformAnywhere.setSpeedrunValue(false);
|
||||
getSettings().game.fastRoll.setSpeedrunValue(false);
|
||||
getSettings().game.fastSpinner.setSpeedrunValue(false);
|
||||
getSettings().game.freeMagicArmor.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.pauseOnFocusLost.setSpeedrunValue(false);
|
||||
aurora_set_pause_on_focus_lost(false);
|
||||
|
||||
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
|
||||
getSettings().game.recordingMode.setSpeedrunValue(false);
|
||||
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -51,6 +51,9 @@ void Button::update_props(Props props) {
|
||||
}
|
||||
|
||||
void ControlledButton::update() {
|
||||
if (mIsDisabled) {
|
||||
set_disabled(mIsDisabled());
|
||||
}
|
||||
if (mIsSelected) {
|
||||
set_selected(mIsSelected());
|
||||
}
|
||||
@@ -64,4 +67,11 @@ bool ControlledButton::selected() const {
|
||||
return Button::selected();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
bool ControlledButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return Button::disabled();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -32,17 +32,21 @@ public:
|
||||
struct Props {
|
||||
Rml::String text;
|
||||
std::function<bool()> isSelected;
|
||||
std::function<bool()> isDisabled;
|
||||
};
|
||||
|
||||
ControlledButton(Rml::Element* parent, Props props, const Rml::String& tagName = "button")
|
||||
: Button(parent, {std::move(props.text)}, tagName),
|
||||
mIsSelected(std::move(props.isSelected)) {}
|
||||
mIsSelected(std::move(props.isSelected)),
|
||||
mIsDisabled(std::move(props.isDisabled)) {}
|
||||
|
||||
void update() override;
|
||||
bool selected() const override;
|
||||
bool disabled() const override;
|
||||
|
||||
private:
|
||||
std::function<bool()> mIsSelected;
|
||||
std::function<bool()> mIsDisabled;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -301,7 +301,11 @@ int rumble_raw_to_percent(u16 raw) {
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerConfigWindow::ControllerConfigWindow() {
|
||||
ControllerConfigWindow::ControllerConfigWindow(bool prelaunch) {
|
||||
if (prelaunch) {
|
||||
mSuppressNavFallback = true;
|
||||
}
|
||||
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
[this](Rml::Event& event) {
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace dusk::ui {
|
||||
|
||||
class ControllerConfigWindow : public Window {
|
||||
public:
|
||||
ControllerConfigWindow();
|
||||
ControllerConfigWindow(bool prelaunch);
|
||||
|
||||
void update() override;
|
||||
void hide(bool close) override;
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
#include "achievements.hpp"
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "dusk/speedrun.h"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "editor.hpp"
|
||||
@@ -100,7 +102,7 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
|
||||
mTabBar->set_active_tab(-1);
|
||||
const auto dismiss = [](Modal& modal) { modal.pop(); };
|
||||
push(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Quit Dusk",
|
||||
.title = "Quit Dusklight",
|
||||
.bodyRml = "Unsaved progress will be lost.",
|
||||
.actions =
|
||||
{
|
||||
@@ -127,6 +129,18 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
|
||||
}));
|
||||
});
|
||||
|
||||
if (getSettings().game.speedrunMode) {
|
||||
mTabBar->add_tab("Reset Timer", [this] {
|
||||
mTabBar->set_active_tab(-1);
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
m_speedrunInfo.reset();
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
dusk::speedrun::reset();
|
||||
}
|
||||
hide(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Hide document after transition completion
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/achievements.h"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/speedrun.h"
|
||||
#include "fmt/format.h"
|
||||
#include "magic_enum.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
@@ -9,6 +12,7 @@
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <algorithm>
|
||||
#include <dolphin/pad.h>
|
||||
#include <m_Do/m_Do_main.h>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
@@ -25,6 +29,10 @@ const Rml::String kDocumentSource = R"RML(
|
||||
</head>
|
||||
<body>
|
||||
<fps id="fps" />
|
||||
<speedrun-timer id="speedrun-timer">
|
||||
<speedrun-rta id="speedrun-rta" />
|
||||
<speedrun-igt id="speedrun-igt" />
|
||||
</speedrun-timer>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
@@ -204,8 +212,17 @@ void Overlay::advance_fps_counter(float& outFps, Uint64 perfFreq) {
|
||||
outFps = static_cast<float>(1.0 / avgSeconds);
|
||||
}
|
||||
|
||||
static std::string FormatTime(OSTime ticks) {
|
||||
OSCalendarTime t;
|
||||
OSTicksToCalendarTime(ticks, &t);
|
||||
return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", t.hour, t.min, t.sec, t.msec);
|
||||
}
|
||||
|
||||
Overlay::Overlay() : Document(kDocumentSource) {
|
||||
mFpsCounter = mDocument->GetElementById("fps");
|
||||
mSpeedrunTimer = mDocument->GetElementById("speedrun-timer");
|
||||
mSpeedrunRta = mDocument->GetElementById("speedrun-rta");
|
||||
mSpeedrunIgt = mDocument->GetElementById("speedrun-igt");
|
||||
|
||||
listen(mDocument, Rml::EventId::Focus, [](Rml::Event&) { Log.warn("Overlay received focus"); });
|
||||
listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
@@ -268,6 +285,63 @@ void Overlay::update() {
|
||||
}
|
||||
}
|
||||
|
||||
#if !(defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST))
|
||||
if (getSettings().game.speedrunMode && getSettings().game.liveSplitEnabled) {
|
||||
dusk::speedrun::updateLiveSplit();
|
||||
if (dusk::speedrun::consumeConnectedEvent()) {
|
||||
push_toast({.title = "LiveSplit connected", .duration = std::chrono::seconds(3)});
|
||||
}
|
||||
if (dusk::speedrun::consumeDisconnectedEvent()) {
|
||||
push_toast({.title = "LiveSplit disconnected", .duration = std::chrono::seconds(3)});
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (mSpeedrunTimer != nullptr && mSpeedrunRta != nullptr && mSpeedrunIgt != nullptr) {
|
||||
if (getSettings().game.speedrunMode) {
|
||||
// L+R+A+Start to reset timer
|
||||
if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) &&
|
||||
mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1))
|
||||
{
|
||||
m_speedrunInfo.reset();
|
||||
}
|
||||
|
||||
// L+R+A+Y to manually stop timer
|
||||
if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) &&
|
||||
mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1))
|
||||
{
|
||||
if (m_speedrunInfo.m_isRunStarted) {
|
||||
m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp;
|
||||
m_speedrunInfo.m_isRunStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
OSTime elapsedTime = 0;
|
||||
if (m_speedrunInfo.m_isRunStarted) {
|
||||
elapsedTime = OSGetTime() - m_speedrunInfo.m_startTimestamp;
|
||||
} else if (m_speedrunInfo.m_endTimestamp != 0) {
|
||||
elapsedTime = m_speedrunInfo.m_endTimestamp;
|
||||
}
|
||||
|
||||
if (!m_speedrunInfo.m_isPauseIGT) {
|
||||
m_speedrunInfo.m_igtTimer = elapsedTime - m_speedrunInfo.m_totalLoadTime;
|
||||
}
|
||||
|
||||
mSpeedrunTimer->SetAttribute("open", "");
|
||||
|
||||
if (getSettings().game.showSpeedrunRTATimer) {
|
||||
mSpeedrunRta->SetAttribute("open", "");
|
||||
mSpeedrunRta->SetInnerRML(escape(fmt::format("RTA {}", FormatTime(elapsedTime))));
|
||||
} else {
|
||||
mSpeedrunRta->RemoveAttribute("open");
|
||||
}
|
||||
|
||||
mSpeedrunIgt->SetInnerRML(escape(fmt::format("IGT {}", FormatTime(m_speedrunInfo.m_igtTimer))));
|
||||
} else {
|
||||
mSpeedrunTimer->RemoveAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 &&
|
||||
PADGetKeyButtonBindings(PAD_CHAN0, nullptr) == nullptr &&
|
||||
dynamic_cast<Window*>(top_document()) == nullptr &&
|
||||
|
||||
@@ -21,6 +21,9 @@ protected:
|
||||
Rml::Element* mCurrentToast = nullptr;
|
||||
Rml::Element* mControllerWarning = nullptr;
|
||||
Rml::Element* mMenuNotification = nullptr;
|
||||
Rml::Element* mSpeedrunTimer = nullptr;
|
||||
Rml::Element* mSpeedrunRta = nullptr;
|
||||
Rml::Element* mSpeedrunIgt = nullptr;
|
||||
clock::time_point mCurrentToastStartTime;
|
||||
clock::time_point mMenuNotificationStartTime;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "prelaunch.hpp"
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/main.h"
|
||||
@@ -43,8 +44,8 @@ const Rml::String kDocumentSource = R"RML(
|
||||
<content id="root" open>
|
||||
<menu>
|
||||
<hero class="intro-item delay-0">
|
||||
<div class="eyebrow"><span>Twilit Realm</span> presents</div>
|
||||
<img src="res/logo-mascot.png" />
|
||||
<eyebrow><span>Twilit Realm</span> presents</eyebrow>
|
||||
<img src="res/logo.png" />
|
||||
</hero>
|
||||
<div id="menu-list" />
|
||||
</menu>
|
||||
@@ -127,7 +128,7 @@ struct UpdateCheckTask {
|
||||
UpdateCheckTask() {
|
||||
worker = std::thread([this] {
|
||||
try {
|
||||
result = update_check::check_latest_github_release("TwilitRealm", "dusk");
|
||||
result = update_check::check_latest_github_release("TwilitRealm", "dusklight");
|
||||
} catch (const std::exception& e) {
|
||||
result = {
|
||||
.status = update_check::Status::Failed,
|
||||
@@ -287,12 +288,12 @@ std::string get_error_msg(iso::ValidationError error) {
|
||||
case iso::ValidationError::InvalidImage:
|
||||
return "The selected file is not a valid disc image.";
|
||||
case iso::ValidationError::WrongGame:
|
||||
return "The selected game is not supported by Dusk.";
|
||||
return "The selected game is not supported by Dusklight.";
|
||||
case iso::ValidationError::WrongVersion:
|
||||
return "Dusk currently supports GameCube USA and PAL disc images only.";
|
||||
return "Dusklight currently supports GameCube USA and PAL disc images only.";
|
||||
case iso::ValidationError::Canceled:
|
||||
return "Disc verification was canceled. Dusk cannot guarantee the selected disc image "
|
||||
"is compatible.";
|
||||
return "Disc verification was canceled. Dusklight cannot guarantee the selected disc "
|
||||
"image is compatible.";
|
||||
case iso::ValidationError::HashMismatch:
|
||||
return "The selected disc image did not pass hash verification. It may be corrupt or "
|
||||
"modified.";
|
||||
@@ -656,6 +657,9 @@ bool is_restart_pending() noexcept {
|
||||
if (!state.activeDiscPath.empty() && state.configuredDiscPath != state.activeDiscPath) {
|
||||
return true;
|
||||
}
|
||||
if (data::is_data_path_restart_pending()) {
|
||||
return true;
|
||||
}
|
||||
if (getSettings().backend.graphicsBackend.getValue() != state.initialGraphicsBackend) {
|
||||
return true;
|
||||
}
|
||||
@@ -798,7 +802,7 @@ void Prelaunch::show() {
|
||||
"A restart is required to apply selected options.<br/><br/>Restart now to "
|
||||
"apply them immediately?" :
|
||||
"A restart is required to apply selected options.<br/><br/>Close and reopen "
|
||||
"Dusk to apply them.",
|
||||
"Dusklight to apply them.",
|
||||
.actions = std::move(actions),
|
||||
.onDismiss = dismiss,
|
||||
}));
|
||||
|
||||
@@ -33,6 +33,7 @@ void applyPresetDusk() {
|
||||
s.game.fastTears.setValue(true);
|
||||
s.game.biggerWallets.setValue(true);
|
||||
s.game.invertCameraXAxis.setValue(true);
|
||||
s.game.invertFirstPersonYAxis.setValue(true);
|
||||
s.game.no2ndFishForCat.setValue(true);
|
||||
s.game.enableAchievementToasts.setValue(true);
|
||||
s.game.enableControllerToasts.setValue(true);
|
||||
@@ -58,7 +59,7 @@ PresetWindow::PresetWindow() : WindowSmall("modal", "modal-dialog") {
|
||||
|
||||
auto* title = append(header, "div");
|
||||
title->SetClass("modal-title", true);
|
||||
title->SetInnerRML("Welcome to Dusk");
|
||||
title->SetInnerRML("Welcome to Dusklight");
|
||||
|
||||
auto* headIcon = append(header, "icon");
|
||||
headIcon->SetClass("celebration", true);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
|
||||
#include "reporting.hpp"
|
||||
|
||||
#include "button.hpp"
|
||||
#include "dusk/crash_reporting.h"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
CrashReportWindow::CrashReportWindow() : WindowSmall("modal", "modal-dialog") {
|
||||
mDialog->SetClass("modal-dialog", true);
|
||||
|
||||
auto* header = append(mDialog, "div");
|
||||
header->SetClass("modal-header", true);
|
||||
|
||||
auto* title = append(header, "div");
|
||||
title->SetClass("modal-title", true);
|
||||
title->SetInnerRML("Send Crash Reports");
|
||||
|
||||
auto* headIcon = append(header, "icon");
|
||||
headIcon->SetClass("question-mark", true);
|
||||
|
||||
auto* intro = append(mDialog, "div");
|
||||
intro->SetClass("modal-body", true);
|
||||
intro->SetInnerRML(
|
||||
"Dusklight can automatically send crash reports to the developers. Crash reports contain the "
|
||||
"following:"
|
||||
"<br/>• Operating system version<br/>• CPU architecture<br/>• GPU model & driver version"
|
||||
"<br/>• File paths (may include account username)<br/>• Stack trace<br/><br/>"
|
||||
"This can be changed in the Settings menu at any time.");
|
||||
|
||||
auto* grid = append(mDialog, "div");
|
||||
grid->SetClass("preset-grid", true);
|
||||
|
||||
struct OptionInfo {
|
||||
const char* name;
|
||||
const char* desc;
|
||||
void (*apply)();
|
||||
};
|
||||
|
||||
static constexpr OptionInfo kOptions[] = {
|
||||
{"Enable",
|
||||
"Send crash reports to Dusklight developers. Reports will include the information described "
|
||||
"above.",
|
||||
[] { crash_reporting::set_consent(true); }},
|
||||
{"Disable",
|
||||
"Do not send crash reports. This may make it more difficult to resolve issues you "
|
||||
"encounter.",
|
||||
[] { crash_reporting::set_consent(false); }},
|
||||
};
|
||||
|
||||
for (const auto& option : kOptions) {
|
||||
auto* col = append(grid, "div");
|
||||
col->SetClass("preset-col", true);
|
||||
|
||||
auto btn = std::make_unique<Button>(col, Rml::String(option.name));
|
||||
btn->on_nav_command([this, apply = option.apply](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
apply();
|
||||
hide(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
mButtons.push_back(std::move(btn));
|
||||
|
||||
auto* desc = append(col, "div");
|
||||
desc->SetClass("preset-desc", true);
|
||||
desc->SetInnerRML(option.desc);
|
||||
}
|
||||
}
|
||||
|
||||
bool CrashReportWindow::focus() {
|
||||
if (!mButtons.empty()) {
|
||||
return mButtons.back()->focus();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CrashReportWindow::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
|
||||
return true;
|
||||
}
|
||||
int direction = 0;
|
||||
if (cmd == NavCommand::Left) {
|
||||
direction = -1;
|
||||
} else if (cmd == NavCommand::Right) {
|
||||
direction = 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
auto* target = event.GetTargetElement();
|
||||
for (int i = 0; i < static_cast<int>(mButtons.size()); ++i) {
|
||||
if (mButtons[i]->contains(target)) {
|
||||
const int next = i + direction;
|
||||
if (next >= 0 && next < static_cast<int>(mButtons.size())) {
|
||||
if (mButtons[next]->focus()) {
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
|
||||
#include "component.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class CrashReportWindow : public WindowSmall {
|
||||
public:
|
||||
CrashReportWindow();
|
||||
|
||||
bool focus() override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<Component>> mButtons;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
#endif
|
||||
@@ -6,8 +6,10 @@
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/main.h"
|
||||
#include "graphics_tuner.hpp"
|
||||
@@ -19,7 +21,15 @@
|
||||
#include "prelaunch.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#include "dusk/crash_reporting.h"
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
@@ -162,29 +172,134 @@ AuroraBackend configured_backend() {
|
||||
void reset_for_speedrun_mode() {
|
||||
mDoMain::developmentMode = -1;
|
||||
|
||||
getSettings().game.damageMultiplier.setValue(1);
|
||||
getSettings().game.instantDeath.setValue(false);
|
||||
getSettings().game.noHeartDrops.setValue(false);
|
||||
getSettings().game.enableTurboKeybind.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setValue(false);
|
||||
getSettings().game.infiniteArrows.setValue(false);
|
||||
getSettings().game.infiniteBombs.setValue(false);
|
||||
getSettings().game.infiniteOil.setValue(false);
|
||||
getSettings().game.infiniteOxygen.setValue(false);
|
||||
getSettings().game.infiniteRupees.setValue(false);
|
||||
getSettings().game.enableIndefiniteItemDrops.setValue(false);
|
||||
getSettings().game.damageMultiplier.setSpeedrunValue(1);
|
||||
getSettings().game.instantDeath.setSpeedrunValue(false);
|
||||
getSettings().game.noHeartDrops.setSpeedrunValue(false);
|
||||
getSettings().game.autoSave.setSpeedrunValue(false);
|
||||
getSettings().game.sunsSong.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.moonJump.setValue(false);
|
||||
getSettings().game.superClawshot.setValue(false);
|
||||
getSettings().game.alwaysGreatspin.setValue(false);
|
||||
getSettings().game.enableFastIronBoots.setValue(false);
|
||||
getSettings().game.canTransformAnywhere.setValue(false);
|
||||
getSettings().game.fastSpinner.setValue(false);
|
||||
getSettings().game.freeMagicArmor.setValue(false);
|
||||
getSettings().game.infiniteHearts.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteArrows.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteBombs.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteOil.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteOxygen.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteRupees.setSpeedrunValue(false);
|
||||
getSettings().game.enableIndefiniteItemDrops.setSpeedrunValue(false);
|
||||
getSettings().game.moonJump.setSpeedrunValue(false);
|
||||
getSettings().game.superClawshot.setSpeedrunValue(false);
|
||||
getSettings().game.alwaysGreatspin.setSpeedrunValue(false);
|
||||
getSettings().game.enableFastIronBoots.setSpeedrunValue(false);
|
||||
getSettings().game.canTransformAnywhere.setSpeedrunValue(false);
|
||||
getSettings().game.fastRoll.setSpeedrunValue(false);
|
||||
getSettings().game.fastSpinner.setSpeedrunValue(false);
|
||||
getSettings().game.freeMagicArmor.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.enableTurboKeybind.setValue(false);
|
||||
getSettings().game.debugFlyCam.setValue(false);
|
||||
getSettings().game.autoSave.setValue(false);
|
||||
getSettings().game.pauseOnFocusLost.setSpeedrunValue(false);
|
||||
aurora_set_pause_on_focus_lost(false);
|
||||
|
||||
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
|
||||
getSettings().game.recordingMode.setSpeedrunValue(false);
|
||||
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
||||
}
|
||||
|
||||
void clear_speedrun_overrides() {
|
||||
config::EnumerateRegistered([](config::ConfigVarBase& cvar) {
|
||||
cvar.clearSpeedrunOverride();
|
||||
});
|
||||
}
|
||||
|
||||
void restore_from_speedrun_mode() {
|
||||
clear_speedrun_overrides();
|
||||
aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue());
|
||||
}
|
||||
|
||||
std::filesystem::path normalized_display_path(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
auto normalized = std::filesystem::weakly_canonical(path, ec);
|
||||
if (!ec) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
normalized = std::filesystem::absolute(path, ec);
|
||||
if (!ec) {
|
||||
return normalized.lexically_normal();
|
||||
}
|
||||
|
||||
return path.lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path user_home_path() {
|
||||
const char* homePath = SDL_GetUserFolder(SDL_FOLDER_HOME);
|
||||
if (homePath == nullptr || homePath[0] == '\0') {
|
||||
return {};
|
||||
}
|
||||
return std::filesystem::path{reinterpret_cast<const char8_t*>(homePath)};
|
||||
}
|
||||
|
||||
Rml::String abbreviated_data_path_string() {
|
||||
const auto path = data::configured_data_path();
|
||||
const auto homePath = user_home_path();
|
||||
if (path.empty() || homePath.empty()) {
|
||||
return io::fs_path_to_string(path);
|
||||
}
|
||||
|
||||
const auto normalizedPath = normalized_display_path(path);
|
||||
const auto normalizedHome = normalized_display_path(homePath);
|
||||
if (normalizedPath == normalizedHome) {
|
||||
return "~";
|
||||
}
|
||||
|
||||
const auto relativePath = normalizedPath.lexically_relative(normalizedHome);
|
||||
if (!relativePath.empty() && !relativePath.is_absolute()) {
|
||||
const auto it = relativePath.begin();
|
||||
if (it == relativePath.end() || *it != "..") {
|
||||
return io::fs_path_to_string(std::filesystem::path{"~"} / relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return io::fs_path_to_string(path);
|
||||
}
|
||||
|
||||
Rml::String configured_data_path_display_name() {
|
||||
const auto path = abbreviated_data_path_string();
|
||||
if (path.empty()) {
|
||||
return "(none)";
|
||||
}
|
||||
|
||||
auto display = display_name_for_path(path);
|
||||
if (display.empty()) {
|
||||
return path;
|
||||
}
|
||||
return display;
|
||||
}
|
||||
|
||||
class DataFolderPathText : public Component {
|
||||
public:
|
||||
explicit DataFolderPathText(Rml::Element* parent) : Component(append(parent, "div")) {}
|
||||
|
||||
void update() override {
|
||||
const Rml::String rml = "<span class=\"data-folder-current\">Current data folder:<br/>" +
|
||||
escape(abbreviated_data_path_string()) + "</span>";
|
||||
if (rml != mCurrentRml) {
|
||||
mRoot->SetInnerRML(rml);
|
||||
mCurrentRml = rml;
|
||||
}
|
||||
Component::update();
|
||||
}
|
||||
|
||||
private:
|
||||
Rml::String mCurrentRml;
|
||||
};
|
||||
|
||||
void data_folder_dialog_callback(void*, const char* path, const char* error) {
|
||||
if (error != nullptr || path == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (data::set_custom_data_path(path)) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
}
|
||||
|
||||
const Rml::String kInternalResolutionHelpText =
|
||||
@@ -194,7 +309,7 @@ const Rml::String kShadowResolutionHelpText =
|
||||
"Configure the shadow-map resolution. Higher values improve shadow quality but increase GPU "
|
||||
"and memory usage.";
|
||||
const Rml::String kBloomHelpText =
|
||||
"Configure the post-processing bloom effect. Classic uses the original bloom pass; Dusk uses "
|
||||
"Configure the post-processing bloom effect. Classic uses the original bloom pass; Dusklight uses "
|
||||
"a higher-quality bloom pass.";
|
||||
const Rml::String kBloomBrightnessHelpText =
|
||||
"Configure bloom intensity. Higher values make bright areas glow more strongly.";
|
||||
@@ -248,6 +363,15 @@ SelectButton& config_bool_select(
|
||||
return button;
|
||||
}
|
||||
|
||||
void add_speedrun_disabled_option(Pane& leftPane, Pane& rightPane, ConfigVar<bool>& var,
|
||||
const Rml::String& key, const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, var, {
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
});
|
||||
}
|
||||
|
||||
SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar<float>& var,
|
||||
Rml::String key, Rml::String helpText, int min, int max, int step = 5,
|
||||
std::function<bool()> isDisabled = {}) {
|
||||
@@ -343,9 +467,52 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
})
|
||||
.on_pressed([] { open_iso_picker(); }),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.add_rml("Set the disc image that Dusk uses to launch the game.<br/><br/>"
|
||||
pane.add_rml("Set the disc image that Dusklight uses to launch the game.<br/><br/>"
|
||||
"Changes require a restart.");
|
||||
});
|
||||
#if DUSK_CAN_CHANGE_DATA_FOLDER
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Data Folder",
|
||||
.getValue = [] { return configured_data_path_display_name(); },
|
||||
.isModified = [] { return data::is_data_path_restart_pending(); },
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.add_text("The data folder is where Dusklight stores settings, saves, "
|
||||
"logs, texture replacements, and other app data.");
|
||||
pane.add_child<DataFolderPathText>();
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
pane.add_button("Open Data Folder").on_pressed([] {
|
||||
if (data::open_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
pane.add_button("Change Data Folder").on_pressed([] {
|
||||
const auto defaultLocation =
|
||||
io::fs_path_to_string(data::configured_data_path());
|
||||
ShowFolderSelect(&data_folder_dialog_callback, nullptr,
|
||||
aurora::window::get_sdl_window(),
|
||||
defaultLocation.empty() ? nullptr : defaultLocation.c_str());
|
||||
});
|
||||
#if defined(_WIN32)
|
||||
pane.add_button("Portable Mode").on_pressed([] {
|
||||
if (data::set_portable_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
pane.add_button({
|
||||
.text = "Reset to Default",
|
||||
.isDisabled = [] { return data::is_default_data_path(); },
|
||||
}).on_pressed([] {
|
||||
if (data::reset_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
});
|
||||
pane.add_rml("Data will be migrated automatically on restart.");
|
||||
});
|
||||
#endif
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Language",
|
||||
@@ -488,7 +655,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
|
||||
{
|
||||
.key = "Pause on Focus Lost",
|
||||
.isDisabled = [] { return IsMobile; },
|
||||
.helpText = "Pause the game when window focus is lost.",
|
||||
.onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); },
|
||||
.isDisabled = [] { return IsMobile || getSettings().game.speedrunMode; },
|
||||
});
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
@@ -598,6 +767,10 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
{
|
||||
.key = "Enable Mini-Map Shadows",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.disableCutscenePillarboxing,
|
||||
{
|
||||
.key = "Disable Cutscene Pillarboxing",
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Input", [this](Rml::Element* content) {
|
||||
@@ -616,7 +789,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
|
||||
leftPane.add_section("Controller");
|
||||
leftPane.register_control(leftPane.add_button("Configure Controller").on_pressed([this] {
|
||||
push(std::make_unique<ControllerConfigWindow>());
|
||||
push(std::make_unique<ControllerConfigWindow>(mPrelaunch));
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.clear();
|
||||
@@ -641,6 +814,10 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.freeCameraSensitivity,
|
||||
"Free Camera Sensitivity", "Adjusts twin-stick camera sensitivity.", 50, 200, 5,
|
||||
[] { return !getSettings().game.freeCamera; });
|
||||
addOption("Invert First Person X Axis", getSettings().game.invertFirstPersonXAxis,
|
||||
"Invert horizontal movement while aiming with items or first person camera. Applies to both stick and gyro aiming.");
|
||||
addOption("Invert First Person Y Axis", getSettings().game.invertFirstPersonYAxis,
|
||||
"Invert vertical movement while aiming with items or first person camera. Applies to both stick and gyro aiming.");
|
||||
|
||||
leftPane.add_section("Gyro");
|
||||
leftPane.register_control(
|
||||
@@ -762,8 +939,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().audio.menuSounds,
|
||||
{
|
||||
.key = "Dusk Menu Sounds",
|
||||
.helpText = "Play sound effects when navigating the Dusk menu.",
|
||||
.key = "Dusklight Menu Sounds",
|
||||
.helpText = "Play sound effects when navigating the Dusklight menu.",
|
||||
});
|
||||
|
||||
leftPane.add_section("Tweaks");
|
||||
@@ -793,12 +970,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
};
|
||||
auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
});
|
||||
add_speedrun_disabled_option(leftPane, rightPane, value, key, helpText);
|
||||
};
|
||||
|
||||
leftPane.add_section("General");
|
||||
@@ -850,12 +1022,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
"Quicker climbing on ladders and vines like the HD version.");
|
||||
addOption("Faster Tears of Light", getSettings().game.fastTears,
|
||||
"Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.autoSave,
|
||||
{
|
||||
.key = "Autosave",
|
||||
.helpText = "Autosaves the game when going to a new area, opening a dungeon door, "
|
||||
"or getting a new item.",
|
||||
});
|
||||
addSpeedrunDisabledOption("Autosave", getSettings().game.autoSave,
|
||||
"Autosaves the game when going to a new area, opening a dungeon door, "
|
||||
"or getting a new item.");
|
||||
addOption("Instant Saves", getSettings().game.instantSaves,
|
||||
"Skips the delay when writing to the Memory Card.");
|
||||
addOption("Hold B for Instant Text", getSettings().game.instantText,
|
||||
@@ -869,7 +1038,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
"Link will not recoil when his sword hits walls.");
|
||||
addOption("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat,
|
||||
"Skip needing to catch a second fish for Sera's cat.");
|
||||
addOption("Sun's Song (R+X)", getSettings().game.sunsSong,
|
||||
addSpeedrunDisabledOption("Sun's Song (R+X)", getSettings().game.sunsSong,
|
||||
"Allows Wolf Link to howl and change the time of day.");
|
||||
addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform,
|
||||
"Transform instantly by pressing R and Y simultaneously.");
|
||||
@@ -880,12 +1049,29 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.key = "Speedrun Mode",
|
||||
.helpText =
|
||||
"Enables speedrunning options while restricting certain gameplay modifiers.",
|
||||
.onChange = [](bool) { reset_for_speedrun_mode(); },
|
||||
.onChange =
|
||||
[](bool enabled) {
|
||||
if (enabled) {
|
||||
reset_for_speedrun_mode();
|
||||
} else {
|
||||
restore_from_speedrun_mode();
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
speedrun::disconnectLiveSplit();
|
||||
}
|
||||
}
|
||||
for (auto& doc : get_document_stack()) {
|
||||
if (dynamic_cast<MenuBar*>(doc.get())) {
|
||||
doc = std::make_unique<MenuBar>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled,
|
||||
{
|
||||
.key = "LiveSplit Connection",
|
||||
.helpText = "Connect to LiveSplit server on localhost:16834.",
|
||||
.helpText = "Connect to LiveSplit server on localhost:16834. For this to work you must right click LiveSplit, and turn on Control -> Start TCP Server."
|
||||
" To see IGT in LiveSplit you must change your comparison to Game Time.",
|
||||
.onChange =
|
||||
[](bool enabled) {
|
||||
if (enabled) {
|
||||
@@ -894,6 +1080,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
speedrun::disconnectLiveSplit();
|
||||
}
|
||||
},
|
||||
.isDisabled = [] { return IsMobile || !getSettings().game.speedrunMode; },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.showSpeedrunRTATimer,
|
||||
{
|
||||
.key = "Show RTA",
|
||||
.helpText = "Display the RTA timer. IGT is always visible.",
|
||||
.isDisabled = [] { return !getSettings().game.speedrunMode; },
|
||||
});
|
||||
});
|
||||
@@ -904,12 +1096,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
|
||||
auto addCheat = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
});
|
||||
add_speedrun_disabled_option(leftPane, rightPane, value, key, helpText);
|
||||
};
|
||||
|
||||
leftPane.add_section("Resources");
|
||||
@@ -936,6 +1123,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
"Speeds up movement while wearing the Iron Boots.");
|
||||
addCheat("Can Transform Anywhere", getSettings().game.canTransformAnywhere,
|
||||
"Allows transforming even if NPCs are looking.");
|
||||
addCheat("Fast Roll", getSettings().game.fastRoll,
|
||||
"Makes Link's roll animation and movement twice as fast.");
|
||||
addCheat("Fast Spinner", getSettings().game.fastSpinner,
|
||||
"Speeds up Spinner movement while holding R.");
|
||||
addCheat("Free Magic Armor", getSettings().game.freeMagicArmor,
|
||||
@@ -946,16 +1135,16 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
leftPane.add_section("Dusk");
|
||||
leftPane.add_section("Dusklight");
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
leftPane.register_control(
|
||||
leftPane.add_button("Open Data Folder").on_pressed([] {
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
dusk::OpenDataFolder();
|
||||
data::open_data_path();
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.add_text(
|
||||
"Open the folder where Dusk stores settings, saves, logs, texture "
|
||||
"Open the folder where Dusklight stores settings, saves, logs, texture "
|
||||
"replacements, and other app data.");
|
||||
});
|
||||
#endif
|
||||
@@ -1024,21 +1213,29 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
pane.add_rml("<br/>Choose which notifications can be displayed.");
|
||||
});
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.enableCrashReporting,
|
||||
{.key = "Crash Reporting",
|
||||
.helpText = "Enable automatic reporting of crashes to the developers.<br/><br/>"
|
||||
"Submissions include logs which may contain sensitive information. "
|
||||
"Refrain from "
|
||||
"enabling reporting if you do not agree with the following "
|
||||
"inclusions:<br/><br/> "
|
||||
"- Operating System<br/>- CPU Architecture<br/>- GPU Model & Driver "
|
||||
"Version<br/>"
|
||||
"- Account Username"});
|
||||
auto& crashReporting = leftPane.add_child<BoolButton>(BoolButton::Props{
|
||||
.key = "Crash Reporting",
|
||||
.getValue =
|
||||
[] { return crash_reporting::get_consent() == crash_reporting::Consent::Given; },
|
||||
.setValue = [](bool enabled) { crash_reporting::set_consent(enabled); },
|
||||
.isDisabled =
|
||||
[] {
|
||||
return crash_reporting::get_consent() == crash_reporting::Consent::Unavailable;
|
||||
},
|
||||
.isModified = [] { return false; },
|
||||
});
|
||||
leftPane.register_control(crashReporting, rightPane, [](Pane& pane) {
|
||||
pane.clear();
|
||||
pane.add_rml("Dusklight can automatically send crash reports to the developers. Crash "
|
||||
"reports contain the following:<br/>• Operating system version<br/>• CPU "
|
||||
"architecture<br/>• GPU model & driver version<br/>• File paths (may "
|
||||
"include account username)<br/>• Stack trace");
|
||||
});
|
||||
#endif
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.skipPreLaunchUI,
|
||||
{
|
||||
.key = "Skip Dusk Main Menu",
|
||||
.helpText = "When starting Dusk, skip the main menu and boot straight into the "
|
||||
.key = "Skip Dusklight Main Menu",
|
||||
.helpText = "When starting Dusklight, skip the main menu and boot straight into the "
|
||||
"game if a disc image is available.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.showPipelineCompilation,
|
||||
@@ -1049,15 +1246,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
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/>"
|
||||
.helpText = "Checks GitHub releases for a new Dusklight 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",
|
||||
@@ -1074,6 +1265,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
}
|
||||
}
|
||||
},
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
});
|
||||
|
||||
leftPane.add_section("Game");
|
||||
@@ -1082,12 +1274,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.key = "Skip TV Settings Screen",
|
||||
.helpText = "Skips the TV calibration screen shown when loading a save.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.recordingMode,
|
||||
{
|
||||
.key = "Recording Mode",
|
||||
.helpText = "Disables the game HUD and all background music.<br/><br/>Useful for "
|
||||
"recording footage.",
|
||||
});
|
||||
add_speedrun_disabled_option(leftPane, rightPane, getSettings().game.recordingMode,
|
||||
"Recording Mode",
|
||||
"Disables the game HUD and all background music.<br/><br/>Useful for recording footage.");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ bool initialize() noexcept {
|
||||
load_font("AlegreyaSC-Regular.ttf");
|
||||
load_font("AlegreyaSC-Bold.ttf");
|
||||
load_font("MaterialSymbolsRounded-Regular.ttf");
|
||||
load_font("NotoMono-Regular.ttf");
|
||||
|
||||
sInitialized = true;
|
||||
return true;
|
||||
|
||||
@@ -335,7 +335,7 @@ Result check_latest_github_release(std::string_view owner, std::string_view repo
|
||||
if (!currentVersion) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("Failed to parse Dusk version '{}'", DUSK_WC_DESCRIBE),
|
||||
.message = fmt::format("Failed to parse Dusklight version '{}'", DUSK_WC_DESCRIBE),
|
||||
.latest = std::move(latest),
|
||||
};
|
||||
}
|
||||
@@ -343,7 +343,7 @@ Result check_latest_github_release(std::string_view owner, std::string_view repo
|
||||
const bool updateAvailable = compare_version(*latestVersion, *currentVersion) > 0;
|
||||
return {
|
||||
.status = updateAvailable ? Status::UpdateAvailable : Status::UpToDate,
|
||||
.message = updateAvailable ? "Update available" : "Dusk is up to date",
|
||||
.message = updateAvailable ? "Update available" : "Dusklight is up to date",
|
||||
.latest = std::move(latest),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "f_op/f_op_overlap_req.h"
|
||||
#include "f_pc/f_pc_manager.h"
|
||||
|
||||
#include "dusk/imgui/ImGuiMenuGame.hpp"
|
||||
#include "dusk/speedrun.h"
|
||||
|
||||
void fopOvlpReq_SetPeektime(overlap_request_class*, u16);
|
||||
|
||||
|
||||
@@ -636,7 +636,7 @@ u8 mDoGph_gInf_c::isWide() {
|
||||
}
|
||||
|
||||
void mDoGph_gInf_c::setWideZoomProjection(Mtx44& m) {
|
||||
if (!isWideZoom()) {
|
||||
IF_NOT_DUSK(if (!isWideZoom())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -682,7 +682,7 @@ void mDoGph_gInf_c::setWideZoomProjection(Mtx44& m) {
|
||||
}
|
||||
|
||||
void mDoGph_gInf_c::setWideZoomLightProjection(Mtx& m) {
|
||||
if (!isWideZoom()) {
|
||||
IF_NOT_DUSK(if (!isWideZoom())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1189,14 +1189,24 @@ static void trimming(view_class* param_0, view_port_class* param_1) {
|
||||
ZoneScoped;
|
||||
UNUSED(param_0);
|
||||
|
||||
#if !TARGET_PC
|
||||
s16 y_orig = (int)param_1->y_orig & ~7;
|
||||
s16 y_orig_pos = y_orig < 0 ? 0 : y_orig;
|
||||
if ((y_orig_pos == 0) && (param_1->scissor.y_orig != param_1->y_orig ||
|
||||
(param_1->scissor.height != param_1->height)))
|
||||
#endif
|
||||
{
|
||||
#if TARGET_PC
|
||||
f32 sc_top = param_1->scissor.y_orig;
|
||||
f32 sc_bottom = param_1->scissor.y_orig + param_1->scissor.height;
|
||||
f32 sc_bottom = sc_top + param_1->scissor.height;
|
||||
|
||||
f32 sc_left = 0.0f;
|
||||
f32 sc_right = param_1->width;
|
||||
|
||||
if (!dusk::getSettings().game.disableCutscenePillarboxing) {
|
||||
sc_left = param_1->scissor.x_orig;
|
||||
sc_right = sc_left + param_1->scissor.width;
|
||||
}
|
||||
#else
|
||||
s32 sc_top = (int)param_1->scissor.y_orig;
|
||||
s32 sc_bottom = param_1->scissor.y_orig + param_1->scissor.height;
|
||||
@@ -1232,17 +1242,32 @@ static void trimming(view_class* param_0, view_port_class* param_1) {
|
||||
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGBA, DUSK_IF_ELSE(GX_F32, GX_RGBA4), 0);
|
||||
GXSetProjection(ortho, GX_ORTHOGRAPHIC);
|
||||
GXSetCurrentMtx(0);
|
||||
GXBegin(GX_QUADS, GX_VTXFMT0, 8);
|
||||
GXBegin(GX_QUADS, GX_VTXFMT0, DUSK_IF_ELSE(16, 8));
|
||||
|
||||
#if TARGET_PC
|
||||
// top trapezoid
|
||||
GXPosition3f32(0, 0, -5);
|
||||
GXPosition3f32(param_1->width, 0, -5);
|
||||
GXPosition3f32(param_1->width, sc_top, -5);
|
||||
GXPosition3f32(0, sc_top, -5);
|
||||
GXPosition3f32(0, sc_bottom, -5);
|
||||
GXPosition3f32(param_1->width, sc_bottom, -5);
|
||||
GXPosition3f32(sc_right, sc_top, -5);
|
||||
GXPosition3f32(sc_left, sc_top, -5);
|
||||
|
||||
// bottom trapezoid
|
||||
GXPosition3f32(sc_left, sc_bottom, -5);
|
||||
GXPosition3f32(sc_right, sc_bottom, -5);
|
||||
GXPosition3f32(param_1->width, param_1->height, -5);
|
||||
GXPosition3f32(0, param_1->height, -5);
|
||||
|
||||
// left trapezoid
|
||||
GXPosition3f32(0, 0, -5);
|
||||
GXPosition3f32(sc_left, sc_top, -5);
|
||||
GXPosition3f32(sc_left, sc_bottom, -5);
|
||||
GXPosition3f32(0, param_1->height, -5);
|
||||
|
||||
// right trapezoid
|
||||
GXPosition3f32(sc_right, sc_top, -5);
|
||||
GXPosition3f32(param_1->width, 0, -5);
|
||||
GXPosition3f32(param_1->width, param_1->height, -5);
|
||||
GXPosition3f32(sc_right, sc_bottom, -5);
|
||||
#else
|
||||
GXPosition3s16(0, 0, -5);
|
||||
GXPosition3s16(FB_WIDTH, 0, -5);
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "SSystem/SComponent/c_API.h"
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/crash_reporting.h"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/game_clock.h"
|
||||
@@ -91,6 +92,10 @@
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#include "dusk/ui/reporting.hpp"
|
||||
#endif
|
||||
|
||||
// --- GLOBALS ---
|
||||
s8 mDoMain::developmentMode = -1;
|
||||
OSTime mDoMain::sPowerOnTime;
|
||||
@@ -121,32 +126,6 @@ void dusk::RequestRestart() noexcept {
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
bool dusk::OpenDataFolder() {
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
std::error_code ec;
|
||||
std::filesystem::path path = std::filesystem::absolute(ConfigPath, ec);
|
||||
if (ec) {
|
||||
DuskLog.warn("Failed to resolve absolute data folder path '{}': {}",
|
||||
io::fs_path_to_string(ConfigPath), ec.message());
|
||||
path = ConfigPath;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
const std::string url = "file:///" + path.generic_string();
|
||||
#else
|
||||
const std::string url = "file://" + path.generic_string();
|
||||
#endif
|
||||
if (!SDL_OpenURL(url.c_str())) {
|
||||
DuskLog.warn(
|
||||
"Failed to open data folder '{}': {}", io::fs_path_to_string(path), SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
s32 LOAD_COPYDATE(void*) {
|
||||
char buffer[32];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
@@ -428,175 +407,6 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) {
|
||||
}
|
||||
}
|
||||
|
||||
static void migrate_directory(const std::filesystem::path& from, const std::filesystem::path& to) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(to, ec);
|
||||
if (ec) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (std::filesystem::recursive_directory_iterator it(
|
||||
from, std::filesystem::directory_options::skip_permission_denied, ec);
|
||||
it != std::filesystem::recursive_directory_iterator(); it.increment(ec))
|
||||
{
|
||||
if (ec) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto relativePath = std::filesystem::relative(it->path(), from, ec);
|
||||
if (ec) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto targetPath = to / relativePath;
|
||||
if (it->is_directory(ec)) {
|
||||
std::filesystem::create_directories(targetPath, ec);
|
||||
if (ec) {
|
||||
return;
|
||||
}
|
||||
} else if (it->is_regular_file(ec) && !std::filesystem::exists(targetPath, ec)) {
|
||||
std::filesystem::create_directories(targetPath.parent_path(), ec);
|
||||
if (ec) {
|
||||
return;
|
||||
}
|
||||
std::filesystem::copy_file(
|
||||
it->path(), targetPath, std::filesystem::copy_options::skip_existing, ec);
|
||||
if (ec) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::filesystem::path calculate_config_path() {
|
||||
#ifdef __APPLE__
|
||||
#if TARGET_OS_IOS && !TARGET_OS_TV
|
||||
const char* documentsPath = SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS);
|
||||
if (!documentsPath) {
|
||||
DuskLog.fatal("Unable to get iOS Documents path: {}", SDL_GetError());
|
||||
}
|
||||
|
||||
std::filesystem::path configPath = reinterpret_cast<const char8_t*>(documentsPath);
|
||||
|
||||
char* oldPrefPath = SDL_GetPrefPath(dusk::OrgName, dusk::AppName);
|
||||
if (oldPrefPath) {
|
||||
const std::filesystem::path oldConfigPath = reinterpret_cast<const char8_t*>(oldPrefPath);
|
||||
SDL_free(oldPrefPath);
|
||||
|
||||
std::error_code ec;
|
||||
if (oldConfigPath != configPath && std::filesystem::exists(oldConfigPath, ec)) {
|
||||
migrate_directory(oldConfigPath, configPath);
|
||||
}
|
||||
}
|
||||
|
||||
return configPath;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName);
|
||||
if (!result) {
|
||||
DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError());
|
||||
}
|
||||
|
||||
return reinterpret_cast<const char8_t*>(result);
|
||||
}
|
||||
|
||||
static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) {
|
||||
if (configDir.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::filesystem::path pipelineCachePath = configDir / "pipeline_cache.db";
|
||||
if (std::filesystem::exists(pipelineCachePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string sourcePathString;
|
||||
SDL_IOStream* source = nullptr;
|
||||
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
if (basePath != nullptr) {
|
||||
sourcePathString = dusk::io::fs_path_to_string(
|
||||
std::filesystem::path(basePath) / "initial_pipeline_cache.db");
|
||||
source = SDL_IOFromFile(sourcePathString.c_str(), "rb");
|
||||
}
|
||||
if (source == nullptr) {
|
||||
sourcePathString = "initial_pipeline_cache.db";
|
||||
source = SDL_IOFromFile(sourcePathString.c_str(), "rb");
|
||||
}
|
||||
if (source == nullptr) {
|
||||
DuskLog.info("No bundled initial pipeline cache found");
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(configDir, ec);
|
||||
if (ec) {
|
||||
DuskLog.warn("Failed to create config directory '{}' for pipeline cache: {}",
|
||||
dusk::io::fs_path_to_string(configDir), ec.message());
|
||||
SDL_CloseIO(source);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pipelineCacheString = dusk::io::fs_path_to_string(pipelineCachePath);
|
||||
SDL_IOStream* destination = SDL_IOFromFile(pipelineCacheString.c_str(), "wb");
|
||||
if (destination == nullptr) {
|
||||
DuskLog.warn("Failed to open '{}' for seeded pipeline cache: {}", pipelineCacheString,
|
||||
SDL_GetError());
|
||||
SDL_CloseIO(source);
|
||||
return;
|
||||
}
|
||||
|
||||
bool copied = true;
|
||||
std::array<char, 64 * 1024> buffer{};
|
||||
while (true) {
|
||||
const size_t bytesRead = SDL_ReadIO(source, buffer.data(), buffer.size());
|
||||
if (bytesRead > 0) {
|
||||
size_t bytesWritten = 0;
|
||||
while (bytesWritten < bytesRead) {
|
||||
const size_t written = SDL_WriteIO(
|
||||
destination, buffer.data() + bytesWritten, bytesRead - bytesWritten);
|
||||
if (written == 0) {
|
||||
DuskLog.warn("Failed to write seeded pipeline cache '{}': {}",
|
||||
pipelineCacheString, SDL_GetError());
|
||||
copied = false;
|
||||
break;
|
||||
}
|
||||
bytesWritten += written;
|
||||
}
|
||||
}
|
||||
|
||||
if (!copied) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (bytesRead < buffer.size()) {
|
||||
if (SDL_GetIOStatus(source) == SDL_IO_STATUS_EOF) {
|
||||
break;
|
||||
}
|
||||
|
||||
DuskLog.warn(
|
||||
"Failed to read bundled pipeline cache '{}': {}", sourcePathString, SDL_GetError());
|
||||
copied = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!SDL_CloseIO(destination)) {
|
||||
DuskLog.warn("Failed to close seeded pipeline cache '{}': {}",
|
||||
dusk::io::fs_path_to_string(pipelineCachePath), SDL_GetError());
|
||||
copied = false;
|
||||
}
|
||||
SDL_CloseIO(source);
|
||||
|
||||
if (!copied) {
|
||||
std::filesystem::remove(pipelineCachePath, ec);
|
||||
return;
|
||||
}
|
||||
|
||||
DuskLog.info("Seeded pipeline cache from '{}'", sourcePathString);
|
||||
}
|
||||
|
||||
static constexpr PADDefaultMapping defaultPadMapping = {
|
||||
.buttons = {
|
||||
{SDL_GAMEPAD_BUTTON_SOUTH, PAD_BUTTON_A},
|
||||
@@ -647,6 +457,14 @@ static void LanguageInit() {
|
||||
selectedLanguage = static_cast<u8>(dusk::getSettings().game.language.getValue());
|
||||
}
|
||||
|
||||
static std::string asset_path(const char* assetName) {
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
if (basePath != nullptr && basePath[0] != '\0') {
|
||||
return std::string(basePath) + "res/" + assetName;
|
||||
}
|
||||
return std::string("res/") + assetName;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PC ENTRY POINT
|
||||
// =========================================================================
|
||||
@@ -664,7 +482,7 @@ int game_main(int argc, char* argv[]) {
|
||||
cxxopts::ParseResult parsed_arg_options;
|
||||
|
||||
try {
|
||||
cxxopts::Options arg_options("Dusk", "PC Port of The Legend of Zelda: Twilight Princess");
|
||||
cxxopts::Options arg_options("Dusklight", "PC Port of a classic adventure game");
|
||||
|
||||
arg_options.add_options()
|
||||
("l,log-level", "Log level from " + std::to_string(AuroraLogLevel::LOG_DEBUG) + " to " + std::to_string(AuroraLogLevel::LOG_FATAL), cxxopts::value<uint8_t>()->default_value("0"))
|
||||
@@ -692,16 +510,24 @@ int game_main(int argc, char* argv[]) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
dusk::ConfigPath = calculate_config_path();
|
||||
const auto startupLogLevel = static_cast<AuroraLogLevel>(parsed_arg_options["log-level"].as<uint8_t>());
|
||||
const auto startupLogLevel =
|
||||
static_cast<AuroraLogLevel>(parsed_arg_options["log-level"].as<uint8_t>());
|
||||
dusk::ConfigPath = dusk::data::initialize_data();
|
||||
dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel);
|
||||
|
||||
dusk::config::LoadFromUserPreferences();
|
||||
ApplyCVarOverrides(parsed_arg_options["cvar"]);
|
||||
dusk::InitializeCrashReporting();
|
||||
EnsureInitialPipelineCache(dusk::ConfigPath);
|
||||
dusk::crash_reporting::initialize();
|
||||
// TODO: How to handle this?
|
||||
//PADSetDefaultMapping(&defaultPadMapping, PAD_TYPE_STANDARD);
|
||||
// PADSetDefaultMapping(&defaultPadMapping, PAD_TYPE_STANDARD);
|
||||
|
||||
{
|
||||
// Load mappings from https://github.com/mdqinc/SDL_GameControllerDB
|
||||
const auto mappingsPath = asset_path("gamecontrollerdb.txt");
|
||||
if (SDL_AddGamepadMappingsFromFile(mappingsPath.c_str()) < 0) {
|
||||
DuskLog.warn("Failed to load gamecontrollerdb.txt: {}", SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const auto configPathString = dusk::ConfigPath.u8string();
|
||||
@@ -732,7 +558,7 @@ int game_main(int argc, char* argv[]) {
|
||||
#endif
|
||||
|
||||
VISetWindowTitle(
|
||||
fmt::format("Dusk {} [{}]", DUSK_WC_DESCRIBE, dusk::backend_name(auroraInfo.backend))
|
||||
fmt::format("Dusklight {} [{}]", DUSK_WC_DESCRIBE, dusk::backend_name(auroraInfo.backend))
|
||||
.c_str());
|
||||
|
||||
if (dusk::getSettings().video.lockAspectRatio) {
|
||||
@@ -749,7 +575,7 @@ int game_main(int argc, char* argv[]) {
|
||||
// Run ImGui UI loop if Aurora couldn't initialize a backend
|
||||
if (auroraInfo.backend == BACKEND_NULL) {
|
||||
launchUILoop();
|
||||
dusk::ShutdownCrashReporting();
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
@@ -765,7 +591,7 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Overlay>(), true, true);
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::MenuBar>(), false);
|
||||
|
||||
// Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up.
|
||||
// Invalidate a bad saved isoPath so that Dusklight 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;
|
||||
@@ -827,7 +653,7 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
// pre game launch ui main loop
|
||||
if (!launchUILoop()) {
|
||||
dusk::ShutdownCrashReporting();
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
@@ -858,6 +684,12 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::IsGameLaunched = true;
|
||||
}
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (dusk::crash_reporting::get_consent() == dusk::crash_reporting::Consent::Unknown) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::CrashReportWindow>());
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!dusk::getSettings().backend.wasPresetChosen) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::PresetWindow>());
|
||||
}
|
||||
@@ -896,7 +728,7 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
dusk::MoviePlayerShutdown();
|
||||
|
||||
dusk::ShutdownCrashReporting();
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#endif
|
||||
|
||||
#define DUSK_PLATFORM_NAME "@PLATFORM_NAME@"
|
||||
#define DUSK_DLPACKAGE "dusk-@DUSK_WC_DESCRIBE@-" DUSK_PLATFORM_NAME "-" DUSK_ARCH
|
||||
#define DUSK_DLPACKAGE "dusklight-@DUSK_WC_DESCRIBE@-" DUSK_PLATFORM_NAME "-" DUSK_ARCH
|
||||
|
||||
#define DUSK_SENTRY_DSN "@DUSK_SENTRY_DSN@"
|
||||
#define DUSK_SENTRY_ENVIRONMENT "@DUSK_SENTRY_ENVIRONMENT@"
|
||||
|
||||