Merge branch 'TwilitRealm:main' into hold-to-mash

This commit is contained in:
baxlek
2026-09-03 08:52:50 -05:00
committed by GitHub
122 changed files with 5455 additions and 715 deletions
+3
View File
@@ -4,3 +4,6 @@
[submodule "extern/borealis"]
path = extern/borealis
url = https://github.com/encounter/borealis.git
[submodule "mods/randomizer"]
path = mods/randomizer
url = https://github.com/TwilitRealm/dusklight-randomizer.git
+85 -74
View File
@@ -91,7 +91,7 @@ option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled
option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON)
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
option(DUSK_ENABLE_CODE_MODS "Enable code mods" ON)
set(DUSK_HAS_FUNCHOOK OFF)
if (DUSK_ENABLE_CODE_MODS AND (NOT APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Darwin"))
@@ -304,7 +304,7 @@ include(cmake/GameABIConfig.cmake)
find_package(Threads REQUIRED)
set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1)
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::io borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
Threads::Threads zstd::libzstd dusklight_game_headers)
if (DUSK_HAS_FUNCHOOK)
list(APPEND GAME_LIBS funchook-static)
@@ -384,20 +384,31 @@ if (CMAKE_CXX_LINK_GROUP_USING_RESCAN_SUPPORTED OR CMAKE_LINK_GROUP_USING_RESCAN
set(JSYSTEM_LINK_LIBRARIES "$<LINK_GROUP:RESCAN,${JSYSTEM_LIBRARIES}>")
endif ()
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_SOURCE_DIR}/miniz.c)
set(_dusklight_all_files src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES})
set(DUSK_INTERNAL_FILES ${_dusklight_all_files})
list(FILTER DUSK_INTERNAL_FILES INCLUDE REGEX "^src/dusk/")
set(GAME_FILES ${_dusklight_all_files})
list(FILTER GAME_FILES EXCLUDE REGEX "^src/dusk/")
add_library(dusk_internal OBJECT ${DUSK_INTERNAL_FILES})
if (ENABLE_ASAN)
target_sources(dusk_internal PRIVATE src/dusk/asan_options.c)
endif ()
target_compile_definitions(dusk_internal PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusk_internal PRIVATE ${miniz_SOURCE_DIR})
target_link_libraries(dusk_internal PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LIBRARIES})
target_precompile_headers(dusk_internal PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
if(ANDROID)
add_library(dusklight SHARED ${DUSK_FILES})
add_library(dusklight SHARED ${GAME_FILES} ${miniz_SOURCE_DIR}/miniz.c)
else ()
add_executable(dusklight ${DUSK_FILES})
add_executable(dusklight ${GAME_FILES} ${miniz_SOURCE_DIR}/miniz.c)
endif ()
borealis_configure_android_application(dusklight)
if (ENABLE_ASAN)
target_sources(dusklight PRIVATE src/dusk/asan_options.c)
endif ()
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusklight PRIVATE ${miniz_SOURCE_DIR})
target_link_libraries(dusklight PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_link_libraries(dusklight PRIVATE dusk_internal aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_precompile_headers(dusklight PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
if (DUSK_ENABLE_CODE_MODS)
@@ -413,76 +424,75 @@ endif ()
# Hook reliability: prevent auto-inlining, guarantee patchable function entries,
# and disable identical-code folding for game functions.
set(DUSK_GAME_ABI_INLINE_OPTIONS)
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-finline-hint-functions>)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-flive-patching=inline-clone>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-functions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-small-functions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-functions-called-once>
$<$<COMPILE_LANGUAGE:CXX>:-fno-early-inlining>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-cp>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-cp-clone>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-sra>
$<$<COMPILE_LANGUAGE:CXX>:-fno-partial-inlining>)
endif ()
endif ()
if (DUSK_GAME_ABI_INLINE_OPTIONS)
set(_game_abi_files ${GAME_BASE_FILES} ${GAME_DEBUG_FILES})
list(FILTER _game_abi_files EXCLUDE REGEX "^src/dusk/")
set_property(SOURCE ${_game_abi_files} APPEND PROPERTY
COMPILE_OPTIONS ${DUSK_GAME_ABI_INLINE_OPTIONS})
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
endforeach()
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_sdk_lib})
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
if (NOT _sdk_lib_imported)
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
endif ()
if (DUSK_ENABLE_CODE_MODS)
set(DUSK_GAME_ABI_INLINE_OPTIONS)
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang")
list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-finline-hint-functions>)
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-flive-patching=inline-clone>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-functions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-small-functions>
$<$<COMPILE_LANGUAGE:CXX>:-fno-inline-functions-called-once>
$<$<COMPILE_LANGUAGE:CXX>:-fno-early-inlining>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-cp>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-cp-clone>
$<$<COMPILE_LANGUAGE:CXX>:-fno-ipa-sra>
$<$<COMPILE_LANGUAGE:CXX>:-fno-partial-inlining>)
endif ()
endforeach ()
endif ()
endif ()
if (MSVC)
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:/hotpatch>)
endif ()
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN:16 /OPT:NOICF)
else ()
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN /OPT:NOICF)
endif ()
elseif (CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang|GNU)$")
if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)$" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=2,1>)
else ()
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=10,4>)
endif ()
endif ()
if (DUSK_GAME_ABI_INLINE_OPTIONS)
target_compile_options(dusklight PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
if (DEFINED DUSK_PATCHABLE_ENTRY_FLAG)
target_compile_options(dusklight PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
endforeach()
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_sdk_lib})
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
if (NOT _sdk_lib_imported)
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
endforeach()
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_sdk_lib})
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
if (NOT _sdk_lib_imported)
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_GAME_ABI_INLINE_OPTIONS})
endif ()
endif ()
endforeach ()
endif ()
if (MSVC)
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:/hotpatch>)
endif ()
endforeach ()
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN:16 /OPT:NOICF)
else ()
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN /OPT:NOICF)
endif ()
elseif (CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang|GNU)$")
if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)$" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=2,1>)
else ()
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=10,4>)
endif ()
endif ()
if (DEFINED DUSK_PATCHABLE_ENTRY_FLAG)
target_compile_options(dusklight PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
endforeach()
foreach(_sdk_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_sdk_lib})
get_target_property(_sdk_lib_imported ${_sdk_lib} IMPORTED)
if (NOT _sdk_lib_imported)
target_compile_options(${_sdk_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
endif ()
endif ()
endforeach ()
endif ()
endif ()
if (WIN32)
@@ -561,6 +571,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR
add_subdirectory(mods/window_demo)
add_subdirectory(mods/flow_demo)
add_subdirectory(mods/basic_cosmetics_mod)
add_subdirectory(mods/randomizer)
endif ()
if (APPLE)
+2 -26
View File
@@ -60,11 +60,7 @@
"type": "BOOL",
"value": false
},
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
}
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
@@ -151,10 +147,6 @@
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
@@ -254,11 +246,7 @@
"type": "BOOL",
"value": false
},
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
}
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
@@ -323,10 +311,6 @@
"type": "BOOL",
"value": false
},
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
@@ -372,10 +356,6 @@
"type": "BOOL",
"value": false
},
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
@@ -406,10 +386,6 @@
"type": "BOOL",
"value": false
},
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
-2
View File
@@ -39,8 +39,6 @@ function(setup_android_exports target)
--format version-script
--exclude cmake_pch
--exclude miniz
--exclude asan_options
--exclude src/dusk
# Resolved from the Java side; the SDL ones live in the statically-linked
# SDL archive, outside the provenance scan.
--extra-sym JNI_OnLoad
-2
View File
@@ -46,8 +46,6 @@ function(setup_apple_exports target)
--out "${_exp}"
--exclude cmake_pch
--exclude miniz
--exclude asan_options
--exclude src/dusk
${_sdk_args}
COMMENT "Generating dusklight exports"
VERBATIM)
+5
View File
@@ -0,0 +1,5 @@
_mod_ctx
_mod_initialize
_mod_meta
_mod_shutdown
_mod_update
+10
View File
@@ -0,0 +1,10 @@
{
global:
mod_ctx;
mod_initialize;
mod_meta;
mod_shutdown;
mod_update;
local:
*;
};
+20
View File
@@ -185,6 +185,20 @@ function(add_mod target_name)
WINDOWS_EXPORT_ALL_SYMBOLS OFF)
target_compile_features(${target_name} PRIVATE cxx_std_20)
target_link_libraries(${target_name} PRIVATE dusklight_mod_api)
if (APPLE)
set(_mod_exports "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ModExports.exp")
target_link_options(${target_name} PRIVATE
-Xlinker -exported_symbols_list -Xlinker "${_mod_exports}")
set_property(TARGET ${target_name} APPEND PROPERTY LINK_DEPENDS "${_mod_exports}")
elseif (UNIX)
set(_mod_exports "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ModExports.ver")
target_link_options(${target_name} PRIVATE
"-Wl,--version-script=${_mod_exports}"
-Wl,--no-undefined-version)
set_property(TARGET ${target_name} APPEND PROPERTY LINK_DEPENDS "${_mod_exports}")
endif ()
foreach (_feature IN LISTS _features)
target_link_libraries(${target_name} PRIVATE dusklight_mod_feature_${_feature})
if (_feature STREQUAL "webgpu")
@@ -302,6 +316,7 @@ function(add_mod target_name)
set(_package_inputs "${_mod_json}")
set(_extra_cmds "")
set(_lib_copy_cmd "")
set(_lib_strip_cmd "")
set(_target_depend "")
if (_has_lib)
list(APPEND _zip_args lib)
@@ -309,6 +324,10 @@ function(add_mod target_name)
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}/lib/${_lib_platform}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:${target_name}>" "${_stage}/lib/${_lib_platform}/${_lib_name}")
if (UNIX AND NOT APPLE)
set(_lib_strip_cmd COMMAND "${CMAKE_STRIP}" --strip-unneeded
"${_stage}/lib/${_lib_platform}/${_lib_name}")
endif ()
set(_target_depend ${target_name})
endif ()
string(TOLOWER "${_lib_name}" _lib_name_key)
@@ -385,6 +404,7 @@ function(add_mod target_name)
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_stage}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${_output_dir}"
${_lib_copy_cmd}
${_lib_strip_cmd}
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_mod_json}" "${_stage}/mod.json"
${_extra_cmds}
COMMAND ${CMAKE_COMMAND} -E chdir "${_stage}" ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
+9 -9
View File
@@ -2,7 +2,7 @@ include_guard(GLOBAL)
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
set(_SYMGEN_VERSION "1.3.3")
set(_SYMGEN_VERSION "1.3.4")
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
mark_as_advanced(SYMGEN_PATH)
@@ -15,32 +15,32 @@ function(symgen_host_asset out_name out_hash)
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
if (_host_processor MATCHES "^(arm64|aarch64)$")
set(_asset "symgen-macos-arm64")
set(_asset_hash "SHA256=e8420df1160242c83bd0e5197efeeb25ceb9fecc69dbc82bdc4927476381e948")
set(_asset_hash "SHA256=983ef6278d30bee38f240f451ca736fd294da0e5705ee15029accac5c7a33829")
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
set(_asset "symgen-macos-x86_64")
set(_asset_hash "SHA256=e51d13bbf0e982d1519f56850ff3c7b5910ca5a7095dd5a82cd8e59750e2d013")
set(_asset_hash "SHA256=b6ab1720a4f04fadcf291f42d133e2ee68ef2e9a75d143e51584929e808af8a2")
endif ()
elseif (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
if (_host_processor MATCHES "^(aarch64|arm64)$")
set(_asset "symgen-linux-aarch64")
set(_asset_hash "SHA256=331190dd21ecee12a52e857b7f90a8b3e0a114608173813f7df74a31cf783f70")
set(_asset_hash "SHA256=ba84486ff1ceb753973e6213c9bc5f195b5ff6f220f5d3322fd861966cdc791c")
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
set(_asset "symgen-linux-x86_64")
set(_asset_hash "SHA256=d727e0ba43242f92a92865af1b69c47aa9d81a0242c4570ce53b90e1ecd15e84")
set(_asset_hash "SHA256=8b81a00a2ef8f5dd4b59adba1badfaca57e472bf272516bc12c4625cefb37718")
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
set(_asset "symgen-linux-i686")
set(_asset_hash "SHA256=0c9f94d36b7fa46d15a199c9d0f1a633b73eb7d9c02621a6a76a676b83a135c7")
set(_asset_hash "SHA256=01aa85d84148a6806fdf69839dea7e962e6ac46956aed63b9ecb7d596f179f93")
endif ()
elseif (CMAKE_HOST_WIN32)
if (_host_processor MATCHES "^(arm64|aarch64)$")
set(_asset "symgen-windows-arm64.exe")
set(_asset_hash "SHA256=c699192957e2086ed68912ed39fa77858e1ae894476ab8b557711804cf2aa7cb")
set(_asset_hash "SHA256=e116099f859177d7e29fc3444d67083596330e5c4fb3a0461b21402c89249d96")
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
set(_asset "symgen-windows-x86_64.exe")
set(_asset_hash "SHA256=6d56a45617c75065ab7a3192d09ca52ff735baf5637b0899f446138d46acc310")
set(_asset_hash "SHA256=57ef1c2f563e33fa4915c40194c4ced540f985c48d9cf081047c6bbcb585fa37")
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
set(_asset "symgen-windows-x86.exe")
set(_asset_hash "SHA256=6cccc3a8d29e7525a4d1ade1dc4d89327b7c3002a604ab1c7decb43eea6fd4ab")
set(_asset_hash "SHA256=cc0ba337c798d3f63d8acbb2d16d531d0c2da878fbdd2b6307702334ec880528")
endif ()
endif ()
-2
View File
@@ -62,8 +62,6 @@ function(setup_windows_exports target)
--out "${_def}"
--exclude cmake_pch
--exclude miniz
--exclude asan_options
--exclude src/dusk
--max-exports 58000
${_sdk_args}
${_forward_args}
+89 -2
View File
@@ -48,7 +48,7 @@ cmake_minimum_required(VERSION 3.26)
project(my_mod CXX)
if (NOT DUSKLIGHT_VERSION)
set(DUSKLIGHT_VERSION "76b56cd8b81809fce0a5c2a44e2f6d437591132f")
set(DUSKLIGHT_VERSION "76b56cd8b81809fce0a5c2a44e2f6d437591132f")
endif ()
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchDusklight.cmake")
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
@@ -212,6 +212,93 @@ if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. The bundle is read-only; use
`HostService::data_dir` for persistent storage.
### FileService (`mods/svc/file.h`)
Provides file and folder pickers, file I/O, exports and folder enumeration.
A location is an opaque UTF-8 string returned by `pick_*`, `export_file`, `join`, or `create_child`.
Save it and pass it back to the service. Never parse it or manually append path segments.
```cpp
#include "mods/svc/file.hpp"
IMPORT_SERVICE(FileService, svc_file);
mods::file::PickOptions options;
options.filters.push_back({"Audio", "wav;ogg"});
mods::file::pick_file(options, [](mods::file::PickResult result) {
if (result.status == MOD_OK && !result.locations.empty()) {
save_location(result.locations.front());
}
});
```
Use `check` before reopening a saved location because removable storage or an access grant may no longer be available.
`open` provides seekable streaming I/O. `read_all` allocates the entire file and should only be used when the file is
small. Folder locations support `list` and child resolution through `join`. Only one picker can be open at a time.
`create_child` never replaces an existing file and returns `MOD_CONFLICT` if one exists. Use the returned location;
document providers may adjust the requested name. `write_all` is a convenience function over
`open`/`write`/`flush`/`close`.
```cpp
std::string location;
if (mods::file::create_child(folder, "report.txt", location) == MOD_OK) {
mods::file::write_all(location, report);
}
mods::file::export_file(location, "report.txt", [](mods::file::PickResult result) {
if (result.status == MOD_OK) {
remember_export_destination(result.locations.front());
}
});
```
`export_file` copies an existing file to a user-selected destination and returns the destination location in its
callback. Mod-owned persistent files belong in `HostService::data_dir`.
### HttpService (`mods/svc/http.h`)
Asynchronous HTTPS requests supporting HTTP/2 and TLS 1.2+. C++ mods should use the helpers in `mods/svc/http.hpp`:
```cpp
#include "mods/svc/http.hpp"
IMPORT_SERVICE(HttpService, svc_http);
mods::http::Pending pendingRequest;
void fetch_manifest() {
mods::http::Request request{
.url = "https://example.com/manifest.json",
.maxBodyBytes = 256 * 1024,
};
pendingRequest = mods::http::request(request, [](mods::http::Response response) {
if (!response.ok()) {
handle_fetch_error(response.error, response.statusCode);
return;
}
std::string manifest{response.body.begin(), response.body.end()};
use_manifest(manifest);
});
if (!pendingRequest) {
handle_start_error(pendingRequest.result());
}
}
```
Keep the returned `Pending` alive until completion. Dropping it or calling `cancel` requests cancellation. Callbacks run
on the game thread.
`Response::ok()` requires a 2xx status. Other HTTP statuses are valid responses, not transport errors, so always check
`statusCode`. In-memory responses default to a 1 MiB limit; set `maxBodyBytes` to increase it if needed.
For large responses, set `downloadPath` to an absolute path in the calling mod's `HostService::data_dir` or
`HostService::mod_dir`. The response is streamed to disk instead of loaded in memory. On success, the callback receives
an empty `body` and the final path in `downloadPath`. Check `Response::ok()` before using the file.
`Pending::progress()` reports download progress when the server provides a total size.
### HostService (`mods/svc/host.h`)
Mod metadata and runtime interaction with the loader:
@@ -587,7 +674,7 @@ svc_ui->dialog_push(mod_ctx, &dialog, nullptr);
```
After an action's `on_pressed`, the dialog closes unless the action sets `keep_open`. It can then be closed later
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body` and
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body` and
`dialog_set_icon` mutate a live dialog.
**Toasts:** `push_toast` enqueues a notification. Titles and bodies accept RML. The optional `type` is applied as an
+1 -1
+17 -5
View File
@@ -244,7 +244,7 @@ set(DOLZEL_FILES
src/CaptureScreen.cpp
)
if(DEBUG)
list(APPEND DOLZEL_FILES src/d/d_event_debug.cpp)
list(APPEND DOLZEL_FILES src/d/d_event_debug.cpp)
endif(DEBUG)
set(Z2AUDIOLIB_FILES
@@ -1404,10 +1404,10 @@ set(REL_FILES
)
set(DOLPHIN_FILES
libs/dolphin/src/gf/GFGeometry.cpp
libs/dolphin/src/gf/GFLight.cpp
libs/dolphin/src/gf/GFPixel.cpp
libs/dolphin/src/gf/GFTev.cpp
libs/dolphin/src/gf/GFGeometry.cpp
libs/dolphin/src/gf/GFLight.cpp
libs/dolphin/src/gf/GFPixel.cpp
libs/dolphin/src/gf/GFTev.cpp
)
set(DUSK_FILES
@@ -1433,11 +1433,16 @@ set(DUSK_FILES
src/dusk/dvd_asset.hpp
src/dusk/extras.c
src/dusk/frame_interpolation.cpp
src/dusk/commands.cpp
src/dusk/commands.hpp
src/dusk/game_clock.cpp
src/dusk/game_mode.cpp
src/dusk/gamepad_color.cpp
src/dusk/globals.cpp
src/dusk/gyro.cpp
src/dusk/game_combos.cpp
src/dusk/trigger_viewer.cpp
#src/dusk/m_Do_ext_dusk.cpp
src/dusk/hq_minimap.cpp
src/dusk/imgui/ImGuiActorSpawner.cpp
src/dusk/imgui/ImGuiBloomWindow.cpp
@@ -1478,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
@@ -1487,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
@@ -1521,6 +1529,8 @@ set(DUSK_FILES
src/dusk/touch_camera.cpp
src/dusk/ui/achievements.cpp
src/dusk/ui/achievements.hpp
src/dusk/ui/command_console.cpp
src/dusk/ui/command_console.hpp
src/dusk/ui/bool_button.cpp
src/dusk/ui/bool_button.hpp
src/dusk/ui/button.cpp
@@ -1542,6 +1552,8 @@ set(DUSK_FILES
src/dusk/ui/graphics_tuner.hpp
src/dusk/ui/group_button.cpp
src/dusk/ui/group_button.hpp
src/dusk/ui/file_button.cpp
src/dusk/ui/file_button.hpp
src/dusk/ui/icon_provider.cpp
src/dusk/ui/icon_provider.hpp
src/dusk/ui/input.cpp
+1 -1
View File
@@ -47,7 +47,7 @@ public:
s32 getBgmLoadStatus(u32 wave) { return getWaveLoadStatus(wave, 1); }
u8 getDemoSeWaveNum() { return loadedDemoWave; }
private:
// private:
/* 0x00 */ JAISoundID BGM_ID;
/* 0x04 */ int sceneNum;
/* 0x08 */ int timer;
+1 -1
View File
@@ -123,7 +123,7 @@ public:
bool checkFlag(u16 flag) { return field_0x68e & flag; }
void setAction(u8 action) { mAction = action; }
private:
// private:
/* 0x56C */ request_of_phase_process_class mPhase1;
/* 0x574 */ request_of_phase_process_class mPhase2;
/* 0x57C */ J3DModel* mModel1;
+1 -1
View File
@@ -296,7 +296,7 @@ public:
static DUSK_GAME_DATA char DUSK_CONST* DUSK_CONST mCutNameList[18];
static DUSK_GAME_DATA cutFunc DUSK_CONST mCutList[18];
private:
// private:
/* 0x0E40 */ NPC_FAIRY_HIO_CLASS* mHIO;
/* 0x0E44 */ dCcD_Cyl mCyl;
/* 0x0F80 */ u8 mType;
+1 -1
View File
@@ -287,7 +287,7 @@ public:
static DUSK_GAME_DATA char DUSK_CONST* DUSK_CONST mCutNameList[10];
static DUSK_GAME_DATA cutFunc DUSK_CONST mCutList[10];
private:
// private:
/* 0x0E40 */ mDoExt_McaMorfSO* mFishModelMorf;
/* 0x0E44 */ mDoExt_McaMorfSO* mLeafModelMorf;
/* 0x0E48 */ NPC_YKM_HIO_CLASS* mpHIO;
+1 -1
View File
@@ -96,7 +96,7 @@ public:
static DUSK_GAME_DATA char DUSK_CONST* DUSK_CONST mEvtCutNameList[2];
static DUSK_GAME_DATA EventFn DUSK_CONST mEvtCutList[2];
private:
// private:
/* 0xB48 */ Z2Creature mCreatureSound;
/* 0xBD8 */ daNpcF_MatAnm_c* mpMatAnm;
/* 0xBDC */ daNpcF_Lookat_c mLookat;
+1 -1
View File
@@ -114,7 +114,7 @@ public:
void himoCalc();
void adjustShapeAngle() {}
private:
// private:
/* 0x0B48 */ Z2Creature mCreatureSound;
/* 0x0BD8 */ daNpcF_MatAnm_c* mpMatAnm;
/* 0x0BDC */ mDoExt_invisibleModel mInvisibleModel;
+1 -1
View File
@@ -60,7 +60,7 @@ public:
bool isFirst() { return fopAcM_GetParamBit(this, 0x1b, 1); }
void setAction(u8 action) { mAction = action; }
private:
// private:
/* 0x568 */ request_of_phase_process_class mPhaseReq;
/* 0x570 */ J3DModel* mpModel;
/* 0x574 */ mDoExt_btkAnm* mpBtkAnm[2];
+1 -1
View File
@@ -64,7 +64,7 @@ public:
current.pos = new_pos;
}
private:
// private:
/* 0x92C */ fpc_ProcID mItemId;
/* 0x930 */ int mCounter;
/* 0x934 */ u8 mStatus;
+1 -1
View File
@@ -51,7 +51,7 @@ public:
static DUSK_GAME_DATA daObjMasterSword_Attr_c const mAttr;
static DUSK_GAME_DATA actionFunc ActionTable[];
private:
// private:
/* 0x568 */ J3DModel* mpModel;
/* 0x56C */ request_of_phase_process_class mPhase;
/* 0x574 */ mDoExt_btkAnm mBtk;
+1 -1
View File
@@ -67,7 +67,7 @@ public:
u8 getModelType() { return fopAcM_GetParamBit(this, 8, 4); }
u8 getItemNo() { return fopAcM_GetParamBit(this, 0, 8); }
private:
// private:
/* 0x718 */ u8 mReturnRupee;
/* 0x71C */ request_of_phase_process_class mPhase;
/* 0x724 */ J3DModel* mpModel;
+4
View File
@@ -1291,6 +1291,7 @@ public:
}
void set(const char*, s8, s16, s8, s8, u8);
void offEnable() { enabled = 0; }
void onEnable() { enabled = 1; }
BOOL isEnable() const { return enabled; }
s8 getWipe() const { return wipe; }
u8 getWipeSpeed() const { return wipe_speed; }
@@ -1351,6 +1352,9 @@ enum dStage_SaveTbl {
const char* dStage_getName2(s16, s8);
dStage_objectNameInf* dStage_searchName(const char*);
#if TARGET_PC
dStage_objectNameInf* dStage_searchNameCI(const char*);
#endif
static int dStage_stageKeepTresureInit(dStage_dt_c*, void*, int, void*);
static int dStage_filiInfo2Init(dStage_dt_c*, void*, int, void*);
static int dStage_mapPathInitCommonLayer(dStage_dt_c*, void*, int, void*);
+4
View File
@@ -83,11 +83,15 @@ extern void __dcbf(void*, int);
extern void __dcbz(void*, int);
extern void __sync();
extern int __abs(int);
#if defined(_MSVC_LANG) && !defined(__clang__)
#define __memcpy memcpy
#else
#if defined(__has_builtin) && __has_builtin(__builtin_memcpy)
#define __memcpy __builtin_memcpy
#else
#define __memcpy memcpy
#endif
#endif
#ifdef __cplusplus
}
#endif
@@ -7,6 +7,7 @@
#include <new>
#include <utility>
#include <cstdint>
#include <type_traits>
class JKRHeap;
typedef void (*JKRErrorHandler)(void*, u32, int);
-1
View File
@@ -17,5 +17,4 @@ add_mod(ao_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
BUNDLE
)
+15 -1
View File
@@ -18,6 +18,7 @@
#include "d/d_kankyo.h"
#include "d/d_kantera_icon_meter.h"
#include "d/d_menu_collect.h"
#include "d/d_menu_dmap.h"
#include "d/d_menu_fishing.h"
#include "d/d_menu_fmap2D.h"
#include "d/d_menu_insect.h"
@@ -345,7 +346,7 @@ void menu_skill_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A, B, and Z buttons on the map screen
// A, B, and Z buttons on the field map screen
DEFINE_HOOK(&dMenu_Fmap_c::_create, MenuFMapCreate);
void menu_fmap_create_post(ModContext*, void* args, void*, void*) {
auto menuFMap = mods::arg<dMenu_Fmap_c*>(args, 0);
@@ -356,6 +357,16 @@ void menu_fmap_create_post(ModContext*, void* args, void*, void*) {
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen);
}
// A and B buttons on the dungeon map screen
DEFINE_HOOK_SYMBOL("dMenu_DmapBg_c::buttonIconScreenInit", void(dMenu_DmapBg_c*), MenuDMapButtonIconScreenInit);
void menu_dmap_button_icon_screen_init_post(ModContext*, void* args, void*, void*) {
auto menuDMap = mods::arg<dMenu_DmapBg_c*>(args, 0);
auto screen = menuDMap->mButtonScreen;
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
}
// A button on the howling screen
DEFINE_HOOK(&dMsgObject_c::talkStartInit, MsgObjectTalkStartInit);
void msg_object_talk_start_init_post(ModContext*, void* args, void*, void*) {
@@ -685,6 +696,8 @@ ModResult add_all_hooks() {
ADD_POST_HOOK(OutFontCreatePane, out_font_create_pane_post, COutFont_c::createPane)
ADD_POST_HOOK(OutFontSetDrawFont, out_font_set_draw_font_post, COutFontSet_c::drawFont)
ADD_POST_HOOK(MenuFMapCreate, menu_fmap_create_post, dMenu_Fmap_c::_create)
ADD_POST_HOOK(MenuDMapButtonIconScreenInit, menu_dmap_button_icon_screen_init_post,
dMenu_DmapBg_c::buttonIconScreenInit)
ADD_POST_HOOK(
MsgObjectTalkStartInit, msg_object_talk_start_init_post, dMsgObject_c::talkStartInit)
ADD_POST_HOOK(MeterHakushaCreate, meter_hakusha_create_post, dMeterHakusha_c::_create)
@@ -724,6 +737,7 @@ ModResult remove_all_hooks() {
UNINSTALL_HOOK(OutFontCreatePane)
UNINSTALL_HOOK(OutFontSetDrawFont)
UNINSTALL_HOOK(MenuFMapCreate)
UNINSTALL_HOOK(MenuDMapButtonIconScreenInit)
UNINSTALL_HOOK(MsgObjectTalkStartInit)
UNINSTALL_HOOK(MeterHakushaCreate)
UNINSTALL_HOOK(ItemBaseCreateItemHeap)
+1
Submodule mods/randomizer added at 61ba8c4058
+1 -1
View File
@@ -4,7 +4,7 @@ This directory contains Dusklight's Android shell built on top of Borealis.
## Prerequisites
- Android SDK installed (`ANDROID_HOME`)
- Android SDK with Platform 37 installed (`ANDROID_HOME`)
- Android NDK version used by CMake presets (`ANDROID_NDK_VERSION`)
- JDK 17+
+1 -1
View File
@@ -1,3 +1,3 @@
plugins {
id 'com.android.application' version '8.13.2' apply false
id 'com.android.application' version '9.1.1' apply false
}
Binary file not shown.
+4 -3
View File
@@ -1,6 +1,7 @@
#Thu Nov 11 18:20:34 PST 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+202 -114
View File
@@ -1,74 +1,128 @@
#!/usr/bin/env bash
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Attempt to set APP_HOME
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn ( ) {
warn () {
echo "$*"
}
} >&2
die ( ) {
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -77,84 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|grep -E -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|grep -E -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+46 -43
View File
@@ -1,4 +1,22 @@
@if "%DEBUG%" == "" @echo off
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -8,26 +26,30 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -35,54 +57,35 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
+95
View File
@@ -0,0 +1,95 @@
*, *:before, *:after {
box-sizing: border-box;
}
body {
display: block;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: visible;
pointer-events: none;
}
console {
position: absolute;
bottom: 10dp;
left: 10dp;
width: 50%;
display: flex;
flex-direction: column;
background-color: rgba(0, 0, 0, 60%);
pointer-events: auto;
font-family: "Noto Mono";
font-size: 14dp;
color: #FFFFFF;
transition: background-color 0.8s linear-in-out;
}
output {
display: block;
overflow: hidden;
max-height: 480dp;
padding: 4dp 8dp;
line-height: 1.4em;
}
output[open] {
height: 480dp;
max-height: 480dp;
overflow-y: auto;
}
console:not([open]) {
pointer-events: none;
}
console:not([open])[fading] {
background-color: rgba(0, 0, 0, 0%);
}
console[open] {
background-color: rgba(0, 0, 0, 60%);
transition: none;
}
line {
display: block;
white-space: nowrap;
opacity: 1;
transition: opacity 0.8s linear-in-out;
}
output:not([open]) line[fading] {
opacity: 0;
}
output:not([open]) line[expired] {
display: none;
}
output[open] line {
opacity: 1;
transition: none;
}
line.cmd {
color: #FFD966;
}
console input {
display: none;
width: 100%;
background-color: rgba(0, 0, 0, 40%);
border: 0dp;
border-top: 1dp rgba(255, 255, 255, 20%);
color: #FFFFFF;
font-family: "Noto Mono";
font-size: 14dp;
padding: 4dp 8dp;
}
console[open] input {
display: block;
}
+14 -1
View File
@@ -115,6 +115,14 @@ mod-entry .mod-entry-status.failed {
color: #cc4444;
}
mod-entry .mod-entry-network {
margin-left: 6dp;
padding: 1dp 5dp;
border-radius: 5dp;
background-color: rgba(67, 151, 219, 20%);
color: #6fb7ef;
}
mod-entry .mod-entry-desc {
font-size: 14dp;
line-height: 1.3;
@@ -216,4 +224,9 @@ window.mods .mod-description {
.mod-info-label.failed {
color: #cc4444;
opacity: 1;
}
}
.status-badge.network {
color: #6fb7ef;
opacity: 1;
}
+3 -1
View File
@@ -10,7 +10,7 @@
#define ITEM_CHECK_FREESTANDING_PREFIX "freestanding:" /* <stage>:<bit> */
#define ITEM_CHECK_GOLDEN_WOLF_PREFIX "golden_wolf:" /* <event_flag> */
#define ITEM_CHECK_POE_PREFIX "poe:" /* <stage>:<switch> */
#define ITEM_CHECK_SHOP_PREFIX "shop:" /* <stage>:<item> */
#define ITEM_CHECK_SHOP_PREFIX "shop:" /* <stage>:<room>:<item> */
#define ITEM_CHECK_SKY_PREFIX "sky:" /* <stage>:<room> */
#define ITEM_CHECK_DUNGEON_REWARD_PREFIX "dungeon_reward:" /* <stage> */
@@ -21,6 +21,8 @@
#define ITEM_CHECK_BULBLIN_KEY "bulblin_key:D_MN09"
#define ITEM_CHECK_CORAL_EARRING "coral_earring"
#define ITEM_CHECK_CORO_BOTTLE "coro_bottle"
#define ITEM_CHECK_CORO_GATE_KEY "coro_gate_key"
#define ITEM_CHECK_CORO_LANTERN "coro_lantern"
#define ITEM_CHECK_DUNGEON_MAP_SNOWPEAK "dungeon_map:D_MN11"
#define ITEM_CHECK_FAIRY_REWARD "fairy_reward:D_SB01"
#define ITEM_CHECK_FISHING_BOTTLE "fishing_bottle"
+2 -2
View File
@@ -46,8 +46,8 @@ extern "C" const unsigned char mod_meta_bounds_end[] __asm("section$end$__DATA$_
#define MOD_META_BOUNDS_BEGIN (mod_meta_bounds_begin)
#define MOD_META_BOUNDS_END (mod_meta_bounds_end)
#else
extern "C" const unsigned char __start_modmeta[];
extern "C" const unsigned char __stop_modmeta[];
extern "C" __attribute__((visibility("hidden"))) const unsigned char __start_modmeta[];
extern "C" __attribute__((visibility("hidden"))) const unsigned char __stop_modmeta[];
#define MOD_META_BOUNDS_DEFN
#define MOD_META_BOUNDS_BEGIN (__start_modmeta)
#define MOD_META_BOUNDS_END (__stop_modmeta)
+118
View File
@@ -0,0 +1,118 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define FILE_SERVICE_ID "dev.twilitrealm.dusklight.file"
#define FILE_SERVICE_MAJOR 1u
#define FILE_SERVICE_MINOR 0u
typedef uint64_t FileStreamHandle;
typedef enum FileOpenMode {
FILE_OPEN_READ = 0,
FILE_OPEN_TRUNCATE = 1,
FILE_OPEN_APPEND = 2,
} FileOpenMode;
typedef struct FileFilter {
const char* name;
/* Semicolon-separated extensions or "*". */
const char* pattern;
} FileFilter;
typedef struct FilePickOptions {
uint32_t struct_size;
const FileFilter* filters;
uint32_t filter_count;
/* Optional previously returned location. */
const char* default_location;
} FilePickOptions;
#define FILE_PICK_OPTIONS_INIT {sizeof(FilePickOptions), NULL, 0u, NULL}
/* Locations and error are valid only for the duration of the callback. Canceled picks report
* MOD_UNAVAILABLE. The callback runs on the game thread. */
typedef void (*FilePickFn)(ModContext* ctx, ModResult status, const char* const* locations,
uint32_t location_count, const char* error, void* user_data);
typedef struct FileBuffer {
uint32_t struct_size;
void* data;
size_t size;
} FileBuffer;
#define FILE_BUFFER_INIT {sizeof(FileBuffer), NULL, 0u}
typedef struct FileEntry {
const char* name;
const char* location;
bool is_directory;
} FileEntry;
/* Called once per entry, then once with entry == NULL. Entries are valid only for the duration of
* the callback. */
typedef void (*FileListFn)(ModContext* ctx, const FileEntry* entry, void* user_data);
/*
* Access to user-selected files and folders.
*
* A location is an opaque UTF-8 string returned by `pick_*`, `export_file`, `join`, `create_child`.
* Save it and pass it back to the service. Never parse it or manually append path segments.
*
* Android: Security grants are restored across launches. Only 512 (or 128 before API 30) grants are
* allowed at a time. If a grant cannot be retained or was revoked, `check`/`open` returns
* MOD_UNAVAILABLE so the user can select the location again.
*
* Calls other than picker completion are synchronous and must run on the game thread. Avoid file
* reads and folder traversal in per-frame callbacks.
*/
typedef struct FileService {
ServiceHeader header;
/* One native dialog is allowed at a time. Returns MOD_CONFLICT while one is outstanding. */
ModResult (*pick_file)(
ModContext* ctx, const FilePickOptions* options, FilePickFn fn, void* user_data);
ModResult (*pick_folder)(
ModContext* ctx, const FilePickOptions* options, FilePickFn fn, void* user_data);
/* Exports an existing file to a user-chosen destination. fn receives its final location. */
ModResult (*export_file)(ModContext* ctx, const char* source_location,
const char* suggested_name, FilePickFn fn, void* user_data);
ModResult (*display_name)(
ModContext* ctx, const char* location, char* buffer, uint32_t buffer_size);
/* MOD_OK, MOD_UNAVAILABLE when gone or inaccessible, or MOD_UNSUPPORTED. */
ModResult (*check)(ModContext* ctx, const char* location);
/* Write modes require an existing writable location and return MOD_UNSUPPORTED otherwise. */
ModResult (*open)(
ModContext* ctx, const char* location, FileOpenMode mode, FileStreamHandle* out_handle);
ModResult (*size)(ModContext* ctx, FileStreamHandle handle, uint64_t* out_size);
ModResult (*read)(ModContext* ctx, FileStreamHandle handle, void* buffer, uint64_t length,
uint64_t* out_read);
ModResult (*write)(
ModContext* ctx, FileStreamHandle handle, const void* buffer, uint64_t length);
ModResult (*seek)(ModContext* ctx, FileStreamHandle handle, uint64_t offset);
ModResult (*flush)(ModContext* ctx, FileStreamHandle handle);
/* Reports flush and close failures; writers must check the result. */
ModResult (*close)(ModContext* ctx, FileStreamHandle handle);
ModResult (*read_all)(ModContext* ctx, const char* location, FileBuffer* out_buffer);
/* Truncates and writes an existing location. This operation is not atomic. */
ModResult (*write_all)(ModContext* ctx, const char* location, const void* data, size_t size);
void (*free)(ModContext* ctx, FileBuffer* buffer);
ModResult (*list)(ModContext* ctx, const char* folder_location, FileListFn fn, void* user_data);
/* out_location remains valid until this mod's next `join` or `create_child` call. */
ModResult (*join)(ModContext* ctx, const char* folder_location, const char* relative_path,
const char** out_location);
/* Creates one file without replacing an existing child. */
ModResult (*create_child)(
ModContext* ctx, const char* folder_location, const char* name, const char** out_location);
} FileService;
MOD_DECLARE_SERVICE(FileService, svc_file, FILE_SERVICE_ID, FILE_SERVICE_MAJOR, FILE_SERVICE_MINOR);
+321
View File
@@ -0,0 +1,321 @@
#pragma once
#include <mods/svc/file.h>
#include <array>
#include <cstdint>
#include <functional>
#include <span>
#include <string>
#include <utility>
#include <vector>
namespace mods::file {
class File {
public:
File() = default;
File(FileStreamHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
~File() { reset(); }
File(const File&) = delete;
File& operator=(const File&) = delete;
File(File&& other) noexcept { *this = std::move(other); }
File& operator=(File&& other) noexcept {
if (this != &other) {
reset();
mHandle = std::exchange(other.mHandle, 0);
mResult = other.mResult;
}
return *this;
}
explicit operator bool() const { return mResult == MOD_OK && mHandle != 0; }
ModResult result() const { return mResult; }
FileStreamHandle handle() const { return mHandle; }
uint64_t size() const {
uint64_t value = 0;
if (mHandle != 0 && svc_file != nullptr) {
svc_file->size(mod_ctx, mHandle, &value);
}
return value;
}
uint64_t read(void* buffer, uint64_t length) {
uint64_t value = 0;
if (mHandle == 0 || svc_file == nullptr) {
mResult = MOD_UNAVAILABLE;
} else {
mResult = svc_file->read(mod_ctx, mHandle, buffer, length, &value);
}
return value;
}
bool seek(uint64_t offset) {
mResult = mHandle != 0 && svc_file != nullptr ? svc_file->seek(mod_ctx, mHandle, offset) :
MOD_UNAVAILABLE;
return mResult == MOD_OK;
}
bool write(const void* buffer, uint64_t length) {
if (mHandle == 0 || svc_file == nullptr) {
mResult = MOD_UNAVAILABLE;
} else {
mResult = svc_file->write(mod_ctx, mHandle, buffer, length);
}
return mResult == MOD_OK;
}
bool write(std::span<const uint8_t> bytes) { return write(bytes.data(), bytes.size()); }
bool flush() {
if (mHandle == 0 || svc_file == nullptr) {
mResult = MOD_UNAVAILABLE;
} else {
mResult = svc_file->flush(mod_ctx, mHandle);
}
return mResult == MOD_OK;
}
bool close() {
if (mHandle == 0) {
return mResult == MOD_OK;
}
mResult = svc_file != nullptr ? svc_file->close(mod_ctx, mHandle) : MOD_UNAVAILABLE;
mHandle = 0;
return mResult == MOD_OK;
}
void reset() { (void)close(); }
private:
FileStreamHandle mHandle = 0;
ModResult mResult = MOD_UNAVAILABLE;
};
inline File open(const std::string& location, FileOpenMode mode = FILE_OPEN_READ) {
if (svc_file == nullptr) {
return {0, MOD_UNAVAILABLE};
}
FileStreamHandle handle = 0;
const auto result = svc_file->open(mod_ctx, location.c_str(), mode, &handle);
return {handle, result};
}
class Buffer {
public:
Buffer() = default;
Buffer(FileBuffer buffer, ModResult result) : mBuffer{buffer}, mResult{result} {}
~Buffer() { reset(); }
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
Buffer(Buffer&& other) noexcept { *this = std::move(other); }
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
reset();
mBuffer = other.mBuffer;
mResult = other.mResult;
other.mBuffer = FILE_BUFFER_INIT;
}
return *this;
}
explicit operator bool() const { return mResult == MOD_OK; }
ModResult result() const { return mResult; }
std::span<const uint8_t> bytes() const {
return {static_cast<const uint8_t*>(mBuffer.data), mBuffer.size};
}
void reset() {
if (mBuffer.data != nullptr && svc_file != nullptr) {
svc_file->free(mod_ctx, &mBuffer);
}
mBuffer.data = nullptr;
mBuffer.size = 0;
}
private:
FileBuffer mBuffer = FILE_BUFFER_INIT;
ModResult mResult = MOD_UNAVAILABLE;
};
inline Buffer read_all(const std::string& location) {
FileBuffer buffer = FILE_BUFFER_INIT;
const auto result = svc_file != nullptr ?
svc_file->read_all(mod_ctx, location.c_str(), &buffer) :
MOD_UNAVAILABLE;
return {buffer, result};
}
inline ModResult check(const std::string& location) {
return svc_file != nullptr ? svc_file->check(mod_ctx, location.c_str()) : MOD_UNAVAILABLE;
}
inline std::string display_name(const std::string& location) {
if (svc_file == nullptr) {
return {};
}
std::array<char, 1024> buffer{};
return svc_file->display_name(mod_ctx, location.c_str(), buffer.data(),
static_cast<uint32_t>(buffer.size())) == MOD_OK ?
std::string{buffer.data()} :
std::string{};
}
inline ModResult join(
const std::string& folder, const std::string& relativePath, std::string& outLocation) {
outLocation.clear();
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
const char* location = nullptr;
const auto result = svc_file->join(mod_ctx, folder.c_str(), relativePath.c_str(), &location);
if (result == MOD_OK && location != nullptr) {
outLocation = location;
}
return result;
}
inline ModResult create_child(
const std::string& folder, const std::string& name, std::string& outLocation) {
outLocation.clear();
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
const char* location = nullptr;
const auto result = svc_file->create_child(mod_ctx, folder.c_str(), name.c_str(), &location);
if (result == MOD_OK && location != nullptr) {
outLocation = location;
}
return result;
}
inline ModResult write_all(const std::string& location, std::span<const uint8_t> bytes) {
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
return svc_file->write_all(mod_ctx, location.c_str(), bytes.data(), bytes.size());
}
struct Entry {
std::string name;
std::string location;
bool isDirectory = false;
};
inline ModResult list(const std::string& folder, std::vector<Entry>& outEntries) {
outEntries.clear();
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
return svc_file->list(
mod_ctx, folder.c_str(),
[](ModContext*, const FileEntry* entry, void* userData) {
if (entry == nullptr) {
return;
}
static_cast<std::vector<Entry>*>(userData)->push_back({
.name = entry->name != nullptr ? entry->name : "",
.location = entry->location != nullptr ? entry->location : "",
.isDirectory = entry->is_directory,
});
},
&outEntries);
}
struct Filter {
std::string name;
std::string pattern;
};
struct PickOptions {
std::vector<Filter> filters;
std::string defaultLocation;
};
struct PickResult {
ModResult status = MOD_ERROR;
std::vector<std::string> locations;
std::string error;
};
namespace detail {
inline std::function<void(PickResult)> pickCallback;
inline void pick_trampoline(ModContext*, ModResult status, const char* const* locations,
uint32_t locationCount, const char* error, void*) {
PickResult result{.status = status, .error = error != nullptr ? error : ""};
result.locations.reserve(locationCount);
for (uint32_t i = 0; i < locationCount; ++i) {
if (locations[i] != nullptr) {
result.locations.emplace_back(locations[i]);
}
}
auto callback = std::move(pickCallback);
pickCallback = {};
if (callback) {
callback(std::move(result));
}
}
inline ModResult pick(
const PickOptions& options, std::function<void(PickResult)> callback, bool folder) {
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
if (!callback) {
return MOD_INVALID_ARGUMENT;
}
if (pickCallback) {
return MOD_CONFLICT;
}
std::vector<FileFilter> filters;
filters.reserve(options.filters.size());
for (const auto& filter : options.filters) {
filters.push_back({filter.name.c_str(), filter.pattern.c_str()});
}
FilePickOptions raw = FILE_PICK_OPTIONS_INIT;
raw.filters = filters.empty() ? nullptr : filters.data();
raw.filter_count = static_cast<uint32_t>(filters.size());
raw.default_location =
options.defaultLocation.empty() ? nullptr : options.defaultLocation.c_str();
pickCallback = std::move(callback);
const auto result = folder ? svc_file->pick_folder(mod_ctx, &raw, pick_trampoline, nullptr) :
svc_file->pick_file(mod_ctx, &raw, pick_trampoline, nullptr);
if (result != MOD_OK) {
pickCallback = {};
}
return result;
}
} // namespace detail
inline ModResult pick_file(const PickOptions& options, std::function<void(PickResult)> callback) {
return detail::pick(options, std::move(callback), false);
}
inline ModResult pick_folder(const PickOptions& options, std::function<void(PickResult)> callback) {
return detail::pick(options, std::move(callback), true);
}
inline ModResult export_file(const std::string& sourceLocation, const std::string& suggestedName,
std::function<void(PickResult)> callback) {
if (svc_file == nullptr) {
return MOD_UNAVAILABLE;
}
if (!callback) {
return MOD_INVALID_ARGUMENT;
}
if (detail::pickCallback) {
return MOD_CONFLICT;
}
detail::pickCallback = std::move(callback);
const auto result = svc_file->export_file(
mod_ctx, sourceLocation.c_str(), suggestedName.c_str(), detail::pick_trampoline, nullptr);
if (result != MOD_OK) {
detail::pickCallback = {};
}
return result;
}
} // namespace mods::file
+98
View File
@@ -0,0 +1,98 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define HTTP_SERVICE_ID "dev.twilitrealm.dusklight.http"
#define HTTP_SERVICE_MAJOR 1u
#define HTTP_SERVICE_MINOR 0u
/* Handle for an in-flight request. 0 is never a valid handle. */
typedef uint64_t HttpRequestHandle;
typedef enum HttpMethod {
HTTP_METHOD_GET = 0,
HTTP_METHOD_POST = 1,
HTTP_METHOD_HEAD = 2,
} HttpMethod;
/* Transport-level outcome. HTTP status errors are reported through status_code. */
typedef enum HttpError {
HTTP_ERROR_NONE = 0,
HTTP_ERROR_INVALID_URL = 1,
HTTP_ERROR_UNSUPPORTED_SCHEME = 2,
HTTP_ERROR_TIMEOUT = 3,
HTTP_ERROR_TOO_LARGE = 4,
HTTP_ERROR_CANCELED = 5,
HTTP_ERROR_IO = 6,
HTTP_ERROR_NETWORK = 7,
} HttpError;
typedef struct HttpHeader {
const char* name;
const char* value;
} HttpHeader;
typedef struct HttpRequestDesc {
uint32_t struct_size;
HttpMethod method;
const char* url;
const HttpHeader* headers;
uint32_t header_count;
/* Request body; POST only. */
const void* body;
size_t body_size;
/* Absolute destination under this mod's data_dir or mod_dir, or NULL for an in-memory body.
* GET and POST only. */
const char* download_path;
uint32_t connect_timeout_ms; /* 0 = 10 seconds */
uint32_t idle_timeout_ms; /* 0 = 10 seconds without network progress */
uint32_t total_timeout_ms; /* 0 = no total timeout */
size_t max_body_bytes; /* 0 = 1 MiB; ignored for downloads */
} HttpRequestDesc;
#define HTTP_REQUEST_DESC_INIT \
{sizeof(HttpRequestDesc), HTTP_METHOD_GET, NULL, NULL, 0u, NULL, 0u, NULL, 0u, 0u, 0u, 0u}
/* Snapshot valid only for the duration of the completion callback. */
typedef struct HttpResult {
uint32_t struct_size;
HttpError error;
const char* error_message;
int32_t status_code;
const HttpHeader* headers;
uint32_t header_count;
const void* body;
size_t body_size;
/* Published absolute destination, or NULL unless a download succeeded. */
const char* download_path;
} HttpResult;
/* Runs on the game thread exactly once, unless the calling mod begins deactivation first. */
typedef void (*HttpCompleteFn)(
ModContext* ctx, HttpRequestHandle request, const HttpResult* result, void* user_data);
typedef struct HttpProgress {
uint32_t struct_size;
uint64_t completed_bytes;
uint64_t total_bytes;
bool total_known;
} HttpProgress;
#define HTTP_PROGRESS_INIT {sizeof(HttpProgress), 0u, 0u, false}
typedef struct HttpService {
ServiceHeader header;
/* Starts an asynchronous HTTPS request. */
ModResult (*request)(ModContext* ctx, const HttpRequestDesc* desc, HttpCompleteFn fn,
void* user_data, HttpRequestHandle* out_handle);
ModResult (*progress)(ModContext* ctx, HttpRequestHandle request, HttpProgress* out_progress);
/* Requests cancellation. The completion callback still runs if the mod remains active. */
ModResult (*cancel)(ModContext* ctx, HttpRequestHandle request);
} HttpService;
MOD_DECLARE_SERVICE(HttpService, svc_http, HTTP_SERVICE_ID, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR);
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include <mods/svc/http.h>
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <functional>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace mods::http {
struct Header {
std::string name;
std::string value;
};
struct Request {
HttpMethod method = HTTP_METHOD_GET;
std::string url;
std::vector<Header> headers;
std::string body;
std::string downloadPath;
uint32_t connectTimeoutMs = 0;
uint32_t idleTimeoutMs = 0;
uint32_t totalTimeoutMs = 0;
size_t maxBodyBytes = 0;
};
struct Response {
HttpError error = HTTP_ERROR_NETWORK;
std::string errorMessage;
int statusCode = 0;
std::vector<Header> headers;
std::vector<uint8_t> body;
std::string downloadPath;
bool ok() const { return error == HTTP_ERROR_NONE && statusCode >= 200 && statusCode < 300; }
const std::string* header(std::string_view name) const {
const auto equal = [](std::string_view left, std::string_view right) {
return left.size() == right.size() &&
std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
};
const auto iter = std::find_if(headers.begin(), headers.end(),
[&](const Header& value) { return equal(value.name, name); });
return iter != headers.end() ? &iter->value : nullptr;
}
};
namespace detail {
struct Completion {
HttpRequestHandle handle = 0;
std::function<void(Response)> callback;
};
inline std::unordered_map<HttpRequestHandle, std::unique_ptr<Completion>> completions;
inline void complete(ModContext*, HttpRequestHandle handle, const HttpResult* raw, void* userData) {
const auto iter = completions.find(handle);
if (iter == completions.end() || iter->second.get() != userData) {
return;
}
auto completion = std::move(iter->second);
completions.erase(iter);
Response response;
if (raw != nullptr) {
response.error = raw->error;
response.errorMessage = raw->error_message != nullptr ? raw->error_message : "";
response.statusCode = raw->status_code;
response.headers.reserve(raw->header_count);
for (uint32_t i = 0; i < raw->header_count; ++i) {
response.headers.push_back({
.name = raw->headers[i].name != nullptr ? raw->headers[i].name : "",
.value = raw->headers[i].value != nullptr ? raw->headers[i].value : "",
});
}
if (raw->body != nullptr && raw->body_size != 0) {
const auto* begin = static_cast<const uint8_t*>(raw->body);
response.body.assign(begin, begin + raw->body_size);
}
response.downloadPath = raw->download_path != nullptr ? raw->download_path : "";
}
if (completion->callback) {
completion->callback(std::move(response));
}
}
} // namespace detail
class Pending {
public:
Pending() = default;
Pending(HttpRequestHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
~Pending() { reset(); }
Pending(const Pending&) = delete;
Pending& operator=(const Pending&) = delete;
Pending(Pending&& other) noexcept { *this = std::move(other); }
Pending& operator=(Pending&& other) noexcept {
if (this != &other) {
reset();
mHandle = std::exchange(other.mHandle, 0);
mResult = other.mResult;
}
return *this;
}
explicit operator bool() const {
if (mResult != MOD_OK || mHandle == 0 || svc_http == nullptr ||
!detail::completions.contains(mHandle))
{
return false;
}
HttpProgress value = HTTP_PROGRESS_INIT;
return svc_http->progress(mod_ctx, mHandle, &value) == MOD_OK;
}
ModResult result() const { return mResult; }
HttpRequestHandle handle() const { return mHandle; }
HttpProgress progress() const {
HttpProgress value = HTTP_PROGRESS_INIT;
if (mHandle != 0 && svc_http != nullptr) {
svc_http->progress(mod_ctx, mHandle, &value);
}
return value;
}
void cancel() {
if (mHandle != 0 && svc_http != nullptr) {
if (svc_http->cancel(mod_ctx, mHandle) != MOD_OK) {
detail::completions.erase(mHandle);
}
}
}
void detach() { mHandle = 0; }
private:
void reset() {
cancel();
mHandle = 0;
}
HttpRequestHandle mHandle = 0;
ModResult mResult = MOD_UNAVAILABLE;
};
inline Pending request(const Request& request, std::function<void(Response)> callback) {
if (svc_http == nullptr) {
return {0, MOD_UNAVAILABLE};
}
if (!callback) {
return {0, MOD_INVALID_ARGUMENT};
}
if (request.headers.size() > std::numeric_limits<uint32_t>::max()) {
return {0, MOD_INVALID_ARGUMENT};
}
std::vector<HttpHeader> headers;
headers.reserve(request.headers.size());
for (const auto& header : request.headers) {
headers.push_back({
.name = header.name.c_str(),
.value = header.value.c_str(),
});
}
HttpRequestDesc desc = HTTP_REQUEST_DESC_INIT;
desc.method = request.method;
desc.url = request.url.c_str();
desc.headers = headers.empty() ? nullptr : headers.data();
desc.header_count = static_cast<uint32_t>(headers.size());
desc.body = request.body.empty() ? nullptr : request.body.data();
desc.body_size = request.body.size();
desc.download_path = request.downloadPath.empty() ? nullptr : request.downloadPath.c_str();
desc.connect_timeout_ms = request.connectTimeoutMs;
desc.idle_timeout_ms = request.idleTimeoutMs;
desc.total_timeout_ms = request.totalTimeoutMs;
desc.max_body_bytes = request.maxBodyBytes;
auto completion = std::make_unique<detail::Completion>();
auto* userData = completion.get();
completion->callback = std::move(callback);
HttpRequestHandle handle = 0;
const auto result = svc_http->request(mod_ctx, &desc, detail::complete, userData, &handle);
if (result != MOD_OK) {
return {0, result};
}
completion->handle = handle;
detail::completions.emplace(handle, std::move(completion));
return {handle, result};
}
} // namespace mods::http
+10 -1
View File
@@ -8,7 +8,7 @@
#define ITEM_SERVICE_ID "dev.twilitrealm.dusklight.item"
#define ITEM_SERVICE_MAJOR 2u
#define ITEM_SERVICE_MINOR 1u
#define ITEM_SERVICE_MINOR 3u
/* 0 is never a valid handle. */
typedef uint64_t ItemCheckHandle;
@@ -32,11 +32,13 @@ typedef struct ItemCheckInfo {
uint8_t vanilla_item;
uint8_t current_item;
uint8_t current_display_item;
bool was_resolved;
} ItemCheckInfo;
typedef struct ItemCheckResolution {
uint8_t item;
uint8_t display_item; /* leave unset (0xFF/NONE) to use the item */
bool was_resolved;
} ItemCheckResolution;
/* Return true and write out_result to replace the result, or false to leave it unchanged. */
@@ -97,6 +99,13 @@ typedef struct ItemService {
ModContext* ctx, ItemGiveObserveFn fn, void* user_data, ItemGiveHandle* out_handle);
ModResult (*unobserve_gives)(ModContext* ctx, ItemGiveHandle handle);
/* Minor version 2 */
/* Resolve a live preview (item + display item) without granting an item or notifying give
* observers. */
ModResult (*resolve_check_full)(ModContext* ctx, const char* name, uint8_t vanilla_item,
ItemCheckResolution* out_resolution);
} ItemService;
MOD_DECLARE_SERVICE(ItemService, svc_item, ITEM_SERVICE_ID, ITEM_SERVICE_MAJOR, ITEM_SERVICE_MINOR);
+14 -7
View File
@@ -2,6 +2,7 @@
#include <mods/api.h>
#include <mods/svc/config.h>
#include <mods/svc/file.h>
#ifdef __cplusplus
#include <mods/service.hpp>
@@ -9,7 +10,7 @@
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
#define UI_SERVICE_MAJOR 2u
#define UI_SERVICE_MINOR 1u
#define UI_SERVICE_MINOR 2u
/*
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts,
@@ -53,6 +54,7 @@ typedef enum UiControlKind {
UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */
UI_CONTROL_COLOR = 5, /* RGB/RGBA color string with a picker */
UI_CONTROL_GROUP = 6, /* navigation row (on_pressed) */
UI_CONTROL_FILE_PICKER = 7, /* file/folder picker with an opaque string location */
} UiControlKind;
typedef enum UiControlBinding {
@@ -62,7 +64,8 @@ typedef enum UiControlBinding {
/* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod)
* directly: persistence, change notifications and the modified indicator (value != default) are
* wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and
* SELECT = int, STRING and COLOR = string. Float vars are not bindable; use callbacks. */
* SELECT = int, STRING, COLOR and FILE_PICKER = string. Float vars are not bindable; use
* callbacks. */
UI_BINDING_CONFIG_VAR = 1,
} UiControlBinding;
@@ -71,10 +74,10 @@ typedef enum UiStringSetMode {
UI_STRING_SET_ON_CHANGE = 1, /* invokes `set` on every text change (e.g. while typing) */
} UiStringSetMode;
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
* and COLOR read string_value. string_value passed to a setter is only valid during the call; a
* getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid until
* the next call into the mod — the host copies it right after the getter returns. */
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING,
* COLOR and FILE_PICKER read string_value. string_value passed to a setter is only valid during the
* call; a getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid
* until the next call into the mod. The host copies it right after the getter returns. */
typedef struct UiControlValue {
uint32_t struct_size;
bool bool_value;
@@ -128,12 +131,16 @@ typedef struct UiControlDesc {
bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */
UiPredicateFn is_selected; /* BUTTON/GROUP: optional selected state */
UiStringSetMode string_set_mode; /* STRING: when to invoke the setter */
/* FILE_PICKER: optional file filters and folder selection mode. */
const FileFilter* file_filters;
size_t file_filter_count;
bool directory_mode;
} UiControlDesc;
#define UI_CONTROL_DESC_INIT \
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false, NULL, \
UI_STRING_SET_ON_COMMIT}
UI_STRING_SET_ON_COMMIT, NULL, 0u, false}
typedef uint64_t UiListHandle;
+3 -2
View File
@@ -157,10 +157,11 @@ f32 daAlink_c::damageMagnification(BOOL i_checkZoraMag, int param_1) {
}
#if TARGET_PC
base_mag *= dusk::getSettings().game.damageMultiplier;
if (dusk::getSettings().game.instantDeath) {
base_mag = 9999.0f;
return 9999.0f; // don't need to multiply this further, 9999 is plenty
}
base_mag *= dusk::getSettings().game.damageMultiplier;
#endif
if (checkWolf() && !checkCargoCarry() && param_1 == 0) {
+6
View File
@@ -113,6 +113,12 @@ void daAlink_c::handleQuickTransform() {
return;
}
// Ensure Link is not underwater
if (!checkNoResetFlg0(FLG0_SWIM_UP)) {
Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
return;
}
// Use the game's default checks for if the player can currently transform
if (!m_midnaActor->checkMetamorphoseEnableBase()) {
Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
+2 -2
View File
@@ -462,7 +462,7 @@ int daDitem_c::create() {
m_itemNo = daDitem_prm::getNo(this);
#if TARGET_PC
const auto [item, displayItem] = dusk::mods::item_check_resolve(mItemGiveTag, m_itemNo, this);
const auto [item, displayItem, _] = dusk::mods::item_check_resolve(mItemGiveTag, m_itemNo, this);
m_itemNo = item;
setDisplayItemNo(displayItem);
const char* arc_name = dItem_data::getArcName(displayItem);
@@ -550,7 +550,7 @@ static int daDitem_Execute(daDitem_c* i_this) {
}
int daDitem_c::draw() {
switch (m_itemNo) {
switch (DUSK_IF_ELSE(getDisplayItemNo(), m_itemNo)) {
case dItemNo_WOOD_STICK_e:
draw_WOOD_STICK();
break;
+3
View File
@@ -7,6 +7,9 @@
#include "d/actor/d_a_e_po.h"
#include "d/actor/d_a_obj_poFire.h"
#if TARGET_PC
#include "d/actor/d_a_alink.h"
#endif
#include "d/d_cc_d.h"
#include "d/d_cc_uty.h"
#include "f_op/f_op_actor_enemy.h"
+1 -1
View File
@@ -3300,7 +3300,7 @@ int daMidna_c::execute() {
if (!checkStateFlg0(FLG0_UNK_8000)) {
offStateFlg0((daMidna_FLG0)(FLG0_NPC_NEAR | FLG0_NPC_FAR));
BOOL far_;
if (fopAcIt_Judge((fopAcIt_JudgeFunc)daMidna_searchNpc, &far_)) {
if (fopAcIt_Judge((fopAcIt_JudgeFunc)daMidna_searchNpc, &far_) IF_DUSK(&& !dusk::getSettings().game.canTransformAnywhere)) {
if (!far_) {
onStateFlg0(FLG0_NPC_NEAR);
} else {
+42 -4
View File
@@ -4022,16 +4022,53 @@ BOOL daNpc_grA_c::talk(void*) {
if (r26 && talkProc(NULL, TRUE, NULL)) {
if (mFlow.getEventId(&sp8) == 1) {
#if TARGET_PC
const u8 originalItem = sp8;
u32 itemGiveTag = 0;
if (sp8 == dItemNo_BOMB_IN_BAG_e) {
bool isItemCheck = false;
if (originalItem == dItemNo_BOMB_IN_BAG_e) {
const auto itemCheck =
dusk::mods::item_check_commit("goron_reward:F_SP113", sp8, this);
dusk::mods::item_check_commit("goron_reward:F_SP113", originalItem, this);
sp8 = itemCheck.itemNo;
itemGiveTag = itemCheck.tag;
isItemCheck = true;
} else {
const char* stage = dComIfGp_getStartStageName();
const bool isAdultGoronShop = (strcmp(stage, "R_SP160") == 0 && originalItem == dItemNo_HYLIA_SHIELD_e) ||
(strcmp(stage, "F_SP116") == 0 && originalItem == dItemNo_ARROW_30_e);
if (isAdultGoronShop) {
const auto itemCheck = dusk::mods::item_check_commit(
dusk::mods::item_give_tag_shop(originalItem), originalItem, this);
sp8 = itemCheck.itemNo;
itemGiveTag = itemCheck.tag;
isItemCheck = true;
}
}
#endif
if (isItemCheck && sp8 == dItemNo_NONE_e) {
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
r29 = 1;
if (mType == 0xb) {
field_0x1691 = 1;
}
} else {
field_0x1480 = fopAcM_createItemForPresentDemo(
&current.pos, sp8, 0, -1, -1, NULL, NULL, itemGiveTag);
if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) {
s16 r25 =
dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff);
dComIfGp_getEvent()->reset(this);
fopAcM_orderChangeEventId(this, r25, 1, -1);
field_0x9ec = 1;
r29 = 1;
mOrderNewEvt = 1;
if (mType == 0xb) {
field_0x1691 = 1;
}
}
}
#else
field_0x1480 = fopAcM_createItemForPresentDemo(&current.pos, sp8, 0, -1, -1, NULL,
NULL IF_DUSK_ARG(itemGiveTag));
NULL);
if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) {
s16 r25 = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff);
dComIfGp_getEvent()->reset(this);
@@ -4043,6 +4080,7 @@ BOOL daNpc_grA_c::talk(void*) {
field_0x1691 = 1;
}
}
#endif
} else {
if (mType == 0xa && field_0x1486 == 0 && daNpcF_chkEvtBit(0x187)) {
dComIfGp_getEvent()->reset(this);
+23 -1
View File
@@ -8,6 +8,9 @@
#include "d/actor/d_a_npc_grc.h"
#include "d/actor/d_a_npc.h"
#include "Z2AudioLib/Z2Instances.h"
#if TARGET_PC
#include "mods/items.h"
#endif
#include <cstring>
enum grC_RES_File_ID {
@@ -1476,8 +1479,27 @@ BOOL daNpc_grC_c::talk(void* param_1) {
rv = TRUE;
if (mFlow.getEventId(&i_itemNo) == 1) {
mItemID = fopAcM_createItemForPresentDemo(&current.pos, i_itemNo, 0, -1, -1, NULL, NULL);
#if TARGET_PC
const bool isCastleTownShop =
strcmp(dComIfGp_getStartStageName(), "R_SP160") == 0 &&
(i_itemNo == dItemNo_RED_BOTTLE_e || i_itemNo == dItemNo_OIL_BOTTLE_e);
u32 itemGiveTag = 0;
if (isCastleTownShop) {
const auto itemCheck = dusk::mods::item_check_commit(
dusk::mods::item_give_tag_shop(i_itemNo), i_itemNo, this);
i_itemNo = itemCheck.itemNo;
itemGiveTag = itemCheck.tag;
}
if (isCastleTownShop && i_itemNo == dItemNo_NONE_e) {
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
} else {
mItemID = fopAcM_createItemForPresentDemo(
&current.pos, i_itemNo, 0, -1, -1, NULL, NULL, itemGiveTag);
}
#else
mItemID = fopAcM_createItemForPresentDemo(&current.pos, i_itemNo, 0, -1, -1, NULL, NULL);
#endif
if (mItemID != fpcM_ERROR_PROCESS_ID_e) {
s16 i_eventID = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xFF);
dComIfGp_getEvent()->reset(this);
+32 -6
View File
@@ -7,6 +7,9 @@
#include "d/actor/d_a_npc_kkri.h"
#include "d/actor/d_a_e_ym.h"
#if TARGET_PC
#include "mods/items.h"
#endif
#include <cstring>
static DUSK_CONSTEXPR int l_bmdData[2][2] = {
@@ -1183,16 +1186,39 @@ int daNpc_Kkri_c::talk(void*) {
if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) {
#if TARGET_PC
u32 itemGiveTag = 0;
if (item_no == dItemNo_OIL_BOTTLE3_e) {
const auto itemCheck =
dusk::mods::item_check_commit("coro_bottle", item_no, this);
const char* itemCheckName = nullptr;
switch (item_no) {
case dItemNo_OIL_BOTTLE3_e:
itemCheckName = ITEM_CHECK_CORO_BOTTLE;
break;
case dItemNo_KANTERA_e:
itemCheckName = ITEM_CHECK_CORO_LANTERN;
break;
case dItemNo_KEY_OF_FILONE_e:
itemCheckName = ITEM_CHECK_CORO_GATE_KEY;
break;
}
if (itemCheckName != nullptr) {
const auto itemCheck = dusk::mods::item_check_commit(
itemCheckName, item_no, this);
item_no = itemCheck.itemNo;
itemGiveTag = itemCheck.tag;
}
if (item_no == dItemNo_NONE_e && itemCheckName != nullptr) {
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
field_0xfd5 = 1;
mEvtNo = 1;
evtChange();
} else {
mItemPartnerId = fopAcM_createItemForPresentDemo(
&current.pos, item_no, 0, -1, -1, NULL, NULL, itemGiveTag);
}
#else
mItemPartnerId = fopAcM_createItemForPresentDemo(
&current.pos, item_no, 0, -1, -1, NULL, NULL);
#endif
mItemPartnerId = fopAcM_createItemForPresentDemo(&current.pos, item_no,
0, -1, -1, NULL,
NULL IF_DUSK_ARG(itemGiveTag));
}
if (fopAcM_IsExecuting(mItemPartnerId)) {
+1 -8
View File
@@ -268,7 +268,7 @@ int daItem_c::_daItem_create() {
const u32 params = fopAcM_GetParam(this);
mOriginalItemNo = params & 0xFF;
mItemGiveTag = dusk::mods::item_give_tag_freestanding(daItem_prm::getItemBitNo(this));
const auto [item, displayItem] =
const auto [item, displayItem, _] =
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
mItemOverridden = item != mOriginalItemNo;
setDisplayItemNo(displayItem);
@@ -935,10 +935,6 @@ void daItem_c::itemGet() {
mItemOverridden = m_itemNo != mOriginalItemNo;
#endif
switch (m_itemNo) {
#if TARGET_PC
case dItemNo_UTAWA_HEART_e:
case dItemNo_KAKERA_HEART_e:
#endif
case dItemNo_HEART_e:
mDoAud_seStart(Z2SE_HEART_PIECE_GET, NULL, 0, 0);
execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this));
@@ -980,9 +976,6 @@ void daItem_c::itemGet() {
case dItemNo_PACHINKO_SHOT_e:
mDoAud_seStart(Z2SE_CONSUMP_ITEM_GET, NULL, 0, 0);
execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this));
#if TARGET_PC
break;
#endif
default:
#if TARGET_PC
if (mItemOverridden) {
+13 -4
View File
@@ -99,6 +99,15 @@ int daObjLife_c::Create() {
mRotateSpeed = 7000;
#if TARGET_PC
// Restore speeds that were skipped if created from a boss with create instead of fastCreate
auto bossItemSpeeds = dusk::mods::get_boss_item_actor_speeds();
if (bossItemSpeeds.f != 0.f || bossItemSpeeds.y != 0.f) {
mOverrideHover = false;
speedF = bossItemSpeeds.f;
speed.y = bossItemSpeeds.y;
dusk::mods::set_boss_item_actor_speeds(0.f, 0.f);
}
if (mOverrideHover) {
fopAcM_SetGravity(this, 0.0f);
mRotateSpeed = 550;
@@ -153,20 +162,20 @@ int daObjLife_c::create() {
if (mItemGiveOriginalNo == dItemNo_NONE_e) {
mOriginalItemNo = parameterItemNo;
mItemGiveTag = dusk::mods::item_give_tag_freestanding(getSaveBitNo());
const auto [item, displayItem] =
const auto [item, displayItem, was_resolved] =
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
setDisplayItemNo(displayItem);
mItemOverridden = item != mOriginalItemNo;
mItemOverridden = was_resolved;
if (mItemOverridden) {
fopAcM_SetParam(this, (params & 0xFFFFFF00) | item);
}
} else if (mGoldenWolfItem) {
mOriginalItemNo = mItemGiveOriginalNo;
mItemGiveTag = dusk::mods::item_give_tag_golden_wolf(static_cast<u16>(field_0x938));
const auto [item, displayItem] =
const auto [item, displayItem, was_resolved] =
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
setDisplayItemNo(displayItem);
mItemOverridden = item != mOriginalItemNo;
mItemOverridden = was_resolved;
if (item != parameterItemNo) {
fopAcM_SetParam(this, (params & 0xFFFFFF00) | item);
}
+3 -8
View File
@@ -151,11 +151,7 @@ void daObjMasterSword_c::create_init() {
int daObjMasterSword_c::create() {
fopAcM_ct(this, daObjMasterSword_c);
#if TARGET_PC
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0264)) {
#else
if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()])) {
#endif
return cPhs_ERROR_e;
}
@@ -199,7 +195,7 @@ int daObjMasterSword_c::execute() {
#if TARGET_PC
const auto masterSword = dusk::mods::item_check_commit(
ITEM_CHECK_MASTER_SWORD, dItemNo_MASTER_SWORD_e, this);
if (masterSword.itemNo == dItemNo_MASTER_SWORD_e) {
if (!masterSword.was_resolved) {
dComIfGs_onItemFirstBit(dItemNo_MASTER_SWORD_e);
dMeter2Info_setSword(dItemNo_MASTER_SWORD_e, false);
dComIfGs_setSelectEquipSword(dItemNo_MASTER_SWORD_e);
@@ -214,7 +210,7 @@ int daObjMasterSword_c::execute() {
const auto shadowCrystal = dusk::mods::item_check_commit(
ITEM_CHECK_SHADOW_CRYSTAL, dItemNo_SHADOW_CRYSTAL_e, this);
if (shadowCrystal.itemNo == dItemNo_SHADOW_CRYSTAL_e) {
if (!shadowCrystal.was_resolved) {
execItemGet(shadowCrystal.itemNo, shadowCrystal.tag, this);
} else if (shadowCrystal.itemNo == dItemNo_NONE_e) {
dusk::mods::item_check_complete(shadowCrystal, this);
@@ -222,15 +218,14 @@ int daObjMasterSword_c::execute() {
dusk::mods::item_check_enqueue(shadowCrystal, dusk::mods::ItemGiveMode::Demo);
}
dComIfGs_onEventBit(dSv_event_flag_c::F_0264);
#else
dComIfGs_onItemFirstBit(dItemNo_MASTER_SWORD_e);
dMeter2Info_setSword(dItemNo_MASTER_SWORD_e, false);
dComIfGs_setSelectEquipSword(dItemNo_MASTER_SWORD_e);
dComIfGp_setItemLifeCount(dComIfGs_getMaxLife(), 0);
dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()]);
#endif
dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()]);
fopAcM_delete(this);
}
+1 -1
View File
@@ -157,7 +157,7 @@ int daKey_c::create() {
#if TARGET_PC
mItemGiveTag = dusk::mods::item_give_tag_freestanding(getSaveBitNo());
const auto [item, displayItem] =
const auto [item, displayItem, _] =
dusk::mods::item_check_resolve(mItemGiveTag, dItemNo_SMALL_KEY_e, this);
m_itemNo = item;
if (m_itemNo == dItemNo_NONE_e) {
+27 -25
View File
@@ -367,31 +367,7 @@ JKRHeap* daPy_anmHeap_c::setAnimeHeap() {
}
#if !PLATFORM_WII
#if TARGET_PC
#include "dusk/dvd_asset.hpp"
using GameVersion = dusk::version::GameVersion;
static const u8* l_sightDL_get() {
static u8 buf[0x89];
static bool _ = (
dusk::LoadDolAsset(
buf,
{
{GameVersion::GcnUsa, 0x803BA0C0},
{GameVersion::GcnPal, 0x803BBDA0},
{GameVersion::GcnJpn, 0x803B4220},
{GameVersion::WiiUsaRev0, 0x803F63C0},
{GameVersion::WiiUsa, 0x803E1640},
{GameVersion::WiiPal, 0x803E23A0},
{GameVersion::WiiJpn, 0x803DF600}
},
0x89
),
true
);
return buf;
}
#define l_sightDL (l_sightDL_get())
#else
#if !TARGET_PC
#include "assets/l_sightDL__d_a_player.h"
#endif
@@ -435,7 +411,33 @@ void daPy_sightPacket_c::draw() {
GXLoadPosMtxImm(mProjMtx, GX_PNMTX0);
GXSetCurrentMtx(0);
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR_NULL);
#if TARGET_PC
GXSetNumTexGens(1);
GXSetNumTevStages(1);
GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
GXSetCullMode(GX_CULL_NONE);
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_C1, GX_CC_C0, GX_CC_TEXC, GX_CC_ZERO);
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_A0, GX_CA_TEXA, GX_CA_ZERO);
GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
GXSetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR);
GXSetZMode(GX_FALSE, GX_LEQUAL, GX_FALSE);
GXSetColorUpdate(GX_TRUE);
GXSetAlphaUpdate(GX_FALSE);
GXSetDither(GX_TRUE);
GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT0, 4);
GXPosition3u8(1, 1, 0);
GXTexCoord2u8(1, 1);
GXPosition3u8(255, 1, 0);
GXTexCoord2u8(0, 1);
GXPosition3u8(1, 255, 0);
GXTexCoord2u8(1, 0);
GXPosition3u8(255, 255, 0);
GXTexCoord2u8(0, 0);
GXEnd();
#else
GXCallDisplayList(l_sightDL, 0x80);
#endif
J3DShape::resetVcdVatCache();
}
+2 -1
View File
@@ -106,8 +106,9 @@ const char* daShopItem_c::getShopArcname() {
if (m_itemNo != dItemNo_NONE_e && mItemGiveOriginalNo == dItemNo_NONE_e) {
mItemGiveOriginalNo = m_itemNo;
mItemGiveTag = dusk::mods::item_give_tag_shop(mItemGiveOriginalNo);
const auto [_, displayItem] =
const auto [_, displayItem, was_resolved] =
dusk::mods::item_check_resolve(mItemGiveTag, mItemGiveOriginalNo, this);
setDisplayItemNo(displayItem);
mItemOverridden = displayItem != mItemGiveOriginalNo;
if (mItemOverridden) {
mOverrideData = mData[mShopItemID];
+8
View File
@@ -191,6 +191,14 @@ void dBrightCheck_c::modeMove() {
mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0);
#ifdef TARGET_PC
toggleAutoSave(true);
if (!dusk::getSettings().game.hideTvSettingsScreen) {
const dusk::gamemode::GameMode* gameMode =
dusk::gamemode::getGameModeManager().getCurrentGameMode();
if (gameMode) {
gameMode->invokeOnSaveLoadedFunction();
}
}
#endif
mCompleteCheck = true;
mMode = MODE_WAIT_e;
+10 -1
View File
@@ -30,6 +30,7 @@
#if TARGET_PC
#include "dusk/camera_operators.hpp"
#include "dusk/commands.hpp"
#include "dusk/frame_interpolation.h"
#include "dusk/logging.h"
#include "dusk/action_bindings.h"
@@ -1054,6 +1055,11 @@ void dCamera_c::debugDrawInit() {
bool dCamera_c::Run() {
#if TARGET_PC
ResetView();
if (dusk::isCameraDetached()) {
mFrameCounter++;
mTicks++;
return true;
}
if (executeDebugFlyCam() || dusk::mods::camera_run_operators(this)) {
mFrameCounter++;
mTicks++;
@@ -7595,7 +7601,7 @@ bool dCamera_c::executeDebugFlyCam() {
if (ImGui::IsKeyDown(ImGuiKey_Q)) rollInput -= 1.0f;
if (ImGui::IsKeyDown(ImGuiKey_E)) rollInput += 1.0f;
}
bool mouseValid = !io.WantCaptureMouse && io.MousePos.x >= 0.0f && io.MousePos.y >= 0.0f;
bool mouseValid = !io.WantCaptureMouse && io.MousePos.x >= 0.0f && io.MousePos.y >= 0.0f && ImGui::IsMouseDown(ImGuiMouseButton_Right);
if (mouseValid && sFlyCamLastMousePos.x >= 0.0f) {
cStickX -= (io.MousePos.x - sFlyCamLastMousePos.x) * 2.0f;
cStickY -= (io.MousePos.y - sFlyCamLastMousePos.y) * 2.0f;
@@ -11123,6 +11129,9 @@ camera_class* dCam_getCamera() {
dCamera_c* dCam_getBody() {
camera_process_class* camera = (camera_process_class*)dCam_getCamera();
#if TARGET_PC
if (camera == nullptr) { return nullptr; }
#endif
return &camera->mCamera;
}
+1 -1
View File
@@ -18,7 +18,7 @@
#include <typeindex>
#include "JSystem/JKernel/JKRHeap.h"
#include "absl/container/flat_hash_map.h"
#include "client/TracyScoped.hpp"
#include "tracy/Tracy.hpp"
#include "dusk/frame_interpolation.h"
#include "helpers/gx_helper.h"
#include "dusk/logging.h"
+8 -2
View File
@@ -1789,10 +1789,16 @@ void dFile_select_c::nameInput2() {
#if TARGET_PC
dusk::mods::svc::save_slot_new(mSelectNum);
const dusk::gamemode::GameMode* gameMode =
dusk::gamemode::getGameModeManager().getCurrentGameMode();
dusk::gamemode::getGameModeManager().getCurrentGameMode();
if (gameMode) {
gameMode->invokeOnNewSaveFunction();
gameMode->invokeOnSaveLoadedFunction();
}
// only do OnSaveLoaded callback here if hiding the brightness check screen
if (dusk::getSettings().game.hideTvSettingsScreen) {
if (gameMode) {
gameMode->invokeOnSaveLoadedFunction();
}
}
#endif
mDataSelProc = DATASELPROC_NEXT_MODE_WAIT;
+4 -1
View File
@@ -269,7 +269,10 @@ static void (*item_func_ptr[256])() = {
item_func_noentry,
};
inline void getItemFunc(u8 i_itemNo) {
#if !TARGET_PC
inline
#endif
void getItemFunc(u8 i_itemNo) {
dComIfGs_onItemFirstBit(i_itemNo);
item_func_ptr[i_itemNo]();
}
+2 -1
View File
@@ -606,7 +606,8 @@ void dMeter2_c::moveLife() {
life_count = (dComIfGs_getMaxLife() / 5) * 4;
}
s16 new_life = dComIfGs_getLife() + dComIfGp_getItemLifeCount();
DUSK_IF_ELSE(int, s16) new_life = dComIfGs_getLife() + dComIfGp_getItemLifeCount(); // prevent an overflow with really large damage values
if (new_life > life_count) {
new_life = life_count;
} else if (new_life < 0) {
+24 -3
View File
@@ -198,6 +198,20 @@ dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap* mp_heap) {
IF_DUSK_BLOCK_END
#endif
#if TARGET_PC
if (dusk::version::isLessThanWiiJpn()) {
if (J2DPicture* b_btn = static_cast<J2DPicture*>(mpScreen->search(MULTI_CHAR('b_btn')))) {
b_btn->setAlpha(255);
b_btn->setWhite(JUtility::TColor(195, 63, 63, 255));
}
for (int i = 0; i < 5; i++) {
static_cast<J2DTextBox*>(mpXYText[i][0]->getPanePtr())->setCharSpace(0.0f);
static_cast<J2DTextBox*>(mpXYText[i][1]->getPanePtr())->setCharSpace(0.0f);
}
}
#endif
init();
field_0xa8 = 0;
field_0x1e4 = 0;
@@ -2460,7 +2474,7 @@ void dMeter2Draw_c::drawButtonA(u8 i_action, f32 i_posX, f32 i_posY, f32 i_textP
mpButtonA->paneTrans(i_posX, i_posY);
mpTextA->scale(var_f30 * i_scale, var_f30 * i_scale);
mpTextA->paneTrans(g_drawHIO.mButtonATextPosX + i_textPosX,
g_drawHIO.mButtonATextPosY + i_textPosY);
g_drawHIO.mButtonATextPosY + i_textPosY IF_DUSK(+ (dusk::version::isLessThanWiiJpn() ? 6.0f : 0.0f)));
}
void dMeter2Draw_c::drawButtonB(u8 i_action, bool param_1, f32 i_posX, f32 i_posY, f32 i_textPosX,
@@ -2658,6 +2672,11 @@ void dMeter2Draw_c::drawButtonXY(int i_no, u8 i_itemNo, u8 i_action, bool param_
static u64 const tag[] = {MULTI_CHAR('item_x_n'), MULTI_CHAR('item_y_n')};
#if TARGET_PC
const float btnXOffsetX = dusk::version::isLessThanWiiJpn() ? 2.0f : 0.0f;
const float btnXOffsetY = dusk::version::isLessThanWiiJpn() ? 38.0f : 0.0f;
#endif
if (!param_3) {
mpScreen->search(tag[i_no])->hide();
@@ -2704,7 +2723,8 @@ void dMeter2Draw_c::drawButtonXY(int i_no, u8 i_itemNo, u8 i_action, bool param_
if (i_no == SELECT_X_e) {
mpTextXY[i_no]->scale(g_drawHIO.mButtonXYTextScale, g_drawHIO.mButtonXYTextScale);
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX, g_drawHIO.mButtonXYTextPosY);
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX IF_DUSK(- btnXOffsetX),
g_drawHIO.mButtonXYTextPosY IF_DUSK(+ btnXOffsetY));
} else if (i_no == SELECT_Y_e) {
mpTextXY[i_no]->scale(g_drawHIO.mButtonXYTextScale, g_drawHIO.mButtonXYTextScale);
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX, g_drawHIO.mButtonXYTextPosY);
@@ -2766,7 +2786,8 @@ void dMeter2Draw_c::drawButtonXY(int i_no, u8 i_itemNo, u8 i_action, bool param_
mpLightXY[0]->setAlphaRate(mButtonXItemBaseAlpha[var_r29] * field_0x7f0);
mpTextXY[i_no]->scale(g_drawHIO.mButtonXYTextScale, g_drawHIO.mButtonXYTextScale);
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX, g_drawHIO.mButtonXYTextPosY);
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX IF_DUSK(- btnXOffsetX),
g_drawHIO.mButtonXYTextPosY IF_DUSK(+ btnXOffsetY));
} else if (i_no == SELECT_Y_e) {
mpButtonXY[1]->scale(g_drawHIO.mButtonYScale, g_drawHIO.mButtonYScale);
mpButtonXY[1]->paneTrans(g_drawHIO.mButtonYPosX, g_drawHIO.mButtonYPosY);
+1 -1
View File
@@ -1762,7 +1762,7 @@ u16 dMsgFlow_c::query042(mesg_flow_node_branch* i_flowNode_p, fopAc_ac_c* i_spea
daMidna_c* midna_p = daPy_py_c::getMidnaActor();
u8 ret = 0;
if (strcmp("F_SP116", dComIfGp_getStartStageName()) == 0 && dComIfGs_isSaveDunSwitch(60)) {
if (strcmp("F_SP116", dComIfGp_getStartStageName()) == 0 && dComIfGs_isSaveDunSwitch(60) IF_DUSK(&& !dusk::getSettings().game.canTransformAnywhere)) {
ret = 4;
} else if (midna_p->checkNpcNear()) {
ret = 1;
+5
View File
@@ -43,6 +43,7 @@
#include "dusk/autosave.h"
#include "dusk/memory.h"
#include "dusk/mods/item.hpp"
#include "dusk/trigger_viewer.h"
#include "dusk/ui/ui.hpp"
#include "mods/items.h"
#endif
@@ -677,6 +678,10 @@ static int dScnPly_Draw(dScnPly_c* i_this) {
dComIfG_Ccsp()->Draw();
dComIfG_Bgsp().Draw();
#if TARGET_PC
dusk::TriggerView::execute();
#endif
#if DEBUG
dPath_Draw();
#endif
+21
View File
@@ -1537,6 +1537,27 @@ dStage_objectNameInf* dStage_searchName(char const* objName) {
return NULL;
}
#if TARGET_PC
dStage_objectNameInf* dStage_searchNameCI(char const* objName) {
dStage_objectNameInf* obj = l_objectName;
for (u32 i = 0; i < ARRAY_SIZEU(l_objectName); i++) {
const char* a = obj->name;
const char* b = objName;
while (*a && *b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) {
++a;
++b;
}
if (*a == '\0' && *b == '\0') {
return obj;
}
obj++;
}
return NULL;
}
#endif
const char* dStage_getName(s16 procName, s8 argument) {
static char tmp_name[dStage_NAME_LENGTH];
+880
View File
@@ -0,0 +1,880 @@
#include "commands.hpp"
#include "JSystem/JUtility/JUTGamePad.h"
#include "SSystem/SComponent/c_math.h"
#include "SSystem/SComponent/c_sxyz.h"
#include "SSystem/SComponent/c_xyz.h"
#include "c/c_damagereaction.h"
#include "d/actor/d_a_alink.h"
#include "d/d_com_inf_actor.h"
#include "d/d_com_inf_game.h"
#include "d/d_camera.h"
#include "d/d_kankyo.h"
#include "d/d_stage.h"
#include "dusk/game_clock.h"
#include "f_op/f_op_actor_mng.h"
#include "f_pc/f_pc_layer.h"
#include "f_pc/f_pc_layer_iter.h"
#include "f_pc/f_pc_manager.h"
#include "f_pc/f_pc_node.h"
#include <algorithm>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "fmt/format.h"
namespace dusk {
namespace {
static constexpr int kMaxHistory = 64;
static std::vector<std::string> SplitArgs(std::string_view input) {
std::vector<std::string> args;
std::string cur;
for (char c : input) {
if (c == ' ' || c == '\t') {
if (!cur.empty()) {
args.push_back(std::move(cur));
cur.clear();
}
} else {
cur += c;
}
}
if (!cur.empty()) {
args.push_back(std::move(cur));
}
return args;
}
static std::optional<long> ParseLong(const std::string& s) {
if (s.empty()) {
return std::nullopt;
}
char* end = nullptr;
const long v = std::strtol(s.c_str(), &end, 0);
return (end != s.c_str() && *end == '\0') ? std::optional<long>{v} : std::nullopt;
}
static std::optional<float> ParseFloat(const std::string& s) {
if (s.empty()) {
return std::nullopt;
}
char* end = nullptr;
const float v = std::strtof(s.c_str(), &end);
return (end != s.c_str() && *end == '\0') ? std::optional<float>{v} : std::nullopt;
}
static const char* ActorShortName(s16 profname) {
const char* n = dStage_getName(profname, -1);
return n ? n : "?";
}
// Resolves @found / @link / @<id> to a process pointer, outputting an error on failure
static base_process_class* ParseProcArg(
const std::string& s, unsigned int foundProcId, const CommandOutput& output) {
if (s.empty() || s[0] != '@') {
output("Error: proc reference must start with @");
return nullptr;
}
const std::string inner = s.substr(1);
unsigned int id;
if (inner == "found") {
if (foundProcId == 0) {
output("Error: @found is not set");
return nullptr;
}
id = foundProcId;
} else if (inner == "link") {
auto* player = dComIfGp_getPlayer(0);
if (player == nullptr) {
output("Error: player not available");
return nullptr;
}
id = (unsigned int)fpcM_GetID(player);
} else {
const auto v = ParseLong(inner);
if (!v) {
output("Error: invalid proc ID");
return nullptr;
}
id = (unsigned int)*v;
}
auto* proc = fpcM_SearchByID(id);
if (proc == nullptr) {
output(fmt::format(FMT_STRING("Error: proc {} not found"), id));
return nullptr;
}
return proc;
}
// Like ParseProcArg but ensures the proc is an actor
static fopAc_ac_c* ParseActorArg(
const std::string& s, unsigned int foundProcId, const CommandOutput& output) {
auto* proc = ParseProcArg(s, foundProcId, output);
if (proc == nullptr) {
return nullptr;
}
if (!fopAcM_IsActor(proc)) {
output("Error: proc is not an actor");
return nullptr;
}
return static_cast<fopAc_ac_c*>(proc);
}
static std::optional<s16> ParseActorId(const std::string& s) {
if (const auto v = ParseLong(s)) {
return (s16)*v;
}
const auto* entry = dStage_searchNameCI(s.c_str());
return entry ? std::optional<s16>{entry->procname} : std::nullopt;
}
static std::optional<cXyz> ParseXYZ(const std::vector<std::string>& args, size_t i) {
const auto x = ParseFloat(args[i]), y = ParseFloat(args[i + 1]), z = ParseFloat(args[i + 2]);
return (x && y && z) ? std::optional<cXyz>{cXyz(*x, *y, *z)} : std::nullopt;
}
static bool TryAngle(
s16& out, const std::vector<std::string>& args, size_t i, const CommandOutput& output) {
if (args.size() <= i) {
return true;
}
const auto a = ParseLong(args[i]);
if (!a) {
output("Error: invalid angle");
return false;
}
out = (s16)*a;
return true;
}
static std::string actorLine(const base_process_class* proc) {
const auto* ac = static_cast<const fopAc_ac_c*>(proc);
return fmt::format(FMT_STRING("procId={} 0x{:04X} ({}) @ ({:.2f}, {:.2f}, {:.2f}) room={}"),
(unsigned int)proc->id, (unsigned int)(u16)proc->profname, ActorShortName(proc->profname),
ac->current.pos.x, ac->current.pos.y, ac->current.pos.z, (int)ac->current.roomNo);
}
static void recurseLayer(void* p, int (*callback)(void*, void*), void* ctx) {
auto* proc = static_cast<base_process_class*>(p);
if (fpcBs_Is_JustOfType(g_fpcNd_type, proc->subtype)) {
fpcLyIt_OnlyHere(&static_cast<process_node_class*>(p)->layer, callback, ctx);
}
}
struct ListContext {
s16 targetId;
std::vector<std::string>* output;
};
struct FindContext {
s16 targetId;
std::vector<base_process_class*> matches;
};
static int ListActorCallback(void* p, void* ctx) {
auto* proc = static_cast<base_process_class*>(p);
auto* context = static_cast<ListContext*>(ctx);
if (fopAcM_IsActor(proc) && (context->targetId < 0 || proc->profname == context->targetId)) {
context->output->push_back(" " + actorLine(proc));
}
recurseLayer(p, ListActorCallback, ctx);
return 1;
}
static int FindActorCallback(void* p, void* ctx) {
auto* proc = static_cast<base_process_class*>(p);
auto* context = static_cast<FindContext*>(ctx);
if (fopAcM_IsActor(proc) && proc->profname == context->targetId) {
context->matches.push_back(proc);
}
recurseLayer(p, FindActorCallback, ctx);
return 1;
}
struct CameraFlyState {
bool active = false;
cXyz startEye;
cXyz startCenter;
cXyz endEye;
cXyz endCenter;
float duration = 0.0f;
float elapsed = 0.0f;
};
CameraFlyState s_cameraFly;
static dCamera_c* getCamera() { return dCam_getBody(); }
static cXyz anglesToDir(s16 h, s16 v) {
return cXyz(cM_scos(v) * cM_ssin(h), cM_ssin(v), cM_scos(v) * cM_scos(h));
}
static void cameraToAngles(dCamera_c* cam, s16& h, s16& v) {
const cXyz eye = cam->iEye();
const cXyz ctr = cam->iCenter();
const float dx = ctr.x - eye.x;
const float dy = ctr.y - eye.y;
const float dz = ctr.z - eye.z;
const float hDist = sqrtf(dx * dx + dz * dz);
h = cM_atan2s(dx, dz);
v = cM_atan2s(dy, hDist);
}
static float smoothstep(float t) {
t = std::clamp(t, 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
static constexpr float kCamTargetDist = 100.0f;
} // namespace
bool isCameraDetached() { return s_cameraFly.active; }
void processCameraCommands() {
if (!s_cameraFly.active) { return; }
dCamera_c* cam = getCamera();
if (cam == nullptr) { s_cameraFly.active = false; return; }
cXyz eye, center;
if (s_cameraFly.duration <= 0.0f) {
eye = s_cameraFly.endEye;
center = s_cameraFly.endCenter;
} else {
s_cameraFly.elapsed += dusk::game_clock::kSimPeriod;
const float t = smoothstep(s_cameraFly.elapsed / s_cameraFly.duration);
eye = s_cameraFly.startEye + (s_cameraFly.endEye - s_cameraFly.startEye) * t;
center = s_cameraFly.startCenter + (s_cameraFly.endCenter - s_cameraFly.startCenter) * t;
if (s_cameraFly.elapsed >= s_cameraFly.duration) {
eye = s_cameraFly.endEye;
center = s_cameraFly.endCenter;
s_cameraFly.startEye = s_cameraFly.endEye;
s_cameraFly.startCenter = s_cameraFly.endCenter;
s_cameraFly.duration = 0.0f;
}
}
cam->Reset(center, eye);
cam->mDebugFlyCam.initialized = false;
}
void runCommand(std::string_view cmdLine, CommandState& state, const CommandOutput& output) {
if (state.echoEnabled) {
output(fmt::format(FMT_STRING("> {}"), cmdLine));
}
if (!cmdLine.empty()) {
if (state.history.empty() || state.history.back() != cmdLine) {
state.history.push_back(std::string(cmdLine));
if ((int)state.history.size() > kMaxHistory) {
state.history.erase(state.history.begin());
}
}
}
auto args = SplitArgs(cmdLine);
if (args.empty()) {
return;
}
const auto& cmd = args[0];
auto requirePlayer = [&]() -> daAlink_c* {
auto* p = (daAlink_c*)dComIfGp_getPlayer(0);
if (p == nullptr) {
output("Error: player not available");
}
return p;
};
if (cmd == "tp") {
auto* player = requirePlayer();
if (player == nullptr) {
return;
}
if (args.size() >= 2 && args[1].starts_with('@')) {
auto* ac = ParseActorArg(args[1], state.foundProcId, output);
if (ac == nullptr) {
return;
}
if (args.size() >= 3 && args[2].starts_with('@')) {
auto* destAc = ParseActorArg(args[2], state.foundProcId, output);
if (destAc == nullptr) {
return;
}
const cXyz destPos = destAc->current.pos;
ac->current.pos = destPos;
output(fmt::format(FMT_STRING("Moved actor {} to ({:.2f}, {:.2f}, {:.2f})"), ac->id,
destPos.x, destPos.y, destPos.z));
return;
}
if (args.size() >= 5) {
const auto pos = ParseXYZ(args, 2);
if (!pos) {
output("Error: invalid coordinates");
return;
}
ac->current.pos = *pos;
if (!TryAngle(ac->shape_angle.y, args, 5, output)) {
return;
}
output(fmt::format(FMT_STRING("Moved actor {} to ({:.2f}, {:.2f}, {:.2f})"), ac->id,
pos->x, pos->y, pos->z));
return;
}
player->current.pos = ac->current.pos;
output(fmt::format(FMT_STRING("Teleported to actor {} ({:.2f}, {:.2f}, {:.2f})"),
ac->id, ac->current.pos.x, ac->current.pos.y, ac->current.pos.z));
return;
}
if (args.size() < 4) {
output("Usage: tp <x> <y> <z> [angle] | tp @<procId> [<x> <y> <z> [angle] | @link]");
return;
}
const auto pos = ParseXYZ(args, 1);
if (!pos) {
output("Error: invalid coordinates");
return;
}
player->current.pos = *pos;
if (!TryAngle(player->shape_angle.y, args, 4, output)) {
return;
}
output(fmt::format(
FMT_STRING("Teleported to ({:.2f}, {:.2f}, {:.2f})"), pos->x, pos->y, pos->z));
return;
}
if (cmd == "spawn") {
if (args.size() < 2) {
output("Usage: spawn <actorId> [params] [x y z] [angle]");
return;
}
auto* player = requirePlayer();
if (player == nullptr) {
return;
}
const auto actorId = ParseActorId(args[1]);
if (!actorId) {
output("Error: unknown actor ID or name");
return;
}
long paramsL = -1;
if (args.size() >= 3) {
const auto p = ParseLong(args[2]);
if (!p) {
output("Error: invalid params");
return;
}
paramsL = *p;
}
cXyz pos = player->current.pos;
if (args.size() >= 6) {
const auto spawnPos = ParseXYZ(args, 3);
if (!spawnPos) {
output("Error: invalid spawn coordinates");
return;
}
pos = *spawnPos;
}
s16 angleY = 0;
if (!TryAngle(angleY, args, 6, output)) {
return;
}
csXyz angle;
angle.set(0, angleY, 0);
cXyz scale(1.0f, 1.0f, 1.0f);
layer_class* savedLayer = fpcLy_CurrentLayer();
base_process_class* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e);
if (playScene != nullptr) {
fpcLy_SetCurrentLayer(&((process_node_class*)playScene)->layer);
}
unsigned int result = fopAcM_create(
*actorId, (u32)paramsL, &pos, player->current.roomNo, &angle, &scale, (s8)-1);
fpcLy_SetCurrentLayer(savedLayer);
output(result != 0 ? fmt::format(FMT_STRING("Spawned actorId=0x{:04X} procId={}"),
(unsigned int)(u16)*actorId, result) :
fmt::format(FMT_STRING("Failed to spawn actorId=0x{:04X}"),
(unsigned int)(u16)*actorId));
return;
}
if (cmd == "reset") {
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
output("Soft reset triggered");
return;
}
if (cmd == "warp") {
if (args.size() < 4) {
output("Usage: warp <stageName> <point> <roomNo> [layer=-1]");
output(" e.g. warp F_SP121 0 0");
return;
}
const auto pointL = ParseLong(args[2]), roomL = ParseLong(args[3]);
if (!pointL || !roomL) {
output("Error: invalid point or room number");
return;
}
long layerL = -1;
if (args.size() >= 5) {
const auto l = ParseLong(args[4]);
if (!l) {
output("Error: invalid layer");
return;
}
layerL = *l;
}
state.lastWarpStage = args[1];
dComIfGp_setNextStage(state.lastWarpStage.c_str(), (s16)*pointL, (s8)*roomL, (s8)layerL);
output(fmt::format(FMT_STRING("Warping to {} point={} room={} layer={}"), args[1],
(int)(s16)*pointL, (int)(s8)*roomL, (int)(s8)layerL));
return;
}
if (cmd == "list") {
s16 targetId = -1;
if (args.size() >= 2) {
const auto id = ParseActorId(args[1]);
if (!id) {
output("Error: unknown actor ID or name");
return;
}
targetId = *id;
}
std::vector<std::string> results;
ListContext ctx{targetId, &results};
fpcLyIt_OnlyHere(fpcLy_RootLayer(), ListActorCallback, &ctx);
output(results.empty() ? "No matching actors found" :
fmt::format(FMT_STRING("Found {} actor(s):"), results.size()));
for (const auto& r : results) {
output(r);
}
return;
}
if (cmd == "killall") {
if (args.size() < 2) {
output("Usage: killall <actorId|name>");
return;
}
const auto targetId = ParseActorId(args[1]);
if (!targetId) {
output("Error: unknown actor ID or name");
return;
}
FindContext ctx{*targetId, {}};
fpcLyIt_OnlyHere(fpcLy_RootLayer(), FindActorCallback, &ctx);
for (auto* proc : ctx.matches) {
fpcM_Delete(proc);
}
output(fmt::format(FMT_STRING("Deleted {} actor(s) of type {} ({})"),
(int)ctx.matches.size(), (unsigned int)(u16)*targetId, ActorShortName(*targetId)));
return;
}
if (cmd == "heal") {
auto* player = requirePlayer();
if (player == nullptr) {
return;
}
const u16 maxLife = dComIfGs_getMaxLife() / 5 * 4;
u16 newLife = maxLife;
if (args.size() >= 2) {
const auto amount = ParseLong(args[1]);
if (!amount) {
output("Error: invalid amount");
return;
}
newLife =
(u16)std::max(0L, std::min((long)maxLife, (long)dComIfGs_getLife() + *amount));
}
dComIfGs_setLife(newLife);
output(fmt::format(FMT_STRING("Health: {}/{}"), (int)newLife, (int)maxLife));
return;
}
if (cmd == "kill") {
if (args.size() >= 2) {
auto* proc = ParseProcArg(args[1], state.foundProcId, output);
if (proc == nullptr) {
return;
}
fpcM_Delete(proc);
output(fmt::format(FMT_STRING("Deleted proc {}"), proc->id));
} else {
auto* player = requirePlayer();
if (player == nullptr) {
return;
}
dComIfGs_setLife(0);
output("Set Link's health to 0");
}
return;
}
if (cmd == "freeze" || cmd == "unfreeze") {
const bool doFreeze = (cmd == "freeze");
if (args.size() < 2) {
g_dComIfAc_gameInfo.mPause = doFreeze;
output(doFreeze ? "Global freeze on" : "Global freeze off");
return;
}
auto* ac = ParseActorArg(args[1], state.foundProcId, output);
if (ac == nullptr) {
return;
}
if (doFreeze) {
fpcM_PauseEnable(ac, 1);
output(fmt::format(FMT_STRING("Froze actor {}"), (unsigned int)ac->id));
} else {
fpcM_PauseDisable(ac, 1);
output(fmt::format(FMT_STRING("Unfroze actor {}"), (unsigned int)ac->id));
}
return;
}
if (cmd == "rate") {
if (args.size() >= 2) {
const auto hz = ParseLong(args[1]);
if (!hz || *hz <= 0) {
output("Error: rate must be a positive integer");
return;
}
dusk::game_clock::set_sim_rate((float)*hz);
}
output(fmt::format(FMT_STRING("Sim rate: {:.0f} hz"), dusk::game_clock::get_sim_rate()));
return;
}
if (cmd == "ebf") {
if (args.size() >= 2) {
const auto val = ParseLong(args[1]);
if (!val || *val < 0 || *val > 255) {
output("Error: value must be 0-255");
return;
}
cDmr_SkipInfo = (u8)*val;
}
output(fmt::format(FMT_STRING("EBF = {}"), (int)cDmr_SkipInfo));
return;
}
if (cmd == "time") {
if (args.size() >= 2) {
const auto t = ParseFloat(args[1]);
if (!t || *t < 0.0f || *t > 360.0f) {
output("Error: time must be a float between 0 and 360");
return;
}
dKy_instant_timechg(*t);
}
output(fmt::format(FMT_STRING("Time: {:.2f} ({}:{:02d})"), dComIfGs_getTime(),
dKy_getdaytime_hour(), dKy_getdaytime_minute()));
return;
}
if (cmd == "rupees") {
const u16 maxRupees = dComIfGs_getRupeeMax();
if (args.size() >= 2) {
const auto amount = ParseLong(args[1]);
if (!amount) {
output("Error: invalid amount");
return;
}
dComIfGs_setRupee((u16)std::max(0L, std::min((long)maxRupees, *amount)));
}
output(fmt::format(FMT_STRING("Rupees: {}/{}"), (int)dComIfGs_getRupee(), (int)maxRupees));
return;
}
if (cmd == "find") {
if (args.size() < 2) {
if (state.foundProcId == 0) {
output("@found is not set");
return;
}
auto* proc = fpcM_SearchByID(state.foundProcId);
output(proc != nullptr && fopAcM_IsActor(proc) ?
"@found = " + actorLine(proc) :
fmt::format(FMT_STRING("@found = {} (no longer exists)"),
(unsigned int)state.foundProcId));
return;
}
const auto targetId = ParseActorId(args[1]);
if (!targetId) {
output("Error: unknown actor ID or name");
return;
}
int targetN = 1;
if (args.size() >= 3) {
const auto n = ParseLong(args[2]);
if (!n || *n < 1) {
output("Error: index must be >= 1");
return;
}
targetN = (int)*n;
}
FindContext ctx{*targetId, {}};
fpcLyIt_OnlyHere(fpcLy_RootLayer(), FindActorCallback, &ctx);
if (ctx.matches.empty()) {
output(fmt::format(FMT_STRING("No actors found for '{}'"), args[1]));
return;
}
std::sort(ctx.matches.begin(), ctx.matches.end(),
[](const base_process_class* a, const base_process_class* b) { return a->id < b->id; });
if (targetN > (int)ctx.matches.size()) {
output(fmt::format(
FMT_STRING("Error: only {} actor(s) of that type exist"), (int)ctx.matches.size()));
return;
}
auto* picked = ctx.matches[(size_t)(targetN - 1)];
state.foundProcId = picked->id;
output(fmt::format(
FMT_STRING("@found [{}/{}] {}"), targetN, (int)ctx.matches.size(), actorLine(picked)));
return;
}
if (cmd == "camera") {
const std::string sub = args.size() >= 2 ? args[1] : "";
if (sub == "attach") {
s_cameraFly.active = false;
output("Camera attached");
return;
}
auto* cam = getCamera();
if (cam == nullptr) { output("Error: camera not available"); return; }
if (sub == "detach") {
s_cameraFly.active = true;
s_cameraFly.duration = 0.0f;
s_cameraFly.startEye = s_cameraFly.endEye = cam->iEye();
s_cameraFly.startCenter = s_cameraFly.endCenter = cam->iCenter();
output("Camera detached");
return;
}
if (sub == "tp") {
// camera tp @<ref>
if (args.size() >= 3 && !args[2].empty() && args[2][0] == '@') {
auto* ac = ParseActorArg(args[2], state.foundProcId, output);
if (ac == nullptr) { return; }
const cXyz actorPos = ac->current.pos;
const cXyz dir = anglesToDir(ac->shape_angle.y, 0);
const cXyz newEye = cXyz(actorPos.x - dir.x * kCamTargetDist,
actorPos.y - dir.y * kCamTargetDist + 50.0f,
actorPos.z - dir.z * kCamTargetDist);
s_cameraFly.active = true; s_cameraFly.duration = 0.0f;
s_cameraFly.startEye = s_cameraFly.endEye = newEye;
s_cameraFly.startCenter = s_cameraFly.endCenter = actorPos;
output(fmt::format(FMT_STRING("Camera moved to actor {}"), ac->id));
return;
}
// camera tp x y z [h] [v]
if (args.size() < 5) {
output("Usage: camera tp <x> <y> <z> [h_angle] [v_angle] | camera tp @<ref>");
return;
}
const auto pos = ParseXYZ(args, 2);
if (!pos) { output("Error: invalid coordinates"); return; }
s16 h = 0, v = 0;
cameraToAngles(cam, h, v);
if (args.size() >= 6) {
const auto hv = ParseLong(args[5]);
if (!hv) { output("Error: invalid h_angle"); return; }
h = (s16)*hv;
}
if (args.size() >= 7) {
const auto vv = ParseLong(args[6]);
if (!vv) { output("Error: invalid v_angle"); return; }
v = (s16)*vv;
}
const cXyz dir = anglesToDir(h, v);
const cXyz center = cXyz(pos->x + dir.x * kCamTargetDist,
pos->y + dir.y * kCamTargetDist,
pos->z + dir.z * kCamTargetDist);
s_cameraFly.active = true; s_cameraFly.duration = 0.0f;
s_cameraFly.startEye = s_cameraFly.endEye = *pos;
s_cameraFly.startCenter = s_cameraFly.endCenter = center;
output(fmt::format(FMT_STRING("Camera teleported to ({:.2f}, {:.2f}, {:.2f})"),
pos->x, pos->y, pos->z));
return;
}
if (sub == "pos") {
s16 h = 0, v = 0;
cameraToAngles(cam, h, v);
const cXyz eye = cam->iEye();
output(fmt::format(FMT_STRING("Camera: ({:.2f}, {:.2f}, {:.2f}) h={} v={}"),
eye.x, eye.y, eye.z, (int)h, (int)v));
return;
}
if (sub == "fly") {
// camera fly <time> <x> <y> <z> [h] [v] | camera fly <time> @<ref>
if (args.size() < 4) {
output("Usage: camera fly <time> <x> <y> <z> [h_angle] [v_angle] | camera fly <time> @<ref>");
return;
}
const auto t = ParseFloat(args[2]);
if (!t || *t <= 0) { output("Error: invalid time"); return; }
const float flyTime = *t;
cXyz targetPos;
s16 h = 0, v = 0;
if (!args[3].empty() && args[3][0] == '@') {
auto* ac = ParseActorArg(args[3], state.foundProcId, output);
if (ac == nullptr) { return; }
targetPos = ac->current.pos;
h = ac->shape_angle.y;
v = 0;
} else {
if (args.size() < 6) {
output("Usage: camera fly <time> <x> <y> <z> [h_angle] [v_angle] | camera fly <time> @<ref>");
return;
}
const auto pos = ParseXYZ(args, 3);
if (!pos) { output("Error: invalid coordinates"); return; }
targetPos = *pos;
cameraToAngles(cam, h, v);
if (args.size() >= 7) {
const auto hv = ParseLong(args[6]); if (!hv) { output("Error: invalid h_angle"); return; } h = (s16)*hv;
}
if (args.size() >= 8) {
const auto vv = ParseLong(args[7]); if (!vv) { output("Error: invalid v_angle"); return; } v = (s16)*vv;
}
}
const cXyz dir = anglesToDir(h, v);
const cXyz endCenter = cXyz(targetPos.x + dir.x * kCamTargetDist,
targetPos.y + dir.y * kCamTargetDist,
targetPos.z + dir.z * kCamTargetDist);
s_cameraFly.active = true;
s_cameraFly.startEye = cam->iEye();
s_cameraFly.startCenter = cam->iCenter();
s_cameraFly.endEye = targetPos;
s_cameraFly.endCenter = endCenter;
s_cameraFly.duration = flyTime;
s_cameraFly.elapsed = 0.0f;
output(fmt::format(FMT_STRING("Camera flying to ({:.2f}, {:.2f}, {:.2f}) over {:.1f}s"),
targetPos.x, targetPos.y, targetPos.z, flyTime));
return;
}
output("Usage: camera detach | attach | tp ... | fly ... | pos");
return;
}
if (cmd == "echo") {
if (args.size() >= 2 && args[1] == "off") {
state.echoEnabled = false;
output("Echo disabled");
} else if (args.size() >= 2 && args[1] == "on") {
state.echoEnabled = true;
output("Echo enabled");
} else {
output(state.echoEnabled ? "Echo: on" : "Echo: off");
}
return;
}
if (cmd == "transform") {
auto* player = requirePlayer();
if (player == nullptr) { return; }
player->procCoMetamorphoseInit();
output("Transforming");
return;
}
if (cmd == "angle") {
auto* player = requirePlayer();
if (player == nullptr) { return; }
if (args.size() >= 2) {
const auto a = ParseLong(args[1]);
if (!a) { output("Error: invalid angle"); return; }
player->shape_angle.y = (s16)*a;
output(fmt::format(FMT_STRING("Angle set to {}"), (int)(s16)*a));
} else {
output(fmt::format(FMT_STRING("Angle: {}"), (int)player->shape_angle.y));
}
return;
}
if (cmd == "pos") {
auto* player = requirePlayer();
if (player == nullptr) {
return;
}
output(fmt::format(FMT_STRING("pos: {:.4f} {:.4f} {:.4f}"), player->current.pos.x,
player->current.pos.y, player->current.pos.z));
output(
fmt::format(FMT_STRING("stage: {} room: {} entry: {}"), dComIfGp_getStartStageName(),
(int)player->current.roomNo, (int)dComIfGp_getStartStagePoint()));
return;
}
if (cmd == "help") {
output("@<ref> = @<procId> | @found | @link");
output("");
output("angle [value] Get or set Link's facing angle");
output("camera detach | attach Detach or reattach camera");
output("camera pos Print camera position and angles");
output("camera tp <x> <y> <z> [h] [v] Teleport camera (h/v are s16 angles)");
output("camera tp @<ref> Teleport camera to actor");
output("camera fly <t> <x> <y> <z> [h] [v] Fly camera over t seconds (h/v are s16 angles)");
output("camera fly <t> @<ref> Fly camera to actor");
output("ebf [0-255] Get or set cDmr_SkipInfo");
output("echo on | off Enable or disable command echo");
output("find <id|name> [n=1] Store nth actor as @found");
output("freeze [@<ref>] Freeze globally, or freeze actor");
output("heal [amount] Heal to max, or by relative amount");
output("kill Set Link health to 0");
output("kill @<ref> Delete proc");
output("killall <id|name> Delete all actors of a type");
output("list [id|name] List actors in scene");
output("pos Print player position and stage");
output("rate [hz] Get or set sim rate (1-480, default 30)");
output("reset Soft reset");
output("rupees [amount] Get or set rupee count");
output("spawn <id|name> [params] [x y z] [angle] Spawn actor");
output("time [0-360] Get or set time of day");
output("tp <x> <y> <z> [angle] Teleport Link to coords");
output("tp @<ref> Teleport Link to actor");
output("tp @<ref> <x> <y> <z> [angle] Move actor to coords");
output("tp @<ref> @<ref> Move actor to actor");
output("transform Force transform");
output("unfreeze [@<ref>] Unfreeze globally, or unfreeze actor");
output("warp <stage> <point> <room> [layer] Warp to stage");
return;
}
output(fmt::format(FMT_STRING("Unknown command '{}' (try 'help')"), cmd));
}
} // namespace dusk
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <functional>
#include <string>
#include <string_view>
#include <vector>
namespace dusk {
using CommandOutput = std::function<void(std::string)>;
struct CommandState {
std::vector<std::string> history;
unsigned int foundProcId = 0;
std::string lastWarpStage;
bool echoEnabled = true;
};
// Execute a single command line. Calls output() for every line of response.
// Manages history internally; callers need only hold a CommandState.
void runCommand(std::string_view cmdLine, CommandState& state, const CommandOutput& output);
// Per-sim-tick update for camera fly animation.
void processCameraCommands();
// Returns true when the dev camera is detached (suppresses the game's camera update).
bool isCameraDetached();
} // namespace dusk
+10
View File
@@ -50,6 +50,16 @@ void reset() {
s_simTickActive = false;
}
void set_sim_rate(float hz) {
const float maximumHz = aurora::time::kMaximumTimeScale / kSimPeriod;
aurora::time::set_scale(std::clamp(hz, 1.0f, maximumHz) * kSimPeriod);
reset();
}
float get_sim_rate() {
return aurora::time::scale() / kSimPeriod;
}
const FrameTiming& advance() {
const auto nativeNow = native_clock::now();
const auto gameNow = game_clock::now();
+4
View File
@@ -28,4 +28,8 @@ float sample_interpolation_step();
float consume_interval(const void* consumer);
// Sets the effective simulation rate through the game clock time scale.
void set_sim_rate(float hz);
float get_sim_rate();
} // namespace dusk::game_clock
+136
View File
@@ -0,0 +1,136 @@
#include "dusk/game_combos.h"
#include "SSystem/SComponent/c_API_controller_pad.h"
#include "SSystem/SComponent/c_xyz.h"
#include "d/actor/d_a_alink.h"
#include "d/d_com_inf_game.h"
#include "dusk/settings.h"
#include "m_Do/m_Do_controller_pad.h"
namespace dusk {
namespace {
cXyz s_savedTeleportPos{};
s16 s_savedTeleportAngle = 0;
bool s_hasTeleportPos = false;
static daAlink_c* getPlayer() {
return (daAlink_c*)dComIfGp_getPlayer(0);
}
static void consumeButtons(u16 mask) {
mDoCPd_c::getCpadInfo(PAD_1).mPressedButtonFlags &= ~mask;
mDoCPd_c::getCpadInfo(PAD_1).mButtonFlags &= ~mask;
}
// Table: holdMask, trigMask, strict, condition, action, consumeMask, exclusive
static const GameCombo kCombos[] = {
// Move Link (L+R+Y), pass-through, non-exclusive
{
PAD_TRIGGER_R | PAD_TRIGGER_L,
PAD_BUTTON_Y,
false,
[] { return (bool)getSettings().game.enableMoveLinkCombo; },
[] { getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive; },
0,
false,
},
// Quick Transform (R+Y, strictly only R held)
{
PAD_TRIGGER_R,
PAD_BUTTON_Y,
true,
[] { return getPlayer() != nullptr; },
[] { getPlayer()->handleQuickTransform(); },
PAD_BUTTON_Y,
false,
},
// Wolf Howl (R+X)
{
PAD_TRIGGER_R,
PAD_BUTTON_X,
false,
[] { return getPlayer() != nullptr; },
[] { getPlayer()->handleWolfHowl(); },
PAD_BUTTON_X,
false,
},
// Teleport save (R+D-pad Up), consumes D-pad Up, exclusive
{
PAD_TRIGGER_R,
PAD_BUTTON_UP,
false,
[] { return getSettings().game.enableTeleportCombo && getPlayer() != nullptr; },
[] {
auto* p = getPlayer();
s_savedTeleportPos = p->current.pos;
s_savedTeleportAngle = p->shape_angle.y;
s_hasTeleportPos = true;
},
PAD_BUTTON_UP,
true,
},
// Teleport load (R+D-pad Down), consumes D-pad Down, exclusive
{
PAD_TRIGGER_R,
PAD_BUTTON_DOWN,
false,
[] {
return getSettings().game.enableTeleportCombo && s_hasTeleportPos &&
getPlayer() != nullptr;
},
[] {
auto* p = getPlayer();
p->current.pos = s_savedTeleportPos;
p->shape_angle.y = s_savedTeleportAngle;
p->mNormalSpeed = 0.0f;
},
PAD_BUTTON_DOWN,
true,
},
// Moon Jump (R+A, hold), continuous, pass-through, non-exclusive
{
PAD_TRIGGER_R | PAD_BUTTON_A,
0,
false,
[] { return getSettings().game.moonJump && getPlayer() != nullptr; },
[] { getPlayer()->speed.y = 56.0f; },
0,
false,
},
};
} // namespace
void processGameCombos() {
if (!getSettings().game.enableMoveLinkCombo) {
getTransientSettings().moveLinkActive = false;
}
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) {
continue;
}
if (combo.strict && (held & ~combo.trigMask) != combo.holdMask) {
continue;
}
if (combo.trigMask != 0 && !(trig & combo.trigMask)) {
continue;
}
if (combo.condition != nullptr && !combo.condition()) {
continue;
}
combo.action();
if (combo.consumeMask != 0) {
consumeButtons(combo.consumeMask);
}
if (combo.exclusive) {
return;
}
}
}
} // namespace dusk
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <dolphin/types.h>
namespace dusk {
struct GameCombo {
using ConditionFn = bool(*)();
using ActionFn = void(*)();
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
u16 consumeMask; // buttons to clear after firing; 0 = pass-through
bool exclusive; // stop evaluating future combos if this fires
};
void processGameCombos();
} // namespace dusk
+9 -4
View File
@@ -64,10 +64,15 @@ namespace dusk {
if (eventRunning) {
ImGui::SetTooltip("Cannot enable while paused or during an active event.");
} else {
ImGui::SetTooltip("Detach camera and fly freely.\n"
"WASD/Arrows/Left stick: move, Mouse/C-stick: look\n"
"Ctrl/L: down, Space/R: up, Shift/Z: fast\n"
"Q Key/Y: roll left, R Key/X: roll right");
ImGui::SetTooltip("Detach camera and fly freely.\n\n"
"Controls:\n"
"WASD/Arrows/Left stick - Move\n"
"Right Click+Mouse/C-stick - Look\n"
"Ctrl/L - Down\n"
"Space/R - Up\n"
"Shift/Z - Faster\n"
"Q Key/Y - Roll Left\n"
"R Key/X - Roll Right");
}
}
if (eventRunning) {
-7
View File
@@ -264,13 +264,6 @@ namespace dusk {
}
previousTurboActive = turboActive;
previousSlowActive = slowDown;
if (frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) {
getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive;
}
if (mDoMain::developmentMode != 1) {
getTransientSettings().moveLinkActive = false;
}
}
void ImGuiConsole::PreDraw() {
+20
View File
@@ -89,6 +89,26 @@ namespace dusk {
ImGui::EndMenu();
}
auto& triggerView = getTransientSettings().triggerView;
if (ImGui::BeginMenu("Trigger View")) {
ImGui::Checkbox("Load Zones", &triggerView.loadZones);
ImGui::Checkbox("Event Areas", &triggerView.eventAreas);
ImGui::Checkbox("Event Tags", &triggerView.eventTags);
ImGui::Checkbox("Switch Areas", &triggerView.switchAreas);
ImGui::Checkbox("Midna Stops", &triggerView.midnaStops);
ImGui::Checkbox("Twilight Gates", &triggerView.twilightGates);
ImGui::Checkbox("Checkpoints", &triggerView.checkpoints);
ImGui::Checkbox("Paths", &triggerView.paths);
ImGui::Separator();
ImGui::Checkbox("Transform Distances", &triggerView.transformDists);
ImGui::Checkbox("Attention Distances", &triggerView.attentionDists);
ImGui::Checkbox("Purple Mist Avoid", &triggerView.purpleMistAvoid);
ImGui::Checkbox("Leever Ranges", &triggerView.leevers);
ImGui::Separator();
ImGui::SliderFloat("Opacity##triggers", &triggerView.opacity, 0.0f, 100.0f);
ImGui::EndMenu();
}
if (!dusk::IsGameLaunched) {
ImGui::BeginDisabled();
}
+1
View File
@@ -14,6 +14,7 @@ constexpr std::string_view kStubFragments[] = {
"Unimplemented: BP register"sv,
"Unhandled BP register"sv,
"Unhandled XF register"sv,
"Unhandled XF memory write"sv,
"but selective updates are not implemented"sv,
};
+8
View File
@@ -20,6 +20,7 @@ struct ItemCheckResult {
uint32_t tag = 0;
uint8_t itemNo = 0;
uint8_t displayItemNo = 0;
bool was_resolved = false;
};
ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_ac_c* giver);
@@ -57,6 +58,13 @@ uint32_t item_check_message(uint16_t group, uint32_t messageId);
bool item_give_queue_dispatching();
uint32_t item_give_queue_take_tag();
struct BossItemActorSpeeds {
f32 f;
f32 y;
};
void set_boss_item_actor_speeds(f32 f, f32 y);
BossItemActorSpeeds get_boss_item_actor_speeds();
namespace detail {
struct CommittedCheck {
uint8_t vanillaItem = 0;
+15
View File
@@ -0,0 +1,15 @@
#include "item.hpp"
namespace dusk::mods {
static BossItemActorSpeeds bossItemActorSpeeds{};
void set_boss_item_actor_speeds(f32 f, f32 y) {
bossItemActorSpeeds.f = f;
bossItemActorSpeeds.y = y;
}
BossItemActorSpeeds get_boss_item_actor_speeds() {
return bossItemActorSpeeds;
}
}
+11 -1
View File
@@ -120,6 +120,12 @@ constexpr std::array kMessageChecks{
.vanillaItem = dItemNo_HORSE_FLUTE_e,
.enqueueAtDisplay = false,
},
MessageCheck{
.group = 2,
.messageId = 6531,
.name = ITEM_CHECK_SHAD_DOMINION_ROD,
.vanillaItem = dItemNo_COPY_ROD_2_e,
}
};
std::unordered_map<LoadedMod*, ModItemChecks> s_modChecks;
@@ -152,7 +158,7 @@ std::string poe_check_name(uint8_t bitNo) {
}
std::string shop_check_name(uint8_t itemNo) {
return fmt::format("shop:{}:{}", current_stage_name(), itemNo);
return fmt::format("shop:{}:{}:{}", current_stage_name(), dStage_roomControl_c::getStayNo(), itemNo);
}
std::string bug_check_name(uint8_t insectId) {
@@ -229,6 +235,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
.vanilla_item = itemNo,
.current_item = itemNo,
.current_display_item = itemNo,
.was_resolved = false,
};
for (const auto& resolve : resolves) {
if (!resolve.mod->active) {
@@ -237,6 +244,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
if (resolve.fixedValue) {
info.current_item = resolve.itemNo;
info.current_display_item = resolve.itemNo;
info.was_resolved = true;
continue;
}
@@ -251,6 +259,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
}
info.current_item = resolution.item;
info.current_display_item = resolution.display_item;
info.was_resolved = true;
}
} catch (const std::exception& e) {
fail_mod(*resolve.mod, MOD_ERROR,
@@ -263,6 +272,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
return {
.item = info.current_item,
.display_item = info.current_display_item,
.was_resolved = info.was_resolved,
};
}
+2
View File
@@ -265,6 +265,7 @@ ItemCheckResult item_check_commit(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c*
.tag = giveTag,
.itemNo = committed->resolution.item,
.displayItemNo = committed->resolution.display_item,
.was_resolved = committed->resolution.was_resolved,
};
}
@@ -274,6 +275,7 @@ ItemCheckResult item_check_commit(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c*
.tag = giveTag,
.itemNo = committed.resolution.item,
.displayItemNo = committed.resolution.display_item,
.was_resolved = committed.resolution.was_resolved,
};
}
+1 -13
View File
@@ -6,14 +6,6 @@
#include <Windows.h>
#endif
#if defined(__SANITIZE_ADDRESS__)
#define ADDRESS_SANITIZER 1
#elif defined(__has_feature)
#if __has_feature(address_sanitizer)
#define ADDRESS_SANITIZER 1
#endif
#endif
namespace {
#if defined(_WIN32)
void* pl_dlopen(const std::filesystem::path& p) {
@@ -39,11 +31,7 @@ std::string pl_dlerror() {
#else
#include <dlfcn.h>
void* pl_dlopen(const std::filesystem::path& p) {
int flags = RTLD_LAZY | RTLD_LOCAL;
#if defined(RTLD_DEEPBIND) && !defined(ADDRESS_SANITIZER)
flags |= RTLD_DEEPBIND;
#endif
return dlopen(p.c_str(), flags);
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL);
}
void* pl_dlsym(void* h, const char* name) {
return dlsym(h, name);
+5 -5
View File
@@ -75,12 +75,12 @@ __declspec(allocate(".symdbh"))
#if defined(__clang__)
__attribute__((used))
#endif
constinit const SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
constinit SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
#elif defined(__APPLE__)
__attribute__((section("__DATA,__symdbh"), used)) constinit const SymdbDescriptor
s_symdbDescriptor{kDescriptorMagic, 0, 0};
__attribute__((section("__DATA,__symdbh"), used)) constinit SymdbDescriptor s_symdbDescriptor{
kDescriptorMagic, 0, 0};
#else
__attribute__((section("symdbh"), used)) constinit const SymdbDescriptor s_symdbDescriptor{
__attribute__((section("symdbh"), used)) constinit SymdbDescriptor s_symdbDescriptor{
kDescriptorMagic, 0, 0};
#endif
@@ -92,7 +92,7 @@ struct State {
uint64_t stringsLen = 0;
uintptr_t imageBase = 0;
// (rva, nameOff) of entries flagged kFlagInlineSites, sorted by rva
std::vector<std::pair<uint64_t, uint32_t> > inlineSites;
std::vector<std::pair<uint64_t, uint32_t>> inlineSites;
bool loaded = false;
bool initialized = false;
};
+703
View File
@@ -0,0 +1,703 @@
#include "registry.hpp"
#include "slot_map.hpp"
#include <aurora/lib/window.hpp>
#include <borealis/file_select.hpp>
#include <borealis/io.hpp>
#include <borealis/log.hpp>
#include "dusk/config.hpp"
#include "dusk/mods/loader/loader.hpp"
#include "mods/svc/file.h"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <limits>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace dusk::mods::svc {
namespace {
constexpr borealis::Log Log{"dusk::mods::file"};
static_assert(static_cast<int>(FILE_OPEN_READ) == static_cast<int>(borealis::io::File::Mode::Read));
static_assert(
static_cast<int>(FILE_OPEN_TRUNCATE) == static_cast<int>(borealis::io::File::Mode::Truncate));
static_assert(
static_cast<int>(FILE_OPEN_APPEND) == static_cast<int>(borealis::io::File::Mode::Append));
SlotMap<borealis::io::File> s_streams;
std::unordered_map<void*, LoadedMod*> s_buffers;
std::unordered_map<LoadedMod*, std::string> s_joinResults;
ConfigVar<std::string> s_pickerOverride{"file.pickerOverride", ""};
struct PendingPick {
LoadedMod* owner = nullptr;
FilePickFn callback = nullptr;
void* userData = nullptr;
std::optional<std::string> overrideLocation;
std::optional<std::string> exportSource;
};
std::shared_ptr<PendingPick> s_pendingPick;
ModResult map_status(borealis::io::Status status) {
switch (status) {
case borealis::io::Status::Ok:
return MOD_OK;
case borealis::io::Status::NotFound:
return MOD_UNAVAILABLE;
case borealis::io::Status::Unsupported:
return MOD_UNSUPPORTED;
case borealis::io::Status::AlreadyExists:
return MOD_CONFLICT;
case borealis::io::Status::Failed:
default:
return MOD_ERROR;
}
}
ModResult map_picker_status(borealis::file_select::Status status) {
switch (status) {
case borealis::file_select::Status::Selected:
return MOD_OK;
case borealis::file_select::Status::Canceled:
return MOD_UNAVAILABLE;
case borealis::file_select::Status::Unsupported:
return MOD_UNSUPPORTED;
case borealis::file_select::Status::Busy:
return MOD_CONFLICT;
case borealis::file_select::Status::Failed:
default:
return MOD_ERROR;
}
}
void invoke_pick(const std::shared_ptr<PendingPick>& pending, ModResult status,
const std::vector<std::string>& locations, const std::string& error) {
if (s_pendingPick == pending) {
s_pendingPick.reset();
}
auto* owner = pending->owner;
const auto callback = pending->callback;
if (owner == nullptr || callback == nullptr || !owner->active) {
return;
}
std::vector<const char*> rawLocations;
rawLocations.reserve(locations.size());
for (const auto& location : locations) {
rawLocations.push_back(location.c_str());
}
try {
callback(owner->context.get(), status, rawLocations.empty() ? nullptr : rawLocations.data(),
static_cast<uint32_t>(rawLocations.size()), error.c_str(), pending->userData);
} catch (const std::exception& exception) {
fail_mod(*owner, MOD_ERROR,
std::string{"exception in file picker callback: "} + exception.what());
} catch (...) {
fail_mod(*owner, MOD_ERROR, "unknown exception in file picker callback");
}
}
bool valid_pick_options(const FilePickOptions* options) {
if (options == nullptr || options->struct_size < sizeof(FilePickOptions) ||
(options->filter_count != 0 && options->filters == nullptr))
{
return false;
}
for (uint32_t i = 0; i < options->filter_count; ++i) {
if (options->filters[i].name == nullptr || options->filters[i].pattern == nullptr) {
return false;
}
}
return true;
}
ModResult begin_pick(ModContext* context, const FilePickOptions* options, FilePickFn callback,
void* userData, bool folder) {
auto* mod = mod_from_context(context);
if (mod == nullptr || callback == nullptr || !valid_pick_options(options)) {
return MOD_INVALID_ARGUMENT;
}
if (s_pendingPick != nullptr || borealis::file_select::busy()) {
return MOD_CONFLICT;
}
auto pending = std::make_shared<PendingPick>(PendingPick{
.owner = mod,
.callback = callback,
.userData = userData,
});
const auto& overrideLocation = s_pickerOverride.getValue();
if (!overrideLocation.empty()) {
pending->overrideLocation = overrideLocation;
s_pendingPick = std::move(pending);
return MOD_OK;
}
s_pendingPick = pending;
const std::string defaultLocation =
options->default_location != nullptr ? options->default_location : "";
if (folder) {
borealis::file_select::open_folder(
{
.parentWindow = aurora::window::get_sdl_window(),
.defaultLocation = defaultLocation,
},
[pending](borealis::file_select::Result result) {
invoke_pick(
pending, map_picker_status(result.status), result.locations, result.message);
});
return MOD_OK;
}
std::vector<borealis::file_select::Filter> filters;
filters.reserve(options->filter_count);
for (uint32_t i = 0; i < options->filter_count; ++i) {
filters.push_back({options->filters[i].name, options->filters[i].pattern});
}
borealis::file_select::open_file(
{
.parentWindow = aurora::window::get_sdl_window(),
.filters = std::move(filters),
.defaultLocation = defaultLocation,
},
[pending](borealis::file_select::Result result) {
invoke_pick(
pending, map_picker_status(result.status), result.locations, result.message);
});
return MOD_OK;
}
ModResult begin_export(ModContext* context, const char* sourceLocation, const char* suggestedName,
FilePickFn callback, void* userData) {
auto* mod = mod_from_context(context);
const std::string_view name = suggestedName != nullptr ? suggestedName : "";
if (mod == nullptr || sourceLocation == nullptr || callback == nullptr || name.empty() ||
name == "." || name == ".." || name.find_first_of("/\\") != std::string_view::npos)
{
return MOD_INVALID_ARGUMENT;
}
if (s_pendingPick != nullptr || borealis::file_select::busy()) {
return MOD_CONFLICT;
}
const auto available = borealis::io::check(sourceLocation);
if (available != borealis::io::Status::Ok) {
return map_status(available);
}
auto pending = std::make_shared<PendingPick>(PendingPick{
.owner = mod,
.callback = callback,
.userData = userData,
});
const auto& overrideLocation = s_pickerOverride.getValue();
if (!overrideLocation.empty()) {
pending->overrideLocation = overrideLocation;
pending->exportSource = sourceLocation;
s_pendingPick = std::move(pending);
return MOD_OK;
}
borealis::file_select::ExportOptions options{
.parentWindow = aurora::window::get_sdl_window(),
.sourceLocation = sourceLocation,
.suggestedName = suggestedName,
};
s_pendingPick = pending;
try {
borealis::file_select::export_file(
std::move(options), [pending](borealis::file_select::Result result) {
invoke_pick(
pending, map_picker_status(result.status), result.locations, result.message);
});
} catch (...) {
if (s_pendingPick == pending) {
s_pendingPick.reset();
}
throw;
}
return MOD_OK;
}
ModResult file_pick_file(
ModContext* context, const FilePickOptions* options, FilePickFn callback, void* userData) {
return begin_pick(context, options, callback, userData, false);
}
ModResult file_pick_folder(
ModContext* context, const FilePickOptions* options, FilePickFn callback, void* userData) {
return begin_pick(context, options, callback, userData, true);
}
ModResult file_export_file(ModContext* context, const char* sourceLocation,
const char* suggestedName, FilePickFn callback, void* userData) {
try {
return begin_export(context, sourceLocation, suggestedName, callback, userData);
} catch (...) {
return MOD_ERROR;
}
}
ModResult file_display_name(
ModContext* context, const char* location, char* buffer, uint32_t bufferSize) {
if (mod_from_context(context) == nullptr || location == nullptr || buffer == nullptr ||
bufferSize == 0)
{
return MOD_INVALID_ARGUMENT;
}
const std::string name = borealis::io::display_name(location);
if (name.size() + 1 > bufferSize) {
return MOD_INVALID_ARGUMENT;
}
std::memcpy(buffer, name.c_str(), name.size() + 1);
return MOD_OK;
}
ModResult file_check(ModContext* context, const char* location) {
if (mod_from_context(context) == nullptr || location == nullptr) {
return MOD_INVALID_ARGUMENT;
}
return map_status(borealis::io::check(location));
}
ModResult file_open(
ModContext* context, const char* location, FileOpenMode mode, FileStreamHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || location == nullptr || outHandle == nullptr || mode < FILE_OPEN_READ ||
mode > FILE_OPEN_APPEND)
{
return MOD_INVALID_ARGUMENT;
}
try {
const auto ioMode = static_cast<borealis::io::File::Mode>(mode);
if (mode != FILE_OPEN_READ) {
const auto available = borealis::io::check(location);
if (available != borealis::io::Status::Ok) {
return map_status(available);
}
}
auto result = borealis::io::open(location, ioMode);
if (result.status != borealis::io::Status::Ok) {
Log.warn("[{}] open '{}' failed: {}", mod->metadata.id,
borealis::io::display_name(location), result.message);
return map_status(result.status);
}
*outHandle = s_streams.emplace(*mod, std::move(result.file));
return MOD_OK;
} catch (...) {
return MOD_ERROR;
}
}
borealis::io::File* find_stream(ModContext* context, FileStreamHandle handle) {
auto* mod = mod_from_context(context);
if (mod == nullptr) {
return nullptr;
}
auto* entry = s_streams.find_owned(handle, *mod);
return entry != nullptr ? &entry->value : nullptr;
}
ModResult file_size(ModContext* context, FileStreamHandle handle, uint64_t* outSize) {
if (outSize == nullptr) {
return MOD_INVALID_ARGUMENT;
}
*outSize = 0;
auto* file = find_stream(context, handle);
if (file == nullptr) {
return MOD_INVALID_ARGUMENT;
}
*outSize = file->size();
return MOD_OK;
}
ModResult file_read(ModContext* context, FileStreamHandle handle, void* buffer, uint64_t length,
uint64_t* outRead) {
if (outRead == nullptr || (buffer == nullptr && length != 0)) {
return MOD_INVALID_ARGUMENT;
}
*outRead = 0;
auto* file = find_stream(context, handle);
if (file == nullptr || file->writable()) {
return MOD_INVALID_ARGUMENT;
}
*outRead = file->read(buffer, length);
return file->error().empty() ? MOD_OK : MOD_ERROR;
}
ModResult file_seek(ModContext* context, FileStreamHandle handle, uint64_t offset) {
auto* file = find_stream(context, handle);
if (file == nullptr) {
return MOD_INVALID_ARGUMENT;
}
return file->seek(offset) ? MOD_OK : MOD_ERROR;
}
ModResult file_close(ModContext* context, FileStreamHandle handle) {
auto* mod = mod_from_context(context);
if (mod == nullptr) {
return MOD_INVALID_ARGUMENT;
}
auto entry = s_streams.take_owned(handle, *mod);
if (!entry.has_value()) {
return MOD_INVALID_ARGUMENT;
}
return entry->value.close() ? MOD_OK : MOD_ERROR;
}
ModResult file_write(
ModContext* context, FileStreamHandle handle, const void* buffer, uint64_t length) {
if ((buffer == nullptr && length != 0) || length > std::numeric_limits<size_t>::max()) {
return MOD_INVALID_ARGUMENT;
}
auto* file = find_stream(context, handle);
if (file == nullptr || !file->writable()) {
return MOD_INVALID_ARGUMENT;
}
const auto bytes =
std::span{static_cast<const std::byte*>(buffer), static_cast<size_t>(length)};
return file->write(bytes) ? MOD_OK : MOD_ERROR;
}
ModResult file_flush(ModContext* context, FileStreamHandle handle) {
auto* file = find_stream(context, handle);
if (file == nullptr || !file->writable()) {
return MOD_INVALID_ARGUMENT;
}
return file->flush() ? MOD_OK : MOD_ERROR;
}
ModResult file_write_all(ModContext* context, const char* location, const void* data, size_t size) {
if (mod_from_context(context) == nullptr || location == nullptr ||
(data == nullptr && size != 0))
{
return MOD_INVALID_ARGUMENT;
}
try {
const auto available = borealis::io::check(location);
if (available != borealis::io::Status::Ok) {
return map_status(available);
}
auto opened = borealis::io::open(location, borealis::io::File::Mode::Truncate);
if (opened.status != borealis::io::Status::Ok) {
return map_status(opened.status);
}
if (!opened.file.write(std::span{static_cast<const std::byte*>(data), size})) {
return MOD_ERROR;
}
return opened.file.close() ? MOD_OK : MOD_ERROR;
} catch (...) {
return MOD_ERROR;
}
}
ModResult file_read_all(ModContext* context, const char* location, FileBuffer* outBuffer) {
if (outBuffer == nullptr || outBuffer->struct_size < sizeof(FileBuffer)) {
return MOD_INVALID_ARGUMENT;
}
outBuffer->data = nullptr;
outBuffer->size = 0;
auto* mod = mod_from_context(context);
if (mod == nullptr || location == nullptr) {
return MOD_INVALID_ARGUMENT;
}
try {
auto opened = borealis::io::open(location);
if (opened.status != borealis::io::Status::Ok) {
return map_status(opened.status);
}
const uint64_t expectedSize = opened.file.size();
if (expectedSize > std::numeric_limits<size_t>::max()) {
return MOD_ERROR;
}
std::vector<unsigned char> bytes;
bytes.resize(static_cast<size_t>(expectedSize));
uint64_t total = 0;
while (total < expectedSize) {
const uint64_t count = opened.file.read(bytes.data() + total, expectedSize - total);
if (count == 0) {
return MOD_ERROR;
}
total += count;
}
if (expectedSize == 0) {
unsigned char chunk[64 * 1024];
while (true) {
const uint64_t count = opened.file.read(chunk, sizeof(chunk));
if (count == 0) {
if (!opened.file.error().empty()) {
return MOD_ERROR;
}
break;
}
bytes.insert(bytes.end(), chunk, chunk + count);
}
}
if (bytes.empty()) {
return MOD_OK;
}
std::unique_ptr<void, decltype(&std::free)> data{std::malloc(bytes.size()), &std::free};
if (!data) {
return MOD_ERROR;
}
std::memcpy(data.get(), bytes.data(), bytes.size());
s_buffers.emplace(data.get(), mod);
outBuffer->data = data.release();
outBuffer->size = bytes.size();
return MOD_OK;
} catch (...) {
return MOD_ERROR;
}
}
void file_free(ModContext* context, FileBuffer* buffer) {
if (buffer == nullptr || buffer->struct_size < sizeof(FileBuffer) || buffer->data == nullptr) {
return;
}
auto* mod = mod_from_context(context);
const auto found = s_buffers.find(buffer->data);
if (mod == nullptr || found == s_buffers.end() || found->second != mod) {
Log.error("[{}] file free: buffer is not owned by this mod", mod_id_from_context(context));
return;
}
s_buffers.erase(found);
std::free(buffer->data);
buffer->data = nullptr;
buffer->size = 0;
}
ModResult file_list(
ModContext* context, const char* folderLocation, FileListFn callback, void* userData) {
auto* mod = mod_from_context(context);
if (mod == nullptr || folderLocation == nullptr || callback == nullptr) {
return MOD_INVALID_ARGUMENT;
}
borealis::io::ListResult result;
try {
result = borealis::io::list(folderLocation);
} catch (...) {
return MOD_ERROR;
}
if (result.status != borealis::io::Status::Ok) {
return map_status(result.status);
}
try {
for (const auto& entry : result.entries) {
const FileEntry raw{
.name = entry.name.c_str(),
.location = entry.location.c_str(),
.is_directory = entry.isDirectory,
};
callback(mod->context.get(), &raw, userData);
if (!mod->active) {
return MOD_ERROR;
}
}
callback(mod->context.get(), nullptr, userData);
} catch (const std::exception& exception) {
fail_mod(
*mod, MOD_ERROR, std::string{"exception in file list callback: "} + exception.what());
return MOD_ERROR;
} catch (...) {
fail_mod(*mod, MOD_ERROR, "unknown exception in file list callback");
return MOD_ERROR;
}
return MOD_OK;
}
ModResult file_join(ModContext* context, const char* folderLocation, const char* relativePath,
const char** outLocation) {
if (outLocation != nullptr) {
*outLocation = nullptr;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || folderLocation == nullptr || relativePath == nullptr ||
outLocation == nullptr)
{
return MOD_INVALID_ARGUMENT;
}
auto result = borealis::io::join(folderLocation, relativePath);
if (result.status != borealis::io::Status::Ok) {
return map_status(result.status);
}
auto& saved = s_joinResults[mod];
saved = std::move(result.location);
*outLocation = saved.c_str();
return MOD_OK;
}
ModResult file_create_child(
ModContext* context, const char* folderLocation, const char* name, const char** outLocation) {
if (outLocation != nullptr) {
*outLocation = nullptr;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || folderLocation == nullptr || name == nullptr || outLocation == nullptr) {
return MOD_INVALID_ARGUMENT;
}
try {
auto result = borealis::io::create_child(folderLocation, name);
if (result.status != borealis::io::Status::Ok) {
return map_status(result.status);
}
auto& saved = s_joinResults[mod];
saved = std::move(result.location);
*outLocation = saved.c_str();
return MOD_OK;
} catch (...) {
return MOD_ERROR;
}
}
void file_initialize() {
config::Register(s_pickerOverride);
}
bool file_available() {
const auto capabilities = borealis::file_select::capabilities();
return capabilities.canOpenFile || capabilities.canOpenFolder || capabilities.canExportFile;
}
ModResult copy_picker_override(
std::string_view source, std::string_view destination, std::string& error) {
try {
if (source == destination) {
return MOD_OK;
}
auto input = borealis::io::open(source);
if (input.status != borealis::io::Status::Ok) {
error = std::move(input.message);
return map_status(input.status);
}
auto output = borealis::io::open(destination, borealis::io::File::Mode::Truncate);
if (output.status != borealis::io::Status::Ok) {
error = std::move(output.message);
return map_status(output.status);
}
std::array<std::byte, 64 * 1024> buffer{};
while (true) {
const uint64_t read = input.file.read(buffer.data(), buffer.size());
if (read == 0) {
if (!input.file.error().empty()) {
error = input.file.error();
return MOD_ERROR;
}
break;
}
if (!output.file.write(std::span{buffer.data(), static_cast<size_t>(read)})) {
error = output.file.error();
return MOD_ERROR;
}
}
if (!output.file.close()) {
error = output.file.error();
return MOD_ERROR;
}
return MOD_OK;
} catch (const std::exception& exception) {
error = exception.what();
return MOD_ERROR;
} catch (...) {
error = "Unable to copy export source";
return MOD_ERROR;
}
}
void file_frame_begin() {
auto pending = s_pendingPick;
if (pending == nullptr || !pending->overrideLocation.has_value()) {
return;
}
if (!pending->exportSource.has_value()) {
invoke_pick(pending, MOD_OK, {*pending->overrideLocation}, "");
return;
}
std::string error;
const ModResult result =
copy_picker_override(*pending->exportSource, *pending->overrideLocation, error);
invoke_pick(pending, result,
result == MOD_OK ? std::vector<std::string>{*pending->overrideLocation} :
std::vector<std::string>{},
error);
}
void file_remove_mod(LoadedMod& mod) {
const size_t streams = s_streams.erase_all(mod);
size_t buffers = 0;
std::erase_if(s_buffers, [&](const auto& entry) {
if (entry.second != &mod) {
return false;
}
std::free(entry.first);
++buffers;
return true;
});
s_joinResults.erase(&mod);
if (s_pendingPick != nullptr && s_pendingPick->owner == &mod) {
s_pendingPick->owner = nullptr;
s_pendingPick->callback = nullptr;
s_pendingPick.reset();
}
if (streams != 0 || buffers != 0) {
Log.warn("[{}] reclaimed {} open stream(s) and {} file buffer(s)", mod.metadata.id, streams,
buffers);
}
}
void file_shutdown() {
config::unregister(s_pickerOverride);
s_pendingPick.reset();
}
constexpr FileService s_fileService{
.header = SERVICE_HEADER(FileService, FILE_SERVICE_MAJOR, FILE_SERVICE_MINOR),
.pick_file = file_pick_file,
.pick_folder = file_pick_folder,
.export_file = file_export_file,
.display_name = file_display_name,
.check = file_check,
.open = file_open,
.size = file_size,
.read = file_read,
.write = file_write,
.seek = file_seek,
.flush = file_flush,
.close = file_close,
.read_all = file_read_all,
.write_all = file_write_all,
.free = file_free,
.list = file_list,
.join = file_join,
.create_child = file_create_child,
};
} // namespace
constinit const ServiceModule g_fileModule{
.id = FILE_SERVICE_ID,
.majorVersion = FILE_SERVICE_MAJOR,
.minorVersion = FILE_SERVICE_MINOR,
.service = &s_fileService,
.available = file_available,
.initialize = file_initialize,
.modDetached = file_remove_mod,
.frameBegin = file_frame_begin,
.shutdown = file_shutdown,
};
} // namespace dusk::mods::svc
+577
View File
@@ -0,0 +1,577 @@
#include "registry.hpp"
#include "slot_map.hpp"
#include "dusk/app_info.hpp"
#include "dusk/main.h"
#include "dusk/mods/loader/loader.hpp"
#include "mods/svc/http.h"
#include <borealis/http.hpp>
#include <borealis/io.hpp>
#include <borealis/version.h>
#include <fmt/format.h>
#include <xxhash.h>
#include <algorithm>
#include <cassert>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <ranges>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
namespace dusk::mods::svc {
namespace {
constexpr size_t MaxRequestsPerMod = 16;
constexpr size_t MaxUrlBytes = 8 * 1024;
constexpr size_t MaxHeaders = 64;
constexpr size_t MaxHeaderBytes = 16 * 1024;
constexpr size_t MaxRequestBodyBytes = 16 * 1024 * 1024;
constexpr size_t DefaultResponseBodyBytes = 1024 * 1024;
constexpr size_t MaxResponseBodyBytes = 64 * 1024 * 1024;
constexpr std::chrono::milliseconds DefaultTimeout{10000};
struct PendingRequest {
HttpCompleteFn callback = nullptr;
void* userData = nullptr;
borealis::Task<borealis::http::Result> task;
std::filesystem::path stagingPath;
std::filesystem::path downloadPath;
bool completing = false;
};
static_assert(std::is_nothrow_move_constructible_v<PendingRequest>);
SlotMap<PendingRequest> s_requests;
bool ascii_iequals(std::string_view left, std::string_view right) {
return left.size() == right.size() && std::ranges::equal(left, right, [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
});
}
bool is_reserved_header(std::string_view name) {
constexpr std::string_view reserved[]{
"User-Agent",
"Host",
"Content-Length",
"Connection",
"Accept-Encoding",
"Range",
"If-Range",
};
return std::ranges::any_of(
reserved, [&](std::string_view value) { return ascii_iequals(name, value); });
}
bool valid_header_name(std::string_view name) {
constexpr std::string_view separators{"()<>@,;:\\\"/[]?={} \t"};
return !name.empty() && std::ranges::all_of(name, [&](unsigned char value) {
return value > 32 && value < 127 &&
separators.find(static_cast<char>(value)) == std::string_view::npos;
});
}
bool valid_url(std::string_view url) {
constexpr std::string_view scheme{"https://"};
if (!url.starts_with(scheme) || url.size() <= scheme.size() || url.size() > MaxUrlBytes) {
return false;
}
if (std::ranges::any_of(url, [](unsigned char value) { return value <= 32 || value == 127; })) {
return false;
}
const auto authorityEnd = url.find_first_of("/?#", scheme.size());
const auto authority = url.substr(scheme.size(), authorityEnd - scheme.size());
return !authority.empty();
}
bool declares_http_import(const LoadedMod& mod) {
return std::ranges::any_of(
mod.manifestInfo.imports, [](const ModManifestInfo::Import& serviceImport) {
return serviceImport.id == HTTP_SERVICE_ID;
});
}
std::filesystem::path normalized_absolute(const std::filesystem::path& path, std::error_code& ec) {
auto result = std::filesystem::absolute(path, ec);
return ec ? std::filesystem::path{} : result.lexically_normal();
}
bool path_is_below(const std::filesystem::path& path, const std::filesystem::path& directory) {
const auto [directoryEnd, pathPosition] =
std::mismatch(directory.begin(), directory.end(), path.begin(), path.end());
return directoryEnd == directory.end() && pathPosition != path.end();
}
bool path_is_at_or_below(
const std::filesystem::path& path, const std::filesystem::path& directory) {
const auto [directoryEnd, pathPosition] =
std::mismatch(directory.begin(), directory.end(), path.begin(), path.end());
(void)pathPosition;
return directoryEnd == directory.end();
}
std::filesystem::path data_root(const LoadedMod& mod, std::error_code& ec) {
if (!mod.dataDirUtf8.empty()) {
return normalized_absolute(borealis::io::fs_path_from_utf8(mod.dataDirUtf8), ec);
}
return normalized_absolute(ConfigPath / "mod_data" / mod.metadata.id, ec);
}
std::optional<std::filesystem::path> validate_download_path(
const LoadedMod& mod, const char* rawPath) {
if (rawPath == nullptr) {
return std::filesystem::path{};
}
const auto supplied = borealis::io::fs_path_from_utf8(rawPath);
if (!supplied.is_absolute()) {
return std::nullopt;
}
std::error_code ec;
const auto path = normalized_absolute(supplied, ec);
if (ec) {
return std::nullopt;
}
const auto modRoot = normalized_absolute(mod.dir, ec);
if (ec) {
return std::nullopt;
}
const auto dataRoot = data_root(mod, ec);
if (ec) {
return std::nullopt;
}
const auto stagingRoot = (modRoot / "downloads").lexically_normal();
if ((!path_is_below(path, modRoot) && !path_is_below(path, dataRoot)) ||
path_is_at_or_below(path, stagingRoot))
{
return std::nullopt;
}
return path;
}
std::filesystem::path staging_path(const LoadedMod& mod, std::string_view url) {
const auto& modId = mod.metadata.id;
const auto modHash = XXH64(modId.data(), modId.size(), 0);
const auto hash = XXH64(url.data(), url.size(), modHash);
return mod.dir / "downloads" / fmt::format("{:016x}.part", hash);
}
HttpError map_error(borealis::http::Error error) {
switch (error) {
case borealis::http::Error::None:
return HTTP_ERROR_NONE;
case borealis::http::Error::InvalidUrl:
return HTTP_ERROR_INVALID_URL;
case borealis::http::Error::UnsupportedScheme:
return HTTP_ERROR_UNSUPPORTED_SCHEME;
case borealis::http::Error::Timeout:
return HTTP_ERROR_TIMEOUT;
case borealis::http::Error::TooLarge:
return HTTP_ERROR_TOO_LARGE;
case borealis::http::Error::Canceled:
return HTTP_ERROR_CANCELED;
case borealis::http::Error::Io:
return HTTP_ERROR_IO;
case borealis::http::Error::NoBackend:
case borealis::http::Error::NotInitialized:
case borealis::http::Error::Network:
return HTTP_ERROR_NETWORK;
default:
return HTTP_ERROR_NETWORK;
}
}
std::optional<borealis::http::Method> borealis_method(HttpMethod method) {
switch (method) {
case HTTP_METHOD_GET:
return borealis::http::Method::Get;
case HTTP_METHOD_POST:
return borealis::http::Method::Post;
case HTTP_METHOD_HEAD:
return borealis::http::Method::Head;
}
return std::nullopt;
}
borealis::http::Result publish_download(borealis::http::Result result,
const std::filesystem::path& staging, const std::filesystem::path& destination) noexcept {
if (result.error != borealis::http::Error::None) {
return result;
}
try {
std::string renameError;
if (borealis::io::atomic_replace(staging, destination, renameError)) {
return result;
}
std::filesystem::path temporary = destination;
temporary += "." + borealis::io::fs_path_to_string(staging.filename()) + ".part";
std::error_code ec;
std::filesystem::copy_file(
staging, temporary, std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
const std::string copyError = ec.message();
std::error_code ignored;
std::filesystem::remove(temporary, ignored);
result.error = borealis::http::Error::Io;
result.message = "Failed to publish download: " + copyError;
return result;
}
std::string replaceError;
if (!borealis::io::atomic_replace(temporary, destination, replaceError)) {
std::filesystem::remove(temporary, ec);
result.error = borealis::http::Error::Io;
result.message = "Failed to publish download: " + replaceError;
return result;
}
std::filesystem::remove(staging, ec);
return result;
} catch (const std::exception& exception) {
result.error = borealis::http::Error::Io;
result.message = std::string{"Failed to publish download: "} + exception.what();
return result;
} catch (...) {
result.error = borealis::http::Error::Io;
result.message = "Failed to publish download";
return result;
}
}
void http_frame_begin() {
std::vector<HttpRequestHandle> ready;
s_requests.for_each([&](const HttpRequestHandle handle, const auto& entry) {
const auto& pending = entry.value;
if (!pending.completing && pending.task.ready()) {
ready.push_back(handle);
}
});
for (const auto handle : ready) {
auto* entry = s_requests.find(handle);
if (entry == nullptr || !entry->owner->active || entry->value.completing) {
continue;
}
auto& pending = entry->value;
borealis::http::Result result;
try {
auto completed = pending.task.try_take();
if (!completed.has_value()) {
continue;
}
result = std::move(*completed);
} catch (const std::exception& exception) {
result = {
.error = borealis::http::Error::Io,
.message = exception.what(),
};
} catch (...) {
result = {
.error = borealis::http::Error::Io,
.message = "HTTP request completion failed",
};
}
auto* owner = entry->owner;
const auto callback = pending.callback;
const auto userData = pending.userData;
pending.completing = true;
std::vector<HttpHeader> headers;
headers.reserve(result.response.headers.size());
for (const auto& header : result.response.headers) {
headers.push_back({.name = header.name.c_str(), .value = header.value.c_str()});
}
const bool downloadSucceeded =
!pending.downloadPath.empty() && result.error == borealis::http::Error::None;
const auto publishedPath = downloadSucceeded ?
borealis::io::fs_path_to_string(pending.downloadPath) :
std::string{};
const HttpResult snapshot{
.struct_size = sizeof(HttpResult),
.error = map_error(result.error),
.error_message = result.message.c_str(),
.status_code = result.response.statusCode,
.headers = headers.empty() ? nullptr : headers.data(),
.header_count = static_cast<uint32_t>(headers.size()),
.body = pending.downloadPath.empty() && !result.response.body.empty() ?
result.response.body.data() :
nullptr,
.body_size = pending.downloadPath.empty() ? result.response.body.size() : 0,
.download_path = downloadSucceeded ? publishedPath.c_str() : nullptr,
};
try {
callback(owner->context.get(), handle, &snapshot, userData);
} catch (const std::exception& exception) {
fail_mod(*owner, MOD_ERROR,
std::string{"exception in HTTP completion callback: "} + exception.what());
} catch (...) {
fail_mod(*owner, MOD_ERROR, "unknown exception in HTTP completion callback");
}
s_requests.erase(handle);
}
}
size_t active_request_count(const LoadedMod& mod) {
size_t count = 0;
s_requests.for_each([&](HttpRequestHandle, const auto& entry) {
if (entry.owner == &mod && !entry.value.completing) {
++count;
}
});
return count;
}
bool staging_path_in_use(const LoadedMod& mod, const std::filesystem::path& path) {
bool inUse = false;
s_requests.for_each([&](HttpRequestHandle, const auto& entry) {
if (entry.owner == &mod && !entry.value.completing && entry.value.stagingPath == path) {
inUse = true;
}
});
return inUse;
}
std::string user_agent_version(std::string_view version) {
std::string result{version};
for (char& ch : result) {
const auto value = static_cast<unsigned char>(ch);
if (value <= 32 || value >= 127) {
ch = '_';
}
}
return result;
}
ModResult start_request(LoadedMod& mod, const HttpRequestDesc& desc, HttpCompleteFn callback,
void* userData, HttpRequestHandle& outHandle) {
const std::string_view url{desc.url};
const auto method = borealis_method(desc.method);
if (!valid_url(url) || !method.has_value() ||
(desc.header_count != 0 && desc.headers == nullptr) ||
(desc.body_size != 0 && desc.body == nullptr) || desc.body_size > MaxRequestBodyBytes ||
((desc.method == HTTP_METHOD_GET || desc.method == HTTP_METHOD_HEAD) &&
desc.body_size != 0) ||
(desc.method == HTTP_METHOD_HEAD && desc.download_path != nullptr) ||
desc.header_count > MaxHeaders)
{
return MOD_INVALID_ARGUMENT;
}
size_t headerBytes = 0;
for (uint32_t i = 0; i < desc.header_count; ++i) {
const auto& header = desc.headers[i];
if (header.name == nullptr || header.value == nullptr) {
return MOD_INVALID_ARGUMENT;
}
const std::string_view name{header.name};
const std::string_view value{header.value};
const bool invalidValue = std::ranges::any_of(
value, [](unsigned char ch) { return (ch < 32 && ch != '\t') || ch == 127; });
if (!valid_header_name(name) || invalidValue || is_reserved_header(name) ||
name.size() > MaxHeaderBytes - headerBytes)
{
return MOD_INVALID_ARGUMENT;
}
headerBytes += name.size();
if (value.size() > MaxHeaderBytes - headerBytes) {
return MOD_INVALID_ARGUMENT;
}
headerBytes += value.size();
}
auto downloadPath = validate_download_path(mod, desc.download_path);
if (!downloadPath.has_value() ||
(downloadPath->empty() && desc.max_body_bytes > MaxResponseBodyBytes))
{
return MOD_INVALID_ARGUMENT;
}
if (active_request_count(mod) >= MaxRequestsPerMod) {
return MOD_CONFLICT;
}
std::filesystem::path staging;
if (!downloadPath->empty()) {
staging = staging_path(mod, url);
if (staging_path_in_use(mod, staging)) {
return MOD_CONFLICT;
}
std::error_code ec;
std::filesystem::create_directories(staging.parent_path(), ec);
if (ec) {
return MOD_ERROR;
}
std::filesystem::create_directories(downloadPath->parent_path(), ec);
if (ec) {
return MOD_ERROR;
}
}
PendingRequest pending{
.callback = callback,
.userData = userData,
.stagingPath = staging,
.downloadPath = *downloadPath,
};
if (!borealis::http::available()) {
return MOD_UNAVAILABLE;
}
borealis::http::Request request{
.method = *method,
.url = std::string{url},
.body = desc.body_size != 0 ?
std::string{static_cast<const char*>(desc.body), desc.body_size} :
std::string{},
.downloadTo = staging,
.connectTimeout = desc.connect_timeout_ms != 0 ?
std::chrono::milliseconds{desc.connect_timeout_ms} :
DefaultTimeout,
.idleTimeout = desc.idle_timeout_ms != 0 ?
std::chrono::milliseconds{desc.idle_timeout_ms} :
DefaultTimeout,
.totalTimeout = desc.total_timeout_ms != 0 ?
std::optional{std::chrono::milliseconds{desc.total_timeout_ms}} :
std::nullopt,
.maxBodyBytes =
desc.max_body_bytes != 0 ? desc.max_body_bytes : DefaultResponseBodyBytes,
};
request.headers.reserve(desc.header_count + 1);
for (uint32_t i = 0; i < desc.header_count; ++i) {
request.headers.push_back({desc.headers[i].name, desc.headers[i].value});
}
request.headers.push_back({
.name = "User-Agent",
.value = fmt::format("{}/{} {}/{}", AppName, BOREALIS_APP_VERSION, mod.metadata.id,
user_agent_version(mod.metadata.version)),
});
auto task = borealis::http::start(std::move(request));
if (task.ready()) {
auto immediate = task.try_take();
if (!immediate.has_value()) {
return MOD_UNAVAILABLE;
}
if (immediate->error == borealis::http::Error::NoBackend ||
immediate->error == borealis::http::Error::NotInitialized)
{
return MOD_UNAVAILABLE;
}
task = borealis::detail::make_ready_task(std::move(*immediate));
}
if (!downloadPath->empty()) {
task = std::move(task).map([staging, destination = *downloadPath](auto&& result) {
return publish_download(std::move(result), staging, destination);
});
}
pending.task = std::move(task);
outHandle = s_requests.emplace(mod, std::move(pending));
return MOD_OK;
}
ModResult http_request(ModContext* context, const HttpRequestDesc* desc, HttpCompleteFn callback,
void* userData, HttpRequestHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(HttpRequestDesc) ||
desc->url == nullptr || callback == nullptr || outHandle == nullptr)
{
return MOD_INVALID_ARGUMENT;
}
if (!declares_http_import(*mod)) {
return MOD_UNSUPPORTED;
}
try {
return start_request(*mod, *desc, callback, userData, *outHandle);
} catch (...) {
return MOD_ERROR;
}
}
ModResult http_progress(ModContext* context, HttpRequestHandle handle, HttpProgress* outProgress) {
const uint32_t structSize = outProgress != nullptr ? outProgress->struct_size : 0;
auto* mod = mod_from_context(context);
if (mod == nullptr || outProgress == nullptr || structSize < sizeof(HttpProgress)) {
return MOD_INVALID_ARGUMENT;
}
*outProgress = HttpProgress{.struct_size = structSize};
const auto* entry = s_requests.find_owned(handle, *mod);
if (entry == nullptr) {
return MOD_UNAVAILABLE;
}
const auto progress = entry->value.task.progress();
outProgress->completed_bytes = progress.completed;
outProgress->total_bytes = progress.total.value_or(0);
outProgress->total_known = progress.total.has_value();
return MOD_OK;
}
ModResult http_cancel(ModContext* context, HttpRequestHandle handle) {
auto* mod = mod_from_context(context);
if (mod == nullptr) {
return MOD_INVALID_ARGUMENT;
}
auto* entry = s_requests.find_owned(handle, *mod);
if (entry == nullptr) {
return MOD_UNAVAILABLE;
}
entry->value.task.cancel();
return MOD_OK;
}
void http_mod_deactivating(LoadedMod& mod) {
(void)s_requests.take_all(mod);
}
void http_mod_detached(LoadedMod& mod) {
bool found = false;
s_requests.for_each(
[&](HttpRequestHandle, const auto& entry) { found = found || entry.owner == &mod; });
assert(!found);
}
void http_shutdown() {
s_requests = {};
}
bool http_available() {
return borealis::http::available() && borealis::http::initialize();
}
constexpr HttpService s_httpService{
.header = SERVICE_HEADER(HttpService, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR),
.request = http_request,
.progress = http_progress,
.cancel = http_cancel,
};
} // namespace
constinit const ServiceModule g_httpModule{
.id = HTTP_SERVICE_ID,
.majorVersion = HTTP_SERVICE_MAJOR,
.minorVersion = HTTP_SERVICE_MINOR,
.service = &s_httpService,
.available = http_available,
.modDeactivating = http_mod_deactivating,
.modDetached = http_mod_detached,
.frameBegin = http_frame_begin,
.shutdown = http_shutdown,
};
} // namespace dusk::mods::svc
+12
View File
@@ -75,6 +75,17 @@ ModResult item_resolve_check(
return MOD_OK;
}
ModResult item_resolve_check_full(ModContext* context, const char* name, uint8_t originalItemNo,
ItemCheckResolution* outResolution) {
if (mod_from_context(context) == nullptr || !is_valid_check_name(name) ||
outResolution == nullptr)
{
return MOD_INVALID_ARGUMENT;
}
*outResolution = item_check_resolve(name, originalItemNo, nullptr);
return MOD_OK;
}
ModResult item_give_item(
ModContext* context, const char* checkName, uint8_t itemNo, uint32_t flags) {
auto* mod = mod_from_context(context);
@@ -130,6 +141,7 @@ constexpr ItemService s_itemService{
.give_item = item_give_item,
.observe_gives = item_observe_gives,
.unobserve_gives = item_unobserve_gives,
.resolve_check_full = item_resolve_check_full,
};
} // namespace
+5
View File
@@ -138,6 +138,9 @@ const ServiceRecord* find_service_record(const char* serviceId, const uint16_t m
}
ModResult register_module(const ServiceModule& module) {
if (module.available != nullptr && !module.available()) {
return MOD_UNAVAILABLE;
}
const auto result = register_service(
module.id, module.majorVersion, module.minorVersion, module.service, nullptr, false);
if (result != MOD_OK) {
@@ -210,6 +213,8 @@ void ModLoader::init_services() {
&svc::g_hostModule,
&svc::g_logModule,
&svc::g_resourceModule,
&svc::g_fileModule,
&svc::g_httpModule,
&svc::g_hookModule,
&svc::g_overlayModule,
&svc::g_textureModule,
+4
View File
@@ -26,6 +26,8 @@ struct ServiceModule {
uint16_t minorVersion = 0;
const void* service = nullptr;
// False prevents registration when a platform dependency is unavailable.
bool (*available)() = nullptr;
// One-time setup, at registration (ModLoader::init_services).
void (*initialize)() = nullptr;
// A mod is beginning deactivation: stop callbacks that may execute concurrently. Service state
@@ -68,6 +70,8 @@ void modules_shutdown();
extern const ServiceModule g_hostModule;
extern const ServiceModule g_logModule;
extern const ServiceModule g_resourceModule;
extern const ServiceModule g_fileModule;
extern const ServiceModule g_httpModule;
extern const ServiceModule g_hookModule;
extern const ServiceModule g_overlayModule;
extern const ServiceModule g_textureModule;
+1
View File
@@ -200,6 +200,7 @@ void save_slot_written(uint32_t slot, const void* slotData) {
store.snapshotCrc = utils::crc32(slotData, kQuestLogSize);
}
flush_sidecar();
s_currentSlot = static_cast<int32_t>(slot);
notify(slot, &SaveObserverRecord::onWritten, "save-written");
}
+25 -1
View File
@@ -45,6 +45,8 @@ constexpr size_t kUiControlSelectedSize =
offsetof(UiControlDesc, is_selected) + sizeof(UiPredicateFn);
constexpr size_t kUiControlStringSetModeSize =
offsetof(UiControlDesc, string_set_mode) + sizeof(UiStringSetMode);
constexpr size_t kUiControlFilePickerSize =
offsetof(UiControlDesc, directory_mode) + sizeof(bool);
constexpr size_t kUiListItemV21Size = offsetof(UiListItem, label) + sizeof(const char*);
constexpr size_t kUiListDescV21Size = offsetof(UiListDesc, user_data) + sizeof(void*);
@@ -303,6 +305,7 @@ void wire_callback_binding(
break;
case UI_CONTROL_STRING:
case UI_CONTROL_COLOR:
case UI_CONTROL_FILE_PICKER:
spec.getString = [getValue]() -> Rml::String {
const UiControlValue value = getValue();
return value.string_value != nullptr ? value.string_value : "";
@@ -382,7 +385,8 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC
return true;
}
case UI_CONTROL_STRING:
case UI_CONTROL_COLOR: {
case UI_CONTROL_COLOR:
case UI_CONTROL_FILE_PICKER: {
const auto find = [modPtr, varHandle] {
return static_cast<ConfigVar<std::string>*>(
config_find_var(*modPtr, varHandle, CONFIG_VAR_STRING));
@@ -642,6 +646,14 @@ ModResult ui_pane_add_control(
spec.colorPresets.emplace_back(desc.color_presets[i]);
}
break;
case UI_CONTROL_FILE_PICKER:
spec.kind = ui::ModControlSpec::Kind::FilePicker;
spec.directoryMode = desc.directory_mode;
for (size_t i = 0; i < desc.file_filter_count; ++i) {
spec.fileFilters.push_back(
{desc.file_filters[i].name, desc.file_filters[i].pattern});
}
break;
case UI_CONTROL_SELECT:
spec.kind = ui::ModControlSpec::Kind::Select;
if (slot->helpPane == nullptr) {
@@ -1257,6 +1269,18 @@ bool valid_control_desc(const UiControlDesc& desc) {
return false;
}
break;
case UI_CONTROL_FILE_PICKER:
if (desc.struct_size < kUiControlFilePickerSize ||
(desc.file_filter_count != 0 && desc.file_filters == nullptr))
{
return false;
}
for (size_t i = 0; i < desc.file_filter_count; ++i) {
if (desc.file_filters[i].name == nullptr || desc.file_filters[i].pattern == nullptr) {
return false;
}
}
break;
default:
return false;
}
+47 -5
View File
@@ -1,8 +1,12 @@
#include "dusk/settings.h"
#include <aurora/aurora.h>
#include "dusk/config.hpp"
#include "dusk/ui/ui.hpp"
#include "dusk/game_mode.hpp"
#include "dusk/texture_replacements.hpp"
#include "dusk/ui/ui.hpp"
#include <aurora/aurora.h>
#include <dolphin/vi.h>
namespace dusk {
@@ -161,6 +165,8 @@ UserSettings g_userSettings = {
.removeQuestMapMarkers {"game.removeQuestMapMarkers", false},
.showInputViewer {"game.showInputViewer", false},
.showInputViewerGyro {"game.showInputViewerGyro", false},
.enableMoveLinkCombo {"game.enableMoveLinkCombo", false},
.enableTeleportCombo {"game.enableTeleportCombo", false},
.lastSelectedGameModeId {"game.lastSelectedGameModeId", gamemode::kVanillaGameModeId}
},
@@ -220,6 +226,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 +308,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);
@@ -314,6 +339,8 @@ void registerSettings() {
Register(g_userSettings.game.removeQuestMapMarkers);
Register(g_userSettings.game.showInputViewer);
Register(g_userSettings.game.showInputViewerGyro);
Register(g_userSettings.game.enableMoveLinkCombo);
Register(g_userSettings.game.enableTeleportCombo);
Register(g_userSettings.game.lastSelectedGameModeId);
Register(g_userSettings.game.fastSpinner);
Register(g_userSettings.game.infiniteHearts);
@@ -405,6 +432,21 @@ static TransientSettings g_transientSettings = {
.colliderViewOpacity = 50.0f,
.drawRange = 100.0f,
},
.triggerView = {
.loadZones = false,
.eventAreas = false,
.switchAreas = false,
.eventTags = false,
.midnaStops = false,
.twilightGates = false,
.checkpoints = false,
.paths = false,
.transformDists = false,
.attentionDists = false,
.purpleMistAvoid = false,
.leevers = false,
.opacity = 75.0f,
},
.turboMode = false,
};
+22
View File
@@ -287,6 +287,8 @@ struct UserSettings {
ConfigVar<bool> removeQuestMapMarkers;
ConfigVar<bool> showInputViewer;
ConfigVar<bool> showInputViewerGyro;
ConfigVar<bool> enableMoveLinkCombo;
ConfigVar<bool> enableTeleportCombo;
ConfigVar<std::string> lastSelectedGameModeId;
} game;
@@ -317,6 +319,9 @@ UserSettings& getSettings();
void registerSettings();
void applyInternalResolutionScale(int scale);
void applyResampler(Resampler resampler);
// Transient settings
struct CollisionViewSettings {
@@ -330,8 +335,25 @@ struct CollisionViewSettings {
float drawRange;
};
struct TriggerViewSettings {
bool loadZones;
bool eventAreas;
bool switchAreas;
bool eventTags;
bool midnaStops;
bool twilightGates;
bool checkpoints;
bool paths;
bool transformDists;
bool attentionDists;
bool purpleMistAvoid;
bool leevers;
float opacity;
};
struct TransientSettings {
CollisionViewSettings collisionView;
TriggerViewSettings triggerView;
bool turboMode;
bool moveLinkActive;
bool stateShareLoadActive;
+5
View File
@@ -19,6 +19,8 @@ static void onSpeedrunModeDeactive() {
if (getSettings().game.liveSplitEnabled) {
speedrun::disconnectLiveSplit();
}
g_speedrunInfo.reset();
reset();
}
void registerSpeedrunGameMode() {
@@ -83,6 +85,9 @@ void resetForSpeedrunMode() {
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
getSettings().game.recordingMode.setSpeedrunValue(false);
getSettings().game.debugFlyCam.setSpeedrunValue(false);
getSettings().game.enableMoveLinkCombo.setSpeedrunValue(false);
getSettings().game.enableTeleportCombo.setSpeedrunValue(false);
}
static void clearSpeedrunOverrides() {
-5
View File
@@ -26,11 +26,6 @@ void reload() {
s_directoryGroup.registrations.size());
}
void set_enabled(bool enabled) {
getSettings().game.enableTextureReplacements.setValue(enabled);
reload();
}
void shutdown() {
aurora::texture::unregister_replacements(s_directoryGroup);
s_directoryGroup.registrations.clear();
-1
View File
@@ -8,7 +8,6 @@ namespace dusk::texture_replacements {
inline constexpr int32_t kUserTextureReplacementPriority = -1'000'000;
void reload();
void set_enabled(bool enabled);
void shutdown();
}
+404
View File
@@ -0,0 +1,404 @@
#include "JSystem/JMath/JMath.h"
#include "SSystem/SComponent/c_list.h"
#include "SSystem/SComponent/c_tag.h"
#include "d/actor/d_a_alink.h"
#include "d/actor/d_a_e_rb.h"
#include "d/actor/d_a_e_rd.h"
#include "d/actor/d_a_kytag08.h"
#include "d/actor/d_a_scene_exit.h"
#include "d/actor/d_a_swc00.h"
#include "d/actor/d_a_tag_chgrestart.h"
#include "d/actor/d_a_tag_mstop.h"
#include "d/d_attention.h"
#include "d/d_com_inf_game.h"
#include "d/d_debug_viewer.h"
#include "d/d_path.h"
#include "d/d_stage.h"
#include "dusk/settings.h"
#include "dusk/trigger_viewer.h"
#include "f_op/f_op_actor_mng.h"
#include "f_op/f_op_actor_tag.h"
#include "f_pc/f_pc_name.h"
#include "m_Do/m_Do_mtx.h"
namespace dusk {
namespace TriggerView {
static u8 s_opacity;
typedef void (*DrawCallback)(fopAc_ac_c*);
static void searchActorForCallback(s16 actorName, DrawCallback callback) {
node_class* node = g_fopAcTg_Queue.mpHead;
for (int i = 0; i < g_fopAcTg_Queue.mSize; i++) {
if (node != NULL) {
create_tag_class* tag = (create_tag_class*)node;
fopAc_ac_c* actorData = (fopAc_ac_c*)tag->mpTagData;
bool checkAll = actorName == -1;
if (actorData != NULL && (fopAcM_GetName(actorData) == actorName || checkAll)) {
callback(actorData);
}
node = node->mpNextNode;
}
}
}
static void drawSceneExit(fopAc_ac_c* actor) {
daScex_c* scex = (daScex_c*)actor;
cXyz points[8];
points[0].set(-actor->scale.x, actor->scale.y, -actor->scale.z);
points[1].set(actor->scale.x, actor->scale.y, -actor->scale.z);
points[2].set(-actor->scale.x, actor->scale.y, actor->scale.z);
points[3].set(actor->scale.x, actor->scale.y, actor->scale.z);
points[4].set(-actor->scale.x, 0.0f, -actor->scale.z);
points[5].set(actor->scale.x, 0.0f, -actor->scale.z);
points[6].set(-actor->scale.x, 0.0f, actor->scale.z);
points[7].set(actor->scale.x, 0.0f, actor->scale.z);
mDoMtx_inverse(scex->mMatrix, mDoMtx_stack_c::get());
mDoMtx_multVecArray(mDoMtx_stack_c::get(), points, points, 8);
GXColor color = {0xFF, 0x00, 0xFF, s_opacity};
dDbVw_drawCube8pXlu(points, color);
}
static void drawMidnaStop(fopAc_ac_c* actor) {
daTagMstop_c* mstop = (daTagMstop_c*)actor;
GXColor color = {0x4A, 0x36, 0xBA, s_opacity};
dDbVw_drawCylinderXlu(mstop->current.pos, mstop->scale.x * 100.0f, mstop->scale.y, color, 1);
}
static void drawPlumTag(fopAc_ac_c* actor) {
GXColor color = {0x00, 0xFF, 0x00, s_opacity};
dDbVw_drawCylinderXlu(actor->current.pos, actor->scale.x * 100.0f, 1000000.0f, color, 1);
}
static void drawPlumSearch(fopAc_ac_c* actor) {
GXColor color = {0xFF, 0x00, 0x00, s_opacity};
const f32 search_dist = 500.0f;
dDbVw_drawCircleXlu(actor->attention_info.position, search_dist + 160.0f, color, 1, 12);
}
static void drawSwitchArea(fopAc_ac_c* actor) {
daSwc00_c* swc = (daSwc00_c*)actor;
int shape_type = (fopAcM_GetParam(actor) >> 0x12) & 3;
GXColor color = {0x00, 0x00, 0xFF, s_opacity};
if (shape_type == 3) {
dDbVw_drawCylinderXlu(swc->current.pos, JMAFastSqrt(swc->scale.x) - 30.0f, swc->scale.y, color, 1);
} else if (shape_type == 0) {
cXyz size = swc->field_0x574 - swc->field_0x568;
size *= 0.5f;
cXyz pos = swc->field_0x568 + size;
csXyz angle(swc->current.angle.x, swc->current.angle.y, swc->current.angle.z);
dDbVw_drawCubeXlu(pos, size, angle, color);
}
}
static void drawEventArea(fopAc_ac_c* actor) {
u8 type = (actor->shape_angle.z & 0xFF);
if (type == 0xFF) {
type = 0;
}
if (type == 15 || type == 16) {
GXColor color = {0xFF, 0xFF, 0x00, s_opacity};
cXyz points[8];
points[0].set(-actor->scale.x, actor->scale.y, -actor->scale.z);
points[1].set(actor->scale.x, actor->scale.y, -actor->scale.z);
points[2].set(-actor->scale.x, actor->scale.y, actor->scale.z);
points[3].set(actor->scale.x, actor->scale.y, actor->scale.z);
points[4].set(-actor->scale.x, 0.0f, -actor->scale.z);
points[5].set(actor->scale.x, 0.0f, -actor->scale.z);
points[6].set(-actor->scale.x, 0.0f, actor->scale.z);
points[7].set(actor->scale.x, 0.0f, actor->scale.z);
mDoMtx_stack_c::transS(actor->home.pos.x, actor->home.pos.y, actor->home.pos.z);
mDoMtx_stack_c::YrotS(actor->current.angle.y);
mDoMtx_multVecArray(mDoMtx_stack_c::get(), points, points, 8);
dDbVw_drawCube8pXlu(points, color);
} else {
GXColor outer_color = {0xFF, 0x00, 0x00, s_opacity};
GXColor inner_color = {0x00, 0xFF, 0x00, s_opacity};
cXyz pos = actor->current.pos;
daAlink_c* player = (daAlink_c*)dComIfGp_getPlayer(0);
if (player != NULL && pos.y < player->mLinkAcch.GetGroundH()) {
pos.y = player->mLinkAcch.GetGroundH() + 100.0f;
}
const f32 inner_scale = 0.83f;
dDbVw_drawCircleXlu(pos, actor->scale.x * inner_scale, inner_color, 1, 20);
dDbVw_drawCircleXlu(pos, actor->scale.x, outer_color, 1, 20);
}
}
static void drawEventTag(fopAc_ac_c* actor) {
GXColor color = {0x00, 0xC8, 0xFF, s_opacity};
u16 area_type = actor->home.angle.x & 0x8000;
if (area_type == 0x8000) {
cXyz start(actor->current.pos.x - (actor->scale.x * 0.5f), actor->current.pos.y,
actor->current.pos.z - (actor->scale.z * 0.5f));
cXyz end(actor->current.pos.x + (actor->scale.x * 0.5f),
actor->current.pos.y + actor->scale.y,
actor->current.pos.z + (actor->scale.z * 0.5f));
cXyz points[8];
points[0].set(start.x, start.y, start.z);
points[1].set(start.x, start.y, end.z);
points[2].set(end.x, start.y, end.z);
points[3].set(end.x, start.y, start.z);
points[4].set(start.x, end.y, start.z);
points[5].set(start.x, end.y, end.z);
points[6].set(end.x, end.y, end.z);
points[7].set(end.x, end.y, start.z);
dDbVw_drawCube8pXlu(points, color);
} else {
cXyz pos = actor->current.pos;
pos.y -= actor->scale.y;
dDbVw_drawCylinderXlu(pos, actor->scale.x, actor->scale.y * 2, color, 1);
}
}
static void drawTWGate(fopAc_ac_c* actor) {
GXColor color = {0xFF, 0xFF, 0xFF, s_opacity};
dDbVw_drawCylinderXlu(actor->current.pos, actor->scale.x * 100.0f, actor->scale.y * 100.0f, color, 1);
}
static u8 s_pathColorIndex = 0;
static void drawPaths(dStage_dPath_c* paths) {
static const GXColor colors[8] = {
{0xFF, 0xFF, 0xFF}, {0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00}, {0x00, 0xFF, 0x00},
{0x00, 0x00, 0xFF}, {0xFF, 0xFF, 0x00}, {0xFF, 0x00, 0xFF}, {0x00, 0xFF, 0xFF},
};
cXyz cubeSize = {30.0f, 30.0f, 30.0f};
csXyz cubeAngle = {0, 0, 0};
for (int i = 0; i < (int)paths->num; i++) {
dPath* path = &paths->m_path[i];
GXColor color = colors[(s_pathColorIndex++) & 7];
color.a = s_opacity;
cXyz a, b;
if (dPath_ChkClose(path) && path->m_num > 2) {
a = (Vec)path->m_points[0].m_position;
b = (Vec)path->m_points[(int)path->m_num - 1].m_position;
dDbVw_drawLineXlu(a, b, color, 1, 10);
}
for (int j = 0; j < (int)path->m_num - 1; j++) {
a = (Vec)path->m_points[j].m_position;
b = (Vec)path->m_points[j + 1].m_position;
dDbVw_drawLineXlu(a, b, color, 1, 10);
dDbVw_drawCubeXlu(a, cubeSize, cubeAngle, color);
}
dDbVw_drawCubeXlu(b, cubeSize, cubeAngle, color);
}
}
static void drawStagePaths() {
dStage_dPath_c* stagePaths = g_dComIfG_gameInfo.play.getStage().getPath2Inf();
if (stagePaths != nullptr) {
drawPaths(stagePaths);
}
}
static void drawCurrentRoomPaths() {
fopAc_ac_c* player = dComIfGp_getPlayer(0);
if (player == nullptr) {
return;
}
s32 roomNo = fopAcM_GetRoomNo(player);
if (roomNo < 0 || roomNo >= 64) {
return;
}
dStage_dPath_c* roomPaths = dStage_roomControl_c::mStatus[roomNo].mRoomDt.getPath2Inf();
if (roomPaths != nullptr) {
drawPaths(roomPaths);
}
}
static void drawCheckpointTag(fopAc_ac_c* actor) {
daTagChgRestart_c* chk = (daTagChgRestart_c*)actor;
GXColor color = {0x29, 0xF0, 0xFF, s_opacity};
cXyz points[8];
mDoMtx_stack_c::transS(actor->current.pos.x, actor->current.pos.y, actor->current.pos.z);
mDoMtx_stack_c::YrotM(actor->current.angle.y);
points[0] = chk->mVertices[0];
points[1] = chk->mVertices[1];
points[2] = chk->mVertices[3];
points[3] = chk->mVertices[2];
points[4] = chk->mVertices[0];
points[5] = chk->mVertices[1];
points[6] = chk->mVertices[3];
points[7] = chk->mVertices[2];
mDoMtx_multVecArray(mDoMtx_stack_c::get(), points, points, 8);
fopAc_ac_c* player = dComIfGp_getPlayer(0);
if (player != nullptr) {
for (int i = 0; i < 8; i++) {
points[i].y = player->current.pos.y;
}
}
for (int i = 0; i < 4; i++) {
points[i].y += 1000.0f;
}
dDbVw_drawCube8pXlu(points, color);
}
static void drawTransformDists(fopAc_ac_c* actor) {
if (fopAcM_GetGroup(actor) == 4 && !(actor->actor_status & fopAcStts_UNK_0x8000000_e)) {
GXColor near_color = {0x00, 0xFF, 0x00, s_opacity};
GXColor far_color = {0xFF, 0x00, 0x00, s_opacity};
const f32 near_dist = 400.0f;
const f32 far_dist = 5000.0f;
dDbVw_drawCircleXlu(actor->eyePos, near_dist, near_color, 1, 20);
dDbVw_drawCircleXlu(actor->eyePos, far_dist, far_color, 1, 20);
const s16 view_range = 0x4000;
cXyz offset(0.0f, 0.0f, far_dist);
cXyz endpos;
mDoMtx_stack_c::transS(actor->eyePos.x, actor->eyePos.y, actor->eyePos.z);
mDoMtx_stack_c::YrotM(actor->shape_angle.y);
mDoMtx_stack_c::YrotM(-view_range);
mDoMtx_stack_c::multVec(&offset, &endpos);
dDbVw_drawLineXlu(actor->eyePos, endpos, far_color, 1, 10);
mDoMtx_stack_c::transS(actor->eyePos.x, actor->eyePos.y, actor->eyePos.z);
mDoMtx_stack_c::YrotM(actor->shape_angle.y);
mDoMtx_stack_c::YrotM(view_range);
mDoMtx_stack_c::multVec(&offset, &endpos);
dDbVw_drawLineXlu(actor->eyePos, endpos, far_color, 1, 10);
mDoMtx_stack_c::transS(actor->eyePos.x, actor->eyePos.y, actor->eyePos.z);
mDoMtx_stack_c::YrotM(actor->shape_angle.y);
mDoMtx_stack_c::multVec(&offset, &endpos);
dDbVw_drawLineXlu(actor->eyePos, endpos, far_color, 1, 10);
}
}
static void drawAttentionDists(fopAc_ac_c* actor) {
if (fopAcM_GetGroup(actor) != 4) {
return;
}
GXColor lock_color = {0x00, 0x00, 0xFF, s_opacity};
GXColor talk_color = {0x00, 0xFF, 0x00, s_opacity};
dist_entry& lock_inf = dAttention_c::getDistTable(actor->attention_info.distances[fopAc_attn_LOCK_e]);
dist_entry& talk_inf = dAttention_c::getDistTable(actor->attention_info.distances[fopAc_attn_TALK_e]);
dDbVw_drawCircleXlu(actor->attention_info.position, lock_inf.mDistMax, lock_color, 1, 20);
dDbVw_drawCircleXlu(actor->attention_info.position, talk_inf.mDistMax, talk_color, 1, 20);
}
static void drawPurpleMistAvoid(fopAc_ac_c* actor) {
kytag08_class* tag = (kytag08_class*)actor;
GXColor avoidColor = {0x00, 0xFF, 0x00, s_opacity};
GXColor targetColor = {0xFF, 0x00, 0xFF, s_opacity};
dDbVw_drawCircleXlu(tag->mAvoidPos, tag->mSize.x * 45.0f * tag->mSizeScale, avoidColor, 1, 20);
cXyz cubeSize(10.0f, 10.0f, 10.0f);
csXyz cubeAngle(0, 0, 0);
dDbVw_drawCubeXlu(tag->mAvoidPos, cubeSize, cubeAngle, avoidColor);
dDbVw_drawCubeXlu(tag->mTargetAvoidPos, cubeSize, cubeAngle, targetColor);
}
static void drawLeeverData(fopAc_ac_c* actor) {
e_rb_class* leever = (e_rb_class*)actor;
if (leever->isChild) {
return;
}
GXColor color = {0xFF, 0x00, 0x00, s_opacity};
GXColor color2 = {0x00, 0x00, 0xFF, s_opacity};
cXyz pos = actor->current.pos;
daAlink_c* player = (daAlink_c*)dComIfGp_getPlayer(0);
if (player != nullptr && pos.y < player->mLinkAcch.GetGroundH()) {
pos.y = player->mLinkAcch.GetGroundH() + 100.0f;
}
dDbVw_drawCircleXlu(pos, leever->appearRange * 100.0f, color, 1, 20);
dDbVw_drawCircleXlu(pos, leever->field_0xa69 * 100.0f, color2, 1, 20);
}
void execute() {
const auto& settings = getTransientSettings().triggerView;
s_opacity = (u8)(255.0f * (settings.opacity / 100.0f));
if (settings.loadZones) {
searchActorForCallback(fpcNm_SCENE_EXIT_e, drawSceneExit);
}
if (settings.midnaStops) {
searchActorForCallback(fpcNm_Tag_Mstop_e, drawMidnaStop);
}
if (settings.switchAreas) {
searchActorForCallback(fpcNm_SWC00_e, drawSwitchArea);
}
if (settings.eventAreas) {
searchActorForCallback(fpcNm_TAG_EVENT_e, drawEventTag);
searchActorForCallback(fpcNm_TAG_EVTAREA_e, drawEventArea);
searchActorForCallback(fpcNm_TAG_MYNA2_e, drawPlumTag);
searchActorForCallback(fpcNm_MYNA2_e, drawPlumSearch);
}
if (settings.twilightGates) {
searchActorForCallback(fpcNm_Tag_TWGate_e, drawTWGate);
}
if (settings.paths) {
s_pathColorIndex = 0;
drawStagePaths();
drawCurrentRoomPaths();
}
if (settings.checkpoints) {
searchActorForCallback(fpcNm_Tag_ChgRestart_e, drawCheckpointTag);
}
if (settings.transformDists) {
searchActorForCallback(-1, drawTransformDists);
}
if (settings.attentionDists) {
searchActorForCallback(-1, drawAttentionDists);
}
if (settings.purpleMistAvoid) {
searchActorForCallback(fpcNm_KYTAG08_e, drawPurpleMistAvoid);
}
if (settings.leevers) {
searchActorForCallback(fpcNm_E_RB_e, drawLeeverData);
}
}
} // namespace TriggerView
} // namespace dusk
+7
View File
@@ -0,0 +1,7 @@
#pragma once
namespace dusk {
namespace TriggerView {
void execute();
}
}

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