diff --git a/.gitmodules b/.gitmodules index f05842fa7b..16ad1921cc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ebfe29dc2..b721eba8ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 "$") 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 "$<$:${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 "$<$:${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 - $<$:-finline-hint-functions>) - elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS - $<$:-flive-patching=inline-clone> - $<$:-fno-inline-functions> - $<$:-fno-inline-small-functions> - $<$:-fno-inline-functions-called-once> - $<$:-fno-early-inlining> - $<$:-fno-ipa-cp> - $<$:-fno-ipa-cp-clone> - $<$:-fno-ipa-sra> - $<$:-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 + $<$:-finline-hint-functions>) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + list(APPEND DUSK_GAME_ABI_INLINE_OPTIONS + $<$:-flive-patching=inline-clone> + $<$:-fno-inline-functions> + $<$:-fno-inline-small-functions> + $<$:-fno-inline-functions-called-once> + $<$:-fno-early-inlining> + $<$:-fno-ipa-cp> + $<$:-fno-ipa-cp-clone> + $<$:-fno-ipa-sra> + $<$:-fno-partial-inlining>) endif () - endforeach () -endif () + endif () -if (MSVC) - if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - set(DUSK_PATCHABLE_ENTRY_FLAG $<$:/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 $<$:-fpatchable-function-entry=2,1>) - else () - set(DUSK_PATCHABLE_ENTRY_FLAG $<$:-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 $<$:/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 $<$:-fpatchable-function-entry=2,1>) + else () + set(DUSK_PATCHABLE_ENTRY_FLAG $<$:-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) diff --git a/CMakePresets.json b/CMakePresets.json index 45a6522625..849e4161cd 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -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 diff --git a/cmake/AndroidExports.cmake b/cmake/AndroidExports.cmake index 66e80d3f4a..a71c18a02b 100644 --- a/cmake/AndroidExports.cmake +++ b/cmake/AndroidExports.cmake @@ -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 diff --git a/cmake/AppleExports.cmake b/cmake/AppleExports.cmake index 2c78939811..6397e353a5 100644 --- a/cmake/AppleExports.cmake +++ b/cmake/AppleExports.cmake @@ -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) diff --git a/cmake/ModExports.exp b/cmake/ModExports.exp new file mode 100644 index 0000000000..3a9ec12942 --- /dev/null +++ b/cmake/ModExports.exp @@ -0,0 +1,5 @@ +_mod_ctx +_mod_initialize +_mod_meta +_mod_shutdown +_mod_update diff --git a/cmake/ModExports.ver b/cmake/ModExports.ver new file mode 100644 index 0000000000..9ddff1866c --- /dev/null +++ b/cmake/ModExports.ver @@ -0,0 +1,10 @@ +{ + global: + mod_ctx; + mod_initialize; + mod_meta; + mod_shutdown; + mod_update; + local: + *; +}; diff --git a/cmake/ModSDK.cmake b/cmake/ModSDK.cmake index da9aa51f38..ec24beba33 100644 --- a/cmake/ModSDK.cmake +++ b/cmake/ModSDK.cmake @@ -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 "$" "${_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} diff --git a/cmake/SymbolManifest.cmake b/cmake/SymbolManifest.cmake index 48935acdf6..7c7e09eaa1 100644 --- a/cmake/SymbolManifest.cmake +++ b/cmake/SymbolManifest.cmake @@ -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 () diff --git a/cmake/WindowsExports.cmake b/cmake/WindowsExports.cmake index 4596ac7b69..dae7f067c0 100644 --- a/cmake/WindowsExports.cmake +++ b/cmake/WindowsExports.cmake @@ -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} diff --git a/docs/modding.md b/docs/modding.md index 317ae32c54..4ad29aa8da 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -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 diff --git a/extern/aurora b/extern/aurora index 2c9c95cfed..f8573d34e6 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 2c9c95cfed9a06610addcb0c00ad5e366e4e8c0d +Subproject commit f8573d34e632aea81039526a728b34f373d247ef diff --git a/extern/borealis b/extern/borealis index 6fd955e7e6..4b76f9ea35 160000 --- a/extern/borealis +++ b/extern/borealis @@ -1 +1 @@ -Subproject commit 6fd955e7e6a2d2d47c5a4eaf363fb49847e490ba +Subproject commit 4b76f9ea35eaa116a57fd732b1c4954b6f4d0316 diff --git a/files.cmake b/files.cmake index c36bc7a53b..38087f28b8 100644 --- a/files.cmake +++ b/files.cmake @@ -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 diff --git a/include/Z2AudioLib/Z2SceneMgr.h b/include/Z2AudioLib/Z2SceneMgr.h index 1b3ef2cc0b..27b43d71f4 100644 --- a/include/Z2AudioLib/Z2SceneMgr.h +++ b/include/Z2AudioLib/Z2SceneMgr.h @@ -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; diff --git a/include/d/actor/d_a_door_shutter.h b/include/d/actor/d_a_door_shutter.h index a8e99f1295..34ea7f7fc9 100644 --- a/include/d/actor/d_a_door_shutter.h +++ b/include/d/actor/d_a_door_shutter.h @@ -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; diff --git a/include/d/actor/d_a_npc_fairy.h b/include/d/actor/d_a_npc_fairy.h index efa9d760c1..da9977ed37 100644 --- a/include/d/actor/d_a_npc_fairy.h +++ b/include/d/actor/d_a_npc_fairy.h @@ -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; diff --git a/include/d/actor/d_a_npc_ykm.h b/include/d/actor/d_a_npc_ykm.h index d486d6fe6a..5ad9e7210b 100644 --- a/include/d/actor/d_a_npc_ykm.h +++ b/include/d/actor/d_a_npc_ykm.h @@ -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; diff --git a/include/d/actor/d_a_npc_zrc.h b/include/d/actor/d_a_npc_zrc.h index f4aeee6ac9..6f6a00061b 100644 --- a/include/d/actor/d_a_npc_zrc.h +++ b/include/d/actor/d_a_npc_zrc.h @@ -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; diff --git a/include/d/actor/d_a_npc_zrz.h b/include/d/actor/d_a_npc_zrz.h index 76c782b7d3..9e03215aa4 100644 --- a/include/d/actor/d_a_npc_zrz.h +++ b/include/d/actor/d_a_npc_zrz.h @@ -114,7 +114,7 @@ public: void himoCalc(); void adjustShapeAngle() {} -private: +// private: /* 0x0B48 */ Z2Creature mCreatureSound; /* 0x0BD8 */ daNpcF_MatAnm_c* mpMatAnm; /* 0x0BDC */ mDoExt_invisibleModel mInvisibleModel; diff --git a/include/d/actor/d_a_obj_bosswarp.h b/include/d/actor/d_a_obj_bosswarp.h index 9b62f1a0f6..71b5b2b58f 100644 --- a/include/d/actor/d_a_obj_bosswarp.h +++ b/include/d/actor/d_a_obj_bosswarp.h @@ -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]; diff --git a/include/d/actor/d_a_obj_life_container.h b/include/d/actor/d_a_obj_life_container.h index d832c63843..d4dc5bb334 100644 --- a/include/d/actor/d_a_obj_life_container.h +++ b/include/d/actor/d_a_obj_life_container.h @@ -64,7 +64,7 @@ public: current.pos = new_pos; } -private: +// private: /* 0x92C */ fpc_ProcID mItemId; /* 0x930 */ int mCounter; /* 0x934 */ u8 mStatus; diff --git a/include/d/actor/d_a_obj_master_sword.h b/include/d/actor/d_a_obj_master_sword.h index a971aa335c..fd41a8d079 100644 --- a/include/d/actor/d_a_obj_master_sword.h +++ b/include/d/actor/d_a_obj_master_sword.h @@ -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; diff --git a/include/d/actor/d_a_tbox2.h b/include/d/actor/d_a_tbox2.h index 9b06e685a3..5e3989db57 100644 --- a/include/d/actor/d_a_tbox2.h +++ b/include/d/actor/d_a_tbox2.h @@ -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; diff --git a/include/d/d_stage.h b/include/d/d_stage.h index 53fbbac0d9..f323726f6e 100644 --- a/include/d/d_stage.h +++ b/include/d/d_stage.h @@ -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*); diff --git a/include/global.h b/include/global.h index c16020936e..c559435fa5 100644 --- a/include/global.h +++ b/include/global.h @@ -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 diff --git a/libs/JSystem/include/JSystem/JKernel/JKRHeap.h b/libs/JSystem/include/JSystem/JKernel/JKRHeap.h index ace28de6e3..e23dca3c02 100644 --- a/libs/JSystem/include/JSystem/JKernel/JKRHeap.h +++ b/libs/JSystem/include/JSystem/JKernel/JKRHeap.h @@ -7,6 +7,7 @@ #include #include #include +#include class JKRHeap; typedef void (*JKRErrorHandler)(void*, u32, int); diff --git a/mods/ao_mod/CMakeLists.txt b/mods/ao_mod/CMakeLists.txt index 723ed55006..7304654d0c 100644 --- a/mods/ao_mod/CMakeLists.txt +++ b/mods/ao_mod/CMakeLists.txt @@ -17,5 +17,4 @@ add_mod(ao_mod SOURCES src/mod.cpp MOD_JSON mod.json RES_DIR res - BUNDLE ) diff --git a/mods/basic_cosmetics_mod/src/hooks.cpp b/mods/basic_cosmetics_mod/src/hooks.cpp index e90e76d8b3..2c878f7632 100644 --- a/mods/basic_cosmetics_mod/src/hooks.cpp +++ b/mods/basic_cosmetics_mod/src/hooks.cpp @@ -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(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(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) diff --git a/mods/randomizer b/mods/randomizer new file mode 160000 index 0000000000..61ba8c4058 --- /dev/null +++ b/mods/randomizer @@ -0,0 +1 @@ +Subproject commit 61ba8c40589884955929f467c1eda74d1c88a444 diff --git a/platforms/android/README.md b/platforms/android/README.md index 6ebcac23cd..c4f8a688df 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -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+ diff --git a/platforms/android/build.gradle b/platforms/android/build.gradle index eba031aa1b..4b1f3fb3f2 100644 --- a/platforms/android/build.gradle +++ b/platforms/android/build.gradle @@ -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 } diff --git a/platforms/android/gradle/wrapper/gradle-wrapper.jar b/platforms/android/gradle/wrapper/gradle-wrapper.jar index 2b338a935b..61285a659d 100644 Binary files a/platforms/android/gradle/wrapper/gradle-wrapper.jar and b/platforms/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/platforms/android/gradle/wrapper/gradle-wrapper.properties b/platforms/android/gradle/wrapper/gradle-wrapper.properties index ec50cfcb6e..37f78a6af8 100644 --- a/platforms/android/gradle/wrapper/gradle-wrapper.properties +++ b/platforms/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/platforms/android/gradlew b/platforms/android/gradlew index 3427607f42..adff685a03 100755 --- a/platforms/android/gradlew +++ b/platforms/android/gradlew @@ -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" "$@" diff --git a/platforms/android/gradlew.bat b/platforms/android/gradlew.bat index 8a0b282aa6..c4bdd3ab8e 100644 --- a/platforms/android/gradlew.bat +++ b/platforms/android/gradlew.bat @@ -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 diff --git a/res/rml/command_console.rcss b/res/rml/command_console.rcss new file mode 100644 index 0000000000..b5378b5d7a --- /dev/null +++ b/res/rml/command_console.rcss @@ -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; +} diff --git a/res/rml/mods.rcss b/res/rml/mods.rcss index 6042e35c0f..34e86f485b 100644 --- a/res/rml/mods.rcss +++ b/res/rml/mods.rcss @@ -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; -} \ No newline at end of file +} + +.status-badge.network { + color: #6fb7ef; + opacity: 1; +} diff --git a/sdk/include/mods/items.h b/sdk/include/mods/items.h index b79563026c..ceee2a0efc 100644 --- a/sdk/include/mods/items.h +++ b/sdk/include/mods/items.h @@ -10,7 +10,7 @@ #define ITEM_CHECK_FREESTANDING_PREFIX "freestanding:" /* : */ #define ITEM_CHECK_GOLDEN_WOLF_PREFIX "golden_wolf:" /* */ #define ITEM_CHECK_POE_PREFIX "poe:" /* : */ -#define ITEM_CHECK_SHOP_PREFIX "shop:" /* : */ +#define ITEM_CHECK_SHOP_PREFIX "shop:" /* :: */ #define ITEM_CHECK_SKY_PREFIX "sky:" /* : */ #define ITEM_CHECK_DUNGEON_REWARD_PREFIX "dungeon_reward:" /* */ @@ -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" diff --git a/sdk/include/mods/meta.hpp b/sdk/include/mods/meta.hpp index 6b1de96ad8..ef85d827c2 100644 --- a/sdk/include/mods/meta.hpp +++ b/sdk/include/mods/meta.hpp @@ -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) diff --git a/sdk/include/mods/svc/file.h b/sdk/include/mods/svc/file.h new file mode 100644 index 0000000000..4d5c82b5a1 --- /dev/null +++ b/sdk/include/mods/svc/file.h @@ -0,0 +1,118 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#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); diff --git a/sdk/include/mods/svc/file.hpp b/sdk/include/mods/svc/file.hpp new file mode 100644 index 0000000000..e297000c2e --- /dev/null +++ b/sdk/include/mods/svc/file.hpp @@ -0,0 +1,321 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +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 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 bytes() const { + return {static_cast(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 buffer{}; + return svc_file->display_name(mod_ctx, location.c_str(), buffer.data(), + static_cast(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 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& 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*>(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 filters; + std::string defaultLocation; +}; + +struct PickResult { + ModResult status = MOD_ERROR; + std::vector locations; + std::string error; +}; + +namespace detail { + +inline std::function 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 callback, bool folder) { + if (svc_file == nullptr) { + return MOD_UNAVAILABLE; + } + if (!callback) { + return MOD_INVALID_ARGUMENT; + } + if (pickCallback) { + return MOD_CONFLICT; + } + std::vector 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(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 callback) { + return detail::pick(options, std::move(callback), false); +} + +inline ModResult pick_folder(const PickOptions& options, std::function callback) { + return detail::pick(options, std::move(callback), true); +} + +inline ModResult export_file(const std::string& sourceLocation, const std::string& suggestedName, + std::function 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 diff --git a/sdk/include/mods/svc/http.h b/sdk/include/mods/svc/http.h new file mode 100644 index 0000000000..fd4d9a98db --- /dev/null +++ b/sdk/include/mods/svc/http.h @@ -0,0 +1,98 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#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); diff --git a/sdk/include/mods/svc/http.hpp b/sdk/include/mods/svc/http.hpp new file mode 100644 index 0000000000..906bf3d0d2 --- /dev/null +++ b/sdk/include/mods/svc/http.hpp @@ -0,0 +1,204 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mods::http { + +struct Header { + std::string name; + std::string value; +}; + +struct Request { + HttpMethod method = HTTP_METHOD_GET; + std::string url; + std::vector
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
headers; + std::vector 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(a)) == + std::tolower(static_cast(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 callback; +}; + +inline std::unordered_map> 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(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 callback) { + if (svc_http == nullptr) { + return {0, MOD_UNAVAILABLE}; + } + if (!callback) { + return {0, MOD_INVALID_ARGUMENT}; + } + + if (request.headers.size() > std::numeric_limits::max()) { + return {0, MOD_INVALID_ARGUMENT}; + } + std::vector 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(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(); + 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 diff --git a/sdk/include/mods/svc/item.h b/sdk/include/mods/svc/item.h index 94fec6fb6c..ff4f31b547 100644 --- a/sdk/include/mods/svc/item.h +++ b/sdk/include/mods/svc/item.h @@ -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); diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index ea8abe2d7c..31bff18c58 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -2,6 +2,7 @@ #include #include +#include #ifdef __cplusplus #include @@ -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; diff --git a/src/d/actor/d_a_alink_damage.inc b/src/d/actor/d_a_alink_damage.inc index a57e71bcee..34701229c3 100644 --- a/src/d/actor/d_a_alink_damage.inc +++ b/src/d/actor/d_a_alink_damage.inc @@ -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) { diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index 8a2bc0025b..4351f807aa 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -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); diff --git a/src/d/actor/d_a_demo_item.cpp b/src/d/actor/d_a_demo_item.cpp index 465532a5f0..0c092cf59b 100644 --- a/src/d/actor/d_a_demo_item.cpp +++ b/src/d/actor/d_a_demo_item.cpp @@ -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; diff --git a/src/d/actor/d_a_e_po.cpp b/src/d/actor/d_a_e_po.cpp index a7a7be6dd1..112ad2d56f 100644 --- a/src/d/actor/d_a_e_po.cpp +++ b/src/d/actor/d_a_e_po.cpp @@ -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" diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp index 1b96147957..a50753cdf0 100644 --- a/src/d/actor/d_a_midna.cpp +++ b/src/d/actor/d_a_midna.cpp @@ -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 { diff --git a/src/d/actor/d_a_npc_gra.cpp b/src/d/actor/d_a_npc_gra.cpp index e5e0d6eed0..d57ce3ab2b 100644 --- a/src/d/actor/d_a_npc_gra.cpp +++ b/src/d/actor/d_a_npc_gra.cpp @@ -4022,16 +4022,53 @@ BOOL daNpc_grA_c::talk(void*) { if (r26 && talkProc(NULL, TRUE, NULL)) { if (mFlow.getEventId(&sp8) == 1) { #if TARGET_PC + const u8 originalItem = sp8; u32 itemGiveTag = 0; - if (sp8 == dItemNo_BOMB_IN_BAG_e) { + bool isItemCheck = false; + if (originalItem == dItemNo_BOMB_IN_BAG_e) { const auto itemCheck = - dusk::mods::item_check_commit("goron_reward:F_SP113", sp8, this); + dusk::mods::item_check_commit("goron_reward:F_SP113", originalItem, this); sp8 = itemCheck.itemNo; itemGiveTag = itemCheck.tag; + isItemCheck = true; + } else { + const char* stage = dComIfGp_getStartStageName(); + const bool isAdultGoronShop = (strcmp(stage, "R_SP160") == 0 && originalItem == dItemNo_HYLIA_SHIELD_e) || + (strcmp(stage, "F_SP116") == 0 && originalItem == dItemNo_ARROW_30_e); + if (isAdultGoronShop) { + const auto itemCheck = dusk::mods::item_check_commit( + dusk::mods::item_give_tag_shop(originalItem), originalItem, this); + sp8 = itemCheck.itemNo; + itemGiveTag = itemCheck.tag; + isItemCheck = true; + } } -#endif + + if (isItemCheck && sp8 == dItemNo_NONE_e) { + dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this); + r29 = 1; + if (mType == 0xb) { + field_0x1691 = 1; + } + } else { + field_0x1480 = fopAcM_createItemForPresentDemo( + ¤t.pos, sp8, 0, -1, -1, NULL, NULL, itemGiveTag); + if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) { + s16 r25 = + dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff); + dComIfGp_getEvent()->reset(this); + fopAcM_orderChangeEventId(this, r25, 1, -1); + field_0x9ec = 1; + r29 = 1; + mOrderNewEvt = 1; + if (mType == 0xb) { + field_0x1691 = 1; + } + } + } +#else field_0x1480 = fopAcM_createItemForPresentDemo(¤t.pos, sp8, 0, -1, -1, NULL, - NULL IF_DUSK_ARG(itemGiveTag)); + NULL); if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) { s16 r25 = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff); dComIfGp_getEvent()->reset(this); @@ -4043,6 +4080,7 @@ BOOL daNpc_grA_c::talk(void*) { field_0x1691 = 1; } } +#endif } else { if (mType == 0xa && field_0x1486 == 0 && daNpcF_chkEvtBit(0x187)) { dComIfGp_getEvent()->reset(this); diff --git a/src/d/actor/d_a_npc_grc.cpp b/src/d/actor/d_a_npc_grc.cpp index 2435f13c77..47ea7490c0 100644 --- a/src/d/actor/d_a_npc_grc.cpp +++ b/src/d/actor/d_a_npc_grc.cpp @@ -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 enum grC_RES_File_ID { @@ -1476,8 +1479,27 @@ BOOL daNpc_grC_c::talk(void* param_1) { rv = TRUE; if (mFlow.getEventId(&i_itemNo) == 1) { - mItemID = fopAcM_createItemForPresentDemo(¤t.pos, i_itemNo, 0, -1, -1, NULL, NULL); +#if TARGET_PC + const bool isCastleTownShop = + strcmp(dComIfGp_getStartStageName(), "R_SP160") == 0 && + (i_itemNo == dItemNo_RED_BOTTLE_e || i_itemNo == dItemNo_OIL_BOTTLE_e); + u32 itemGiveTag = 0; + if (isCastleTownShop) { + const auto itemCheck = dusk::mods::item_check_commit( + dusk::mods::item_give_tag_shop(i_itemNo), i_itemNo, this); + i_itemNo = itemCheck.itemNo; + itemGiveTag = itemCheck.tag; + } + if (isCastleTownShop && i_itemNo == dItemNo_NONE_e) { + dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this); + } else { + mItemID = fopAcM_createItemForPresentDemo( + ¤t.pos, i_itemNo, 0, -1, -1, NULL, NULL, itemGiveTag); + } +#else + mItemID = fopAcM_createItemForPresentDemo(¤t.pos, i_itemNo, 0, -1, -1, NULL, NULL); +#endif if (mItemID != fpcM_ERROR_PROCESS_ID_e) { s16 i_eventID = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xFF); dComIfGp_getEvent()->reset(this); diff --git a/src/d/actor/d_a_npc_kkri.cpp b/src/d/actor/d_a_npc_kkri.cpp index 755257bff7..97e375e4ec 100644 --- a/src/d/actor/d_a_npc_kkri.cpp +++ b/src/d/actor/d_a_npc_kkri.cpp @@ -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 static DUSK_CONSTEXPR int l_bmdData[2][2] = { @@ -1183,16 +1186,39 @@ int daNpc_Kkri_c::talk(void*) { if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) { #if TARGET_PC u32 itemGiveTag = 0; - if (item_no == dItemNo_OIL_BOTTLE3_e) { - const auto itemCheck = - dusk::mods::item_check_commit("coro_bottle", item_no, this); + const char* itemCheckName = nullptr; + switch (item_no) { + case dItemNo_OIL_BOTTLE3_e: + itemCheckName = ITEM_CHECK_CORO_BOTTLE; + break; + case dItemNo_KANTERA_e: + itemCheckName = ITEM_CHECK_CORO_LANTERN; + break; + case dItemNo_KEY_OF_FILONE_e: + itemCheckName = ITEM_CHECK_CORO_GATE_KEY; + break; + } + + if (itemCheckName != nullptr) { + const auto itemCheck = dusk::mods::item_check_commit( + itemCheckName, item_no, this); item_no = itemCheck.itemNo; itemGiveTag = itemCheck.tag; } + + if (item_no == dItemNo_NONE_e && itemCheckName != nullptr) { + dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this); + field_0xfd5 = 1; + mEvtNo = 1; + evtChange(); + } else { + mItemPartnerId = fopAcM_createItemForPresentDemo( + ¤t.pos, item_no, 0, -1, -1, NULL, NULL, itemGiveTag); + } +#else + mItemPartnerId = fopAcM_createItemForPresentDemo( + ¤t.pos, item_no, 0, -1, -1, NULL, NULL); #endif - mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, item_no, - 0, -1, -1, NULL, - NULL IF_DUSK_ARG(itemGiveTag)); } if (fopAcM_IsExecuting(mItemPartnerId)) { diff --git a/src/d/actor/d_a_obj_item.cpp b/src/d/actor/d_a_obj_item.cpp index fba0ad7d91..e06eb37e50 100644 --- a/src/d/actor/d_a_obj_item.cpp +++ b/src/d/actor/d_a_obj_item.cpp @@ -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) { diff --git a/src/d/actor/d_a_obj_life_container.cpp b/src/d/actor/d_a_obj_life_container.cpp index b2eb30167a..85553cda28 100644 --- a/src/d/actor/d_a_obj_life_container.cpp +++ b/src/d/actor/d_a_obj_life_container.cpp @@ -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(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); } diff --git a/src/d/actor/d_a_obj_master_sword.cpp b/src/d/actor/d_a_obj_master_sword.cpp index 36fa786808..a1ef6b1709 100644 --- a/src/d/actor/d_a_obj_master_sword.cpp +++ b/src/d/actor/d_a_obj_master_sword.cpp @@ -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); } diff --git a/src/d/actor/d_a_obj_smallkey.cpp b/src/d/actor/d_a_obj_smallkey.cpp index 6914cee5f0..1fd6025ce6 100644 --- a/src/d/actor/d_a_obj_smallkey.cpp +++ b/src/d/actor/d_a_obj_smallkey.cpp @@ -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) { diff --git a/src/d/actor/d_a_player.cpp b/src/d/actor/d_a_player.cpp index 48ad37e2ec..8c97bffb6f 100644 --- a/src/d/actor/d_a_player.cpp +++ b/src/d/actor/d_a_player.cpp @@ -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(); } diff --git a/src/d/actor/d_a_shop_item.cpp b/src/d/actor/d_a_shop_item.cpp index a7a159ab1c..edd16b9ecf 100644 --- a/src/d/actor/d_a_shop_item.cpp +++ b/src/d/actor/d_a_shop_item.cpp @@ -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]; diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index 7ec4f09b10..b9ea46e58e 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -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; diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 7dc4fd21ce..b38a336e02 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -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; } diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index e8b4464dee..cf4a830799 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -18,7 +18,7 @@ #include #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" diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 4b23d3ded9..3f64b8b239 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -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; diff --git a/src/d/d_item.cpp b/src/d/d_item.cpp index a388ef155c..f89d4f37e8 100644 --- a/src/d/d_item.cpp +++ b/src/d/d_item.cpp @@ -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](); } diff --git a/src/d/d_meter2.cpp b/src/d/d_meter2.cpp index 8d396e0185..a90124ebb6 100644 --- a/src/d/d_meter2.cpp +++ b/src/d/d_meter2.cpp @@ -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) { diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index 865b57be59..a1425355ab 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -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(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(mpXYText[i][0]->getPanePtr())->setCharSpace(0.0f); + static_cast(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); diff --git a/src/d/d_msg_flow.cpp b/src/d/d_msg_flow.cpp index 8ce605e122..222e323e78 100644 --- a/src/d/d_msg_flow.cpp +++ b/src/d/d_msg_flow.cpp @@ -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; diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp index 433e9065b1..ab342d6654 100644 --- a/src/d/d_s_play.cpp +++ b/src/d/d_s_play.cpp @@ -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 diff --git a/src/d/d_stage.cpp b/src/d/d_stage.cpp index c92ce7493b..0b933bdb34 100644 --- a/src/d/d_stage.cpp +++ b/src/d/d_stage.cpp @@ -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]; diff --git a/src/dusk/commands.cpp b/src/dusk/commands.cpp new file mode 100644 index 0000000000..b953095936 --- /dev/null +++ b/src/dusk/commands.cpp @@ -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 +#include +#include +#include +#include + +#include "fmt/format.h" + +namespace dusk { +namespace { + +static constexpr int kMaxHistory = 64; + +static std::vector SplitArgs(std::string_view input) { + std::vector 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 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{v} : std::nullopt; +} + +static std::optional 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{v} : std::nullopt; +} + +static const char* ActorShortName(s16 profname) { + const char* n = dStage_getName(profname, -1); + return n ? n : "?"; +} + +// Resolves @found / @link / @ 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(proc); +} + +static std::optional 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{entry->procname} : std::nullopt; +} + +static std::optional ParseXYZ(const std::vector& 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(*x, *y, *z)} : std::nullopt; +} + +static bool TryAngle( + s16& out, const std::vector& 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(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(p); + if (fpcBs_Is_JustOfType(g_fpcNd_type, proc->subtype)) { + fpcLyIt_OnlyHere(&static_cast(p)->layer, callback, ctx); + } +} + +struct ListContext { + s16 targetId; + std::vector* output; +}; +struct FindContext { + s16 targetId; + std::vector matches; +}; + +static int ListActorCallback(void* p, void* ctx) { + auto* proc = static_cast(p); + auto* context = static_cast(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(p); + auto* context = static_cast(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 [angle] | tp @ [ [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 [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 [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 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 "); + 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 @ + 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 [h_angle] [v_angle] | camera tp @"); + 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