Compare commits

...

19 Commits

Author SHA1 Message Date
Luke Street f83664074e Split randomizer mod into submodule
Co-authored-by: TakaRikka <takarikka@outlook.com>
Co-authored-by: gymnast86 <stock.joann@gmail.com>
2026-09-01 21:22:36 -06:00
Luke Street a5eca78939 Mods: Avoid RTLD_DEEPBIND; configure exports manually (#2371)
* Mods: Avoid RTLD_DEEPBIND; configure exports manually

* Make version script anonymous
2026-09-01 16:05:51 -06:00
Luke Street 6cc568d081 Mods: HttpService (#2370)
* Update Android toolchain to platform 37

* Mods: HttpService

* Update modding.md

* Update modding.md

* Set minor version back to 0
2026-09-01 16:05:45 -06:00
TakaRikka d60cc9095f fix combo input checking 2026-09-01 14:50:23 -07:00
Irastris 0fd17fe082 Refactor GraphicsTuner, no longer tanks performance when open (#2365)
* Sort includes, move logic into ConfigVars, add reusable helpers

* Refactor GraphicsTuner, only refresh labels when actually needed
2026-09-01 09:07:49 -06:00
Luke Street 455150ad90 Mods: FileService (#2367) 2026-09-01 09:06:01 -06:00
TakaRikka b76917ed48 Merge pull request #2363 from TwilitRealm/instant-death-fixes
Instant Death Fixes
2026-09-01 04:19:35 -07:00
TakaRikka ba44971bde Merge pull request #2368 from TwilitRealm/speedrun-fixes
various speedrun game mode fixes
2026-09-01 03:36:39 -07:00
TakaRikka a746b23944 make sure tools are disabled for speedrun mode and reset run info when mode is deactivated 2026-09-01 03:11:04 -07:00
TakaRikka 6c966876b9 fix speedrun timer resetting 2026-09-01 02:46:53 -07:00
TakaRikka 2570c8f072 fix gamemode onSaveLoaded handling with hideTvSettingsScreen setting 2026-09-01 01:09:42 -07:00
SuperDude88 2e19ffbf50 Shorten int Change 2026-08-31 20:15:15 -04:00
Pieter-Jan Briers 01e3c4616f global.h MSVC warning fix (#2359) 2026-08-31 18:10:07 -06:00
Pieter-Jan Briers 57f0185a3c Suppress new XF memory write stub logs (#2362) 2026-08-31 18:09:38 -06:00
Luke Street 48171f0f0a Only use inlining flags w/ mod support
Also enables mod support by default. It was already enabled in the
CMakePresets.json, so this just moves it to the CMakeLists itself.
2026-08-31 18:02:44 -06:00
Luke Street a29f13452f Revert accidental CMakePresets.json change 2026-08-31 17:18:29 -06:00
Luke Street 905da94b40 Fix game compile flags on Linux/GCC
There was a conflict with per-target compile flags and the PCH.
The PCH was causing the game code to be built without the inlining
flags actually taking effect (internal `src/dusk` files did not use
them), which caused mods to fail to resolve static functions for
hooks. Fix: split Dusklight internals (`src/dusk/`) into a separate
static library altogether, cleanly allowing us to disable inlining
and patching flags only for internal code while also fixing the PCH
flags issue.

Additionally, this updates symgen to v1.3.4, which includes a fix
for GCC function fragments and specializations getting resolved as
display name aliases, conflicting with the actual functions.

Aurora also updated to include https://github.com/encounter/aurora/pull/244
2026-08-31 17:11:05 -06:00
SuperDude88 4a632ff5dc Instant Death Fixes
- Fix some overflows with instant death mode
2026-08-31 18:24:30 -04:00
Luke Street 93f9ad6f1d Remove const from symdb descriptor (avoids unintentional DCE) 2026-08-31 15:33:26 -06:00
93 changed files with 3241 additions and 651 deletions
+3
View File
@@ -4,3 +4,6 @@
[submodule "extern/borealis"]
path = extern/borealis
url = https://github.com/encounter/borealis.git
[submodule "mods/randomizer"]
path = mods/randomizer
url = https://github.com/TwilitRealm/dusklight-randomizer.git
+85 -74
View File
@@ -91,7 +91,7 @@ option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled
option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON)
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
option(DUSK_ENABLE_CODE_MODS "Enable code mods" ON)
set(DUSK_HAS_FUNCHOOK OFF)
if (DUSK_ENABLE_CODE_MODS AND (NOT APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Darwin"))
@@ -304,7 +304,7 @@ include(cmake/GameABIConfig.cmake)
find_package(Threads REQUIRED)
set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1)
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::io borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
Threads::Threads zstd::libzstd dusklight_game_headers)
if (DUSK_HAS_FUNCHOOK)
list(APPEND GAME_LIBS funchook-static)
@@ -384,20 +384,31 @@ if (CMAKE_CXX_LINK_GROUP_USING_RESCAN_SUPPORTED OR CMAKE_LINK_GROUP_USING_RESCAN
set(JSYSTEM_LINK_LIBRARIES "$<LINK_GROUP:RESCAN,${JSYSTEM_LIBRARIES}>")
endif ()
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_SOURCE_DIR}/miniz.c)
set(_dusklight_all_files src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES})
set(DUSK_INTERNAL_FILES ${_dusklight_all_files})
list(FILTER DUSK_INTERNAL_FILES INCLUDE REGEX "^src/dusk/")
set(GAME_FILES ${_dusklight_all_files})
list(FILTER GAME_FILES EXCLUDE REGEX "^src/dusk/")
add_library(dusk_internal OBJECT ${DUSK_INTERNAL_FILES})
if (ENABLE_ASAN)
target_sources(dusk_internal PRIVATE src/dusk/asan_options.c)
endif ()
target_compile_definitions(dusk_internal PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusk_internal PRIVATE ${miniz_SOURCE_DIR})
target_link_libraries(dusk_internal PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LIBRARIES})
target_precompile_headers(dusk_internal PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
if(ANDROID)
add_library(dusklight SHARED ${DUSK_FILES})
add_library(dusklight SHARED ${GAME_FILES} ${miniz_SOURCE_DIR}/miniz.c)
else ()
add_executable(dusklight ${DUSK_FILES})
add_executable(dusklight ${GAME_FILES} ${miniz_SOURCE_DIR}/miniz.c)
endif ()
borealis_configure_android_application(dusklight)
if (ENABLE_ASAN)
target_sources(dusklight PRIVATE src/dusk/asan_options.c)
endif ()
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusklight PRIVATE ${miniz_SOURCE_DIR})
target_link_libraries(dusklight PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_link_libraries(dusklight PRIVATE dusk_internal aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_precompile_headers(dusklight PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
if (DUSK_ENABLE_CODE_MODS)
@@ -413,76 +424,75 @@ endif ()
# Hook reliability: prevent auto-inlining, guarantee patchable function entries,
# and disable identical-code folding for game functions.
set(DUSK_GAME_ABI_INLINE_OPTIONS)
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-finline-hint-functions>)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-flive-patching=inline-clone>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-functions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-small-functions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-functions-called-once>
$<$<COMPILE_LANGUAGE:CXX>:-fno-early-inlining>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-cp>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-cp-clone>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-sra>
$<$<COMPILE_LANGUAGE:CXX>:-fno-partial-inlining>)
endif ()
endif ()
if (DUSK_GAME_ABI_INLINE_OPTIONS)
set(_game_abi_files ${GAME_BASE_FILES} ${GAME_DEBUG_FILES})
list(FILTER _game_abi_files EXCLUDE REGEX "^src/dusk/")
set_property(SOURCE ${_game_abi_files} APPEND PROPERTY
COMPILE_OPTIONS ${DUSK_GAME_ABI_INLINE_OPTIONS})
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
endforeach()
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_sdk_lib})
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
if (NOT _sdk_lib_imported)
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
endif ()
if (DUSK_ENABLE_CODE_MODS)
set(DUSK_GAME_ABI_INLINE_OPTIONS)
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-finline-hint-functions>)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-flive-patching=inline-clone>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-functions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-small-functions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-functions-called-once>
$<$<COMPILE_LANGUAGE:CXX>:-fno-early-inlining>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-cp>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-cp-clone>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-sra>
$<$<COMPILE_LANGUAGE:CXX>:-fno-partial-inlining>)
endif ()
endforeach ()
endif ()
endif ()
if (MSVC)
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:/hotpatch>)
endif ()
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN:16 /OPT:NOICF)
else ()
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN /OPT:NOICF)
endif ()
elseif (CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang|GNU)$")
if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)$" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=2,1>)
else ()
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=10,4>)
endif ()
endif ()
if (DUSK_GAME_ABI_INLINE_OPTIONS)
target_compile_options(dusklight PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
if (DEFINED DUSK_PATCHABLE_ENTRY_FLAG)
target_compile_options(dusklight PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
endforeach()
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_sdk_lib})
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
if (NOT _sdk_lib_imported)
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
endforeach()
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_sdk_lib})
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
if (NOT _sdk_lib_imported)
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
endif ()
endif ()
endforeach ()
endif ()
if (MSVC)
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:/hotpatch>)
endif ()
endforeach ()
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN:16 /OPT:NOICF)
else ()
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN /OPT:NOICF)
endif ()
elseif (CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang|GNU)$")
if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)$" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=2,1>)
else ()
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=10,4>)
endif ()
endif ()
if (DEFINED DUSK_PATCHABLE_ENTRY_FLAG)
target_compile_options(dusklight PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
endforeach()
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_sdk_lib})
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
if (NOT _sdk_lib_imported)
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
endif ()
endif ()
endforeach ()
endif ()
endif ()
if (WIN32)
@@ -561,6 +571,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR
add_subdirectory(mods/window_demo)
add_subdirectory(mods/flow_demo)
add_subdirectory(mods/basic_cosmetics_mod)
add_subdirectory(mods/randomizer)
endif ()
if (APPLE)
+2 -26
View File
@@ -60,11 +60,7 @@
"type": "BOOL",
"value": false
},
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
}
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
@@ -151,10 +147,6 @@
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
@@ -254,11 +246,7 @@
"type": "BOOL",
"value": false
},
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
}
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
@@ -323,10 +311,6 @@
"type": "BOOL",
"value": false
},
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
@@ -372,10 +356,6 @@
"type": "BOOL",
"value": false
},
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
@@ -406,10 +386,6 @@
"type": "BOOL",
"value": false
},
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
-2
View File
@@ -39,8 +39,6 @@ function(setup_android_exports target)
--format version-script
--exclude cmake_pch
--exclude miniz
--exclude asan_options
--exclude src/dusk
# Resolved from the Java side; the SDL ones live in the statically-linked
# SDL archive, outside the provenance scan.
--extra-sym JNI_OnLoad
-2
View File
@@ -46,8 +46,6 @@ function(setup_apple_exports target)
--out "${_exp}"
--exclude cmake_pch
--exclude miniz
--exclude asan_options
--exclude src/dusk
${_sdk_args}
COMMENT "Generating dusklight exports"
VERBATIM)
+5
View File
@@ -0,0 +1,5 @@
_mod_ctx
_mod_initialize
_mod_meta
_mod_shutdown
_mod_update
+10
View File
@@ -0,0 +1,10 @@
{
global:
mod_ctx;
mod_initialize;
mod_meta;
mod_shutdown;
mod_update;
local:
*;
};
+14
View File
@@ -185,6 +185,20 @@ function(add_mod target_name)
WINDOWS_EXPORT_ALL_SYMBOLS OFF)
target_compile_features(${target_name} PRIVATE cxx_std_20)
target_link_libraries(${target_name} PRIVATE dusklight_mod_api)
if (APPLE)
set(_mod_exports "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ModExports.exp")
target_link_options(${target_name} PRIVATE
-Xlinker -exported_symbols_list -Xlinker "${_mod_exports}")
set_property(TARGET ${target_name} APPEND PROPERTY LINK_DEPENDS "${_mod_exports}")
elseif (UNIX)
set(_mod_exports "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ModExports.ver")
target_link_options(${target_name} PRIVATE
"-Wl,--version-script=${_mod_exports}"
-Wl,--no-undefined-version)
set_property(TARGET ${target_name} APPEND PROPERTY LINK_DEPENDS "${_mod_exports}")
endif ()
foreach (_feature IN LISTS _features)
target_link_libraries(${target_name} PRIVATE dusklight_mod_feature_${_feature})
if (_feature STREQUAL "webgpu")
+9 -9
View File
@@ -2,7 +2,7 @@ include_guard(GLOBAL)
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
set(_SYMGEN_VERSION "1.3.3")
set(_SYMGEN_VERSION "1.3.4")
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
mark_as_advanced(SYMGEN_PATH)
@@ -15,32 +15,32 @@ function(symgen_host_asset out_name out_hash)
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
if (_host_processor MATCHES "^(arm64|aarch64)$")
set(_asset "symgen-macos-arm64")
set(_asset_hash "SHA256=e8420df1160242c83bd0e5197efeeb25ceb9fecc69dbc82bdc4927476381e948")
set(_asset_hash "SHA256=983ef6278d30bee38f240f451ca736fd294da0e5705ee15029accac5c7a33829")
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
set(_asset "symgen-macos-x86_64")
set(_asset_hash "SHA256=e51d13bbf0e982d1519f56850ff3c7b5910ca5a7095dd5a82cd8e59750e2d013")
set(_asset_hash "SHA256=b6ab1720a4f04fadcf291f42d133e2ee68ef2e9a75d143e51584929e808af8a2")
endif ()
elseif (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
if (_host_processor MATCHES "^(aarch64|arm64)$")
set(_asset "symgen-linux-aarch64")
set(_asset_hash "SHA256=331190dd21ecee12a52e857b7f90a8b3e0a114608173813f7df74a31cf783f70")
set(_asset_hash "SHA256=ba84486ff1ceb753973e6213c9bc5f195b5ff6f220f5d3322fd861966cdc791c")
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
set(_asset "symgen-linux-x86_64")
set(_asset_hash "SHA256=d727e0ba43242f92a92865af1b69c47aa9d81a0242c4570ce53b90e1ecd15e84")
set(_asset_hash "SHA256=8b81a00a2ef8f5dd4b59adba1badfaca57e472bf272516bc12c4625cefb37718")
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
set(_asset "symgen-linux-i686")
set(_asset_hash "SHA256=0c9f94d36b7fa46d15a199c9d0f1a633b73eb7d9c02621a6a76a676b83a135c7")
set(_asset_hash "SHA256=01aa85d84148a6806fdf69839dea7e962e6ac46956aed63b9ecb7d596f179f93")
endif ()
elseif (CMAKE_HOST_WIN32)
if (_host_processor MATCHES "^(arm64|aarch64)$")
set(_asset "symgen-windows-arm64.exe")
set(_asset_hash "SHA256=c699192957e2086ed68912ed39fa77858e1ae894476ab8b557711804cf2aa7cb")
set(_asset_hash "SHA256=e116099f859177d7e29fc3444d67083596330e5c4fb3a0461b21402c89249d96")
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
set(_asset "symgen-windows-x86_64.exe")
set(_asset_hash "SHA256=6d56a45617c75065ab7a3192d09ca52ff735baf5637b0899f446138d46acc310")
set(_asset_hash "SHA256=57ef1c2f563e33fa4915c40194c4ced540f985c48d9cf081047c6bbcb585fa37")
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
set(_asset "symgen-windows-x86.exe")
set(_asset_hash "SHA256=6cccc3a8d29e7525a4d1ade1dc4d89327b7c3002a604ab1c7decb43eea6fd4ab")
set(_asset_hash "SHA256=cc0ba337c798d3f63d8acbb2d16d531d0c2da878fbdd2b6307702334ec880528")
endif ()
endif ()
-2
View File
@@ -62,8 +62,6 @@ function(setup_windows_exports target)
--out "${_def}"
--exclude cmake_pch
--exclude miniz
--exclude asan_options
--exclude src/dusk
--max-exports 58000
${_sdk_args}
${_forward_args}
+89 -2
View File
@@ -48,7 +48,7 @@ cmake_minimum_required(VERSION 3.26)
project(my_mod CXX)
if (NOT DUSKLIGHT_VERSION)
set(DUSKLIGHT_VERSION "76b56cd8b81809fce0a5c2a44e2f6d437591132f")
set(DUSKLIGHT_VERSION "76b56cd8b81809fce0a5c2a44e2f6d437591132f")
endif ()
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchDusklight.cmake")
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
@@ -212,6 +212,93 @@ if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. The bundle is read-only; use
`HostService::data_dir` for persistent storage.
### FileService (`mods/svc/file.h`)
Provides file and folder pickers, file I/O, exports and folder enumeration.
A location is an opaque UTF-8 string returned by `pick_*`, `export_file`, `join`, or `create_child`.
Save it and pass it back to the service. Never parse it or manually append path segments.
```cpp
#include "mods/svc/file.hpp"
IMPORT_SERVICE(FileService, svc_file);
mods::file::PickOptions options;
options.filters.push_back({"Audio", "wav;ogg"});
mods::file::pick_file(options, [](mods::file::PickResult result) {
if (result.status == MOD_OK && !result.locations.empty()) {
save_location(result.locations.front());
}
});
```
Use `check` before reopening a saved location because removable storage or an access grant may no longer be available.
`open` provides seekable streaming I/O. `read_all` allocates the entire file and should only be used when the file is
small. Folder locations support `list` and child resolution through `join`. Only one picker can be open at a time.
`create_child` never replaces an existing file and returns `MOD_CONFLICT` if one exists. Use the returned location;
document providers may adjust the requested name. `write_all` is a convenience function over
`open`/`write`/`flush`/`close`.
```cpp
std::string location;
if (mods::file::create_child(folder, "report.txt", location) == MOD_OK) {
mods::file::write_all(location, report);
}
mods::file::export_file(location, "report.txt", [](mods::file::PickResult result) {
if (result.status == MOD_OK) {
remember_export_destination(result.locations.front());
}
});
```
`export_file` copies an existing file to a user-selected destination and returns the destination location in its
callback. Mod-owned persistent files belong in `HostService::data_dir`.
### HttpService (`mods/svc/http.h`)
Asynchronous HTTPS requests supporting HTTP/2 and TLS 1.2+. C++ mods should use the helpers in `mods/svc/http.hpp`:
```cpp
#include "mods/svc/http.hpp"
IMPORT_SERVICE(HttpService, svc_http);
mods::http::Pending pendingRequest;
void fetch_manifest() {
mods::http::Request request{
.url = "https://example.com/manifest.json",
.maxBodyBytes = 256 * 1024,
};
pendingRequest = mods::http::request(request, [](mods::http::Response response) {
if (!response.ok()) {
handle_fetch_error(response.error, response.statusCode);
return;
}
std::string manifest{response.body.begin(), response.body.end()};
use_manifest(manifest);
});
if (!pendingRequest) {
handle_start_error(pendingRequest.result());
}
}
```
Keep the returned `Pending` alive until completion. Dropping it or calling `cancel` requests cancellation. Callbacks run
on the game thread.
`Response::ok()` requires a 2xx status. Other HTTP statuses are valid responses, not transport errors, so always check
`statusCode`. In-memory responses default to a 1 MiB limit; set `maxBodyBytes` to increase it if needed.
For large responses, set `downloadPath` to an absolute path in the calling mod's `HostService::data_dir` or
`HostService::mod_dir`. The response is streamed to disk instead of loaded in memory. On success, the callback receives
an empty `body` and the final path in `downloadPath`. Check `Response::ok()` before using the file.
`Pending::progress()` reports download progress when the server provides a total size.
### HostService (`mods/svc/host.h`)
Mod metadata and runtime interaction with the loader:
@@ -587,7 +674,7 @@ svc_ui->dialog_push(mod_ctx, &dialog, nullptr);
```
After an action's `on_pressed`, the dialog closes unless the action sets `keep_open`. It can then be closed later
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body` and
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body` and
`dialog_set_icon` mutate a live dialog.
**Toasts:** `push_toast` enqueues a notification. Titles and bodies accept RML. The optional `type` is applied as an
+1 -1
+5
View File
@@ -1483,6 +1483,7 @@ set(DUSK_FILES
src/dusk/mods/loader/prepatch.cpp
src/dusk/mods/loader/prepatch.hpp
src/dusk/mods/item.hpp
src/dusk/mods/item_actor.cpp
src/dusk/mods/item_checks.cpp
src/dusk/mods/item_gives.cpp
src/dusk/mods/log_buffer.cpp
@@ -1492,11 +1493,13 @@ set(DUSK_FILES
src/dusk/mods/svc/camera.cpp
src/dusk/mods/svc/config.cpp
src/dusk/mods/svc/config.hpp
src/dusk/mods/svc/file.cpp
src/dusk/mods/svc/game.cpp
src/dusk/mods/svc/gfx.cpp
src/dusk/mods/svc/flow.cpp
src/dusk/mods/svc/hook.cpp
src/dusk/mods/svc/host.cpp
src/dusk/mods/svc/http.cpp
src/dusk/mods/svc/item.cpp
src/dusk/mods/svc/item.hpp
src/dusk/mods/svc/log.cpp
@@ -1549,6 +1552,8 @@ set(DUSK_FILES
src/dusk/ui/graphics_tuner.hpp
src/dusk/ui/group_button.cpp
src/dusk/ui/group_button.hpp
src/dusk/ui/file_button.cpp
src/dusk/ui/file_button.hpp
src/dusk/ui/icon_provider.cpp
src/dusk/ui/icon_provider.hpp
src/dusk/ui/input.cpp
+1 -1
View File
@@ -47,7 +47,7 @@ public:
s32 getBgmLoadStatus(u32 wave) { return getWaveLoadStatus(wave, 1); }
u8 getDemoSeWaveNum() { return loadedDemoWave; }
private:
// private:
/* 0x00 */ JAISoundID BGM_ID;
/* 0x04 */ int sceneNum;
/* 0x08 */ int timer;
+1 -1
View File
@@ -123,7 +123,7 @@ public:
bool checkFlag(u16 flag) { return field_0x68e & flag; }
void setAction(u8 action) { mAction = action; }
private:
// private:
/* 0x56C */ request_of_phase_process_class mPhase1;
/* 0x574 */ request_of_phase_process_class mPhase2;
/* 0x57C */ J3DModel* mModel1;
+1 -1
View File
@@ -296,7 +296,7 @@ public:
static DUSK_GAME_DATA char DUSK_CONST* DUSK_CONST mCutNameList[18];
static DUSK_GAME_DATA cutFunc DUSK_CONST mCutList[18];
private:
// private:
/* 0x0E40 */ NPC_FAIRY_HIO_CLASS* mHIO;
/* 0x0E44 */ dCcD_Cyl mCyl;
/* 0x0F80 */ u8 mType;
+1 -1
View File
@@ -287,7 +287,7 @@ public:
static DUSK_GAME_DATA char DUSK_CONST* DUSK_CONST mCutNameList[10];
static DUSK_GAME_DATA cutFunc DUSK_CONST mCutList[10];
private:
// private:
/* 0x0E40 */ mDoExt_McaMorfSO* mFishModelMorf;
/* 0x0E44 */ mDoExt_McaMorfSO* mLeafModelMorf;
/* 0x0E48 */ NPC_YKM_HIO_CLASS* mpHIO;
+1 -1
View File
@@ -96,7 +96,7 @@ public:
static DUSK_GAME_DATA char DUSK_CONST* DUSK_CONST mEvtCutNameList[2];
static DUSK_GAME_DATA EventFn DUSK_CONST mEvtCutList[2];
private:
// private:
/* 0xB48 */ Z2Creature mCreatureSound;
/* 0xBD8 */ daNpcF_MatAnm_c* mpMatAnm;
/* 0xBDC */ daNpcF_Lookat_c mLookat;
+1 -1
View File
@@ -114,7 +114,7 @@ public:
void himoCalc();
void adjustShapeAngle() {}
private:
// private:
/* 0x0B48 */ Z2Creature mCreatureSound;
/* 0x0BD8 */ daNpcF_MatAnm_c* mpMatAnm;
/* 0x0BDC */ mDoExt_invisibleModel mInvisibleModel;
+1 -1
View File
@@ -60,7 +60,7 @@ public:
bool isFirst() { return fopAcM_GetParamBit(this, 0x1b, 1); }
void setAction(u8 action) { mAction = action; }
private:
// private:
/* 0x568 */ request_of_phase_process_class mPhaseReq;
/* 0x570 */ J3DModel* mpModel;
/* 0x574 */ mDoExt_btkAnm* mpBtkAnm[2];
+1 -1
View File
@@ -64,7 +64,7 @@ public:
current.pos = new_pos;
}
private:
// private:
/* 0x92C */ fpc_ProcID mItemId;
/* 0x930 */ int mCounter;
/* 0x934 */ u8 mStatus;
+1 -1
View File
@@ -51,7 +51,7 @@ public:
static DUSK_GAME_DATA daObjMasterSword_Attr_c const mAttr;
static DUSK_GAME_DATA actionFunc ActionTable[];
private:
// private:
/* 0x568 */ J3DModel* mpModel;
/* 0x56C */ request_of_phase_process_class mPhase;
/* 0x574 */ mDoExt_btkAnm mBtk;
+1 -1
View File
@@ -67,7 +67,7 @@ public:
u8 getModelType() { return fopAcM_GetParamBit(this, 8, 4); }
u8 getItemNo() { return fopAcM_GetParamBit(this, 0, 8); }
private:
// private:
/* 0x718 */ u8 mReturnRupee;
/* 0x71C */ request_of_phase_process_class mPhase;
/* 0x724 */ J3DModel* mpModel;
+1
View File
@@ -1291,6 +1291,7 @@ public:
}
void set(const char*, s8, s16, s8, s8, u8);
void offEnable() { enabled = 0; }
void onEnable() { enabled = 1; }
BOOL isEnable() const { return enabled; }
s8 getWipe() const { return wipe; }
u8 getWipeSpeed() const { return wipe_speed; }
+4
View File
@@ -83,11 +83,15 @@ extern void __dcbf(void*, int);
extern void __dcbz(void*, int);
extern void __sync();
extern int __abs(int);
#if defined(_MSVC_LANG) && !defined(__clang__)
#define __memcpy memcpy
#else
#if defined(__has_builtin) && __has_builtin(__builtin_memcpy)
#define __memcpy __builtin_memcpy
#else
#define __memcpy memcpy
#endif
#endif
#ifdef __cplusplus
}
#endif
@@ -7,6 +7,7 @@
#include <new>
#include <utility>
#include <cstdint>
#include <type_traits>
class JKRHeap;
typedef void (*JKRErrorHandler)(void*, u32, int);
-1
View File
@@ -17,5 +17,4 @@ add_mod(ao_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
BUNDLE
)
+15 -1
View File
@@ -18,6 +18,7 @@
#include "d/d_kankyo.h"
#include "d/d_kantera_icon_meter.h"
#include "d/d_menu_collect.h"
#include "d/d_menu_dmap.h"
#include "d/d_menu_fishing.h"
#include "d/d_menu_fmap2D.h"
#include "d/d_menu_insect.h"
@@ -345,7 +346,7 @@ void menu_skill_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A, B, and Z buttons on the map screen
// A, B, and Z buttons on the field map screen
DEFINE_HOOK(&dMenu_Fmap_c::_create, MenuFMapCreate);
void menu_fmap_create_post(ModContext*, void* args, void*, void*) {
auto menuFMap = mods::arg<dMenu_Fmap_c*>(args, 0);
@@ -356,6 +357,16 @@ void menu_fmap_create_post(ModContext*, void* args, void*, void*) {
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen);
}
// A and B buttons on the dungeon map screen
DEFINE_HOOK_SYMBOL("dMenu_DmapBg_c::buttonIconScreenInit", void(dMenu_DmapBg_c*), MenuDMapButtonIconScreenInit);
void menu_dmap_button_icon_screen_init_post(ModContext*, void* args, void*, void*) {
auto menuDMap = mods::arg<dMenu_DmapBg_c*>(args, 0);
auto screen = menuDMap->mButtonScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A button on the howling screen
DEFINE_HOOK(&dMsgObject_c::talkStartInit, MsgObjectTalkStartInit);
void msg_object_talk_start_init_post(ModContext*, void* args, void*, void*) {
@@ -685,6 +696,8 @@ ModResult add_all_hooks() {
ADD_POST_HOOK(OutFontCreatePane, out_font_create_pane_post, COutFont_c::createPane)
ADD_POST_HOOK(OutFontSetDrawFont, out_font_set_draw_font_post, COutFontSet_c::drawFont)
ADD_POST_HOOK(MenuFMapCreate, menu_fmap_create_post, dMenu_Fmap_c::_create)
ADD_POST_HOOK(MenuDMapButtonIconScreenInit, menu_dmap_button_icon_screen_init_post,
dMenu_DmapBg_c::buttonIconScreenInit)
ADD_POST_HOOK(
MsgObjectTalkStartInit, msg_object_talk_start_init_post, dMsgObject_c::talkStartInit)
ADD_POST_HOOK(MeterHakushaCreate, meter_hakusha_create_post, dMeterHakusha_c::_create)
@@ -724,6 +737,7 @@ ModResult remove_all_hooks() {
UNINSTALL_HOOK(OutFontCreatePane)
UNINSTALL_HOOK(OutFontSetDrawFont)
UNINSTALL_HOOK(MenuFMapCreate)
UNINSTALL_HOOK(MenuDMapButtonIconScreenInit)
UNINSTALL_HOOK(MsgObjectTalkStartInit)
UNINSTALL_HOOK(MeterHakushaCreate)
UNINSTALL_HOOK(ItemBaseCreateItemHeap)
+1
Submodule mods/randomizer added at 61ba8c4058
+1 -1
View File
@@ -4,7 +4,7 @@ This directory contains Dusklight's Android shell built on top of Borealis.
## Prerequisites
- Android SDK installed (`ANDROID_HOME`)
- Android SDK with Platform 37 installed (`ANDROID_HOME`)
- Android NDK version used by CMake presets (`ANDROID_NDK_VERSION`)
- JDK 17+
+1 -1
View File
@@ -1,3 +1,3 @@
plugins {
id 'com.android.application' version '8.13.2' apply false
id 'com.android.application' version '9.1.1' apply false
}
Binary file not shown.
+4 -3
View File
@@ -1,6 +1,7 @@
#Thu Nov 11 18:20:34 PST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+202 -114
View File
@@ -1,74 +1,128 @@
#!/usr/bin/env bash
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Attempt to set APP_HOME
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn ( ) {
warn () {
echo "$*"
}
} >&2
die ( ) {
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -77,84 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|grep -E -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|grep -E -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+46 -43
View File
@@ -1,4 +1,22 @@
@if "%DEBUG%" == "" @echo off
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -8,26 +26,30 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -35,54 +57,35 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
+14 -1
View File
@@ -115,6 +115,14 @@ mod-entry .mod-entry-status.failed {
color: #cc4444;
}
mod-entry .mod-entry-network {
margin-left: 6dp;
padding: 1dp 5dp;
border-radius: 5dp;
background-color: rgba(67, 151, 219, 20%);
color: #6fb7ef;
}
mod-entry .mod-entry-desc {
font-size: 14dp;
line-height: 1.3;
@@ -216,4 +224,9 @@ window.mods .mod-description {
.mod-info-label.failed {
color: #cc4444;
opacity: 1;
}
}
.status-badge.network {
color: #6fb7ef;
opacity: 1;
}
+3 -1
View File
@@ -10,7 +10,7 @@
#define ITEM_CHECK_FREESTANDING_PREFIX "freestanding:" /* <stage>:<bit> */
#define ITEM_CHECK_GOLDEN_WOLF_PREFIX "golden_wolf:" /* <event_flag> */
#define ITEM_CHECK_POE_PREFIX "poe:" /* <stage>:<switch> */
#define ITEM_CHECK_SHOP_PREFIX "shop:" /* <stage>:<item> */
#define ITEM_CHECK_SHOP_PREFIX "shop:" /* <stage>:<room>:<item> */
#define ITEM_CHECK_SKY_PREFIX "sky:" /* <stage>:<room> */
#define ITEM_CHECK_DUNGEON_REWARD_PREFIX "dungeon_reward:" /* <stage> */
@@ -21,6 +21,8 @@
#define ITEM_CHECK_BULBLIN_KEY "bulblin_key:D_MN09"
#define ITEM_CHECK_CORAL_EARRING "coral_earring"
#define ITEM_CHECK_CORO_BOTTLE "coro_bottle"
#define ITEM_CHECK_CORO_GATE_KEY "coro_gate_key"
#define ITEM_CHECK_CORO_LANTERN "coro_lantern"
#define ITEM_CHECK_DUNGEON_MAP_SNOWPEAK "dungeon_map:D_MN11"
#define ITEM_CHECK_FAIRY_REWARD "fairy_reward:D_SB01"
#define ITEM_CHECK_FISHING_BOTTLE "fishing_bottle"
+2 -2
View File
@@ -46,8 +46,8 @@ extern "C" const unsigned char mod_meta_bounds_end[] __asm("section$end$__DATA$_
#define MOD_META_BOUNDS_BEGIN (mod_meta_bounds_begin)
#define MOD_META_BOUNDS_END (mod_meta_bounds_end)
#else
extern "C" const unsigned char __start_modmeta[];
extern "C" const unsigned char __stop_modmeta[];
extern "C" __attribute__((visibility("hidden"))) const unsigned char __start_modmeta[];
extern "C" __attribute__((visibility("hidden"))) const unsigned char __stop_modmeta[];
#define MOD_META_BOUNDS_DEFN
#define MOD_META_BOUNDS_BEGIN (__start_modmeta)
#define MOD_META_BOUNDS_END (__stop_modmeta)
+118
View File
@@ -0,0 +1,118 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define FILE_SERVICE_ID "dev.twilitrealm.dusklight.file"
#define FILE_SERVICE_MAJOR 1u
#define FILE_SERVICE_MINOR 0u
typedef uint64_t FileStreamHandle;
typedef enum FileOpenMode {
FILE_OPEN_READ = 0,
FILE_OPEN_TRUNCATE = 1,
FILE_OPEN_APPEND = 2,
} FileOpenMode;
typedef struct FileFilter {
const char* name;
/* Semicolon-separated extensions or "*". */
const char* pattern;
} FileFilter;
typedef struct FilePickOptions {
uint32_t struct_size;
const FileFilter* filters;
uint32_t filter_count;
/* Optional previously returned location. */
const char* default_location;
} FilePickOptions;
#define FILE_PICK_OPTIONS_INIT {sizeof(FilePickOptions), NULL, 0u, NULL}
/* Locations and error are valid only for the duration of the callback. Canceled picks report
* MOD_UNAVAILABLE. The callback runs on the game thread. */
typedef void (*FilePickFn)(ModContext* ctx, ModResult status, const char* const* locations,
uint32_t location_count, const char* error, void* user_data);
typedef struct FileBuffer {
uint32_t struct_size;
void* data;
size_t size;
} FileBuffer;
#define FILE_BUFFER_INIT {sizeof(FileBuffer), NULL, 0u}
typedef struct FileEntry {
const char* name;
const char* location;
bool is_directory;
} FileEntry;
/* Called once per entry, then once with entry == NULL. Entries are valid only for the duration of
* the callback. */
typedef void (*FileListFn)(ModContext* ctx, const FileEntry* entry, void* user_data);
/*
* Access to user-selected files and folders.
*
* A location is an opaque UTF-8 string returned by `pick_*`, `export_file`, `join`, `create_child`.
* Save it and pass it back to the service. Never parse it or manually append path segments.
*
* Android: Security grants are restored across launches. Only 512 (or 128 before API 30) grants are
* allowed at a time. If a grant cannot be retained or was revoked, `check`/`open` returns
* MOD_UNAVAILABLE so the user can select the location again.
*
* Calls other than picker completion are synchronous and must run on the game thread. Avoid file
* reads and folder traversal in per-frame callbacks.
*/
typedef struct FileService {
ServiceHeader header;
/* One native dialog is allowed at a time. Returns MOD_CONFLICT while one is outstanding. */
ModResult (*pick_file)(
ModContext* ctx, const FilePickOptions* options, FilePickFn fn, void* user_data);
ModResult (*pick_folder)(
ModContext* ctx, const FilePickOptions* options, FilePickFn fn, void* user_data);
/* Exports an existing file to a user-chosen destination. fn receives its final location. */
ModResult (*export_file)(ModContext* ctx, const char* source_location,
const char* suggested_name, FilePickFn fn, void* user_data);
ModResult (*display_name)(
ModContext* ctx, const char* location, char* buffer, uint32_t buffer_size);
/* MOD_OK, MOD_UNAVAILABLE when gone or inaccessible, or MOD_UNSUPPORTED. */
ModResult (*check)(ModContext* ctx, const char* location);
/* Write modes require an existing writable location and return MOD_UNSUPPORTED otherwise. */
ModResult (*open)(
ModContext* ctx, const char* location, FileOpenMode mode, FileStreamHandle* out_handle);
ModResult (*size)(ModContext* ctx, FileStreamHandle handle, uint64_t* out_size);
ModResult (*read)(ModContext* ctx, FileStreamHandle handle, void* buffer, uint64_t length,
uint64_t* out_read);
ModResult (*write)(
ModContext* ctx, FileStreamHandle handle, const void* buffer, uint64_t length);
ModResult (*seek)(ModContext* ctx, FileStreamHandle handle, uint64_t offset);
ModResult (*flush)(ModContext* ctx, FileStreamHandle handle);
/* Reports flush and close failures; writers must check the result. */
ModResult (*close)(ModContext* ctx, FileStreamHandle handle);
ModResult (*read_all)(ModContext* ctx, const char* location, FileBuffer* out_buffer);
/* Truncates and writes an existing location. This operation is not atomic. */
ModResult (*write_all)(ModContext* ctx, const char* location, const void* data, size_t size);
void (*free)(ModContext* ctx, FileBuffer* buffer);
ModResult (*list)(ModContext* ctx, const char* folder_location, FileListFn fn, void* user_data);
/* out_location remains valid until this mod's next `join` or `create_child` call. */
ModResult (*join)(ModContext* ctx, const char* folder_location, const char* relative_path,
const char** out_location);
/* Creates one file without replacing an existing child. */
ModResult (*create_child)(
ModContext* ctx, const char* folder_location, const char* name, const char** out_location);
} FileService;
MOD_DECLARE_SERVICE(FileService, svc_file, FILE_SERVICE_ID, FILE_SERVICE_MAJOR, FILE_SERVICE_MINOR);
+321
View File
@@ -0,0 +1,321 @@
#pragma once
#include <mods/svc/file.h>
#include <array>
#include <cstdint>
#include <functional>
#include <span>
#include <string>
#include <utility>
#include <vector>
namespace mods::file {
class File {
public:
File() = default;
File(FileStreamHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
~File() { reset(); }
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&& other) noexcept { *this = std::move(other); }
File& operator=(File&& other) noexcept {
if (this != &other) {
reset();
mHandle = std::exchange(other.mHandle, 0);
mResult = other.mResult;
}
return *this;
}
explicit operator bool() const { return mResult == MOD_OK && mHandle != 0; }
ModResult result() const { return mResult; }
FileStreamHandle handle() const { return mHandle; }
uint64_t size() const {
uint64_t value = 0;
if (mHandle != 0 && svc_file != nullptr) {
svc_file->size(mod_ctx, mHandle, &value);
}
return value;
}
uint64_t read(void* buffer, uint64_t length) {
uint64_t value = 0;
if (mHandle == 0 || svc_file == nullptr) {
mResult = MOD_UNAVAILABLE;
} else {
mResult = svc_file->read(mod_ctx, mHandle, buffer, length, &value);
}
return value;
}
bool seek(uint64_t offset) {
mResult = mHandle != 0 && svc_file != nullptr ? svc_file->seek(mod_ctx, mHandle, offset) :
MOD_UNAVAILABLE;
return mResult == MOD_OK;
}
bool write(const void* buffer, uint64_t length) {
if (mHandle == 0 || svc_file == nullptr) {
mResult = MOD_UNAVAILABLE;
} else {
mResult = svc_file->write(mod_ctx, mHandle, buffer, length);
}
return mResult == MOD_OK;
}
bool write(std::span<const uint8_t> bytes) { return write(bytes.data(), bytes.size()); }
bool flush() {
if (mHandle == 0 || svc_file == nullptr) {
mResult = MOD_UNAVAILABLE;
} else {
mResult = svc_file->flush(mod_ctx, mHandle);
}
return mResult == MOD_OK;
}
bool close() {
if (mHandle == 0) {
return mResult == MOD_OK;
}
mResult = svc_file != nullptr ? svc_file->close(mod_ctx, mHandle) : MOD_UNAVAILABLE;
mHandle = 0;
return mResult == MOD_OK;
}
void reset() { (void)close(); }
private:
FileStreamHandle mHandle = 0;
ModResult mResult = MOD_UNAVAILABLE;
};
inline File open(const std::string& location, FileOpenMode mode = FILE_OPEN_READ) {
if (svc_file == nullptr) {
return {0, MOD_UNAVAILABLE};
}
FileStreamHandle handle = 0;
const auto result = svc_file->open(mod_ctx, location.c_str(), mode, &handle);
return {handle, result};
}
class Buffer {
public:
Buffer() = default;
Buffer(FileBuffer buffer, ModResult result) : mBuffer{buffer}, mResult{result} {}
~Buffer() { reset(); }
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
Buffer(Buffer&& other) noexcept { *this = std::move(other); }
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
reset();
mBuffer = other.mBuffer;
mResult = other.mResult;
other.mBuffer = FILE_BUFFER_INIT;
}
return *this;
}
explicit operator bool() const { return mResult == MOD_OK; }
ModResult result() const { return mResult; }
std::span<const uint8_t> bytes() const {
return {static_cast<const uint8_t*>(mBuffer.data), mBuffer.size};
}
void reset() {
if (mBuffer.data != nullptr && svc_file != nullptr) {
svc_file->free(mod_ctx, &mBuffer);
}
mBuffer.data = nullptr;
mBuffer.size = 0;
}
private:
FileBuffer mBuffer = FILE_BUFFER_INIT;
ModResult mResult = MOD_UNAVAILABLE;
};
inline Buffer read_all(const std::string& location) {
FileBuffer buffer = FILE_BUFFER_INIT;
const auto result = svc_file != nullptr ?
svc_file->read_all(mod_ctx, location.c_str(), &buffer) :
MOD_UNAVAILABLE;
return {buffer, result};
}
inline ModResult check(const std::string& location) {
return svc_file != nullptr ? svc_file->check(mod_ctx, location.c_str()) : MOD_UNAVAILABLE;
}
inline std::string display_name(const std::string& location) {
if (svc_file == nullptr) {
return {};
}
std::array<char, 1024> buffer{};
return svc_file->display_name(mod_ctx, location.c_str(), buffer.data(),
static_cast<uint32_t>(buffer.size())) == MOD_OK ?
std::string{buffer.data()} :
std::string{};
}
inline ModResult join(
const std::string& folder, const std::string& relativePath, std::string& outLocation) {
outLocation.clear();
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
const char* location = nullptr;
const auto result = svc_file->join(mod_ctx, folder.c_str(), relativePath.c_str(), &location);
if (result == MOD_OK && location != nullptr) {
outLocation = location;
}
return result;
}
inline ModResult create_child(
const std::string& folder, const std::string& name, std::string& outLocation) {
outLocation.clear();
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
const char* location = nullptr;
const auto result = svc_file->create_child(mod_ctx, folder.c_str(), name.c_str(), &location);
if (result == MOD_OK && location != nullptr) {
outLocation = location;
}
return result;
}
inline ModResult write_all(const std::string& location, std::span<const uint8_t> bytes) {
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
return svc_file->write_all(mod_ctx, location.c_str(), bytes.data(), bytes.size());
}
struct Entry {
std::string name;
std::string location;
bool isDirectory = false;
};
inline ModResult list(const std::string& folder, std::vector<Entry>& outEntries) {
outEntries.clear();
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
return svc_file->list(
mod_ctx, folder.c_str(),
[](ModContext*, const FileEntry* entry, void* userData) {
if (entry == nullptr) {
return;
}
static_cast<std::vector<Entry>*>(userData)->push_back({
.name = entry->name != nullptr ? entry->name : "",
.location = entry->location != nullptr ? entry->location : "",
.isDirectory = entry->is_directory,
});
},
&outEntries);
}
struct Filter {
std::string name;
std::string pattern;
};
struct PickOptions {
std::vector<Filter> filters;
std::string defaultLocation;
};
struct PickResult {
ModResult status = MOD_ERROR;
std::vector<std::string> locations;
std::string error;
};
namespace detail {
inline std::function<void(PickResult)> pickCallback;
inline void pick_trampoline(ModContext*, ModResult status, const char* const* locations,
uint32_t locationCount, const char* error, void*) {
PickResult result{.status = status, .error = error != nullptr ? error : ""};
result.locations.reserve(locationCount);
for (uint32_t i = 0; i < locationCount; ++i) {
if (locations[i] != nullptr) {
result.locations.emplace_back(locations[i]);
}
}
auto callback = std::move(pickCallback);
pickCallback = {};
if (callback) {
callback(std::move(result));
}
}
inline ModResult pick(
const PickOptions& options, std::function<void(PickResult)> callback, bool folder) {
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
if (!callback) {
return MOD_INVALID_ARGUMENT;
}
if (pickCallback) {
return MOD_CONFLICT;
}
std::vector<FileFilter> filters;
filters.reserve(options.filters.size());
for (const auto& filter : options.filters) {
filters.push_back({filter.name.c_str(), filter.pattern.c_str()});
}
FilePickOptions raw = FILE_PICK_OPTIONS_INIT;
raw.filters = filters.empty() ? nullptr : filters.data();
raw.filter_count = static_cast<uint32_t>(filters.size());
raw.default_location =
options.defaultLocation.empty() ? nullptr : options.defaultLocation.c_str();
pickCallback = std::move(callback);
const auto result = folder ? svc_file->pick_folder(mod_ctx, &raw, pick_trampoline, nullptr) :
svc_file->pick_file(mod_ctx, &raw, pick_trampoline, nullptr);
if (result != MOD_OK) {
pickCallback = {};
}
return result;
}
} // namespace detail
inline ModResult pick_file(const PickOptions& options, std::function<void(PickResult)> callback) {
return detail::pick(options, std::move(callback), false);
}
inline ModResult pick_folder(const PickOptions& options, std::function<void(PickResult)> callback) {
return detail::pick(options, std::move(callback), true);
}
inline ModResult export_file(const std::string& sourceLocation, const std::string& suggestedName,
std::function<void(PickResult)> callback) {
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
if (!callback) {
return MOD_INVALID_ARGUMENT;
}
if (detail::pickCallback) {
return MOD_CONFLICT;
}
detail::pickCallback = std::move(callback);
const auto result = svc_file->export_file(
mod_ctx, sourceLocation.c_str(), suggestedName.c_str(), detail::pick_trampoline, nullptr);
if (result != MOD_OK) {
detail::pickCallback = {};
}
return result;
}
} // namespace mods::file
+98
View File
@@ -0,0 +1,98 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define HTTP_SERVICE_ID "dev.twilitrealm.dusklight.http"
#define HTTP_SERVICE_MAJOR 1u
#define HTTP_SERVICE_MINOR 0u
/* Handle for an in-flight request. 0 is never a valid handle. */
typedef uint64_t HttpRequestHandle;
typedef enum HttpMethod {
HTTP_METHOD_GET = 0,
HTTP_METHOD_POST = 1,
HTTP_METHOD_HEAD = 2,
} HttpMethod;
/* Transport-level outcome. HTTP status errors are reported through status_code. */
typedef enum HttpError {
HTTP_ERROR_NONE = 0,
HTTP_ERROR_INVALID_URL = 1,
HTTP_ERROR_UNSUPPORTED_SCHEME = 2,
HTTP_ERROR_TIMEOUT = 3,
HTTP_ERROR_TOO_LARGE = 4,
HTTP_ERROR_CANCELED = 5,
HTTP_ERROR_IO = 6,
HTTP_ERROR_NETWORK = 7,
} HttpError;
typedef struct HttpHeader {
const char* name;
const char* value;
} HttpHeader;
typedef struct HttpRequestDesc {
uint32_t struct_size;
HttpMethod method;
const char* url;
const HttpHeader* headers;
uint32_t header_count;
/* Request body; POST only. */
const void* body;
size_t body_size;
/* Absolute destination under this mod's data_dir or mod_dir, or NULL for an in-memory body.
* GET and POST only. */
const char* download_path;
uint32_t connect_timeout_ms; /* 0 = 10 seconds */
uint32_t idle_timeout_ms; /* 0 = 10 seconds without network progress */
uint32_t total_timeout_ms; /* 0 = no total timeout */
size_t max_body_bytes; /* 0 = 1 MiB; ignored for downloads */
} HttpRequestDesc;
#define HTTP_REQUEST_DESC_INIT \
{sizeof(HttpRequestDesc), HTTP_METHOD_GET, NULL, NULL, 0u, NULL, 0u, NULL, 0u, 0u, 0u, 0u}
/* Snapshot valid only for the duration of the completion callback. */
typedef struct HttpResult {
uint32_t struct_size;
HttpError error;
const char* error_message;
int32_t status_code;
const HttpHeader* headers;
uint32_t header_count;
const void* body;
size_t body_size;
/* Published absolute destination, or NULL unless a download succeeded. */
const char* download_path;
} HttpResult;
/* Runs on the game thread exactly once, unless the calling mod begins deactivation first. */
typedef void (*HttpCompleteFn)(
ModContext* ctx, HttpRequestHandle request, const HttpResult* result, void* user_data);
typedef struct HttpProgress {
uint32_t struct_size;
uint64_t completed_bytes;
uint64_t total_bytes;
bool total_known;
} HttpProgress;
#define HTTP_PROGRESS_INIT {sizeof(HttpProgress), 0u, 0u, false}
typedef struct HttpService {
ServiceHeader header;
/* Starts an asynchronous HTTPS request. */
ModResult (*request)(ModContext* ctx, const HttpRequestDesc* desc, HttpCompleteFn fn,
void* user_data, HttpRequestHandle* out_handle);
ModResult (*progress)(ModContext* ctx, HttpRequestHandle request, HttpProgress* out_progress);
/* Requests cancellation. The completion callback still runs if the mod remains active. */
ModResult (*cancel)(ModContext* ctx, HttpRequestHandle request);
} HttpService;
MOD_DECLARE_SERVICE(HttpService, svc_http, HTTP_SERVICE_ID, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR);
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include <mods/svc/http.h>
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace mods::http {
struct Header {
std::string name;
std::string value;
};
struct Request {
HttpMethod method = HTTP_METHOD_GET;
std::string url;
std::vector<Header> headers;
std::string body;
std::string downloadPath;
uint32_t connectTimeoutMs = 0;
uint32_t idleTimeoutMs = 0;
uint32_t totalTimeoutMs = 0;
size_t maxBodyBytes = 0;
};
struct Response {
HttpError error = HTTP_ERROR_NETWORK;
std::string errorMessage;
int statusCode = 0;
std::vector<Header> headers;
std::vector<uint8_t> body;
std::string downloadPath;
bool ok() const { return error == HTTP_ERROR_NONE && statusCode >= 200 && statusCode < 300; }
const std::string* header(std::string_view name) const {
const auto equal = [](std::string_view left, std::string_view right) {
return left.size() == right.size() &&
std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
};
const auto iter = std::find_if(headers.begin(), headers.end(),
[&](const Header& value) { return equal(value.name, name); });
return iter != headers.end() ? &iter->value : nullptr;
}
};
namespace detail {
struct Completion {
HttpRequestHandle handle = 0;
std::function<void(Response)> callback;
};
inline std::unordered_map<HttpRequestHandle, std::unique_ptr<Completion>> completions;
inline void complete(ModContext*, HttpRequestHandle handle, const HttpResult* raw, void* userData) {
const auto iter = completions.find(handle);
if (iter == completions.end() || iter->second.get() != userData) {
return;
}
auto completion = std::move(iter->second);
completions.erase(iter);
Response response;
if (raw != nullptr) {
response.error = raw->error;
response.errorMessage = raw->error_message != nullptr ? raw->error_message : "";
response.statusCode = raw->status_code;
response.headers.reserve(raw->header_count);
for (uint32_t i = 0; i < raw->header_count; ++i) {
response.headers.push_back({
.name = raw->headers[i].name != nullptr ? raw->headers[i].name : "",
.value = raw->headers[i].value != nullptr ? raw->headers[i].value : "",
});
}
if (raw->body != nullptr && raw->body_size != 0) {
const auto* begin = static_cast<const uint8_t*>(raw->body);
response.body.assign(begin, begin + raw->body_size);
}
response.downloadPath = raw->download_path != nullptr ? raw->download_path : "";
}
if (completion->callback) {
completion->callback(std::move(response));
}
}
} // namespace detail
class Pending {
public:
Pending() = default;
Pending(HttpRequestHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
~Pending() { reset(); }
Pending(const Pending&) = delete;
Pending& operator=(const Pending&) = delete;
Pending(Pending&& other) noexcept { *this = std::move(other); }
Pending& operator=(Pending&& other) noexcept {
if (this != &other) {
reset();
mHandle = std::exchange(other.mHandle, 0);
mResult = other.mResult;
}
return *this;
}
explicit operator bool() const {
if (mResult != MOD_OK || mHandle == 0 || svc_http == nullptr ||
!detail::completions.contains(mHandle))
{
return false;
}
HttpProgress value = HTTP_PROGRESS_INIT;
return svc_http->progress(mod_ctx, mHandle, &value) == MOD_OK;
}
ModResult result() const { return mResult; }
HttpRequestHandle handle() const { return mHandle; }
HttpProgress progress() const {
HttpProgress value = HTTP_PROGRESS_INIT;
if (mHandle != 0 && svc_http != nullptr) {
svc_http->progress(mod_ctx, mHandle, &value);
}
return value;
}
void cancel() {
if (mHandle != 0 && svc_http != nullptr) {
if (svc_http->cancel(mod_ctx, mHandle) != MOD_OK) {
detail::completions.erase(mHandle);
}
}
}
void detach() { mHandle = 0; }
private:
void reset() {
cancel();
mHandle = 0;
}
HttpRequestHandle mHandle = 0;
ModResult mResult = MOD_UNAVAILABLE;
};
inline Pending request(const Request& request, std::function<void(Response)> callback) {
if (svc_http == nullptr) {
return {0, MOD_UNAVAILABLE};
}
if (!callback) {
return {0, MOD_INVALID_ARGUMENT};
}
if (request.headers.size() > std::numeric_limits<uint32_t>::max()) {
return {0, MOD_INVALID_ARGUMENT};
}
std::vector<HttpHeader> headers;
headers.reserve(request.headers.size());
for (const auto& header : request.headers) {
headers.push_back({
.name = header.name.c_str(),
.value = header.value.c_str(),
});
}
HttpRequestDesc desc = HTTP_REQUEST_DESC_INIT;
desc.method = request.method;
desc.url = request.url.c_str();
desc.headers = headers.empty() ? nullptr : headers.data();
desc.header_count = static_cast<uint32_t>(headers.size());
desc.body = request.body.empty() ? nullptr : request.body.data();
desc.body_size = request.body.size();
desc.download_path = request.downloadPath.empty() ? nullptr : request.downloadPath.c_str();
desc.connect_timeout_ms = request.connectTimeoutMs;
desc.idle_timeout_ms = request.idleTimeoutMs;
desc.total_timeout_ms = request.totalTimeoutMs;
desc.max_body_bytes = request.maxBodyBytes;
auto completion = std::make_unique<detail::Completion>();
auto* userData = completion.get();
completion->callback = std::move(callback);
HttpRequestHandle handle = 0;
const auto result = svc_http->request(mod_ctx, &desc, detail::complete, userData, &handle);
if (result != MOD_OK) {
return {0, result};
}
completion->handle = handle;
detail::completions.emplace(handle, std::move(completion));
return {handle, result};
}
} // namespace mods::http
+10 -1
View File
@@ -8,7 +8,7 @@
#define ITEM_SERVICE_ID "dev.twilitrealm.dusklight.item"
#define ITEM_SERVICE_MAJOR 2u
#define ITEM_SERVICE_MINOR 1u
#define ITEM_SERVICE_MINOR 3u
/* 0 is never a valid handle. */
typedef uint64_t ItemCheckHandle;
@@ -32,11 +32,13 @@ typedef struct ItemCheckInfo {
uint8_t vanilla_item;
uint8_t current_item;
uint8_t current_display_item;
bool was_resolved;
} ItemCheckInfo;
typedef struct ItemCheckResolution {
uint8_t item;
uint8_t display_item; /* leave unset (0xFF/NONE) to use the item */
bool was_resolved;
} ItemCheckResolution;
/* Return true and write out_result to replace the result, or false to leave it unchanged. */
@@ -97,6 +99,13 @@ typedef struct ItemService {
ModContext* ctx, ItemGiveObserveFn fn, void* user_data, ItemGiveHandle* out_handle);
ModResult (*unobserve_gives)(ModContext* ctx, ItemGiveHandle handle);
/* Minor version 2 */
/* Resolve a live preview (item + display item) without granting an item or notifying give
* observers. */
ModResult (*resolve_check_full)(ModContext* ctx, const char* name, uint8_t vanilla_item,
ItemCheckResolution* out_resolution);
} ItemService;
MOD_DECLARE_SERVICE(ItemService, svc_item, ITEM_SERVICE_ID, ITEM_SERVICE_MAJOR, ITEM_SERVICE_MINOR);
+14 -7
View File
@@ -2,6 +2,7 @@
#include <mods/api.h>
#include <mods/svc/config.h>
#include <mods/svc/file.h>
#ifdef __cplusplus
#include <mods/service.hpp>
@@ -9,7 +10,7 @@
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
#define UI_SERVICE_MAJOR 2u
#define UI_SERVICE_MINOR 1u
#define UI_SERVICE_MINOR 2u
/*
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts,
@@ -53,6 +54,7 @@ typedef enum UiControlKind {
UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */
UI_CONTROL_COLOR = 5, /* RGB/RGBA color string with a picker */
UI_CONTROL_GROUP = 6, /* navigation row (on_pressed) */
UI_CONTROL_FILE_PICKER = 7, /* file/folder picker with an opaque string location */
} UiControlKind;
typedef enum UiControlBinding {
@@ -62,7 +64,8 @@ typedef enum UiControlBinding {
/* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod)
* directly: persistence, change notifications and the modified indicator (value != default) are
* wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and
* SELECT = int, STRING and COLOR = string. Float vars are not bindable; use callbacks. */
* SELECT = int, STRING, COLOR and FILE_PICKER = string. Float vars are not bindable; use
* callbacks. */
UI_BINDING_CONFIG_VAR = 1,
} UiControlBinding;
@@ -71,10 +74,10 @@ typedef enum UiStringSetMode {
UI_STRING_SET_ON_CHANGE = 1, /* invokes `set` on every text change (e.g. while typing) */
} UiStringSetMode;
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
* and COLOR read string_value. string_value passed to a setter is only valid during the call; a
* getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid until
* the next call into the mod the host copies it right after the getter returns. */
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING,
* COLOR and FILE_PICKER read string_value. string_value passed to a setter is only valid during the
* call; a getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid
* until the next call into the mod. The host copies it right after the getter returns. */
typedef struct UiControlValue {
uint32_t struct_size;
bool bool_value;
@@ -128,12 +131,16 @@ typedef struct UiControlDesc {
bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */
UiPredicateFn is_selected; /* BUTTON/GROUP: optional selected state */
UiStringSetMode string_set_mode; /* STRING: when to invoke the setter */
/* FILE_PICKER: optional file filters and folder selection mode. */
const FileFilter* file_filters;
size_t file_filter_count;
bool directory_mode;
} UiControlDesc;
#define UI_CONTROL_DESC_INIT \
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false, NULL, \
UI_STRING_SET_ON_COMMIT}
UI_STRING_SET_ON_COMMIT, NULL, 0u, false}
typedef uint64_t UiListHandle;
+3 -2
View File
@@ -157,10 +157,11 @@ f32 daAlink_c::damageMagnification(BOOL i_checkZoraMag, int param_1) {
}
#if TARGET_PC
base_mag *= dusk::getSettings().game.damageMultiplier;
if (dusk::getSettings().game.instantDeath) {
base_mag = 9999.0f;
return 9999.0f; // don't need to multiply this further, 9999 is plenty
}
base_mag *= dusk::getSettings().game.damageMultiplier;
#endif
if (checkWolf() && !checkCargoCarry() && param_1 == 0) {
+2 -2
View File
@@ -462,7 +462,7 @@ int daDitem_c::create() {
m_itemNo = daDitem_prm::getNo(this);
#if TARGET_PC
const auto [item, displayItem] = dusk::mods::item_check_resolve(mItemGiveTag, m_itemNo, this);
const auto [item, displayItem, _] = dusk::mods::item_check_resolve(mItemGiveTag, m_itemNo, this);
m_itemNo = item;
setDisplayItemNo(displayItem);
const char* arc_name = dItem_data::getArcName(displayItem);
@@ -550,7 +550,7 @@ static int daDitem_Execute(daDitem_c* i_this) {
}
int daDitem_c::draw() {
switch (m_itemNo) {
switch (DUSK_IF_ELSE(getDisplayItemNo(), m_itemNo)) {
case dItemNo_WOOD_STICK_e:
draw_WOOD_STICK();
break;
+3
View File
@@ -7,6 +7,9 @@
#include "d/actor/d_a_e_po.h"
#include "d/actor/d_a_obj_poFire.h"
#if TARGET_PC
#include "d/actor/d_a_alink.h"
#endif
#include "d/d_cc_d.h"
#include "d/d_cc_uty.h"
#include "f_op/f_op_actor_enemy.h"
+42 -4
View File
@@ -4022,16 +4022,53 @@ BOOL daNpc_grA_c::talk(void*) {
if (r26 && talkProc(NULL, TRUE, NULL)) {
if (mFlow.getEventId(&sp8) == 1) {
#if TARGET_PC
const u8 originalItem = sp8;
u32 itemGiveTag = 0;
if (sp8 == dItemNo_BOMB_IN_BAG_e) {
bool isItemCheck = false;
if (originalItem == dItemNo_BOMB_IN_BAG_e) {
const auto itemCheck =
dusk::mods::item_check_commit("goron_reward:F_SP113", sp8, this);
dusk::mods::item_check_commit("goron_reward:F_SP113", originalItem, this);
sp8 = itemCheck.itemNo;
itemGiveTag = itemCheck.tag;
isItemCheck = true;
} else {
const char* stage = dComIfGp_getStartStageName();
const bool isAdultGoronShop = (strcmp(stage, "R_SP160") == 0 && originalItem == dItemNo_HYLIA_SHIELD_e) ||
(strcmp(stage, "F_SP116") == 0 && originalItem == dItemNo_ARROW_30_e);
if (isAdultGoronShop) {
const auto itemCheck = dusk::mods::item_check_commit(
dusk::mods::item_give_tag_shop(originalItem), originalItem, this);
sp8 = itemCheck.itemNo;
itemGiveTag = itemCheck.tag;
isItemCheck = true;
}
}
#endif
if (isItemCheck && sp8 == dItemNo_NONE_e) {
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
r29 = 1;
if (mType == 0xb) {
field_0x1691 = 1;
}
} else {
field_0x1480 = fopAcM_createItemForPresentDemo(
&current.pos, sp8, 0, -1, -1, NULL, NULL, itemGiveTag);
if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) {
s16 r25 =
dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff);
dComIfGp_getEvent()->reset(this);
fopAcM_orderChangeEventId(this, r25, 1, -1);
field_0x9ec = 1;
r29 = 1;
mOrderNewEvt = 1;
if (mType == 0xb) {
field_0x1691 = 1;
}
}
}
#else
field_0x1480 = fopAcM_createItemForPresentDemo(&current.pos, sp8, 0, -1, -1, NULL,
NULL IF_DUSK_ARG(itemGiveTag));
NULL);
if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) {
s16 r25 = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff);
dComIfGp_getEvent()->reset(this);
@@ -4043,6 +4080,7 @@ BOOL daNpc_grA_c::talk(void*) {
field_0x1691 = 1;
}
}
#endif
} else {
if (mType == 0xa && field_0x1486 == 0 && daNpcF_chkEvtBit(0x187)) {
dComIfGp_getEvent()->reset(this);
+23 -1
View File
@@ -8,6 +8,9 @@
#include "d/actor/d_a_npc_grc.h"
#include "d/actor/d_a_npc.h"
#include "Z2AudioLib/Z2Instances.h"
#if TARGET_PC
#include "mods/items.h"
#endif
#include <cstring>
enum grC_RES_File_ID {
@@ -1476,8 +1479,27 @@ BOOL daNpc_grC_c::talk(void* param_1) {
rv = TRUE;
if (mFlow.getEventId(&i_itemNo) == 1) {
mItemID = fopAcM_createItemForPresentDemo(&current.pos, i_itemNo, 0, -1, -1, NULL, NULL);
#if TARGET_PC
const bool isCastleTownShop =
strcmp(dComIfGp_getStartStageName(), "R_SP160") == 0 &&
(i_itemNo == dItemNo_RED_BOTTLE_e || i_itemNo == dItemNo_OIL_BOTTLE_e);
u32 itemGiveTag = 0;
if (isCastleTownShop) {
const auto itemCheck = dusk::mods::item_check_commit(
dusk::mods::item_give_tag_shop(i_itemNo), i_itemNo, this);
i_itemNo = itemCheck.itemNo;
itemGiveTag = itemCheck.tag;
}
if (isCastleTownShop && i_itemNo == dItemNo_NONE_e) {
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
} else {
mItemID = fopAcM_createItemForPresentDemo(
&current.pos, i_itemNo, 0, -1, -1, NULL, NULL, itemGiveTag);
}
#else
mItemID = fopAcM_createItemForPresentDemo(&current.pos, i_itemNo, 0, -1, -1, NULL, NULL);
#endif
if (mItemID != fpcM_ERROR_PROCESS_ID_e) {
s16 i_eventID = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xFF);
dComIfGp_getEvent()->reset(this);
+32 -6
View File
@@ -7,6 +7,9 @@
#include "d/actor/d_a_npc_kkri.h"
#include "d/actor/d_a_e_ym.h"
#if TARGET_PC
#include "mods/items.h"
#endif
#include <cstring>
static DUSK_CONSTEXPR int l_bmdData[2][2] = {
@@ -1183,16 +1186,39 @@ int daNpc_Kkri_c::talk(void*) {
if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) {
#if TARGET_PC
u32 itemGiveTag = 0;
if (item_no == dItemNo_OIL_BOTTLE3_e) {
const auto itemCheck =
dusk::mods::item_check_commit("coro_bottle", item_no, this);
const char* itemCheckName = nullptr;
switch (item_no) {
case dItemNo_OIL_BOTTLE3_e:
itemCheckName = ITEM_CHECK_CORO_BOTTLE;
break;
case dItemNo_KANTERA_e:
itemCheckName = ITEM_CHECK_CORO_LANTERN;
break;
case dItemNo_KEY_OF_FILONE_e:
itemCheckName = ITEM_CHECK_CORO_GATE_KEY;
break;
}
if (itemCheckName != nullptr) {
const auto itemCheck = dusk::mods::item_check_commit(
itemCheckName, item_no, this);
item_no = itemCheck.itemNo;
itemGiveTag = itemCheck.tag;
}
if (item_no == dItemNo_NONE_e && itemCheckName != nullptr) {
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
field_0xfd5 = 1;
mEvtNo = 1;
evtChange();
} else {
mItemPartnerId = fopAcM_createItemForPresentDemo(
&current.pos, item_no, 0, -1, -1, NULL, NULL, itemGiveTag);
}
#else
mItemPartnerId = fopAcM_createItemForPresentDemo(
&current.pos, item_no, 0, -1, -1, NULL, NULL);
#endif
mItemPartnerId = fopAcM_createItemForPresentDemo(&current.pos, item_no,
0, -1, -1, NULL,
NULL IF_DUSK_ARG(itemGiveTag));
}
if (fopAcM_IsExecuting(mItemPartnerId)) {
+1 -8
View File
@@ -268,7 +268,7 @@ int daItem_c::_daItem_create() {
const u32 params = fopAcM_GetParam(this);
mOriginalItemNo = params & 0xFF;
mItemGiveTag = dusk::mods::item_give_tag_freestanding(daItem_prm::getItemBitNo(this));
const auto [item, displayItem] =
const auto [item, displayItem, _] =
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
mItemOverridden = item != mOriginalItemNo;
setDisplayItemNo(displayItem);
@@ -935,10 +935,6 @@ void daItem_c::itemGet() {
mItemOverridden = m_itemNo != mOriginalItemNo;
#endif
switch (m_itemNo) {
#if TARGET_PC
case dItemNo_UTAWA_HEART_e:
case dItemNo_KAKERA_HEART_e:
#endif
case dItemNo_HEART_e:
mDoAud_seStart(Z2SE_HEART_PIECE_GET, NULL, 0, 0);
execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this));
@@ -980,9 +976,6 @@ void daItem_c::itemGet() {
case dItemNo_PACHINKO_SHOT_e:
mDoAud_seStart(Z2SE_CONSUMP_ITEM_GET, NULL, 0, 0);
execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this));
#if TARGET_PC
break;
#endif
default:
#if TARGET_PC
if (mItemOverridden) {
+13 -4
View File
@@ -99,6 +99,15 @@ int daObjLife_c::Create() {
mRotateSpeed = 7000;
#if TARGET_PC
// Restore speeds that were skipped if created from a boss with create instead of fastCreate
auto bossItemSpeeds = dusk::mods::get_boss_item_actor_speeds();
if (bossItemSpeeds.f != 0.f || bossItemSpeeds.y != 0.f) {
mOverrideHover = false;
speedF = bossItemSpeeds.f;
speed.y = bossItemSpeeds.y;
dusk::mods::set_boss_item_actor_speeds(0.f, 0.f);
}
if (mOverrideHover) {
fopAcM_SetGravity(this, 0.0f);
mRotateSpeed = 550;
@@ -153,20 +162,20 @@ int daObjLife_c::create() {
if (mItemGiveOriginalNo == dItemNo_NONE_e) {
mOriginalItemNo = parameterItemNo;
mItemGiveTag = dusk::mods::item_give_tag_freestanding(getSaveBitNo());
const auto [item, displayItem] =
const auto [item, displayItem, was_resolved] =
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
setDisplayItemNo(displayItem);
mItemOverridden = item != mOriginalItemNo;
mItemOverridden = was_resolved;
if (mItemOverridden) {
fopAcM_SetParam(this, (params & 0xFFFFFF00) | item);
}
} else if (mGoldenWolfItem) {
mOriginalItemNo = mItemGiveOriginalNo;
mItemGiveTag = dusk::mods::item_give_tag_golden_wolf(static_cast<u16>(field_0x938));
const auto [item, displayItem] =
const auto [item, displayItem, was_resolved] =
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
setDisplayItemNo(displayItem);
mItemOverridden = item != mOriginalItemNo;
mItemOverridden = was_resolved;
if (item != parameterItemNo) {
fopAcM_SetParam(this, (params & 0xFFFFFF00) | item);
}
+3 -8
View File
@@ -151,11 +151,7 @@ void daObjMasterSword_c::create_init() {
int daObjMasterSword_c::create() {
fopAcM_ct(this, daObjMasterSword_c);
#if TARGET_PC
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0264)) {
#else
if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()])) {
#endif
return cPhs_ERROR_e;
}
@@ -199,7 +195,7 @@ int daObjMasterSword_c::execute() {
#if TARGET_PC
const auto masterSword = dusk::mods::item_check_commit(
ITEM_CHECK_MASTER_SWORD, dItemNo_MASTER_SWORD_e, this);
if (masterSword.itemNo == dItemNo_MASTER_SWORD_e) {
if (!masterSword.was_resolved) {
dComIfGs_onItemFirstBit(dItemNo_MASTER_SWORD_e);
dMeter2Info_setSword(dItemNo_MASTER_SWORD_e, false);
dComIfGs_setSelectEquipSword(dItemNo_MASTER_SWORD_e);
@@ -214,7 +210,7 @@ int daObjMasterSword_c::execute() {
const auto shadowCrystal = dusk::mods::item_check_commit(
ITEM_CHECK_SHADOW_CRYSTAL, dItemNo_SHADOW_CRYSTAL_e, this);
if (shadowCrystal.itemNo == dItemNo_SHADOW_CRYSTAL_e) {
if (!shadowCrystal.was_resolved) {
execItemGet(shadowCrystal.itemNo, shadowCrystal.tag, this);
} else if (shadowCrystal.itemNo == dItemNo_NONE_e) {
dusk::mods::item_check_complete(shadowCrystal, this);
@@ -222,15 +218,14 @@ int daObjMasterSword_c::execute() {
dusk::mods::item_check_enqueue(shadowCrystal, dusk::mods::ItemGiveMode::Demo);
}
dComIfGs_onEventBit(dSv_event_flag_c::F_0264);
#else
dComIfGs_onItemFirstBit(dItemNo_MASTER_SWORD_e);
dMeter2Info_setSword(dItemNo_MASTER_SWORD_e, false);
dComIfGs_setSelectEquipSword(dItemNo_MASTER_SWORD_e);
dComIfGp_setItemLifeCount(dComIfGs_getMaxLife(), 0);
dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()]);
#endif
dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()]);
fopAcM_delete(this);
}
+1 -1
View File
@@ -157,7 +157,7 @@ int daKey_c::create() {
#if TARGET_PC
mItemGiveTag = dusk::mods::item_give_tag_freestanding(getSaveBitNo());
const auto [item, displayItem] =
const auto [item, displayItem, _] =
dusk::mods::item_check_resolve(mItemGiveTag, dItemNo_SMALL_KEY_e, this);
m_itemNo = item;
if (m_itemNo == dItemNo_NONE_e) {
+2 -1
View File
@@ -106,8 +106,9 @@ const char* daShopItem_c::getShopArcname() {
if (m_itemNo != dItemNo_NONE_e && mItemGiveOriginalNo == dItemNo_NONE_e) {
mItemGiveOriginalNo = m_itemNo;
mItemGiveTag = dusk::mods::item_give_tag_shop(mItemGiveOriginalNo);
const auto [_, displayItem] =
const auto [_, displayItem, was_resolved] =
dusk::mods::item_check_resolve(mItemGiveTag, mItemGiveOriginalNo, this);
setDisplayItemNo(displayItem);
mItemOverridden = displayItem != mItemGiveOriginalNo;
if (mItemOverridden) {
mOverrideData = mData[mShopItemID];
+8
View File
@@ -191,6 +191,14 @@ void dBrightCheck_c::modeMove() {
mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0);
#ifdef TARGET_PC
toggleAutoSave(true);
if (!dusk::getSettings().game.hideTvSettingsScreen) {
const dusk::gamemode::GameMode* gameMode =
dusk::gamemode::getGameModeManager().getCurrentGameMode();
if (gameMode) {
gameMode->invokeOnSaveLoadedFunction();
}
}
#endif
mCompleteCheck = true;
mMode = MODE_WAIT_e;
+8 -2
View File
@@ -1789,10 +1789,16 @@ void dFile_select_c::nameInput2() {
#if TARGET_PC
dusk::mods::svc::save_slot_new(mSelectNum);
const dusk::gamemode::GameMode* gameMode =
dusk::gamemode::getGameModeManager().getCurrentGameMode();
dusk::gamemode::getGameModeManager().getCurrentGameMode();
if (gameMode) {
gameMode->invokeOnNewSaveFunction();
gameMode->invokeOnSaveLoadedFunction();
}
// only do OnSaveLoaded callback here if hiding the brightness check screen
if (dusk::getSettings().game.hideTvSettingsScreen) {
if (gameMode) {
gameMode->invokeOnSaveLoadedFunction();
}
}
#endif
mDataSelProc = DATASELPROC_NEXT_MODE_WAIT;
+4 -1
View File
@@ -269,7 +269,10 @@ static void (*item_func_ptr[256])() = {
item_func_noentry,
};
inline void getItemFunc(u8 i_itemNo) {
#if !TARGET_PC
inline
#endif
void getItemFunc(u8 i_itemNo) {
dComIfGs_onItemFirstBit(i_itemNo);
item_func_ptr[i_itemNo]();
}
+2 -1
View File
@@ -606,7 +606,8 @@ void dMeter2_c::moveLife() {
life_count = (dComIfGs_getMaxLife() / 5) * 4;
}
s16 new_life = dComIfGs_getLife() + dComIfGp_getItemLifeCount();
DUSK_IF_ELSE(int, s16) new_life = dComIfGs_getLife() + dComIfGp_getItemLifeCount(); // prevent an overflow with really large damage values
if (new_life > life_count) {
new_life = life_count;
} else if (new_life < 0) {
+3 -3
View File
@@ -18,7 +18,7 @@ static daAlink_c* getPlayer() {
return (daAlink_c*)dComIfGp_getPlayer(0);
}
static void consumeButtons(u32 mask) {
static void consumeButtons(u16 mask) {
mDoCPd_c::getCpadInfo(PAD_1).mPressedButtonFlags &= ~mask;
mDoCPd_c::getCpadInfo(PAD_1).mButtonFlags &= ~mask;
}
@@ -107,8 +107,8 @@ void processGameCombos() {
getTransientSettings().moveLinkActive = false;
}
const u32 held = mDoCPd_c::getHold(PAD_1);
const u32 trig = mDoCPd_c::getTrig(PAD_1);
const u16 held = mDoCPd_c::getHold(PAD_1) & 0xFFFF;
const u16 trig = mDoCPd_c::getTrig(PAD_1) & 0xFFFF;
for (const auto& combo : kCombos) {
if ((held & combo.holdMask) != combo.holdMask) {
+3 -3
View File
@@ -8,12 +8,12 @@ struct GameCombo {
using ConditionFn = bool(*)();
using ActionFn = void(*)();
u32 holdMask; // all of these must be held (getHold)
u32 trigMask; // at least one must be newly triggered (getTrig); 0 = fires every frame while holdMask is held
u16 holdMask; // all of these must be held (getHold)
u16 trigMask; // at least one must be newly triggered (getTrig); 0 = fires every frame while holdMask is held
bool strict; // if true: (held & ~trigMask) must equal holdMask exactly, no extra buttons held
ConditionFn condition; // extra game-state guard; nullptr = no extra check
ActionFn action; // runs when the combo is fired
u32 consumeMask; // buttons to clear after firing; 0 = pass-through
u16 consumeMask; // buttons to clear after firing; 0 = pass-through
bool exclusive; // stop evaluating future combos if this fires
};
+1
View File
@@ -14,6 +14,7 @@ constexpr std::string_view kStubFragments[] = {
"Unimplemented: BP register"sv,
"Unhandled BP register"sv,
"Unhandled XF register"sv,
"Unhandled XF memory write"sv,
"but selective updates are not implemented"sv,
};
+8
View File
@@ -20,6 +20,7 @@ struct ItemCheckResult {
uint32_t tag = 0;
uint8_t itemNo = 0;
uint8_t displayItemNo = 0;
bool was_resolved = false;
};
ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_ac_c* giver);
@@ -57,6 +58,13 @@ uint32_t item_check_message(uint16_t group, uint32_t messageId);
bool item_give_queue_dispatching();
uint32_t item_give_queue_take_tag();
struct BossItemActorSpeeds {
f32 f;
f32 y;
};
void set_boss_item_actor_speeds(f32 f, f32 y);
BossItemActorSpeeds get_boss_item_actor_speeds();
namespace detail {
struct CommittedCheck {
uint8_t vanillaItem = 0;
+15
View File
@@ -0,0 +1,15 @@
#include "item.hpp"
namespace dusk::mods {
static BossItemActorSpeeds bossItemActorSpeeds{};
void set_boss_item_actor_speeds(f32 f, f32 y) {
bossItemActorSpeeds.f = f;
bossItemActorSpeeds.y = y;
}
BossItemActorSpeeds get_boss_item_actor_speeds() {
return bossItemActorSpeeds;
}
}
+11 -1
View File
@@ -120,6 +120,12 @@ constexpr std::array kMessageChecks{
.vanillaItem = dItemNo_HORSE_FLUTE_e,
.enqueueAtDisplay = false,
},
MessageCheck{
.group = 2,
.messageId = 6531,
.name = ITEM_CHECK_SHAD_DOMINION_ROD,
.vanillaItem = dItemNo_COPY_ROD_2_e,
}
};
std::unordered_map<LoadedMod*, ModItemChecks> s_modChecks;
@@ -152,7 +158,7 @@ std::string poe_check_name(uint8_t bitNo) {
}
std::string shop_check_name(uint8_t itemNo) {
return fmt::format("shop:{}:{}", current_stage_name(), itemNo);
return fmt::format("shop:{}:{}:{}", current_stage_name(), dStage_roomControl_c::getStayNo(), itemNo);
}
std::string bug_check_name(uint8_t insectId) {
@@ -229,6 +235,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
.vanilla_item = itemNo,
.current_item = itemNo,
.current_display_item = itemNo,
.was_resolved = false,
};
for (const auto& resolve : resolves) {
if (!resolve.mod->active) {
@@ -237,6 +244,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
if (resolve.fixedValue) {
info.current_item = resolve.itemNo;
info.current_display_item = resolve.itemNo;
info.was_resolved = true;
continue;
}
@@ -251,6 +259,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
}
info.current_item = resolution.item;
info.current_display_item = resolution.display_item;
info.was_resolved = true;
}
} catch (const std::exception& e) {
fail_mod(*resolve.mod, MOD_ERROR,
@@ -263,6 +272,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
return {
.item = info.current_item,
.display_item = info.current_display_item,
.was_resolved = info.was_resolved,
};
}
+2
View File
@@ -265,6 +265,7 @@ ItemCheckResult item_check_commit(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c*
.tag = giveTag,
.itemNo = committed->resolution.item,
.displayItemNo = committed->resolution.display_item,
.was_resolved = committed->resolution.was_resolved,
};
}
@@ -274,6 +275,7 @@ ItemCheckResult item_check_commit(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c*
.tag = giveTag,
.itemNo = committed.resolution.item,
.displayItemNo = committed.resolution.display_item,
.was_resolved = committed.resolution.was_resolved,
};
}
+1 -13
View File
@@ -6,14 +6,6 @@
#include <Windows.h>
#endif
#if defined(__SANITIZE_ADDRESS__)
#define ADDRESS_SANITIZER 1
#elif defined(__has_feature)
#if __has_feature(address_sanitizer)
#define ADDRESS_SANITIZER 1
#endif
#endif
namespace {
#if defined(_WIN32)
void* pl_dlopen(const std::filesystem::path& p) {
@@ -39,11 +31,7 @@ std::string pl_dlerror() {
#else
#include <dlfcn.h>
void* pl_dlopen(const std::filesystem::path& p) {
int flags = RTLD_LAZY | RTLD_LOCAL;
#if defined(RTLD_DEEPBIND) && !defined(ADDRESS_SANITIZER)
flags |= RTLD_DEEPBIND;
#endif
return dlopen(p.c_str(), flags);
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL);
}
void* pl_dlsym(void* h, const char* name) {
return dlsym(h, name);
+5 -5
View File
@@ -75,12 +75,12 @@ __declspec(allocate(".symdbh"))
#if defined(__clang__)
__attribute__((used))
#endif
constinit const SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
constinit SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
#elif defined(__APPLE__)
__attribute__((section("__DATA,__symdbh"), used)) constinit const SymdbDescriptor
s_symdbDescriptor{kDescriptorMagic, 0, 0};
__attribute__((section("__DATA,__symdbh"), used)) constinit SymdbDescriptor s_symdbDescriptor{
kDescriptorMagic, 0, 0};
#else
__attribute__((section("symdbh"), used)) constinit const SymdbDescriptor s_symdbDescriptor{
__attribute__((section("symdbh"), used)) constinit SymdbDescriptor s_symdbDescriptor{
kDescriptorMagic, 0, 0};
#endif
@@ -92,7 +92,7 @@ struct State {
uint64_t stringsLen = 0;
uintptr_t imageBase = 0;
// (rva, nameOff) of entries flagged kFlagInlineSites, sorted by rva
std::vector<std::pair<uint64_t, uint32_t> > inlineSites;
std::vector<std::pair<uint64_t, uint32_t>> inlineSites;
bool loaded = false;
bool initialized = false;
};
+703
View File
@@ -0,0 +1,703 @@
#include "registry.hpp"
#include "slot_map.hpp"
#include <aurora/lib/window.hpp>
#include <borealis/file_select.hpp>
#include <borealis/io.hpp>
#include <borealis/log.hpp>
#include "dusk/config.hpp"
#include "dusk/mods/loader/loader.hpp"
#include "mods/svc/file.h"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <limits>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace dusk::mods::svc {
namespace {
constexpr borealis::Log Log{"dusk::mods::file"};
static_assert(static_cast<int>(FILE_OPEN_READ) == static_cast<int>(borealis::io::File::Mode::Read));
static_assert(
static_cast<int>(FILE_OPEN_TRUNCATE) == static_cast<int>(borealis::io::File::Mode::Truncate));
static_assert(
static_cast<int>(FILE_OPEN_APPEND) == static_cast<int>(borealis::io::File::Mode::Append));
SlotMap<borealis::io::File> s_streams;
std::unordered_map<void*, LoadedMod*> s_buffers;
std::unordered_map<LoadedMod*, std::string> s_joinResults;
ConfigVar<std::string> s_pickerOverride{"file.pickerOverride", ""};
struct PendingPick {
LoadedMod* owner = nullptr;
FilePickFn callback = nullptr;
void* userData = nullptr;
std::optional<std::string> overrideLocation;
std::optional<std::string> exportSource;
};
std::shared_ptr<PendingPick> s_pendingPick;
ModResult map_status(borealis::io::Status status) {
switch (status) {
case borealis::io::Status::Ok:
return MOD_OK;
case borealis::io::Status::NotFound:
return MOD_UNAVAILABLE;
case borealis::io::Status::Unsupported:
return MOD_UNSUPPORTED;
case borealis::io::Status::AlreadyExists:
return MOD_CONFLICT;
case borealis::io::Status::Failed:
default:
return MOD_ERROR;
}
}
ModResult map_picker_status(borealis::file_select::Status status) {
switch (status) {
case borealis::file_select::Status::Selected:
return MOD_OK;
case borealis::file_select::Status::Canceled:
return MOD_UNAVAILABLE;
case borealis::file_select::Status::Unsupported:
return MOD_UNSUPPORTED;
case borealis::file_select::Status::Busy:
return MOD_CONFLICT;
case borealis::file_select::Status::Failed:
default:
return MOD_ERROR;
}
}
void invoke_pick(const std::shared_ptr<PendingPick>& pending, ModResult status,
const std::vector<std::string>& locations, const std::string& error) {
if (s_pendingPick == pending) {
s_pendingPick.reset();
}
auto* owner = pending->owner;
const auto callback = pending->callback;
if (owner == nullptr || callback == nullptr || !owner->active) {
return;
}
std::vector<const char*> rawLocations;
rawLocations.reserve(locations.size());
for (const auto& location : locations) {
rawLocations.push_back(location.c_str());
}
try {
callback(owner->context.get(), status, rawLocations.empty() ? nullptr : rawLocations.data(),
static_cast<uint32_t>(rawLocations.size()), error.c_str(), pending->userData);
} catch (const std::exception& exception) {
fail_mod(*owner, MOD_ERROR,
std::string{"exception in file picker callback: "} + exception.what());
} catch (...) {
fail_mod(*owner, MOD_ERROR, "unknown exception in file picker callback");
}
}
bool valid_pick_options(const FilePickOptions* options) {
if (options == nullptr || options->struct_size < sizeof(FilePickOptions) ||
(options->filter_count != 0 && options->filters == nullptr))
{
return false;
}
for (uint32_t i = 0; i < options->filter_count; ++i) {
if (options->filters[i].name == nullptr || options->filters[i].pattern == nullptr) {
return false;
}
}
return true;
}
ModResult begin_pick(ModContext* context, const FilePickOptions* options, FilePickFn callback,
void* userData, bool folder) {
auto* mod = mod_from_context(context);
if (mod == nullptr || callback == nullptr || !valid_pick_options(options)) {
return MOD_INVALID_ARGUMENT;
}
if (s_pendingPick != nullptr || borealis::file_select::busy()) {
return MOD_CONFLICT;
}
auto pending = std::make_shared<PendingPick>(PendingPick{
.owner = mod,
.callback = callback,
.userData = userData,
});
const auto& overrideLocation = s_pickerOverride.getValue();
if (!overrideLocation.empty()) {
pending->overrideLocation = overrideLocation;
s_pendingPick = std::move(pending);
return MOD_OK;
}
s_pendingPick = pending;
const std::string defaultLocation =
options->default_location != nullptr ? options->default_location : "";
if (folder) {
borealis::file_select::open_folder(
{
.parentWindow = aurora::window::get_sdl_window(),
.defaultLocation = defaultLocation,
},
[pending](borealis::file_select::Result result) {
invoke_pick(
pending, map_picker_status(result.status), result.locations, result.message);
});
return MOD_OK;
}
std::vector<borealis::file_select::Filter> filters;
filters.reserve(options->filter_count);
for (uint32_t i = 0; i < options->filter_count; ++i) {
filters.push_back({options->filters[i].name, options->filters[i].pattern});
}
borealis::file_select::open_file(
{
.parentWindow = aurora::window::get_sdl_window(),
.filters = std::move(filters),
.defaultLocation = defaultLocation,
},
[pending](borealis::file_select::Result result) {
invoke_pick(
pending, map_picker_status(result.status), result.locations, result.message);
});
return MOD_OK;
}
ModResult begin_export(ModContext* context, const char* sourceLocation, const char* suggestedName,
FilePickFn callback, void* userData) {
auto* mod = mod_from_context(context);
const std::string_view name = suggestedName != nullptr ? suggestedName : "";
if (mod == nullptr || sourceLocation == nullptr || callback == nullptr || name.empty() ||
name == "." || name == ".." || name.find_first_of("/\\") != std::string_view::npos)
{
return MOD_INVALID_ARGUMENT;
}
if (s_pendingPick != nullptr || borealis::file_select::busy()) {
return MOD_CONFLICT;
}
const auto available = borealis::io::check(sourceLocation);
if (available != borealis::io::Status::Ok) {
return map_status(available);
}
auto pending = std::make_shared<PendingPick>(PendingPick{
.owner = mod,
.callback = callback,
.userData = userData,
});
const auto& overrideLocation = s_pickerOverride.getValue();
if (!overrideLocation.empty()) {
pending->overrideLocation = overrideLocation;
pending->exportSource = sourceLocation;
s_pendingPick = std::move(pending);
return MOD_OK;
}
borealis::file_select::ExportOptions options{
.parentWindow = aurora::window::get_sdl_window(),
.sourceLocation = sourceLocation,
.suggestedName = suggestedName,
};
s_pendingPick = pending;
try {
borealis::file_select::export_file(
std::move(options), [pending](borealis::file_select::Result result) {
invoke_pick(
pending, map_picker_status(result.status), result.locations, result.message);
});
} catch (...) {
if (s_pendingPick == pending) {
s_pendingPick.reset();
}
throw;
}
return MOD_OK;
}
ModResult file_pick_file(
ModContext* context, const FilePickOptions* options, FilePickFn callback, void* userData) {
return begin_pick(context, options, callback, userData, false);
}
ModResult file_pick_folder(
ModContext* context, const FilePickOptions* options, FilePickFn callback, void* userData) {
return begin_pick(context, options, callback, userData, true);
}
ModResult file_export_file(ModContext* context, const char* sourceLocation,
const char* suggestedName, FilePickFn callback, void* userData) {
try {
return begin_export(context, sourceLocation, suggestedName, callback, userData);
} catch (...) {
return MOD_ERROR;
}
}
ModResult file_display_name(
ModContext* context, const char* location, char* buffer, uint32_t bufferSize) {
if (mod_from_context(context) == nullptr || location == nullptr || buffer == nullptr ||
bufferSize == 0)
{
return MOD_INVALID_ARGUMENT;
}
const std::string name = borealis::io::display_name(location);
if (name.size() + 1 > bufferSize) {
return MOD_INVALID_ARGUMENT;
}
std::memcpy(buffer, name.c_str(), name.size() + 1);
return MOD_OK;
}
ModResult file_check(ModContext* context, const char* location) {
if (mod_from_context(context) == nullptr || location == nullptr) {
return MOD_INVALID_ARGUMENT;
}
return map_status(borealis::io::check(location));
}
ModResult file_open(
ModContext* context, const char* location, FileOpenMode mode, FileStreamHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || location == nullptr || outHandle == nullptr || mode < FILE_OPEN_READ ||
mode > FILE_OPEN_APPEND)
{
return MOD_INVALID_ARGUMENT;
}
try {
const auto ioMode = static_cast<borealis::io::File::Mode>(mode);
if (mode != FILE_OPEN_READ) {
const auto available = borealis::io::check(location);
if (available != borealis::io::Status::Ok) {
return map_status(available);
}
}
auto result = borealis::io::open(location, ioMode);
if (result.status != borealis::io::Status::Ok) {
Log.warn("[{}] open '{}' failed: {}", mod->metadata.id,
borealis::io::display_name(location), result.message);
return map_status(result.status);
}
*outHandle = s_streams.emplace(*mod, std::move(result.file));
return MOD_OK;
} catch (...) {
return MOD_ERROR;
}
}
borealis::io::File* find_stream(ModContext* context, FileStreamHandle handle) {
auto* mod = mod_from_context(context);
if (mod == nullptr) {
return nullptr;
}
auto* entry = s_streams.find_owned(handle, *mod);
return entry != nullptr ? &entry->value : nullptr;
}
ModResult file_size(ModContext* context, FileStreamHandle handle, uint64_t* outSize) {
if (outSize == nullptr) {
return MOD_INVALID_ARGUMENT;
}
*outSize = 0;
auto* file = find_stream(context, handle);
if (file == nullptr) {
return MOD_INVALID_ARGUMENT;
}
*outSize = file->size();
return MOD_OK;
}
ModResult file_read(ModContext* context, FileStreamHandle handle, void* buffer, uint64_t length,
uint64_t* outRead) {
if (outRead == nullptr || (buffer == nullptr && length != 0)) {
return MOD_INVALID_ARGUMENT;
}
*outRead = 0;
auto* file = find_stream(context, handle);
if (file == nullptr || file->writable()) {
return MOD_INVALID_ARGUMENT;
}
*outRead = file->read(buffer, length);
return file->error().empty() ? MOD_OK : MOD_ERROR;
}
ModResult file_seek(ModContext* context, FileStreamHandle handle, uint64_t offset) {
auto* file = find_stream(context, handle);
if (file == nullptr) {
return MOD_INVALID_ARGUMENT;
}
return file->seek(offset) ? MOD_OK : MOD_ERROR;
}
ModResult file_close(ModContext* context, FileStreamHandle handle) {
auto* mod = mod_from_context(context);
if (mod == nullptr) {
return MOD_INVALID_ARGUMENT;
}
auto entry = s_streams.take_owned(handle, *mod);
if (!entry.has_value()) {
return MOD_INVALID_ARGUMENT;
}
return entry->value.close() ? MOD_OK : MOD_ERROR;
}
ModResult file_write(
ModContext* context, FileStreamHandle handle, const void* buffer, uint64_t length) {
if ((buffer == nullptr && length != 0) || length > std::numeric_limits<size_t>::max()) {
return MOD_INVALID_ARGUMENT;
}
auto* file = find_stream(context, handle);
if (file == nullptr || !file->writable()) {
return MOD_INVALID_ARGUMENT;
}
const auto bytes =
std::span{static_cast<const std::byte*>(buffer), static_cast<size_t>(length)};
return file->write(bytes) ? MOD_OK : MOD_ERROR;
}
ModResult file_flush(ModContext* context, FileStreamHandle handle) {
auto* file = find_stream(context, handle);
if (file == nullptr || !file->writable()) {
return MOD_INVALID_ARGUMENT;
}
return file->flush() ? MOD_OK : MOD_ERROR;
}
ModResult file_write_all(ModContext* context, const char* location, const void* data, size_t size) {
if (mod_from_context(context) == nullptr || location == nullptr ||
(data == nullptr && size != 0))
{
return MOD_INVALID_ARGUMENT;
}
try {
const auto available = borealis::io::check(location);
if (available != borealis::io::Status::Ok) {
return map_status(available);
}
auto opened = borealis::io::open(location, borealis::io::File::Mode::Truncate);
if (opened.status != borealis::io::Status::Ok) {
return map_status(opened.status);
}
if (!opened.file.write(std::span{static_cast<const std::byte*>(data), size})) {
return MOD_ERROR;
}
return opened.file.close() ? MOD_OK : MOD_ERROR;
} catch (...) {
return MOD_ERROR;
}
}
ModResult file_read_all(ModContext* context, const char* location, FileBuffer* outBuffer) {
if (outBuffer == nullptr || outBuffer->struct_size < sizeof(FileBuffer)) {
return MOD_INVALID_ARGUMENT;
}
outBuffer->data = nullptr;
outBuffer->size = 0;
auto* mod = mod_from_context(context);
if (mod == nullptr || location == nullptr) {
return MOD_INVALID_ARGUMENT;
}
try {
auto opened = borealis::io::open(location);
if (opened.status != borealis::io::Status::Ok) {
return map_status(opened.status);
}
const uint64_t expectedSize = opened.file.size();
if (expectedSize > std::numeric_limits<size_t>::max()) {
return MOD_ERROR;
}
std::vector<unsigned char> bytes;
bytes.resize(static_cast<size_t>(expectedSize));
uint64_t total = 0;
while (total < expectedSize) {
const uint64_t count = opened.file.read(bytes.data() + total, expectedSize - total);
if (count == 0) {
return MOD_ERROR;
}
total += count;
}
if (expectedSize == 0) {
unsigned char chunk[64 * 1024];
while (true) {
const uint64_t count = opened.file.read(chunk, sizeof(chunk));
if (count == 0) {
if (!opened.file.error().empty()) {
return MOD_ERROR;
}
break;
}
bytes.insert(bytes.end(), chunk, chunk + count);
}
}
if (bytes.empty()) {
return MOD_OK;
}
std::unique_ptr<void, decltype(&std::free)> data{std::malloc(bytes.size()), &std::free};
if (!data) {
return MOD_ERROR;
}
std::memcpy(data.get(), bytes.data(), bytes.size());
s_buffers.emplace(data.get(), mod);
outBuffer->data = data.release();
outBuffer->size = bytes.size();
return MOD_OK;
} catch (...) {
return MOD_ERROR;
}
}
void file_free(ModContext* context, FileBuffer* buffer) {
if (buffer == nullptr || buffer->struct_size < sizeof(FileBuffer) || buffer->data == nullptr) {
return;
}
auto* mod = mod_from_context(context);
const auto found = s_buffers.find(buffer->data);
if (mod == nullptr || found == s_buffers.end() || found->second != mod) {
Log.error("[{}] file free: buffer is not owned by this mod", mod_id_from_context(context));
return;
}
s_buffers.erase(found);
std::free(buffer->data);
buffer->data = nullptr;
buffer->size = 0;
}
ModResult file_list(
ModContext* context, const char* folderLocation, FileListFn callback, void* userData) {
auto* mod = mod_from_context(context);
if (mod == nullptr || folderLocation == nullptr || callback == nullptr) {
return MOD_INVALID_ARGUMENT;
}
borealis::io::ListResult result;
try {
result = borealis::io::list(folderLocation);
} catch (...) {
return MOD_ERROR;
}
if (result.status != borealis::io::Status::Ok) {
return map_status(result.status);
}
try {
for (const auto& entry : result.entries) {
const FileEntry raw{
.name = entry.name.c_str(),
.location = entry.location.c_str(),
.is_directory = entry.isDirectory,
};
callback(mod->context.get(), &raw, userData);
if (!mod->active) {
return MOD_ERROR;
}
}
callback(mod->context.get(), nullptr, userData);
} catch (const std::exception& exception) {
fail_mod(
*mod, MOD_ERROR, std::string{"exception in file list callback: "} + exception.what());
return MOD_ERROR;
} catch (...) {
fail_mod(*mod, MOD_ERROR, "unknown exception in file list callback");
return MOD_ERROR;
}
return MOD_OK;
}
ModResult file_join(ModContext* context, const char* folderLocation, const char* relativePath,
const char** outLocation) {
if (outLocation != nullptr) {
*outLocation = nullptr;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || folderLocation == nullptr || relativePath == nullptr ||
outLocation == nullptr)
{
return MOD_INVALID_ARGUMENT;
}
auto result = borealis::io::join(folderLocation, relativePath);
if (result.status != borealis::io::Status::Ok) {
return map_status(result.status);
}
auto& saved = s_joinResults[mod];
saved = std::move(result.location);
*outLocation = saved.c_str();
return MOD_OK;
}
ModResult file_create_child(
ModContext* context, const char* folderLocation, const char* name, const char** outLocation) {
if (outLocation != nullptr) {
*outLocation = nullptr;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || folderLocation == nullptr || name == nullptr || outLocation == nullptr) {
return MOD_INVALID_ARGUMENT;
}
try {
auto result = borealis::io::create_child(folderLocation, name);
if (result.status != borealis::io::Status::Ok) {
return map_status(result.status);
}
auto& saved = s_joinResults[mod];
saved = std::move(result.location);
*outLocation = saved.c_str();
return MOD_OK;
} catch (...) {
return MOD_ERROR;
}
}
void file_initialize() {
config::Register(s_pickerOverride);
}
bool file_available() {
const auto capabilities = borealis::file_select::capabilities();
return capabilities.canOpenFile || capabilities.canOpenFolder || capabilities.canExportFile;
}
ModResult copy_picker_override(
std::string_view source, std::string_view destination, std::string& error) {
try {
if (source == destination) {
return MOD_OK;
}
auto input = borealis::io::open(source);
if (input.status != borealis::io::Status::Ok) {
error = std::move(input.message);
return map_status(input.status);
}
auto output = borealis::io::open(destination, borealis::io::File::Mode::Truncate);
if (output.status != borealis::io::Status::Ok) {
error = std::move(output.message);
return map_status(output.status);
}
std::array<std::byte, 64 * 1024> buffer{};
while (true) {
const uint64_t read = input.file.read(buffer.data(), buffer.size());
if (read == 0) {
if (!input.file.error().empty()) {
error = input.file.error();
return MOD_ERROR;
}
break;
}
if (!output.file.write(std::span{buffer.data(), static_cast<size_t>(read)})) {
error = output.file.error();
return MOD_ERROR;
}
}
if (!output.file.close()) {
error = output.file.error();
return MOD_ERROR;
}
return MOD_OK;
} catch (const std::exception& exception) {
error = exception.what();
return MOD_ERROR;
} catch (...) {
error = "Unable to copy export source";
return MOD_ERROR;
}
}
void file_frame_begin() {
auto pending = s_pendingPick;
if (pending == nullptr || !pending->overrideLocation.has_value()) {
return;
}
if (!pending->exportSource.has_value()) {
invoke_pick(pending, MOD_OK, {*pending->overrideLocation}, "");
return;
}
std::string error;
const ModResult result =
copy_picker_override(*pending->exportSource, *pending->overrideLocation, error);
invoke_pick(pending, result,
result == MOD_OK ? std::vector<std::string>{*pending->overrideLocation} :
std::vector<std::string>{},
error);
}
void file_remove_mod(LoadedMod& mod) {
const size_t streams = s_streams.erase_all(mod);
size_t buffers = 0;
std::erase_if(s_buffers, [&](const auto& entry) {
if (entry.second != &mod) {
return false;
}
std::free(entry.first);
++buffers;
return true;
});
s_joinResults.erase(&mod);
if (s_pendingPick != nullptr && s_pendingPick->owner == &mod) {
s_pendingPick->owner = nullptr;
s_pendingPick->callback = nullptr;
s_pendingPick.reset();
}
if (streams != 0 || buffers != 0) {
Log.warn("[{}] reclaimed {} open stream(s) and {} file buffer(s)", mod.metadata.id, streams,
buffers);
}
}
void file_shutdown() {
config::unregister(s_pickerOverride);
s_pendingPick.reset();
}
constexpr FileService s_fileService{
.header = SERVICE_HEADER(FileService, FILE_SERVICE_MAJOR, FILE_SERVICE_MINOR),
.pick_file = file_pick_file,
.pick_folder = file_pick_folder,
.export_file = file_export_file,
.display_name = file_display_name,
.check = file_check,
.open = file_open,
.size = file_size,
.read = file_read,
.write = file_write,
.seek = file_seek,
.flush = file_flush,
.close = file_close,
.read_all = file_read_all,
.write_all = file_write_all,
.free = file_free,
.list = file_list,
.join = file_join,
.create_child = file_create_child,
};
} // namespace
constinit const ServiceModule g_fileModule{
.id = FILE_SERVICE_ID,
.majorVersion = FILE_SERVICE_MAJOR,
.minorVersion = FILE_SERVICE_MINOR,
.service = &s_fileService,
.available = file_available,
.initialize = file_initialize,
.modDetached = file_remove_mod,
.frameBegin = file_frame_begin,
.shutdown = file_shutdown,
};
} // namespace dusk::mods::svc
+577
View File
@@ -0,0 +1,577 @@
#include "registry.hpp"
#include "slot_map.hpp"
#include "dusk/app_info.hpp"
#include "dusk/main.h"
#include "dusk/mods/loader/loader.hpp"
#include "mods/svc/http.h"
#include <borealis/http.hpp>
#include <borealis/io.hpp>
#include <borealis/version.h>
#include <fmt/format.h>
#include <xxhash.h>
#include <algorithm>
#include <cassert>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <ranges>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
namespace dusk::mods::svc {
namespace {
constexpr size_t MaxRequestsPerMod = 16;
constexpr size_t MaxUrlBytes = 8 * 1024;
constexpr size_t MaxHeaders = 64;
constexpr size_t MaxHeaderBytes = 16 * 1024;
constexpr size_t MaxRequestBodyBytes = 16 * 1024 * 1024;
constexpr size_t DefaultResponseBodyBytes = 1024 * 1024;
constexpr size_t MaxResponseBodyBytes = 64 * 1024 * 1024;
constexpr std::chrono::milliseconds DefaultTimeout{10000};
struct PendingRequest {
HttpCompleteFn callback = nullptr;
void* userData = nullptr;
borealis::Task<borealis::http::Result> task;
std::filesystem::path stagingPath;
std::filesystem::path downloadPath;
bool completing = false;
};
static_assert(std::is_nothrow_move_constructible_v<PendingRequest>);
SlotMap<PendingRequest> s_requests;
bool ascii_iequals(std::string_view left, std::string_view right) {
return left.size() == right.size() && std::ranges::equal(left, right, [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
}
bool is_reserved_header(std::string_view name) {
constexpr std::string_view reserved[]{
"User-Agent",
"Host",
"Content-Length",
"Connection",
"Accept-Encoding",
"Range",
"If-Range",
};
return std::ranges::any_of(
reserved, [&](std::string_view value) { return ascii_iequals(name, value); });
}
bool valid_header_name(std::string_view name) {
constexpr std::string_view separators{"()<>@,;:\\\"/[]?={} \t"};
return !name.empty() && std::ranges::all_of(name, [&](unsigned char value) {
return value > 32 && value < 127 &&
separators.find(static_cast<char>(value)) == std::string_view::npos;
});
}
bool valid_url(std::string_view url) {
constexpr std::string_view scheme{"https://"};
if (!url.starts_with(scheme) || url.size() <= scheme.size() || url.size() > MaxUrlBytes) {
return false;
}
if (std::ranges::any_of(url, [](unsigned char value) { return value <= 32 || value == 127; })) {
return false;
}
const auto authorityEnd = url.find_first_of("/?#", scheme.size());
const auto authority = url.substr(scheme.size(), authorityEnd - scheme.size());
return !authority.empty();
}
bool declares_http_import(const LoadedMod& mod) {
return std::ranges::any_of(
mod.manifestInfo.imports, [](const ModManifestInfo::Import& serviceImport) {
return serviceImport.id == HTTP_SERVICE_ID;
});
}
std::filesystem::path normalized_absolute(const std::filesystem::path& path, std::error_code& ec) {
auto result = std::filesystem::absolute(path, ec);
return ec ? std::filesystem::path{} : result.lexically_normal();
}
bool path_is_below(const std::filesystem::path& path, const std::filesystem::path& directory) {
const auto [directoryEnd, pathPosition] =
std::mismatch(directory.begin(), directory.end(), path.begin(), path.end());
return directoryEnd == directory.end() && pathPosition != path.end();
}
bool path_is_at_or_below(
const std::filesystem::path& path, const std::filesystem::path& directory) {
const auto [directoryEnd, pathPosition] =
std::mismatch(directory.begin(), directory.end(), path.begin(), path.end());
(void)pathPosition;
return directoryEnd == directory.end();
}
std::filesystem::path data_root(const LoadedMod& mod, std::error_code& ec) {
if (!mod.dataDirUtf8.empty()) {
return normalized_absolute(borealis::io::fs_path_from_utf8(mod.dataDirUtf8), ec);
}
return normalized_absolute(ConfigPath / "mod_data" / mod.metadata.id, ec);
}
std::optional<std::filesystem::path> validate_download_path(
const LoadedMod& mod, const char* rawPath) {
if (rawPath == nullptr) {
return std::filesystem::path{};
}
const auto supplied = borealis::io::fs_path_from_utf8(rawPath);
if (!supplied.is_absolute()) {
return std::nullopt;
}
std::error_code ec;
const auto path = normalized_absolute(supplied, ec);
if (ec) {
return std::nullopt;
}
const auto modRoot = normalized_absolute(mod.dir, ec);
if (ec) {
return std::nullopt;
}
const auto dataRoot = data_root(mod, ec);
if (ec) {
return std::nullopt;
}
const auto stagingRoot = (modRoot / "downloads").lexically_normal();
if ((!path_is_below(path, modRoot) && !path_is_below(path, dataRoot)) ||
path_is_at_or_below(path, stagingRoot))
{
return std::nullopt;
}
return path;
}
std::filesystem::path staging_path(const LoadedMod& mod, std::string_view url) {
const auto& modId = mod.metadata.id;
const auto modHash = XXH64(modId.data(), modId.size(), 0);
const auto hash = XXH64(url.data(), url.size(), modHash);
return mod.dir / "downloads" / fmt::format("{:016x}.part", hash);
}
HttpError map_error(borealis::http::Error error) {
switch (error) {
case borealis::http::Error::None:
return HTTP_ERROR_NONE;
case borealis::http::Error::InvalidUrl:
return HTTP_ERROR_INVALID_URL;
case borealis::http::Error::UnsupportedScheme:
return HTTP_ERROR_UNSUPPORTED_SCHEME;
case borealis::http::Error::Timeout:
return HTTP_ERROR_TIMEOUT;
case borealis::http::Error::TooLarge:
return HTTP_ERROR_TOO_LARGE;
case borealis::http::Error::Canceled:
return HTTP_ERROR_CANCELED;
case borealis::http::Error::Io:
return HTTP_ERROR_IO;
case borealis::http::Error::NoBackend:
case borealis::http::Error::NotInitialized:
case borealis::http::Error::Network:
return HTTP_ERROR_NETWORK;
default:
return HTTP_ERROR_NETWORK;
}
}
std::optional<borealis::http::Method> borealis_method(HttpMethod method) {
switch (method) {
case HTTP_METHOD_GET:
return borealis::http::Method::Get;
case HTTP_METHOD_POST:
return borealis::http::Method::Post;
case HTTP_METHOD_HEAD:
return borealis::http::Method::Head;
}
return std::nullopt;
}
borealis::http::Result publish_download(borealis::http::Result result,
const std::filesystem::path& staging, const std::filesystem::path& destination) noexcept {
if (result.error != borealis::http::Error::None) {
return result;
}
try {
std::string renameError;
if (borealis::io::atomic_replace(staging, destination, renameError)) {
return result;
}
std::filesystem::path temporary = destination;
temporary += "." + borealis::io::fs_path_to_string(staging.filename()) + ".part";
std::error_code ec;
std::filesystem::copy_file(
staging, temporary, std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
const std::string copyError = ec.message();
std::error_code ignored;
std::filesystem::remove(temporary, ignored);
result.error = borealis::http::Error::Io;
result.message = "Failed to publish download: " + copyError;
return result;
}
std::string replaceError;
if (!borealis::io::atomic_replace(temporary, destination, replaceError)) {
std::filesystem::remove(temporary, ec);
result.error = borealis::http::Error::Io;
result.message = "Failed to publish download: " + replaceError;
return result;
}
std::filesystem::remove(staging, ec);
return result;
} catch (const std::exception& exception) {
result.error = borealis::http::Error::Io;
result.message = std::string{"Failed to publish download: "} + exception.what();
return result;
} catch (...) {
result.error = borealis::http::Error::Io;
result.message = "Failed to publish download";
return result;
}
}
void http_frame_begin() {
std::vector<HttpRequestHandle> ready;
s_requests.for_each([&](const HttpRequestHandle handle, const auto& entry) {
const auto& pending = entry.value;
if (!pending.completing && pending.task.ready()) {
ready.push_back(handle);
}
});
for (const auto handle : ready) {
auto* entry = s_requests.find(handle);
if (entry == nullptr || !entry->owner->active || entry->value.completing) {
continue;
}
auto& pending = entry->value;
borealis::http::Result result;
try {
auto completed = pending.task.try_take();
if (!completed.has_value()) {
continue;
}
result = std::move(*completed);
} catch (const std::exception& exception) {
result = {
.error = borealis::http::Error::Io,
.message = exception.what(),
};
} catch (...) {
result = {
.error = borealis::http::Error::Io,
.message = "HTTP request completion failed",
};
}
auto* owner = entry->owner;
const auto callback = pending.callback;
const auto userData = pending.userData;
pending.completing = true;
std::vector<HttpHeader> headers;
headers.reserve(result.response.headers.size());
for (const auto& header : result.response.headers) {
headers.push_back({.name = header.name.c_str(), .value = header.value.c_str()});
}
const bool downloadSucceeded =
!pending.downloadPath.empty() && result.error == borealis::http::Error::None;
const auto publishedPath = downloadSucceeded ?
borealis::io::fs_path_to_string(pending.downloadPath) :
std::string{};
const HttpResult snapshot{
.struct_size = sizeof(HttpResult),
.error = map_error(result.error),
.error_message = result.message.c_str(),
.status_code = result.response.statusCode,
.headers = headers.empty() ? nullptr : headers.data(),
.header_count = static_cast<uint32_t>(headers.size()),
.body = pending.downloadPath.empty() && !result.response.body.empty() ?
result.response.body.data() :
nullptr,
.body_size = pending.downloadPath.empty() ? result.response.body.size() : 0,
.download_path = downloadSucceeded ? publishedPath.c_str() : nullptr,
};
try {
callback(owner->context.get(), handle, &snapshot, userData);
} catch (const std::exception& exception) {
fail_mod(*owner, MOD_ERROR,
std::string{"exception in HTTP completion callback: "} + exception.what());
} catch (...) {
fail_mod(*owner, MOD_ERROR, "unknown exception in HTTP completion callback");
}
s_requests.erase(handle);
}
}
size_t active_request_count(const LoadedMod& mod) {
size_t count = 0;
s_requests.for_each([&](HttpRequestHandle, const auto& entry) {
if (entry.owner == &mod && !entry.value.completing) {
++count;
}
});
return count;
}
bool staging_path_in_use(const LoadedMod& mod, const std::filesystem::path& path) {
bool inUse = false;
s_requests.for_each([&](HttpRequestHandle, const auto& entry) {
if (entry.owner == &mod && !entry.value.completing && entry.value.stagingPath == path) {
inUse = true;
}
});
return inUse;
}
std::string user_agent_version(std::string_view version) {
std::string result{version};
for (char& ch : result) {
const auto value = static_cast<unsigned char>(ch);
if (value <= 32 || value >= 127) {
ch = '_';
}
}
return result;
}
ModResult start_request(LoadedMod& mod, const HttpRequestDesc& desc, HttpCompleteFn callback,
void* userData, HttpRequestHandle& outHandle) {
const std::string_view url{desc.url};
const auto method = borealis_method(desc.method);
if (!valid_url(url) || !method.has_value() ||
(desc.header_count != 0 && desc.headers == nullptr) ||
(desc.body_size != 0 && desc.body == nullptr) || desc.body_size > MaxRequestBodyBytes ||
((desc.method == HTTP_METHOD_GET || desc.method == HTTP_METHOD_HEAD) &&
desc.body_size != 0) ||
(desc.method == HTTP_METHOD_HEAD && desc.download_path != nullptr) ||
desc.header_count > MaxHeaders)
{
return MOD_INVALID_ARGUMENT;
}
size_t headerBytes = 0;
for (uint32_t i = 0; i < desc.header_count; ++i) {
const auto& header = desc.headers[i];
if (header.name == nullptr || header.value == nullptr) {
return MOD_INVALID_ARGUMENT;
}
const std::string_view name{header.name};
const std::string_view value{header.value};
const bool invalidValue = std::ranges::any_of(
value, [](unsigned char ch) { return (ch < 32 && ch != '\t') || ch == 127; });
if (!valid_header_name(name) || invalidValue || is_reserved_header(name) ||
name.size() > MaxHeaderBytes - headerBytes)
{
return MOD_INVALID_ARGUMENT;
}
headerBytes += name.size();
if (value.size() > MaxHeaderBytes - headerBytes) {
return MOD_INVALID_ARGUMENT;
}
headerBytes += value.size();
}
auto downloadPath = validate_download_path(mod, desc.download_path);
if (!downloadPath.has_value() ||
(downloadPath->empty() && desc.max_body_bytes > MaxResponseBodyBytes))
{
return MOD_INVALID_ARGUMENT;
}
if (active_request_count(mod) >= MaxRequestsPerMod) {
return MOD_CONFLICT;
}
std::filesystem::path staging;
if (!downloadPath->empty()) {
staging = staging_path(mod, url);
if (staging_path_in_use(mod, staging)) {
return MOD_CONFLICT;
}
std::error_code ec;
std::filesystem::create_directories(staging.parent_path(), ec);
if (ec) {
return MOD_ERROR;
}
std::filesystem::create_directories(downloadPath->parent_path(), ec);
if (ec) {
return MOD_ERROR;
}
}
PendingRequest pending{
.callback = callback,
.userData = userData,
.stagingPath = staging,
.downloadPath = *downloadPath,
};
if (!borealis::http::available()) {
return MOD_UNAVAILABLE;
}
borealis::http::Request request{
.method = *method,
.url = std::string{url},
.body = desc.body_size != 0 ?
std::string{static_cast<const char*>(desc.body), desc.body_size} :
std::string{},
.downloadTo = staging,
.connectTimeout = desc.connect_timeout_ms != 0 ?
std::chrono::milliseconds{desc.connect_timeout_ms} :
DefaultTimeout,
.idleTimeout = desc.idle_timeout_ms != 0 ?
std::chrono::milliseconds{desc.idle_timeout_ms} :
DefaultTimeout,
.totalTimeout = desc.total_timeout_ms != 0 ?
std::optional{std::chrono::milliseconds{desc.total_timeout_ms}} :
std::nullopt,
.maxBodyBytes =
desc.max_body_bytes != 0 ? desc.max_body_bytes : DefaultResponseBodyBytes,
};
request.headers.reserve(desc.header_count + 1);
for (uint32_t i = 0; i < desc.header_count; ++i) {
request.headers.push_back({desc.headers[i].name, desc.headers[i].value});
}
request.headers.push_back({
.name = "User-Agent",
.value = fmt::format("{}/{} {}/{}", AppName, BOREALIS_APP_VERSION, mod.metadata.id,
user_agent_version(mod.metadata.version)),
});
auto task = borealis::http::start(std::move(request));
if (task.ready()) {
auto immediate = task.try_take();
if (!immediate.has_value()) {
return MOD_UNAVAILABLE;
}
if (immediate->error == borealis::http::Error::NoBackend ||
immediate->error == borealis::http::Error::NotInitialized)
{
return MOD_UNAVAILABLE;
}
task = borealis::detail::make_ready_task(std::move(*immediate));
}
if (!downloadPath->empty()) {
task = std::move(task).map([staging, destination = *downloadPath](auto&& result) {
return publish_download(std::move(result), staging, destination);
});
}
pending.task = std::move(task);
outHandle = s_requests.emplace(mod, std::move(pending));
return MOD_OK;
}
ModResult http_request(ModContext* context, const HttpRequestDesc* desc, HttpCompleteFn callback,
void* userData, HttpRequestHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(HttpRequestDesc) ||
desc->url == nullptr || callback == nullptr || outHandle == nullptr)
{
return MOD_INVALID_ARGUMENT;
}
if (!declares_http_import(*mod)) {
return MOD_UNSUPPORTED;
}
try {
return start_request(*mod, *desc, callback, userData, *outHandle);
} catch (...) {
return MOD_ERROR;
}
}
ModResult http_progress(ModContext* context, HttpRequestHandle handle, HttpProgress* outProgress) {
const uint32_t structSize = outProgress != nullptr ? outProgress->struct_size : 0;
auto* mod = mod_from_context(context);
if (mod == nullptr || outProgress == nullptr || structSize < sizeof(HttpProgress)) {
return MOD_INVALID_ARGUMENT;
}
*outProgress = HttpProgress{.struct_size = structSize};
const auto* entry = s_requests.find_owned(handle, *mod);
if (entry == nullptr) {
return MOD_UNAVAILABLE;
}
const auto progress = entry->value.task.progress();
outProgress->completed_bytes = progress.completed;
outProgress->total_bytes = progress.total.value_or(0);
outProgress->total_known = progress.total.has_value();
return MOD_OK;
}
ModResult http_cancel(ModContext* context, HttpRequestHandle handle) {
auto* mod = mod_from_context(context);
if (mod == nullptr) {
return MOD_INVALID_ARGUMENT;
}
auto* entry = s_requests.find_owned(handle, *mod);
if (entry == nullptr) {
return MOD_UNAVAILABLE;
}
entry->value.task.cancel();
return MOD_OK;
}
void http_mod_deactivating(LoadedMod& mod) {
(void)s_requests.take_all(mod);
}
void http_mod_detached(LoadedMod& mod) {
bool found = false;
s_requests.for_each(
[&](HttpRequestHandle, const auto& entry) { found = found || entry.owner == &mod; });
assert(!found);
}
void http_shutdown() {
s_requests = {};
}
bool http_available() {
return borealis::http::available() && borealis::http::initialize();
}
constexpr HttpService s_httpService{
.header = SERVICE_HEADER(HttpService, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR),
.request = http_request,
.progress = http_progress,
.cancel = http_cancel,
};
} // namespace
constinit const ServiceModule g_httpModule{
.id = HTTP_SERVICE_ID,
.majorVersion = HTTP_SERVICE_MAJOR,
.minorVersion = HTTP_SERVICE_MINOR,
.service = &s_httpService,
.available = http_available,
.modDeactivating = http_mod_deactivating,
.modDetached = http_mod_detached,
.frameBegin = http_frame_begin,
.shutdown = http_shutdown,
};
} // namespace dusk::mods::svc
+12
View File
@@ -75,6 +75,17 @@ ModResult item_resolve_check(
return MOD_OK;
}
ModResult item_resolve_check_full(ModContext* context, const char* name, uint8_t originalItemNo,
ItemCheckResolution* outResolution) {
if (mod_from_context(context) == nullptr || !is_valid_check_name(name) ||
outResolution == nullptr)
{
return MOD_INVALID_ARGUMENT;
}
*outResolution = item_check_resolve(name, originalItemNo, nullptr);
return MOD_OK;
}
ModResult item_give_item(
ModContext* context, const char* checkName, uint8_t itemNo, uint32_t flags) {
auto* mod = mod_from_context(context);
@@ -130,6 +141,7 @@ constexpr ItemService s_itemService{
.give_item = item_give_item,
.observe_gives = item_observe_gives,
.unobserve_gives = item_unobserve_gives,
.resolve_check_full = item_resolve_check_full,
};
} // namespace
+5
View File
@@ -138,6 +138,9 @@ const ServiceRecord* find_service_record(const char* serviceId, const uint16_t m
}
ModResult register_module(const ServiceModule& module) {
if (module.available != nullptr && !module.available()) {
return MOD_UNAVAILABLE;
}
const auto result = register_service(
module.id, module.majorVersion, module.minorVersion, module.service, nullptr, false);
if (result != MOD_OK) {
@@ -210,6 +213,8 @@ void ModLoader::init_services() {
&svc::g_hostModule,
&svc::g_logModule,
&svc::g_resourceModule,
&svc::g_fileModule,
&svc::g_httpModule,
&svc::g_hookModule,
&svc::g_overlayModule,
&svc::g_textureModule,
+4
View File
@@ -26,6 +26,8 @@ struct ServiceModule {
uint16_t minorVersion = 0;
const void* service = nullptr;
// False prevents registration when a platform dependency is unavailable.
bool (*available)() = nullptr;
// One-time setup, at registration (ModLoader::init_services).
void (*initialize)() = nullptr;
// A mod is beginning deactivation: stop callbacks that may execute concurrently. Service state
@@ -68,6 +70,8 @@ void modules_shutdown();
extern const ServiceModule g_hostModule;
extern const ServiceModule g_logModule;
extern const ServiceModule g_resourceModule;
extern const ServiceModule g_fileModule;
extern const ServiceModule g_httpModule;
extern const ServiceModule g_hookModule;
extern const ServiceModule g_overlayModule;
extern const ServiceModule g_textureModule;
+1
View File
@@ -200,6 +200,7 @@ void save_slot_written(uint32_t slot, const void* slotData) {
store.snapshotCrc = utils::crc32(slotData, kQuestLogSize);
}
flush_sidecar();
s_currentSlot = static_cast<int32_t>(slot);
notify(slot, &SaveObserverRecord::onWritten, "save-written");
}
+25 -1
View File
@@ -45,6 +45,8 @@ constexpr size_t kUiControlSelectedSize =
offsetof(UiControlDesc, is_selected) + sizeof(UiPredicateFn);
constexpr size_t kUiControlStringSetModeSize =
offsetof(UiControlDesc, string_set_mode) + sizeof(UiStringSetMode);
constexpr size_t kUiControlFilePickerSize =
offsetof(UiControlDesc, directory_mode) + sizeof(bool);
constexpr size_t kUiListItemV21Size = offsetof(UiListItem, label) + sizeof(const char*);
constexpr size_t kUiListDescV21Size = offsetof(UiListDesc, user_data) + sizeof(void*);
@@ -303,6 +305,7 @@ void wire_callback_binding(
break;
case UI_CONTROL_STRING:
case UI_CONTROL_COLOR:
case UI_CONTROL_FILE_PICKER:
spec.getString = [getValue]() -> Rml::String {
const UiControlValue value = getValue();
return value.string_value != nullptr ? value.string_value : "";
@@ -382,7 +385,8 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC
return true;
}
case UI_CONTROL_STRING:
case UI_CONTROL_COLOR: {
case UI_CONTROL_COLOR:
case UI_CONTROL_FILE_PICKER: {
const auto find = [modPtr, varHandle] {
return static_cast<ConfigVar<std::string>*>(
config_find_var(*modPtr, varHandle, CONFIG_VAR_STRING));
@@ -642,6 +646,14 @@ ModResult ui_pane_add_control(
spec.colorPresets.emplace_back(desc.color_presets[i]);
}
break;
case UI_CONTROL_FILE_PICKER:
spec.kind = ui::ModControlSpec::Kind::FilePicker;
spec.directoryMode = desc.directory_mode;
for (size_t i = 0; i < desc.file_filter_count; ++i) {
spec.fileFilters.push_back(
{desc.file_filters[i].name, desc.file_filters[i].pattern});
}
break;
case UI_CONTROL_SELECT:
spec.kind = ui::ModControlSpec::Kind::Select;
if (slot->helpPane == nullptr) {
@@ -1257,6 +1269,18 @@ bool valid_control_desc(const UiControlDesc& desc) {
return false;
}
break;
case UI_CONTROL_FILE_PICKER:
if (desc.struct_size < kUiControlFilePickerSize ||
(desc.file_filter_count != 0 && desc.file_filters == nullptr))
{
return false;
}
for (size_t i = 0; i < desc.file_filter_count; ++i) {
if (desc.file_filters[i].name == nullptr || desc.file_filters[i].pattern == nullptr) {
return false;
}
}
break;
default:
return false;
}
+28 -5
View File
@@ -1,8 +1,12 @@
#include "dusk/settings.h"
#include <aurora/aurora.h>
#include "dusk/config.hpp"
#include "dusk/ui/ui.hpp"
#include "dusk/game_mode.hpp"
#include "dusk/texture_replacements.hpp"
#include "dusk/ui/ui.hpp"
#include <aurora/aurora.h>
#include <dolphin/vi.h>
namespace dusk {
@@ -221,6 +225,22 @@ UserSettings& getSettings() {
return g_userSettings;
}
void applyInternalResolutionScale(int scale) {
VISetFrameBufferScale(static_cast<float>(scale));
}
void applyResampler(Resampler resampler) {
switch (resampler) {
case Resampler::Area:
aurora_set_resampler(SAMPLER_AREA);
break;
case Resampler::Bilinear:
default:
aurora_set_resampler(SAMPLER_BILINEAR);
break;
}
}
void registerSettings() {
// Video
Register(g_userSettings.video.enableFullscreen);
@@ -286,9 +306,12 @@ void registerSettings() {
Register(g_userSettings.game.bloomMultiplier);
Register(g_userSettings.game.depthOfFieldMode);
Register(g_userSettings.game.disableWaterRefraction);
Register(g_userSettings.game.enableTextureReplacements);
Register(g_userSettings.game.internalResolutionScale);
Register(g_userSettings.game.resampler);
Register(g_userSettings.game.enableTextureReplacements,
[](const bool&, const bool&) { texture_replacements::reload(); });
Register(g_userSettings.game.internalResolutionScale,
[](const int& value, const int&) { applyInternalResolutionScale(value); });
Register(g_userSettings.game.resampler,
[](const Resampler& value, const Resampler&) { applyResampler(value); });
Register(g_userSettings.game.shadowResolutionMultiplier);
Register(g_userSettings.game.enableMapBackground);
Register(g_userSettings.game.disableCutscenePillarboxing);
+3
View File
@@ -318,6 +318,9 @@ UserSettings& getSettings();
void registerSettings();
void applyInternalResolutionScale(int scale);
void applyResampler(Resampler resampler);
// Transient settings
struct CollisionViewSettings {
+5
View File
@@ -19,6 +19,8 @@ static void onSpeedrunModeDeactive() {
if (getSettings().game.liveSplitEnabled) {
speedrun::disconnectLiveSplit();
}
g_speedrunInfo.reset();
reset();
}
void registerSpeedrunGameMode() {
@@ -82,6 +84,9 @@ void resetForSpeedrunMode() {
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
getSettings().game.recordingMode.setSpeedrunValue(false);
getSettings().game.debugFlyCam.setSpeedrunValue(false);
getSettings().game.enableMoveLinkCombo.setSpeedrunValue(false);
getSettings().game.enableTeleportCombo.setSpeedrunValue(false);
}
static void clearSpeedrunOverrides() {
-5
View File
@@ -26,11 +26,6 @@ void reload() {
s_directoryGroup.registrations.size());
}
void set_enabled(bool enabled) {
getSettings().game.enableTextureReplacements.setValue(enabled);
reload();
}
void shutdown() {
aurora::texture::unregister_replacements(s_directoryGroup);
s_directoryGroup.registrations.clear();
-1
View File
@@ -8,7 +8,6 @@ namespace dusk::texture_replacements {
inline constexpr int32_t kUserTextureReplacementPriority = -1'000'000;
void reload();
void set_enabled(bool enabled);
void shutdown();
}
+71
View File
@@ -0,0 +1,71 @@
#include "file_button.hpp"
#include <aurora/lib/window.hpp>
#include <borealis/io.hpp>
#include <utility>
namespace dusk::ui {
FileButton::FileButton(Rml::Element* parent, Props props)
: BaseControlledSelectButton{parent, {.key = std::move(props.key)}},
mGetValue{std::move(props.getValue)}, mSetValue{std::move(props.setValue)},
mIsDisabled{std::move(props.isDisabled)}, mIsModified{std::move(props.isModified)},
mFilters{std::move(props.filters)}, mDirectoryMode{props.directoryMode} {}
bool FileButton::modified() const {
return mIsModified ? mIsModified() : BaseControlledSelectButton::modified();
}
bool FileButton::disabled() const {
return borealis::file_select::busy() || (mIsDisabled && mIsDisabled());
}
Rml::String FileButton::format_value() {
const Rml::String location = mGetValue ? mGetValue() : "";
if (location.empty()) {
return "(none)";
}
const auto name = borealis::io::display_name(location);
return name.empty() ? location : name;
}
bool FileButton::handle_nav_command(NavCommand command) {
if (command != NavCommand::Confirm) {
return false;
}
open_picker();
return true;
}
void FileButton::open_picker() {
if (disabled()) {
return;
}
const std::string defaultLocation = mGetValue ? mGetValue() : "";
auto complete = [setValue = mSetValue](borealis::file_select::Result result) {
if (result.status == borealis::file_select::Status::Selected && !result.locations.empty() &&
setValue)
{
setValue(std::move(result.locations.front()));
}
};
if (mDirectoryMode) {
borealis::file_select::open_folder(
{
.parentWindow = aurora::window::get_sdl_window(),
.defaultLocation = defaultLocation,
},
std::move(complete));
return;
}
borealis::file_select::open_file(
{
.parentWindow = aurora::window::get_sdl_window(),
.filters = mFilters,
.defaultLocation = defaultLocation,
},
std::move(complete));
}
} // namespace dusk::ui
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "select_button.hpp"
#include <borealis/file_select.hpp>
#include <functional>
#include <vector>
namespace dusk::ui {
class FileButton : public BaseControlledSelectButton {
public:
struct Props {
Rml::String key;
std::function<Rml::String()> getValue;
std::function<void(Rml::String)> setValue;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
std::vector<borealis::file_select::Filter> filters;
bool directoryMode = false;
};
FileButton(Rml::Element* parent, Props props);
bool modified() const override;
bool disabled() const override;
protected:
Rml::String format_value() override;
bool handle_nav_command(NavCommand command) override;
private:
void open_picker();
std::function<Rml::String()> mGetValue;
std::function<void(Rml::String)> mSetValue;
std::function<bool()> mIsDisabled;
std::function<bool()> mIsModified;
std::vector<borealis::file_select::Filter> mFilters;
bool mDirectoryMode = false;
};
} // namespace dusk::ui
+146 -127
View File
@@ -1,20 +1,18 @@
#include "graphics_tuner.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <aurora/aurora.h>
#include <aurora/gfx.h>
#include <dolphin/gx/GXAurora.h>
#include <dolphin/vi.h>
#include <fmt/format.h>
#include "button.hpp"
#include "dusk/config.hpp"
#include "dusk/logging.h"
#include "dusk/settings.h"
#include "dusk/texture_replacements.hpp"
#include "m_Do/m_Do_audio.h"
#include <dolphin/gx/GXAurora.h>
#include <fmt/format.h>
#include <algorithm>
#include <string>
#include <type_traits>
namespace dusk::ui {
namespace {
@@ -40,70 +38,84 @@ const Rml::String kDocumentSource = R"RML(
</rml>
)RML";
int get_value(GraphicsOption option) {
switch (option) {
case GraphicsOption::InternalResolution:
return getSettings().game.internalResolutionScale.getValue();
case GraphicsOption::ShadowResolution:
return getSettings().game.shadowResolutionMultiplier.getValue();
case GraphicsOption::Resampler:
return static_cast<int>(getSettings().game.resampler.getValue());
case GraphicsOption::BloomMode:
return static_cast<int>(getSettings().game.bloomMode.getValue());
case GraphicsOption::BloomMultiplier:
return std::clamp(
static_cast<int>(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0,
100);
case GraphicsOption::DepthOfFieldMode:
return static_cast<int>(getSettings().game.depthOfFieldMode.getValue());
case GraphicsOption::TextureReplacements:
return getSettings().game.enableTextureReplacements.getValue();
Rml::String format_internal_resolution(int value) {
u32 width = 0;
u32 height = 0;
AuroraGetRenderSize(&width, &height);
if (value <= 0) {
return fmt::format("Auto ({}×{})", width, height);
}
return 0;
return fmt::format("{}× ({}×{})", value, width, height);
}
void set_value(GraphicsOption option, int value) {
switch (option) {
case GraphicsOption::InternalResolution:
getSettings().game.internalResolutionScale.setValue(value);
VISetFrameBufferScale(static_cast<float>(value));
break;
case GraphicsOption::ShadowResolution:
getSettings().game.shadowResolutionMultiplier.setValue(value);
break;
case GraphicsOption::Resampler: {
const auto sampler = static_cast<Resampler>(std::clamp(value,
static_cast<int>(Resampler::Bilinear),
static_cast<int>(Resampler::Area)));
getSettings().game.resampler.setValue(sampler);
switch (sampler) {
case Resampler::Area:
aurora_set_resampler(SAMPLER_AREA);
break;
case Resampler::Bilinear:
default:
aurora_set_resampler(SAMPLER_BILINEAR);
break;
}
break;
Rml::String format_resampler(int value) {
switch (static_cast<Resampler>(value)) {
case Resampler::Bilinear:
return "Bilinear";
case Resampler::Area:
return "Area";
default:
return "";
}
case GraphicsOption::BloomMode:
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
value, static_cast<int>(BloomMode::Off), static_cast<int>(BloomMode::Dusk))));
break;
case GraphicsOption::DepthOfFieldMode:
getSettings().game.depthOfFieldMode.setValue(static_cast<DepthOfFieldMode>(std::clamp(
value, static_cast<int>(DepthOfFieldMode::Off), static_cast<int>(DepthOfFieldMode::Dusk))));
break;
case GraphicsOption::BloomMultiplier:
getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
break;
case GraphicsOption::TextureReplacements:
texture_replacements::set_enabled(static_cast<bool>(value));
break;
}
Rml::String format_post_process_mode(int value) {
switch (static_cast<BloomMode>(value)) {
case BloomMode::Off:
return "Off";
case BloomMode::Classic:
return "Classic";
case BloomMode::Dusk:
return "Dusklight";
default:
return "";
}
}
Rml::String format_times(int value) { return fmt::format("{}×", value); }
Rml::String format_percent(int value) { return fmt::format("{}%", value); }
Rml::String format_bool(int value) { return value ? "On" : "Off"; }
template <typename T>
int read_cvar(const ConfigVar<T>& var) {
if constexpr (std::is_same_v<T, float>) {
return static_cast<int>(var.getValue() * 100.0f + 0.5f);
} else {
return static_cast<int>(var.getValue());
}
}
template <typename T>
void write_cvar(ConfigVar<T>& var, int value) {
if constexpr (std::is_same_v<T, float>) {
var.setValue(static_cast<float>(value) / 100.0f);
} else if constexpr (std::is_same_v<T, bool>) {
var.setValue(static_cast<bool>(value));
} else {
var.setValue(static_cast<T>(value));
}
}
template <auto Var, typename Min, typename Max, typename Def>
const GraphicsSetting& bind(Min min, Max max, Def def, int step, Rml::String (*label)(int),
bool watchSize = false) {
static const GraphicsSetting desc{
.min = static_cast<int>(min),
.max = static_cast<int>(max),
.defaultValue = static_cast<int>(def),
.step = step,
.watchesRenderSize = watchSize,
.read = []() -> int { return read_cvar(Var()); },
.write = [](int value) { write_cvar(Var(), value); },
.label = label,
.cvarName = []() -> const char* { return Var().getName(); },
.isModified = []() -> bool { return Var().getValue() != Var().getDefaultValue(); },
};
return desc;
}
Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
auto* doc = parent->GetOwnerDocument();
auto root = doc->CreateElement("div");
@@ -129,6 +141,35 @@ void update_carousel_arrow_color(Rml::Element* arrow, bool dim) {
} // namespace
const GraphicsSetting& GraphicsSetting::of(GraphicsOption option) {
switch (option) {
case GraphicsOption::InternalResolution:
return bind<[]() -> auto& { return getSettings().game.internalResolutionScale; }>(
0, 12, 0, 1, format_internal_resolution, true);
case GraphicsOption::ShadowResolution:
return bind<[]() -> auto& { return getSettings().game.shadowResolutionMultiplier; }>(
1, 8, 1, 1, format_times);
case GraphicsOption::Resampler:
return bind<[]() -> auto& { return getSettings().game.resampler; }>(
Resampler::Bilinear, Resampler::Area, Resampler::Bilinear, 1, format_resampler);
case GraphicsOption::BloomMode:
return bind<[]() -> auto& { return getSettings().game.bloomMode; }>(
BloomMode::Off, BloomMode::Dusk, BloomMode::Classic, 1, format_post_process_mode);
case GraphicsOption::BloomMultiplier:
return bind<[]() -> auto& { return getSettings().game.bloomMultiplier; }>(
0, 100, 100, 10, format_percent);
case GraphicsOption::DepthOfFieldMode:
return bind<[]() -> auto& { return getSettings().game.depthOfFieldMode; }>(
DepthOfFieldMode::Off, DepthOfFieldMode::Dusk, DepthOfFieldMode::Classic, 1,
format_post_process_mode);
case GraphicsOption::TextureReplacements:
return bind<[]() -> auto& { return getSettings().game.enableTextureReplacements; }>(
0, 1, 0, 1, format_bool);
}
DuskLog.error("{} is an invalid GraphicsOption", static_cast<int>(option));
abort();
}
SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
: Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
mPrevElem = create_stepped_carousel_arrow(mRoot, "prev", "&#xe5cb;");
@@ -152,7 +193,9 @@ bool SteppedCarousel::focus() {
return Component::focus();
}
void SteppedCarousel::update() {
void SteppedCarousel::update() {}
void SteppedCarousel::refresh() {
if (mValueElem == nullptr) {
return;
}
@@ -194,59 +237,9 @@ void SteppedCarousel::apply(int value) {
}
}
Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
switch (option) {
case GraphicsOption::InternalResolution: {
u32 width = 0;
u32 height = 0;
AuroraGetRenderSize(&width, &height);
if (value <= 0) {
return fmt::format("Auto ({}×{})", width, height);
} else {
return fmt::format("{}× ({}×{})", value, width, height);
}
}
case GraphicsOption::ShadowResolution:
return fmt::format("{}×", value);
case GraphicsOption::Resampler:
switch (static_cast<Resampler>(value)) {
case Resampler::Bilinear:
return "Bilinear";
case Resampler::Area:
return "Area";
}
break;
case GraphicsOption::BloomMode:
switch (static_cast<BloomMode>(value)) {
case BloomMode::Off:
return "Off";
case BloomMode::Classic:
return "Classic";
case BloomMode::Dusk:
return "Dusklight";
}
break;
case GraphicsOption::DepthOfFieldMode:
switch (static_cast<DepthOfFieldMode>(value)) {
case DepthOfFieldMode::Off:
return "Off";
case DepthOfFieldMode::Classic:
return "Classic";
case DepthOfFieldMode::Dusk:
return "Dusklight";
}
break;
case GraphicsOption::BloomMultiplier:
return fmt::format("{}%", value);
case GraphicsOption::TextureReplacements:
return static_cast<bool>(value) ? "On" : "Off";
}
return "";
}
GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
: Document(kDocumentSource, false, DocumentScope::GraphicsTuner), mOption(props.option),
mValueMin(props.valueMin), mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
: Document(kDocumentSource, false, DocumentScope::GraphicsTuner),
mSetting(GraphicsSetting::of(props.option)) {
if (mDocument == nullptr) {
return;
}
@@ -260,13 +253,12 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
mCarousel = &add_component<SteppedCarousel>(carouselParent,
SteppedCarousel::Props{
.min = mValueMin,
.max = mValueMax,
.step = props.step,
.getValue = [this] { return get_value(mOption); },
.onChange = [this](int value) { set_value(mOption, value); },
.formatValue =
[this](int value) { return format_graphics_setting_value(mOption, value); },
.min = mSetting.min,
.max = mSetting.max,
.step = mSetting.step,
.getValue = [this] { return mSetting.read(); },
.onChange = [this](int value) { mSetting.set(value); },
.formatValue = [this](int value) { return mSetting.label(value); },
});
}
@@ -282,6 +274,17 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
resetButton.root()->SetClass("reset", true);
}
if (mCarousel != nullptr) {
if (const char* name = mSetting.cvarName()) {
mSubscription = config::subscribe(name,
[this](config::ConfigVarBase&, const void*) { mCarousel->refresh(); });
}
mCarousel->refresh();
if (mSetting.watchesRenderSize) {
AuroraGetRenderSize(&mLastRenderWidth, &mLastRenderHeight);
}
}
// Hide document after transition completion
mRoot = mDocument->GetElementById("root");
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
@@ -293,6 +296,12 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
});
}
GraphicsTuner::~GraphicsTuner() {
if (mSubscription != 0) {
config::unsubscribe(mSubscription);
}
}
void GraphicsTuner::show() {
Document::show();
mRoot->SetAttribute("open", "");
@@ -309,6 +318,16 @@ void GraphicsTuner::hide(bool close) {
}
void GraphicsTuner::update() {
if (mSetting.watchesRenderSize && mCarousel != nullptr) {
u32 width = 0;
u32 height = 0;
AuroraGetRenderSize(&width, &height);
if (width != mLastRenderWidth || height != mLastRenderHeight) {
mLastRenderWidth = width;
mLastRenderHeight = height;
mCarousel->refresh();
}
}
for (const auto& component : mComponents) {
component->update();
}
@@ -342,7 +361,7 @@ bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) {
}
void GraphicsTuner::reset_default() {
set_value(mOption, mDefaultValue);
mSetting.set(mSetting.defaultValue);
}
} // namespace dusk::ui
+25 -11
View File
@@ -1,10 +1,10 @@
#pragma once
#include "button.hpp"
#include "component.hpp"
#include "document.hpp"
#include "ui.hpp"
#include <algorithm>
#include <functional>
#include <memory>
#include <type_traits>
@@ -28,6 +28,7 @@ public:
bool focus() override;
void update() override;
void refresh();
bool handle_nav_command(NavCommand cmd);
private:
@@ -49,21 +50,34 @@ enum class GraphicsOption {
TextureReplacements,
};
Rml::String format_graphics_setting_value(GraphicsOption option, int value);
struct GraphicsSetting {
int min = 0;
int max = 0;
int defaultValue = 0;
int step = 1;
bool watchesRenderSize = false;
int (*read)() = nullptr;
void (*write)(int) = nullptr;
Rml::String (*label)(int) = nullptr;
const char* (*cvarName)() = nullptr;
bool (*isModified)() = nullptr;
static const GraphicsSetting& of(GraphicsOption option);
void set(int value) const { write(std::clamp(value, min, max)); }
Rml::String text() const { return label(read()); }
};
struct GraphicsTunerProps {
GraphicsOption option;
Rml::String title;
Rml::String helpText;
int valueMin = 0;
int valueMax = 0;
int defaultValue = 0;
int step = 1;
};
class GraphicsTuner : public Document {
public:
explicit GraphicsTuner(GraphicsTunerProps props);
~GraphicsTuner() override;
void show() override;
void hide(bool close) override;
@@ -85,13 +99,13 @@ private:
void reset_default();
GraphicsOption mOption;
int mValueMin = 0;
int mValueMax = 0;
int mDefaultValue = 0;
GraphicsSetting mSetting;
std::vector<std::unique_ptr<Component> > mComponents;
SteppedCarousel* mCarousel;
SteppedCarousel* mCarousel = nullptr;
Rml::Element* mRoot;
u64 mSubscription = 0;
u32 mLastRenderWidth = 0;
u32 mLastRenderHeight = 0;
};
} // namespace dusk::ui
+3 -8
View File
@@ -160,17 +160,12 @@ void MenuBar::build_tabs() {
});
if (dusk::speedrun::isActive()) {
mTabBar->add_tab("Reset Timer", [this] {
mTabBar->add_tab("Reset Run", [this] {
mTabBar->set_active_tab(-1);
mDoAud_seStartMenu(kSoundClick);
dusk::speedrun::g_speedrunInfo.reset();
if (getSettings().game.liveSplitEnabled) {
dusk::speedrun::reset();
}
auto* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e);
if (playScene != nullptr) {
fopScnM_ChangeReq((scene_class*)playScene, fpcNm_NAME_SCENE_e, 0x7FFF, 0);
}
dusk::speedrun::reset();
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
hide(false);
});
}
+12
View File
@@ -2,6 +2,7 @@
#include "bool_button.hpp"
#include "color_input.hpp"
#include "file_button.hpp"
#include "number_button.hpp"
#include "string_button.hpp"
@@ -83,6 +84,17 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
.alpha = s.colorAlpha,
});
break;
case ModControlSpec::Kind::FilePicker:
control = &pane.add_child<FileButton>(FileButton::Props{
.key = s.label,
.getValue = s.getString,
.setValue = s.setString,
.isDisabled = s.isDisabled,
.isModified = s.isModified,
.filters = s.fileFilters,
.directoryMode = s.directoryMode,
});
break;
case ModControlSpec::Kind::Select:
if (helpPane == nullptr || s.options.empty()) {
return nullptr;
+5
View File
@@ -3,6 +3,8 @@
#include "pane.hpp"
#include "window.hpp"
#include <borealis/file_select.hpp>
#include <climits>
namespace dusk::ui {
@@ -16,6 +18,7 @@ struct ModControlSpec {
String,
Select,
Color,
FilePicker,
};
Kind kind = Kind::Button;
@@ -42,6 +45,8 @@ struct ModControlSpec {
bool stringSetOnChange = false;
std::vector<Rml::String> colorPresets;
bool colorAlpha = false;
std::vector<borealis::file_select::Filter> fileFilters;
bool directoryMode = false;
};
Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec);
+17 -2
View File
@@ -5,12 +5,14 @@
#include "fmt/format.h"
#include "logs_window.hpp"
#include "mod_texture_provider.hpp"
#include "mods/svc/http.h"
#include "pane.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <memory>
#include <ranges>
#include <string>
#include <string_view>
@@ -39,6 +41,13 @@ ModStatus mod_status(const mods::LoadedMod& mod) {
return {"", "Disabled"};
}
bool mod_uses_network(const mods::LoadedMod& mod) {
return std::ranges::any_of(
mod.manifestInfo.imports, [](const mods::ModManifestInfo::Import& serviceImport) {
return serviceImport.id == HTTP_SERVICE_ID;
});
}
// Truncates to at most maxBytes without splitting a UTF-8 sequence.
std::string snippet(std::string_view text, size_t maxBytes) {
if (text.size() <= maxBytes) {
@@ -63,16 +72,19 @@ public:
iconRml = R"(<icon class="mod-icon placeholder"/>)";
}
const auto status = mod_status(mod);
const auto networkBadge = mod_uses_network(mod) ?
R"(<span class="mod-entry-network">Network</span>)" :
Rml::String{};
mRoot->SetInnerRML(fmt::format(
R"({})"
R"(<div class="mod-entry-info">)"
R"(<div class="mod-entry-name"><span class="mod-entry-name-text">{}</span>)"
R"(<span class="mod-entry-version">v{}</span></div>)"
R"(<div class="mod-entry-sub">{} - <span class="mod-entry-status {}">{}</span></div>)"
R"(<div class="mod-entry-sub">{} - <span class="mod-entry-status {}">{}</span>{}</div>)"
R"(<div class="mod-entry-desc">{}</div>)"
R"(</div>)",
iconRml, escape(mod.metadata.name), escape(mod.metadata.version),
escape(mod.metadata.author), status.badgeClass, status.text,
escape(mod.metadata.author), status.badgeClass, status.text, networkBadge,
escape(snippet(mod.metadata.description, 90))));
mRoot->SetClass("inactive", !mod.active);
mRoot->SetClass("failed", mod.loadFailed);
@@ -225,6 +237,9 @@ void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
statusBadge = fmt::format(
R"(&nbsp;<span class="status-badge {}">{}</span>)", status.badgeClass, status.text);
}
if (mod_uses_network(mod)) {
statusBadge += R"(&nbsp;<span class="status-badge network">Network</span>)";
}
pane.add_rml(fmt::format(R"(<div class="mod-title">{} )"
R"(<span class="mod-title-version">v{}</span>{}</div>)"
R"(<div class="mod-author">by {}</div>)",
+9 -44
View File
@@ -18,6 +18,7 @@
#include <SDL3/SDL_misc.h>
#include <aurora/lib/window.hpp>
#include <borealis/file_select.hpp>
#include <borealis/io.hpp>
#include <borealis/log.hpp>
#include <borealis/update.hpp>
#include <borealis/version.h>
@@ -130,42 +131,7 @@ struct DiscVerificationTask {
std::unique_ptr<DiscVerificationTask> sDiscVerificationTask;
bool sDiscVerificationModalPushed = false;
struct UpdateCheckTask {
UpdateCheckTask() {
worker = std::thread([this] {
try {
result = borealis::update::check_latest_github_release(AppInfo);
} catch (const std::exception& e) {
result = {
.status = borealis::update::Status::Failed,
.message = fmt::format("Update check failed with exception: {}", e.what()),
};
} catch (...) {
result = {
.status = borealis::update::Status::Failed,
.message = "Update check failed with an unknown exception",
};
}
done.store(true, std::memory_order_release);
});
}
~UpdateCheckTask() { join(); }
void join() {
if (worker.joinable()) {
worker.join();
}
}
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
borealis::update::Result result;
std::atomic_bool done = false;
std::thread worker;
};
std::unique_ptr<UpdateCheckTask> sUpdateCheckTask;
borealis::Task<borealis::update::Result> sUpdateCheck;
std::optional<borealis::update::Result> sUpdateCheckResult;
bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
@@ -243,20 +209,19 @@ void begin_update_check() {
if (!getSettings().backend.checkForUpdates.getValue()) {
return;
}
if (sUpdateCheckTask != nullptr || sUpdateCheckResult.has_value()) {
if (sUpdateCheck || sUpdateCheckResult.has_value()) {
return;
}
sUpdateCheckTask = std::make_unique<UpdateCheckTask>();
sUpdateCheck = borealis::update::check_latest_github_release(AppInfo);
}
std::optional<borealis::update::Result> take_finished_update_check() {
if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) {
if (!sUpdateCheck || !sUpdateCheck.ready()) {
return std::nullopt;
}
sUpdateCheckTask->join();
auto result = std::move(sUpdateCheckTask->result);
sUpdateCheckTask.reset();
auto result = sUpdateCheck.try_take();
sUpdateCheck = {};
return result;
}
@@ -479,7 +444,7 @@ private:
}
if (mFileName != nullptr) {
std::string fileName = borealis::file_select::display_name(sDiscVerificationTask->path);
std::string fileName = borealis::io::display_name(sDiscVerificationTask->path);
if (fileName.empty()) {
fileName = sDiscVerificationTask->path;
}
@@ -1167,7 +1132,7 @@ void Prelaunch::update() {
sUpdateCheckResult = std::move(*result);
}
if (sUpdateCheckTask != nullptr) {
if (sUpdateCheck) {
mUpdateStatus->SetAttribute("state", "checking");
mUpdateMessage->SetInnerRML("Checking for updates...");
} else if (!sUpdateCheckResult.has_value() ||
+12 -45
View File
@@ -231,7 +231,7 @@ Rml::String configured_data_path_display_name() {
return "(none)";
}
auto display = borealis::file_select::display_name(path);
auto display = borealis::io::display_name(path);
if (display.empty()) {
return path;
}
@@ -440,24 +440,15 @@ SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>&
return button;
}
template <typename T>
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& var,
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane,
const GraphicsTunerProps& props) {
const auto setting = GraphicsSetting::of(props.option);
leftPane.register_control(
leftPane
.add_select_button({
.key = props.title,
.getValue =
[&var, option = props.option] {
if constexpr (std::is_same_v<T, float>) {
return format_graphics_setting_value(
option, float_setting_percent(var));
} else {
return format_graphics_setting_value(
option, static_cast<int>(var.getValue()));
}
},
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
.getValue = [setting] { return setting.text(); },
.isModified = [setting] { return setting.isModified(); },
.submit = false,
})
.on_nav_command([&window, props](Rml::Event&, NavCommand cmd) {
@@ -493,7 +484,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
if (path.empty()) {
display = "(none)";
} else {
display = borealis::file_select::display_name(path);
display = borealis::io::display_name(path);
if (display.empty()) {
display = path;
}
@@ -539,6 +530,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
{
.parentWindow = aurora::window::get_sdl_window(),
.defaultLocation = defaultLocation,
.requireRealPath = true,
},
&data_folder_dialog_callback);
});
@@ -778,75 +770,50 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
leftPane.add_section("Resolution");
graphics_tuner_control(*this, leftPane, rightPane,
getSettings().game.internalResolutionScale,
GraphicsTunerProps{
.option = GraphicsOption::InternalResolution,
.title = "Internal Resolution",
.helpText = kInternalResolutionHelpText,
.valueMin = 0,
.valueMax = 12,
.defaultValue = 0,
});
graphics_tuner_control(*this, leftPane, rightPane,
getSettings().game.shadowResolutionMultiplier,
GraphicsTunerProps{
.option = GraphicsOption::ShadowResolution,
.title = "Shadow Resolution",
.helpText = kShadowResolutionHelpText,
.valueMin = 1,
.valueMax = 8,
.defaultValue = 1,
});
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.resampler,
graphics_tuner_control(*this, leftPane, rightPane,
GraphicsTunerProps{
.option = GraphicsOption::Resampler,
.title = "Output Resampling",
.helpText = kResamplerHelpText,
.valueMin = static_cast<int>(Resampler::Bilinear),
.valueMax = static_cast<int>(Resampler::Area),
.defaultValue = static_cast<int>(Resampler::Bilinear),
});
leftPane.add_section("Post-Processing");
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
graphics_tuner_control(*this, leftPane, rightPane,
GraphicsTunerProps{
.option = GraphicsOption::BloomMode,
.title = "Bloom",
.helpText = kBloomHelpText,
.valueMin = static_cast<int>(BloomMode::Off),
.valueMax = static_cast<int>(BloomMode::Dusk),
.defaultValue = static_cast<int>(BloomMode::Classic),
});
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
graphics_tuner_control(*this, leftPane, rightPane,
GraphicsTunerProps{
.option = GraphicsOption::BloomMultiplier,
.title = "Bloom Brightness",
.helpText = kBloomBrightnessHelpText,
.valueMin = 0,
.valueMax = 100,
.defaultValue = 100,
.step = 10,
});
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.depthOfFieldMode,
graphics_tuner_control(*this, leftPane, rightPane,
GraphicsTunerProps{
.option = GraphicsOption::DepthOfFieldMode,
.title = "Depth of Field",
.helpText = kDepthOfFieldHelpText,
.valueMin = static_cast<int>(DepthOfFieldMode::Off),
.valueMax = static_cast<int>(DepthOfFieldMode::Dusk),
.defaultValue = static_cast<int>(DepthOfFieldMode::Classic),
});
leftPane.add_section("Rendering");
graphics_tuner_control(*this, leftPane, rightPane,
getSettings().game.enableTextureReplacements,
GraphicsTunerProps{
.option = GraphicsOption::TextureReplacements,
.title = "Enable Texture Replacements",
.helpText = kTextureReplacementHelpText,
.valueMin = static_cast<int>(false),
.valueMax = static_cast<int>(true),
.defaultValue = static_cast<int>(false),
});
leftPane.register_control(
leftPane.add_select_button({
@@ -1239,7 +1206,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
{
.key = "Speedrun Mode",
.helpText =
"Enables speedrunning options while restricting certain gameplay modifiers.",
"Enables Speedrun game mode option in the Dusklight launch menu.",
.onChange =
[this](bool enabled) {
if (enabled) {
+1
View File
@@ -1621,6 +1621,7 @@ fpc_ProcID fopAcM_createItemForBoss(const cXyz* i_pos, int i_itemNo, int i_roomN
if (i_itemNo != originalItemNo) {
const u32 params = 0xFFFF0000 | param_8 << 8 | (i_itemNo & 0xFF);
dusk::mods::set_boss_item_actor_speeds(i_speedF, i_speedY);
return fopAcM_create(fpcNm_Obj_LifeContainer_e, params, i_pos, i_roomNo, i_angle,
i_scale, -1, giveTag, originalItemNo);
}
+44 -31
View File
@@ -47,6 +47,7 @@
#include <borealis/aurora_log.h>
#include <borealis/cli.hpp>
#include <borealis/crash.hpp>
#include <borealis/http.hpp>
#include <borealis/io.hpp>
#include <borealis/sentry.hpp>
#include <borealis/version.h>
@@ -747,16 +748,8 @@ int game_main(int argc, char* argv[]) {
} else {
AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH);
}
VISetFrameBufferScale(dusk::getSettings().game.internalResolutionScale.getValue());
switch (dusk::getSettings().game.resampler.getValue()) {
case dusk::Resampler::Area:
aurora_set_resampler(SAMPLER_AREA);
break;
case dusk::Resampler::Bilinear:
default:
aurora_set_resampler(SAMPLER_BILINEAR);
break;
}
dusk::applyInternalResolutionScale(dusk::getSettings().game.internalResolutionScale.getValue());
dusk::applyResampler(dusk::getSettings().game.resampler.getValue());
dusk::audio::SetMasterVolume(dusk::audio::MasterVolumeToLinear(dusk::getSettings().audio.masterVolume / 100.0f));
dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb);
@@ -777,6 +770,10 @@ int game_main(int argc, char* argv[]) {
return 0;
}
if (borealis::http::available() && !borealis::http::initialize()) {
DuskLog.warn("Failed to initialize the HTTP worker pool");
}
if (dusk::getSettings().game.enableHighQualityMinimapTextures.getValue()) {
dusk::hq_minimap::set_active(true);
}
@@ -794,12 +791,19 @@ int game_main(int argc, char* argv[]) {
bool forcePreLaunchUI = false;
bool saveConfigBeforePrelaunch = false;
const std::string p = dusk::getSettings().backend.isoPath;
borealis::io::PathAccess dvdPathAccess;
const auto resolveDvdLocation = [&dvdPathAccess](const std::string& location) {
dvdPathAccess = borealis::io::access_path(location);
return dvdPathAccess ? borealis::io::fs_path_to_string(dvdPathAccess.path()) : location;
};
const std::string savedLocation = dusk::getSettings().backend.isoPath;
dusk::iso::DiscInfo discInfo{};
if (!p.empty() &&
dusk::iso::inspect(p.c_str(), discInfo) != dusk::iso::ValidationError::Success)
if (!savedLocation.empty() &&
dusk::iso::inspect(savedLocation.c_str(), discInfo) != dusk::iso::ValidationError::Success)
{
DuskLog.warn("Saved DVD image path failed validation, clearing configured path: {}", p);
DuskLog.warn("Saved DVD image location failed validation, clearing it: {}",
borealis::io::display_name(savedLocation));
dusk::getSettings().backend.isoPath.setValue("");
dusk::getSettings().backend.isoVerification.setValue(dusk::DiscVerificationState::Unknown);
forcePreLaunchUI = true;
@@ -808,18 +812,23 @@ int game_main(int argc, char* argv[]) {
bool skipPreLaunchUI = dusk::getSettings().backend.skipPreLaunchUI.getValue();
std::string dvd_path = dusk::getSettings().backend.isoPath;
std::string dvdLocation = dusk::getSettings().backend.isoPath;
std::string dvdPath = resolveDvdLocation(dvdLocation);
bool dvd_opened = false;
if (parsed_arg_options.count("dvd")) {
dvd_path = parsed_arg_options["dvd"].as<std::string>();
if (dusk::iso::inspect(dvd_path.c_str(), discInfo) == dusk::iso::ValidationError::Success) {
DuskLog.info("Loading DVD image from command line: {}", dvd_path);
dvd_opened = aurora_dvd_open(dvd_path.c_str());
dvdLocation = parsed_arg_options["dvd"].as<std::string>();
dvdPath = resolveDvdLocation(dvdLocation);
if (dusk::iso::inspect(dvdLocation.c_str(), discInfo) ==
dusk::iso::ValidationError::Success)
{
DuskLog.info("Loading DVD image from command line: {}", dvdPath);
dvd_opened = aurora_dvd_open(dvdPath.c_str());
if (!dvd_opened) {
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI",
dvdPath);
forcePreLaunchUI = true;
} else {
dusk::getSettings().backend.isoPath.setValue(dvd_path);
dusk::getSettings().backend.isoPath.setValue(dvdLocation);
dusk::getSettings().backend.isoVerification.setValue(
dusk::DiscVerificationState::Unknown);
dusk::config::save();
@@ -827,13 +836,14 @@ int game_main(int argc, char* argv[]) {
skipPreLaunchUI = true;
}
} else {
DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path);
DuskLog.warn(
"DVD image from command line failed validation: {}, opening prelaunch UI", dvdPath);
forcePreLaunchUI = true;
}
}
// If we can't load right into the game, stop requesting to load a stage or save
if (forcePreLaunchUI || dvd_path.empty()) {
if (forcePreLaunchUI || dvdPath.empty()) {
if (dusk::StageRequested.set) {
DuskLog.warn("Cannot load stage {} because no iso path is set, opening prelaunch UI",dusk::StageRequested.stage);
dusk::StageRequested = {};
@@ -880,6 +890,7 @@ int game_main(int argc, char* argv[]) {
// pre game launch ui main loop
if (!launchUILoop()) {
borealis::http::shutdown();
borealis::sentry::shutdown();
borealis::log::shutdown();
fflush(stdout);
@@ -893,18 +904,19 @@ int game_main(int argc, char* argv[]) {
}
}
dvd_path = dusk::getSettings().backend.isoPath;
if (dvd_path.empty()) {
dvdLocation = dusk::getSettings().backend.isoPath;
dvdPath = resolveDvdLocation(dvdLocation);
if (dvdPath.empty()) {
DuskLog.fatal("No DVD image specified, unable to boot!");
}
if (!dusk::IsGameLaunched &&
dusk::iso::inspect(dvd_path.c_str(), discInfo) != dusk::iso::ValidationError::Success)
if (!dusk::IsGameLaunched && dusk::iso::inspect(dvdLocation.c_str(), discInfo) !=
dusk::iso::ValidationError::Success)
{
DuskLog.fatal("DVD image failed validation: {}", dvd_path);
DuskLog.fatal("DVD image failed validation: {}", dvdPath);
}
DuskLog.info("Loading DVD image: {}", dvd_path);
if (!aurora_dvd_open(dvd_path.c_str())) {
DuskLog.fatal("Failed to open DVD image: {}", dvd_path);
DuskLog.info("Loading DVD image: {}", dvdPath);
if (!aurora_dvd_open(dvdPath.c_str())) {
DuskLog.fatal("Failed to open DVD image: {}", dvdPath);
}
dusk::IsGameLaunched = true;
@@ -969,6 +981,7 @@ int game_main(int argc, char* argv[]) {
OSReport("Starting main01 (Game Loop)...\n");
main01();
borealis::http::shutdown();
// We need to cleanly shut down the threads to avoid crashes on shutdown.
if (daMP_c::m_myObj) {