mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-09-02 00:38:46 -04:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f83664074e | |||
| a5eca78939 | |||
| 6cc568d081 | |||
| d60cc9095f | |||
| 0fd17fe082 | |||
| 455150ad90 | |||
| b76917ed48 | |||
| ba44971bde | |||
| a746b23944 | |||
| 6c966876b9 | |||
| 2570c8f072 | |||
| 2e19ffbf50 | |||
| 01e3c4616f | |||
| 57f0185a3c | |||
| 48171f0f0a | |||
| a29f13452f | |||
| 905da94b40 | |||
| 4a632ff5dc | |||
| 93f9ad6f1d |
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
_mod_ctx
|
||||
_mod_initialize
|
||||
_mod_meta
|
||||
_mod_shutdown
|
||||
_mod_update
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
global:
|
||||
mod_ctx;
|
||||
mod_initialize;
|
||||
mod_meta;
|
||||
mod_shutdown;
|
||||
mod_update;
|
||||
local:
|
||||
*;
|
||||
};
|
||||
@@ -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")
|
||||
|
||||
@@ -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 ()
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 2c9c95cfed...f8573d34e6
Vendored
+1
-1
Submodule extern/borealis updated: 6fd955e7e6...4b76f9ea35
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -114,7 +114,7 @@ public:
|
||||
void himoCalc();
|
||||
void adjustShapeAngle() {}
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x0B48 */ Z2Creature mCreatureSound;
|
||||
/* 0x0BD8 */ daNpcF_MatAnm_c* mpMatAnm;
|
||||
/* 0x0BDC */ mDoExt_invisibleModel mInvisibleModel;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
current.pos = new_pos;
|
||||
}
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x92C */ fpc_ProcID mItemId;
|
||||
/* 0x930 */ int mCounter;
|
||||
/* 0x934 */ u8 mStatus;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -17,5 +17,4 @@ add_mod(ao_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
BUNDLE
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Submodule
+1
Submodule mods/randomizer added at 61ba8c4058
@@ -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,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.
@@ -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
|
||||
|
||||
Vendored
+202
-114
@@ -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" "$@"
|
||||
|
||||
Vendored
+46
-43
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
¤t.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(¤t.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);
|
||||
|
||||
@@ -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(¤t.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(
|
||||
¤t.pos, i_itemNo, 0, -1, -1, NULL, NULL, itemGiveTag);
|
||||
}
|
||||
#else
|
||||
mItemID = fopAcM_createItemForPresentDemo(¤t.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);
|
||||
|
||||
@@ -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(
|
||||
¤t.pos, item_no, 0, -1, -1, NULL, NULL, itemGiveTag);
|
||||
}
|
||||
#else
|
||||
mItemPartnerId = fopAcM_createItemForPresentDemo(
|
||||
¤t.pos, item_no, 0, -1, -1, NULL, NULL);
|
||||
#endif
|
||||
mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, item_no,
|
||||
0, -1, -1, NULL,
|
||||
NULL IF_DUSK_ARG(itemGiveTag));
|
||||
}
|
||||
|
||||
if (fopAcM_IsExecuting(mItemPartnerId)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -318,6 +318,9 @@ UserSettings& getSettings();
|
||||
|
||||
void registerSettings();
|
||||
|
||||
void applyInternalResolutionScale(int scale);
|
||||
void applyResampler(Resampler resampler);
|
||||
|
||||
// Transient settings
|
||||
|
||||
struct CollisionViewSettings {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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", "");
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"( <span class="status-badge {}">{}</span>)", status.badgeClass, status.text);
|
||||
}
|
||||
if (mod_uses_network(mod)) {
|
||||
statusBadge += R"( <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>)",
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user