diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80257624c5..4712b36439 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,6 +80,7 @@ jobs: path: | build/install/Dusklight-*.AppImage build/install/debug.tar.* + build/install/sdk/ build-apple: name: Build Apple (${{matrix.name}}) @@ -148,6 +149,7 @@ jobs: path: | build/install/Dusklight.app build/install/debug.tar.* + build/install/sdk/ build-android: name: Build Android (${{matrix.name}}) @@ -203,6 +205,9 @@ jobs: - name: Build native library run: cmake --build --preset ${{matrix.preset}} --target dusklight + - name: Build bundled mods + run: cmake --build --preset ${{matrix.preset}} --target dusklight_mods + - name: Stage stripped JNI library run: ANDROID_STAGE_ABIS="${{matrix.abi}}" platforms/android/scripts/stage-jni-libs.sh @@ -210,11 +215,17 @@ jobs: working-directory: platforms/android run: ./gradlew :app:assembleRelease --rerun-tasks + - name: Stage artifacts + run: | + mkdir -p upload/sdk + cp build/*/stub-android-*.so upload/sdk/ + cp platforms/android/app/build/outputs/apk/release/app-${{matrix.abi}}-release-unsigned.apk upload/ + - name: Upload artifacts uses: actions/upload-artifact@v7 with: name: dusklight-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}} - path: platforms/android/app/build/outputs/apk/release/app-${{matrix.abi}}-release-unsigned.apk + path: upload/ build-windows: name: Build Windows (${{matrix.name}}) @@ -280,4 +291,6 @@ jobs: build/install/*.exe build/install/*.dll build/install/res/ + build/install/mods/ build/install/debug.7z + build/install/sdk/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 049d56b14e..bf1d95ca97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -511,9 +511,12 @@ if (WIN32) target_link_libraries(dusklight PRIVATE Psapi) endif () if (APPLE) - # Mods resolve game symbols from the executable at dlopen time. - target_link_options(dusklight PRIVATE -Wl,-export_dynamic) -elseif (UNIX AND NOT ANDROID) + include(cmake/AppleExports.cmake) + setup_apple_exports(dusklight) +elseif (ANDROID) + include(cmake/AndroidExports.cmake) + setup_android_exports(dusklight) +elseif (UNIX) target_link_options(dusklight PRIVATE -rdynamic) endif () @@ -536,6 +539,8 @@ endif () if (CMAKE_SYSTEM_NAME STREQUAL Linux) target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1") target_link_libraries(dusklight PRIVATE dl) +elseif (ANDROID) + target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1") endif () if (NOT APPLE) diff --git a/CMakePresets.json b/CMakePresets.json index 394a1ddef5..e6485ddac1 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -22,18 +22,6 @@ "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreadedDLL" } }, - { - "name": "release", - "hidden": true, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CMAKE_MSVC_RUNTIME_LIBRARY": "MultiThreadedDLL", - "CMAKE_INTERPROCEDURAL_OPTIMIZATION": { - "type": "BOOL", - "value": true - } - } - }, { "name": "ci", "hidden": true, @@ -208,17 +196,13 @@ { "name": "windows-arm64-msvc", "displayName": "Windows ARM64 (MSVC)", - "generator": "Ninja", - "binaryDir": "${sourceDir}/build/${presetName}", + "inherits": [ + "windows-msvc" + ], "architecture": { "value": "arm64", "strategy": "external" }, - "cacheVariables": { - "CMAKE_C_COMPILER": "cl", - "CMAKE_CXX_COMPILER": "cl", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install" - }, "vendor": { "microsoft.com/VisualStudioSettings/CMake/1.0": { "hostOS": [ @@ -445,8 +429,7 @@ "hidden": true, "inherits": [ "android-base", - "ci", - "release" + "ci" ], "cacheVariables": { "DUSK_ENABLE_SENTRY_NATIVE": { diff --git a/cmake/AndroidExports.cmake b/cmake/AndroidExports.cmake new file mode 100644 index 0000000000..66e80d3f4a --- /dev/null +++ b/cmake/AndroidExports.cmake @@ -0,0 +1,63 @@ +include_guard(GLOBAL) + +get_filename_component(_dir "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + +# Android mod linking: symgen scans and filters the game's exports, generating a version script used for the executable +# link step, and generates a shared object stub that mods can link against without having to build the whole game. +function(setup_android_exports target) + include("${_dir}/SymbolManifest.cmake") + ensure_symgen(TRUE) + add_dependencies(${target} symgen) + + set(_rsp_lines "$") + foreach (_lib IN LISTS JSYSTEM_LIBRARIES) + list(APPEND _rsp_lines "$") + endforeach () + list(JOIN _rsp_lines "\n" _rsp_content) + set(_rsp "${CMAKE_BINARY_DIR}/dusklight_exports_input.rsp") + file(GENERATE OUTPUT "${_rsp}" CONTENT "${_rsp_content}") + + set(_sdk_args) + foreach (_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx + aurora_os aurora_pad aurora_si aurora_vi) + if (TARGET ${_lib}) + list(APPEND _sdk_args --sdk-lib "$") + endif () + endforeach () + if (TARGET dawn::webgpu_dawn) + get_target_property(_dawn_type dawn::webgpu_dawn TYPE) + if (_dawn_type STREQUAL "STATIC_LIBRARY") + list(APPEND _sdk_args --sdk-lib "$") + endif () + endif () + + set(_vscript "${CMAKE_BINARY_DIR}/dusklight_exports.ver") + add_custom_command(TARGET ${target} PRE_LINK + COMMAND "${SYMGEN_EXE}" exports + "@${_rsp}" + --out "${_vscript}" + --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 + --extra-sym SDL_main + --extra-sym "Java_*" + ${_sdk_args} + COMMENT "Generating dusklight exports" + VERBATIM) + target_link_options(${target} PRIVATE "-Wl,--version-script=${_vscript}") + + string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _arch) + set(_stub "${CMAKE_BINARY_DIR}/stub-android-${_arch}.so") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${SYMGEN_EXE}" stub -f elf "$" -o "${_stub}" + --soname "$" --arch "${_arch}" + BYPRODUCTS "${_stub}" + COMMENT "Generating dusklight link stub" + VERBATIM) + install(FILES "${_stub}" DESTINATION sdk) +endfunction() diff --git a/cmake/AppleExports.cmake b/cmake/AppleExports.cmake new file mode 100644 index 0000000000..2c78939811 --- /dev/null +++ b/cmake/AppleExports.cmake @@ -0,0 +1,90 @@ +include_guard(GLOBAL) + +get_filename_component(_dir "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + +# Apple mod linking: symgen scans and filters the game's exports, generating an exports list used for the executable +# link step, and generates a MH_EXECUTE Mach-O stub that mods can link against without having to build the whole game. +function(setup_apple_exports target) + include("${_dir}/SymbolManifest.cmake") + ensure_symgen(TRUE) + set(_symgen "${SYMGEN_EXE}") + add_dependencies(${target} symgen) + + set(_config_subdir "") + if (CMAKE_CONFIGURATION_TYPES) + set(_config_subdir "$/") + endif () + + set(_rsp_lines "$") + foreach (_lib IN LISTS JSYSTEM_LIBRARIES) + list(APPEND _rsp_lines "$") + endforeach () + list(JOIN _rsp_lines "\n" _rsp_content) + set(_rsp "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports_input.rsp") + file(GENERATE OUTPUT "${_rsp}" CONTENT "${_rsp_content}") + + set(_sdk_args) + foreach (_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx + aurora_os aurora_pad aurora_si aurora_vi) + if (TARGET ${_lib}) + list(APPEND _sdk_args --sdk-lib "$") + endif () + endforeach () + + # Dawn is linked statically on Apple; mods reach wgpu* through the executable. + if (TARGET dawn::webgpu_dawn) + get_target_property(_dawn_type dawn::webgpu_dawn TYPE) + if (_dawn_type STREQUAL "STATIC_LIBRARY") + list(APPEND _sdk_args --sdk-lib "$") + endif () + endif () + + set(_exp "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.exp") + add_custom_command(TARGET ${target} PRE_LINK + COMMAND "${_symgen}" exports + "@${_rsp}" + --out "${_exp}" + --exclude cmake_pch + --exclude miniz + --exclude asan_options + --exclude src/dusk + ${_sdk_args} + COMMENT "Generating dusklight exports" + VERBATIM) + target_link_options(${target} PRIVATE -Xlinker -exported_symbols_list -Xlinker "${_exp}") + + # Generate the stub executable mods link against via -bundle_loader. + set(_stub_args) + if (IOS) + set(_stub_platform "ios") + list(APPEND _stub_args --platform ios) + elseif (TVOS) + set(_stub_platform "tvos") + list(APPEND _stub_args --platform tvos) + else () + set(_stub_platform "macos") + endif () + if (CMAKE_OSX_DEPLOYMENT_TARGET) + list(APPEND _stub_args --min-os "${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif () + if (CMAKE_OSX_ARCHITECTURES) + set(_archs "${CMAKE_OSX_ARCHITECTURES}") + else () + set(_archs "${CMAKE_SYSTEM_PROCESSOR}") + endif () + set(_arch_names "") + foreach (_arch IN LISTS _archs) + string(TOLOWER "${_arch}" _arch) + list(APPEND _stub_args --arch "${_arch}") + list(APPEND _arch_names "${_arch}") + endforeach () + list(JOIN _arch_names "_" _arch_name) + + set(_stub "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight-stub") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${_symgen}" stub -f macho "${_exp}" -o "${_stub}" ${_stub_args} + BYPRODUCTS "${_stub}" + COMMENT "Generating dusklight link stub" + VERBATIM) + install(FILES "${_stub}" DESTINATION sdk RENAME "stub-${_stub_platform}-${_arch_name}") +endfunction() diff --git a/cmake/GameABIConfig.cmake b/cmake/GameABIConfig.cmake index d48be445a2..8b95dcaf67 100644 --- a/cmake/GameABIConfig.cmake +++ b/cmake/GameABIConfig.cmake @@ -9,24 +9,47 @@ if (ANDROID) list(APPEND _game_compile_defs TARGET_ANDROID=1) endif () -set(_game_include_dirs +# Public game headers +set(_game_abi_include_dirs ${_game_root}/include - ${_game_root}/src - ${_game_root}/assets/GZ2E01 # TODO: make this dynamic if needed? + ${_game_root}/assets/GZ2E01 ${_game_root}/libs/JSystem/include - ${_game_root}/libs ${_game_root}/extern/aurora/include/dolphin ${_game_root}/extern/aurora/include + ${_game_root}/sdk/include +) + +# Internal game headers +set(_game_include_dirs + ${_game_abi_include_dirs} + ${_game_root}/src ${_game_root}/extern ${CMAKE_CURRENT_BINARY_DIR} ) -# Interface target for mods and sub-projects to inherit game headers/defines. +# Mod API, including services +add_library(dusklight_mod_api INTERFACE) +target_include_directories(dusklight_mod_api INTERFACE ${_game_root}/sdk/include) + +# Full internal headers used to build the game add_library(dusklight_game_headers INTERFACE) target_include_directories(dusklight_game_headers INTERFACE ${_game_include_dirs}) target_compile_definitions(dusklight_game_headers INTERFACE ${_game_compile_defs}) -if (TARGET dawn::dawncpp_headers) - target_link_libraries(dusklight_game_headers INTERFACE dawn::dawncpp_headers) -elseif (TARGET dawn::webgpu_dawn) - target_link_libraries(dusklight_game_headers INTERFACE dawn::webgpu_dawn) -endif () + +# Public game ABI for mods +add_library(dusklight_game_abi_headers INTERFACE) +target_include_directories(dusklight_game_abi_headers INTERFACE ${_game_abi_include_dirs}) +target_compile_definitions(dusklight_game_abi_headers INTERFACE ${_game_compile_defs}) + +# Mod feature targets +add_library(dusklight_mod_feature_game INTERFACE) +target_link_libraries(dusklight_mod_feature_game INTERFACE + dusklight_mod_api + dusklight_game_abi_headers) +target_compile_definitions(dusklight_mod_feature_game INTERFACE DUSK_MOD_FEATURE_GAME=1) +target_sources(dusklight_mod_feature_game INTERFACE + ${_game_root}/sdk/src/game_feature.cpp) + +add_library(dusklight_mod_feature_webgpu INTERFACE) +target_link_libraries(dusklight_mod_feature_webgpu INTERFACE dusklight_mod_api) +target_compile_definitions(dusklight_mod_feature_webgpu INTERFACE DUSK_MOD_FEATURE_WEBGPU=1) diff --git a/cmake/ModSDK.cmake b/cmake/ModSDK.cmake index 833e433360..87125e12fd 100644 --- a/cmake/ModSDK.cmake +++ b/cmake/ModSDK.cmake @@ -1,8 +1,9 @@ -# add_mod( SOURCES ... MOD_JSON [RES_DIR ] [OVERLAY_DIR ] +# add_mod( [FEATURES ...] SOURCES ... MOD_JSON +# [RUNTIME_LIBRARIES ...] [RES_DIR ] [OVERLAY_DIR ] # [TEXTURES_DIR ] [OUTPUT_DIR ] [BUNDLE]) set(DUSK_MODS_OUTPUT_DIR "${CMAKE_BINARY_DIR}/mods" CACHE PATH "Directory to write mod packages into") -function(_mod_lib_name out_var) +function(_mod_lib_info out_platform_var out_name_var) set(_arch "${CMAKE_SYSTEM_PROCESSOR}") if (APPLE AND CMAKE_OSX_ARCHITECTURES) list(LENGTH CMAKE_OSX_ARCHITECTURES _count) @@ -12,18 +13,20 @@ function(_mod_lib_name out_var) set(_arch "${CMAKE_OSX_ARCHITECTURES}") endif () string(TOLOWER "${CMAKE_SYSTEM_NAME}" _platform) + if (_platform STREQUAL "darwin") + set(_platform "macos") + endif () string(TOLOWER "${_arch}" _arch) if (_arch MATCHES "^(i[3-6]86|x86)$") set(_arch "x86") endif () if (WIN32) set(_ext ".dll") - elseif (APPLE) - set(_ext ".dylib") else () set(_ext ".so") endif () - set(${out_var} "${_platform}-${_arch}${_ext}" PARENT_SCOPE) + set(${out_platform_var} "${_platform}-${_arch}" PARENT_SCOPE) + set(${out_name_var} "mod${_ext}" PARENT_SCOPE) endfunction() function(_mod_resolve_source_path out_var path) @@ -44,8 +47,28 @@ function(_mod_collect_assets out_var dir) set(${out_var} ${_files} PARENT_SCOPE) endfunction() +function(_mod_add_webgpu_headers target_name) + if (NOT TARGET dawn::dawncpp_headers AND NOT TARGET dawn::webgpu_dawn) + include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../extern/aurora/cmake/AuroraDependencyVersions.cmake") + set(AURORA_DAWN_PROVIDER "package" CACHE STRING "How to provide Dawn for the mod SDK") + include("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../extern/aurora/cmake/AuroraDawnProvider.cmake") + endif () + + if (TARGET dawn::dawncpp_headers) + target_link_libraries(${target_name} PRIVATE dawn::dawncpp_headers) + elseif (TARGET dawn::webgpu_dawn) + target_link_libraries(${target_name} PRIVATE dawn::webgpu_dawn) + else () + message(FATAL_ERROR "add_mod: FEATURES webgpu could not provide WebGPU headers") + endif () +endfunction() + function(add_mod target_name) - cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR" "SOURCES" ${ARGN}) + cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR" + "SOURCES;RUNTIME_LIBRARIES;FEATURES" ${ARGN}) + if (ARG_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "add_mod: unknown arguments: ${ARG_UNPARSED_ARGUMENTS}") + endif () if (NOT ARG_MOD_JSON) message(FATAL_ERROR "add_mod: MOD_JSON is required") endif () @@ -54,12 +77,33 @@ function(add_mod target_name) message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}") endif () + set(_supported_features game webgpu) + set(_features "") + foreach (_feature IN LISTS ARG_FEATURES) + list(FIND _supported_features "${_feature}" _feature_index) + if (_feature_index EQUAL -1) + list(JOIN _supported_features ", " _supported_features_text) + message(FATAL_ERROR + "add_mod: unknown feature '${_feature}' (supported: ${_supported_features_text})") + endif () + list(FIND _features "${_feature}" _duplicate_index) + if (NOT _duplicate_index EQUAL -1) + message(FATAL_ERROR "add_mod: duplicate feature '${_feature}'") + endif () + list(APPEND _features "${_feature}") + endforeach () + if (_features AND NOT ARG_SOURCES) + message(FATAL_ERROR "add_mod: FEATURES requires SOURCES") + endif () + set(_has_lib FALSE) + set(_needs_host_link FALSE) + set(_lib_platform "") set(_lib_name "") if (ARG_SOURCES) set(_has_lib TRUE) - add_library(${target_name} SHARED ${ARG_SOURCES}) - _mod_lib_name(_lib_name) + add_library(${target_name} MODULE ${ARG_SOURCES}) + _mod_lib_info(_lib_platform _lib_name) set_target_properties(${target_name} PROPERTIES PREFIX "" C_VISIBILITY_PRESET hidden @@ -67,7 +111,16 @@ function(add_mod target_name) VISIBILITY_INLINES_HIDDEN ON WINDOWS_EXPORT_ALL_SYMBOLS OFF) target_compile_features(${target_name} PRIVATE cxx_std_20) - target_link_libraries(${target_name} PRIVATE dusklight_game_headers) + target_link_libraries(${target_name} PRIVATE dusklight_mod_api) + foreach (_feature IN LISTS _features) + target_link_libraries(${target_name} PRIVATE dusklight_mod_feature_${_feature}) + if (_feature STREQUAL "webgpu") + _mod_add_webgpu_headers(${target_name}) + endif () + if (_feature STREQUAL "game" OR _feature STREQUAL "webgpu") + set(_needs_host_link TRUE) + endif () + endforeach () if (NOT TARGET dusklight) # Apply global compile options for out-of-tree mod builds @@ -90,50 +143,77 @@ function(add_mod target_name) endif () if (APPLE) - # Game symbols resolve against the host executable at dlopen time. - target_link_options(${target_name} PRIVATE -undefined dynamic_lookup) + if (_needs_host_link) + if (TARGET dusklight) + set(_game_exe "$") + add_dependencies(${target_name} dusklight) + elseif (DUSK_GAME_EXE) + _mod_resolve_source_path(_game_exe "${DUSK_GAME_EXE}") + else () + message(FATAL_ERROR + "add_mod: FEATURES ${_features} requires DUSK_GAME_EXE (game executable)") + endif () + target_link_options(${target_name} PRIVATE + -Xlinker -bundle_loader -Xlinker "${_game_exe}") + set_property(TARGET ${target_name} APPEND PROPERTY LINK_DEPENDS "${_game_exe}") + endif () + set_target_properties(${target_name} PROPERTIES + BUILD_RPATH "@loader_path" + INSTALL_RPATH "@loader_path") elseif (ANDROID) - if (TARGET dusklight) - target_link_libraries(${target_name} PRIVATE dusklight) - elseif (DUSK_GAME_SOLIB) - target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_SOLIB}") - else () - message(FATAL_ERROR "add_mod: DUSK_GAME_SOLIB is not set (libmain.so)") + if (_needs_host_link) + if (TARGET dusklight) + target_link_libraries(${target_name} PRIVATE dusklight) + elseif (DUSK_GAME_EXE) + _mod_resolve_source_path(_game_lib "${DUSK_GAME_EXE}") + target_link_libraries(${target_name} PRIVATE "${_game_lib}") + else () + message(FATAL_ERROR + "add_mod: FEATURES ${_features} requires DUSK_GAME_EXE (libmain.so or stub)") + endif () endif () + set_target_properties(${target_name} PROPERTIES + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN") elseif (UNIX) - target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined) - elseif (WIN32) - # Link against the generated import library (game ABI surface). Function calls - # resolve through import thunks. Data is toolchain dependent: - # - clang-cl: lld's mingw mode auto-imports data references, fixed up at load by - # the mod SDK's pseudo-relocation runtime (pseudo_reloc.cpp). - # - cl (MSVC): only DUSK_GAME_DATA-annotated data is reachable. Un-annotated - # references fail to link. - if (NOT DUSK_GAME_IMPLIB) - message(FATAL_ERROR "add_mod: DUSK_GAME_IMPLIB is not set.") + if (_needs_host_link) + target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined) + endif () + set_target_properties(${target_name} PROPERTIES + BUILD_RPATH "$ORIGIN" + INSTALL_RPATH "$ORIGIN") + elseif (WIN32) + # Mods link against the game's import library (sdk/windows-.lib, generated by + # setup_windows_exports); cl and clang-cl both consume it in MSVC mode. Function + # calls resolve through import thunks; game data is reachable only through + # __declspec(dllimport), i.e. DUSK_GAME_DATA-annotated declarations. + if (_needs_host_link) + if (TARGET dusklight) + if (NOT DUSK_GAME_IMPLIB) + message(FATAL_ERROR + "add_mod: DUSK_GAME_IMPLIB is not set (see setup_windows_exports)") + endif () + set(_game_lib "${DUSK_GAME_IMPLIB}") + elseif (DUSK_GAME_EXE) + _mod_resolve_source_path(_game_lib "${DUSK_GAME_EXE}") + if (NOT _game_lib MATCHES "\\.lib$") + message(FATAL_ERROR + "add_mod: DUSK_GAME_EXE must be an import library on Windows " + "(sdk/windows-.lib)") + endif () + else () + message(FATAL_ERROR + "add_mod: FEATURES ${_features} requires DUSK_GAME_EXE (import library)") + endif () + target_link_libraries(${target_name} PRIVATE "${_game_lib}") endif () - target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_IMPLIB}") set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") target_compile_definitions(${target_name} PRIVATE _ITERATOR_DEBUG_LEVEL=0) - if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - target_compile_options(${target_name} PRIVATE "$<$:/clang:-mcmodel=large>") - target_sources(${target_name} PRIVATE "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../sdk/pseudo_reloc.cpp") - # lld mingw mode rewrites /DEFAULTLIB directives to -l style and skips %LIB%, so - # the CRT libraries and search paths are spelled out explicitly. - target_link_options(${target_name} PRIVATE -lldmingw /nodefaultlib /INCREMENTAL:NO) - target_link_libraries(${target_name} PRIVATE - msvcrt.lib msvcprt.lib vcruntime.lib ucrt.lib - oldnames.lib uuid.lib kernel32.lib user32.lib) - set(_lib_dirs "$ENV{LIB}") - if ("${_lib_dirs}" STREQUAL "") - message(FATAL_ERROR "add_mod: %LIB% is empty; configure from a VS dev shell") - endif () - foreach (_libdir IN LISTS _lib_dirs) - target_link_options(${target_name} PRIVATE "/libpath:${_libdir}") - endforeach () - endif () endif () endif () + if (ARG_RUNTIME_LIBRARIES AND NOT _has_lib) + message(FATAL_ERROR "add_mod: RUNTIME_LIBRARIES requires SOURCES") + endif () set(_output_dir "${DUSK_MODS_OUTPUT_DIR}") if (ARG_OUTPUT_DIR) @@ -142,18 +222,39 @@ function(add_mod target_name) set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage") set(_out "${_output_dir}/${target_name}.dusk") - set(_zip_args "${_lib_name}" mod.json) + set(_zip_args mod.json) set(_package_deps "${_mod_json}") set(_package_inputs "${_mod_json}") set(_extra_cmds "") set(_lib_copy_cmd "") set(_target_depend "") if (_has_lib) - list(APPEND _zip_args "${_lib_name}") - set(_lib_copy_cmd COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$" "${_stage}/${_lib_name}") + list(APPEND _zip_args lib) + set(_lib_copy_cmd + COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}/lib/${_lib_platform}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" "${_stage}/lib/${_lib_platform}/${_lib_name}") set(_target_depend ${target_name}) endif () + string(TOLOWER "${_lib_name}" _lib_name_key) + set(_runtime_lib_name_keys "${_lib_name_key}") + foreach (_runtime_lib IN LISTS ARG_RUNTIME_LIBRARIES) + _mod_resolve_source_path(_runtime_lib_path "${_runtime_lib}") + if (NOT EXISTS "${_runtime_lib_path}" OR IS_DIRECTORY "${_runtime_lib_path}") + message(FATAL_ERROR "add_mod: runtime library does not exist or is not a file: ${_runtime_lib_path}") + endif () + get_filename_component(_runtime_lib_name "${_runtime_lib_path}" NAME) + string(TOLOWER "${_runtime_lib_name}" _runtime_lib_name_key) + list(FIND _runtime_lib_name_keys "${_runtime_lib_name_key}" _runtime_lib_name_index) + if (NOT _runtime_lib_name_index EQUAL -1) + message(FATAL_ERROR "add_mod: duplicate runtime library filename: ${_runtime_lib_name}") + endif () + list(APPEND _runtime_lib_name_keys "${_runtime_lib_name_key}") + list(APPEND _package_deps "${_runtime_lib_path}") + list(APPEND _package_inputs "${_runtime_lib_path}") + list(APPEND _lib_copy_cmd COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${_runtime_lib_path}" "${_stage}/lib/${_lib_platform}/${_runtime_lib_name}") + endforeach () if (ARG_RES_DIR) _mod_resolve_source_path(_res_dir "${ARG_RES_DIR}") _mod_collect_assets(_res_deps "${_res_dir}") @@ -190,6 +291,7 @@ function(add_mod target_name) set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_IDS "${_mod_id}") set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_STAGES "${_stage}") set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_PACKAGES "${_out}") + set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_LIB_PLATFORMS "${_lib_platform}") set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES "${_lib_name}") set(_bundle_cmds COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundled_mods" @@ -228,9 +330,10 @@ endfunction() # user cache). # - Linux: pre-extracted stage dirs into /mods so native libs dlopen in place from # read-only installs. -# - macOS: pre-extracted stage dirs into the installed app's Contents/Resources/mods, dylibs -# ad-hoc signed in place, then the whole bundle re-signed. -# - iOS/tvOS: assets into /mods/ and the dylib into Frameworks/.dylib. +# - macOS: pre-extracted stage dirs into the installed app's Contents/Resources/mods, native +# libs ad-hoc signed in place, then the whole bundle re-signed. +# - iOS/tvOS: assets into /mods/, the mod library into Frameworks/.so, +# and runtime libraries alongside it. # - Android: nothing here; gradle packs ${CMAKE_BINARY_DIR}/bundled_mods into APK assets. function(install_bundled_mods) get_property(_targets GLOBAL PROPERTY DUSK_BUNDLED_MOD_TARGETS) @@ -239,6 +342,7 @@ function(install_bundled_mods) endif () get_property(_ids GLOBAL PROPERTY DUSK_BUNDLED_MOD_IDS) get_property(_stages GLOBAL PROPERTY DUSK_BUNDLED_MOD_STAGES) + get_property(_lib_platforms GLOBAL PROPERTY DUSK_BUNDLED_MOD_LIB_PLATFORMS) get_property(_lib_names GLOBAL PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES) list(LENGTH _targets _count) math(EXPR _last "${_count} - 1") @@ -254,19 +358,30 @@ function(install_bundled_mods) list(GET _targets ${_i} _target) list(GET _ids ${_i} _id) list(GET _stages ${_i} _stage) + list(GET _lib_platforms ${_i} _lib_platform) list(GET _lib_names ${_i} _lib_name) install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/mods/${_id}" - PATTERN "${_lib_name}" EXCLUDE) + PATTERN "lib" EXCLUDE) install(PROGRAMS "$" - DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.dylib") + DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.so") + install(DIRECTORY "${_stage}/lib/${_lib_platform}/" + DESTINATION "${_bundle_dir}/Frameworks" + PATTERN "${_lib_name}" EXCLUDE) endforeach () else () foreach (_i RANGE ${_last}) list(GET _ids ${_i} _id) list(GET _stages ${_i} _stage) + list(GET _lib_platforms ${_i} _lib_platform) list(GET _lib_names ${_i} _lib_name) install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/Contents/Resources/mods/${_id}") - install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/Resources/mods/${_id}/${_lib_name}\" COMMAND_ERROR_IS_FATAL ANY)") + install(CODE " + file(GLOB _mod_libs \"${_bundle_dir}/Contents/Resources/mods/${_id}/lib/${_lib_platform}/*\") + foreach (_mod_lib IN LISTS _mod_libs) + if (NOT IS_DIRECTORY \"\${_mod_lib}\") + execute_process(COMMAND /usr/bin/codesign --force --sign - \"\${_mod_lib}\" COMMAND_ERROR_IS_FATAL ANY) + endif () + endforeach ()") endforeach () if (TARGET crashpad_handler) install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/MacOS/$\" COMMAND_ERROR_IS_FATAL ANY)") diff --git a/cmake/SymbolManifest.cmake b/cmake/SymbolManifest.cmake index 927040a47f..657afa2a78 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.1.1") +set(_SYMGEN_VERSION "1.2.3") 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) @@ -108,18 +108,21 @@ function(setup_symbol_manifest target) if (WIN32) set(_input --pdb "$") - set(_out "$/dusklight.symdb") else () set(_input --binary "$") - if (APPLE) - set(_out "$/Resources/dusklight.symdb") - else () - set(_out "$/dusklight.symdb") - endif () endif () + if (APPLE) + # Room for the load command `symgen manifest --embed` inserts post-link. + target_link_options(${target} PRIVATE "LINKER:-headerpad,0x200") + endif () + + # The manifest is embedded into the image as a new section, located at runtime through + # the descriptor manifest.cpp reserves. On Apple platforms this command must stay + # attached before the ad-hoc codesign POST_BUILD command: the patch invalidates any + # existing signature. add_custom_command(TARGET ${target} POST_BUILD - COMMAND "${SYMGEN_EXE}" manifest ${_input} --out "${_out}" - COMMENT "Generating symbol manifest" + COMMAND "${SYMGEN_EXE}" manifest ${_input} --embed "$" + COMMENT "Embedding symbol manifest" VERBATIM) endfunction() diff --git a/cmake/WindowsExports.cmake b/cmake/WindowsExports.cmake index 3010c82f25..4596ac7b69 100644 --- a/cmake/WindowsExports.cmake +++ b/cmake/WindowsExports.cmake @@ -6,9 +6,16 @@ get_filename_component(_DUSK_WINDOWS_EXPORTS_CMAKE_DIR "${CMAKE_CURRENT_LIST_FIL # import library mods link against. symgen scans the built objects, filters by source, and # writes a .def used by the main link and import library generation. function(setup_windows_exports target) - if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8) - message(WARNING "dusklight: Windows code-mod exports are x64-only for now; skipping") - return() + string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _implib_arch) + if (_implib_arch STREQUAL "arm64") + set(_dlltool_machine "arm64") + set(_lib_machine "arm64") + elseif (_implib_arch MATCHES "^(amd64|x86_64)$") + set(_dlltool_machine "i386:x86-64") + set(_lib_machine "x64") + else () + message(FATAL_ERROR + "dusklight: no Windows mod linking support for ${CMAKE_SYSTEM_PROCESSOR}") endif () include("${_DUSK_WINDOWS_EXPORTS_CMAKE_DIR}/SymbolManifest.cmake") @@ -50,14 +57,13 @@ function(setup_windows_exports target) # Generate curated exports list from the main binary set(_def "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.def") add_custom_command(TARGET ${target} PRE_LINK - # TODO: src/dusk/ is NOT excluded: inline code in game headers - # currently call into it (e.g. dusk::frame_interp::lookup_replacement). COMMAND "${_symgen}" def - --rsp "${_rsp}" + "@${_rsp}" --out "${_def}" --exclude cmake_pch --exclude miniz --exclude asan_options + --exclude src/dusk --max-exports 58000 ${_sdk_args} ${_forward_args} @@ -70,11 +76,11 @@ function(setup_windows_exports target) get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) find_program(DUSK_LLVM_DLLTOOL llvm-dlltool HINTS "${_compiler_dir}") if (DUSK_LLVM_DLLTOOL) - set(_implib_cmd "${DUSK_LLVM_DLLTOOL}" -d "${_def}" -D dusklight.exe -m i386:x86-64 - -l "${_implib}") + set(_implib_cmd "${DUSK_LLVM_DLLTOOL}" -d "${_def}" -D dusklight.exe + -m "${_dlltool_machine}" -l "${_implib}") else () - set(_implib_cmd "${CMAKE_AR}" /nologo "/def:${_def}" /machine:x64 /name:dusklight.exe - "/out:${_implib}") + set(_implib_cmd "${CMAKE_AR}" /nologo "/def:${_def}" "/machine:${_lib_machine}" + /name:dusklight.exe "/out:${_implib}") endif () add_custom_command(TARGET ${target} POST_BUILD COMMAND ${_implib_cmd} @@ -82,9 +88,6 @@ function(setup_windows_exports target) COMMENT "Generating dusklight import library" VERBATIM) set(DUSK_GAME_IMPLIB "${_implib}" CACHE INTERNAL "Import library for Windows mod linking") - set(DUSK_GAME_DEF "${_def}" CACHE INTERNAL "Curated export .def for the game executable") - # Ship the import library as sdk/dusklight.lib in the install tree: mods may use it to - # compile against Dusklight without having to build the whole game. (See DUSK_GAME_IMPLIB) - install(FILES "${_implib}" DESTINATION sdk RENAME dusklight.lib) + install(FILES "${_implib}" DESTINATION sdk RENAME "windows-${_implib_arch}.lib") endfunction() diff --git a/docs/modding.md b/docs/modding.md index 6f2d1a8580..8afe8701e5 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -22,7 +22,7 @@ function, read and write data fields, and hook the vast majority of game functio 7. [Asset Overlays](#asset-overlays) 8. [Runtime Lifecycle](#runtime-lifecycle) 9. [Error Handling](#error-handling) -10. [Advanced: Exporting Services](#advanced-exporting-services) +10. [Advanced](#advanced) --- @@ -50,6 +50,7 @@ set(DUSKLIGHT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusklight" CACHE PATH "Path to du add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL) add_mod(my_mod + FEATURES game # optional SOURCES src/mod.cpp MOD_JSON mod.json RES_DIR res # optional @@ -58,6 +59,12 @@ add_mod(my_mod ) ``` +Available features: +- `game`: Allows calling into and hooking game code. Mods that **only** use services may omit it, providing a wider + range of compatibility with Dusklight versions and a slightly faster build process. +- `webgpu`: Allows importing the WebGPU API (`webgpu/webgpu.h`). Must be enabled when using + [GfxService](#gfxservice-modssvcgfxh). + Building produces `my_mod.dusk` in `build//mods/` (configurable via the `DUSK_MODS_OUTPUT_DIR` cache variable). Dusklight searches a `mods/` directory next to the app in addition to the user directory, so a dev build launched from `build//` picks these up automatically: rebuild, relaunch (or click Reload), done. @@ -131,26 +138,30 @@ A service is a struct of C function pointers with a version header. You declare loader resolves it before your mod initializes: ```cpp -IMPORT_SERVICE(LogService, svc_log); // required, any minor version -IMPORT_SERVICE_VERSION(LogService, svc_log, 2); // required, minor version >= 2 +IMPORT_SERVICE(LogService, svc_log); // required, latest minor version +IMPORT_SERVICE_VERSION(LogService, svc_log, 0); // required, minimum minor version 0 (for backwards compatibility) IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null ``` Each service is individually versioned, and there may be multiple major versions of a service provided at once, allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major** bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new -functions to the end of the struct without breaking existing callers and simply bumps the minor version. Mods that -want the newer functions may use `IMPORT_SERVICE_VERSION` to require that minor at **load time**, or `SERVICE_HAS` to -check at **runtime** whether a specific function is available. +functions to the end of the struct without breaking existing callers and simply bumps the minor version. -The contract (see `include/mods/api.h` for the full version): +`IMPORT_SERVICE` and `IMPORT_OPTIONAL_SERVICE` require the latest minor version compiled against, making every field in +the service safe to call. A mod can use `IMPORT_SERVICE_VERSION` (or its optional counterpart) with an older minor +version to remain compatible with older Dusklight versions, then use `SERVICE_HAS` to check at runtime for fields added +after that explicitly requested version. + +The contract (see `sdk/include/mods/api.h` for the full version): - **A required import is guaranteed valid.** If the service is missing or too old, the mod fails to load with a clear error. No need to null check at call sites. -- **Anything at or below the minor version you imported can be called unconditionally.** +- **Anything at or below the minor version you imported can be called unconditionally.** The default macros import + the service type's current minor version; the versioned macros explicitly override that minimum. - Optional imports may be null; check once in `mod_initialize`. -- Fields newer than your imported minor must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a null - check. +- Fields newer than your imported minor version must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a + null check. --- @@ -200,13 +211,13 @@ const char* dir = svc_host->mod_dir(mod_ctx); // writable per-mod directory svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); // disables the mod ``` -`get_service`/`publish_service` provide dynamic service lookup; see [Advanced](#advanced-exporting-services). +`get_service`/`publish_service` provide dynamic service lookup; see [Exporting Services](#exporting-services). **Lifecycle watches.** If your mod provides a service that hands out per-caller state (registrations, callbacks, handles), watch other mods' lifecycle and drop what you hold for a mod when it detaches. ```cpp -IMPORT_SERVICE_VERSION(HostService, svc_host, 1); +IMPORT_SERVICE(HostService, svc_host); void on_mod_lifecycle(ModContext* ctx, ModContext* subject, const char* subject_id, ModLifecycleEvent event, void* user_data) { @@ -411,6 +422,8 @@ unless changing host UI is intentional. ### GfxService (`mods/svc/gfx.h`) +**Requires `add_mod(... FEATURES webgpu)`** + Direct WebGPU access at various stages of the rendering pipeline. Mods use the `wgpu*` C API (via `webgpu/webgpu.h`) for custom draws and compute dispatches. Mods must manage their own WebGPU state, including pipelines and bind groups. @@ -461,6 +474,8 @@ first in-game frame. Projection matrices match the renderer's WebGPU clip conven ## Hooking Game Functions +**Requires `add_mod(... FEATURES game)`** + Mods may hook the vast majority of game functions, including file-local static, private and virtual functions. `mods/hook.hpp` provides typed helpers over the hook service: @@ -469,8 +484,14 @@ Mods may hook the vast majority of game functions, including file-local static, #include "mods/svc/hook.h" IMPORT_SERVICE(HookService, svc_hook); + +DEFINE_HOOK(&daAlink_c::posMove, LinkPosMove); +DEFINE_HOOK(&daAlink_c::execute, LinkExecute); ``` +Every hook target must be **declared** at namespace scope with `DEFINE_HOOK` (a target you can name in C++) or +`DEFINE_HOOK_SYMBOL` (a symbol name). + ### Pre-hooks Run before the original. Return `HOOK_SKIP_ORIGINAL` to cancel it (post-hooks still run). @@ -484,7 +505,7 @@ HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata return HOOK_CONTINUE; } -dusk::mods::hook_add_pre<&daAlink_c::posMove>(svc_hook, on_pos_move_pre); +dusk::mods::hook_add_pre(svc_hook, on_pos_move_pre); ``` ### Post-hooks @@ -495,24 +516,22 @@ if any. ```cpp void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... } -dusk::mods::hook_add_post<&daAlink_c::posMove>(svc_hook, on_pos_move_post); +dusk::mods::hook_add_post(svc_hook, on_pos_move_post); ``` ### Replace-hooks -Substitute the original entirely. Call through to it via `Hook<...>::g_orig` if needed: +Substitute the original entirely. Call through to it via the declaration's `g_orig` if needed: ```cpp -using ExecuteEntry = dusk::mods::Hook<&daAlink_c::execute>; - void on_execute_replace(ModContext*, void* args, void* retval, void*) { - int result = ExecuteEntry::g_orig(dusk::mods::arg(args, 0)); + int result = LinkExecute::g_orig(dusk::mods::arg(args, 0)); if (retval != nullptr) { *static_cast(retval) = result; } } -dusk::mods::hook_replace<&daAlink_c::execute>(svc_hook, on_execute_replace); +dusk::mods::hook_replace(svc_hook, on_execute_replace); ``` By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`, @@ -525,10 +544,8 @@ Functions you can't name in C++ (file-local statics, private class members, anyt symbol name instead. You must supply the signature along with the name. ```cpp -// static callback used by Link's hookshot collider in d_a_alink_hook.inc -using HookshotHit = dusk::mods::NamedHook< - "daAlink_hookshotAtHitCallBack", - void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>; +DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack", + void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit); dusk::mods::hook_add_pre(svc_hook, on_hookshot_hit_pre); ... @@ -537,9 +554,16 @@ HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the o Class member functions must include `Class*` as the first argument. -The install fails with the resolve error when the name is missing (`MOD_UNAVAILABLE`), ambiguous (`MOD_CONFLICT`), -or the manifest is absent (`MOD_UNSUPPORTED`). Unlike `Hook<&Class::method>`, the signature is **not** -compiler-checked: a mismatched signature will corrupt the call. +Two spellings work on every platform: + +- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. They carry no + signature, so overload sets (and file-local statics sharing a name) return `MOD_CONFLICT`. +- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled spelling in + dlsym convention (no Mach-O leading underscore). The escape hatch for overloads. + +Installing fails with `MOD_UNAVAILABLE` when it didn't resolve (missing, ambiguous, or no symbol manifest). Unlike +`DEFINE_HOOK`, the signature is **not** compiler-checked: a mismatched signature will corrupt the +call. ### Reading and writing arguments @@ -552,6 +576,8 @@ T& ref = dusk::mods::arg_ref(args, n); // read/write reference ``` ```cpp +DEFINE_HOOK(fopAcM_createItem, CreateItem); + // fpc_ProcID fopAcM_createItem(..., int itemNo, ...): turn heart drops into green rupees HookAction on_create_item_pre(ModContext*, void* args, void*, void*) { int& itemNo = dusk::mods::arg_ref(args, 1); @@ -561,54 +587,11 @@ HookAction on_create_item_pre(ModContext*, void* args, void*, void*) { return HOOK_CONTINUE; } -dusk::mods::hook_add_pre<&fopAcM_createItem>(svc_hook, on_create_item_pre); +dusk::mods::hook_add_pre(svc_hook, on_create_item_pre); ``` For reference parameters (e.g. `const cXyz& pos`), `arg_ref` yields a direct reference. -### Resolving symbols by name - -`resolve` looks a symbol up in the **symbol manifest**: a name→address map generated alongside every game build and -keyed to that exact binary. It covers the whole image, including functions that aren't exported (file-local statics), -which makes them hookable: - -```cpp -IMPORT_SERVICE(HookService, svc_hook); - -void* addr = nullptr; -uint32_t flags = 0; -if (svc_hook->resolve(mod_ctx, "GXSetProjection", &addr, &flags) == MOD_OK) { - // addr is the function's real address in the running game; hook or call it. -} -``` - -Two spellings work on every platform: - -- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. They carry no - signature, so overload sets (and file-local statics sharing a name) return `MOD_CONFLICT`. -- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled spelling in - dlsym convention (no Mach-O leading underscore). The escape hatch for overloads. - -`MOD_UNSUPPORTED` means the manifest is missing or was built for a different game binary. - -### Game code ABI contract - -A primary consideration when letting mods link against the game is maintaining ABI stability across Dusklight -versions. If your mod calls or hooks game code directly (anything beyond the service APIs), import `GameService` -(`mods/svc/game.h`): - -```cpp -IMPORT_SERVICE(GameService, svc_game); -``` - -Its major version is the game code ABI epoch: it's bumped when game struct or vtable layouts change incompatibly, and -the ordinary service version check then rejects your mod with a clear error instead of letting it corrupt memory in a -version it wasn't built for. Service-only and asset-only mods should *not* import it and will continue to work across -game ABI changes. - -The more you can do through services, the better: a mod that avoids touching game code directly sidesteps future ABI -breaks entirely and plays nicer with other enabled mods. - --- ## Asset Overlays @@ -680,7 +663,9 @@ explicit results. --- -## Advanced: Exporting Services +## Advanced + +### Exporting Services Mods may export services of their own, permitting framework mods and cross-mod integration. Define the interface in a header both mods share: @@ -704,6 +689,7 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = MY_MOD_SERVICE_ID; static constexpr uint16_t major_version = MY_MOD_SERVICE_MAJOR; + static constexpr uint16_t minor_version = MY_MOD_SERVICE_MINOR; }; #endif ``` @@ -732,7 +718,7 @@ svc_my_mod->do_thing(mod_ctx, 42); The loader registers all exports before resolving any imports, so declaration order between mods doesn't matter. Note that the `ctx` a provider receives identifies the *calling* mod. -### Dependencies between mods +#### Dependencies between mods Service imports are also dependency declarations: the loader initializes mods in dependency order, so by the time your `mod_initialize` runs, every mod you import services from (required *or* optional) has already finished its own @@ -762,3 +748,31 @@ For services whose construction can't happen at static-init time, declare the ex and publish the pointer later via `svc_host->publish_service(...)`. Consumers can fetch services dynamically with `svc_host->get_service(...)`; prefer manifest imports whenever possible, since they give the loader dependency information and fail fast with good errors. + +### Native Runtime Libraries + +`RUNTIME_LIBRARIES` passed to `add_mod` are packaged beside the mod's native module in `lib//`. Dusklight +extracts the whole directory before loading the mod, so libraries linked by the mod resolve normally. The SDK links the +mod itself with `$ORIGIN` on Linux and `@loader_path` on Apple platforms; runtime libraries with their own non-system +dependencies must also be built with origin-relative lookup paths. On Windows, Dusklight uses an isolated DLL search +rooted at this directory. + +```cmake +add_mod(my_mod + SOURCES src/mod.cpp + MOD_JSON mod.json + RUNTIME_LIBRARIES "${VENDOR_RUNTIME_LIBRARY}") +``` + +SDKs that load plugins by directory can pass them the absolute runtime path from the current HostService: + +```cpp +IMPORT_SERVICE(HostService, svc_host); + +const char* nativeDir = svc_host->native_dir(mod_ctx); // read-only +``` + +Libraries loaded explicitly by the mod remain its responsibility: stop their threads and unload them during +`mod_shutdown`. Do not write into `native_dir`; use `mod_dir` for writable state. Native library namespaces are +process-wide on some platforms, so two mods cannot safely assume that incompatible libraries with the same filename +will remain isolated. diff --git a/files.cmake b/files.cmake index 52b33a3e5a..f4d9721a83 100644 --- a/files.cmake +++ b/files.cmake @@ -1411,70 +1411,103 @@ set(DOLPHIN_FILES ) set(DUSK_FILES - include/dusk/action_bindings.h - include/dusk/endian_gx.hpp - include/dusk/config.hpp - include/dusk/dvd_asset.hpp - include/dusk/scope_guard.hpp - src/dusk/dvd_asset.cpp + include/helpers/batch.hpp + include/helpers/endian_gx.hpp src/d/actor/d_a_alink_dusk.cpp - src/dusk/android_frame_rate.hpp + src/dusk/OSContext.cpp + src/dusk/OSMutex.cpp + src/dusk/OSReport.cpp + src/dusk/OSThread.cpp + src/dusk/achievements.cpp + src/dusk/action_bindings.cpp + src/dusk/action_bindings.h src/dusk/android_frame_rate.cpp + src/dusk/android_frame_rate.hpp src/dusk/asserts.cpp - src/dusk/batch.cpp - src/dusk/batch.hpp + src/dusk/autosave.cpp src/dusk/config.cpp + src/dusk/config.hpp src/dusk/crash_handler.cpp src/dusk/crash_reporting.cpp src/dusk/data.cpp src/dusk/data.hpp - src/dusk/endian.cpp + src/dusk/discord.cpp + src/dusk/discord.hpp + src/dusk/discord_presence.cpp + src/dusk/dvd_asset.cpp + src/dusk/dvd_asset.hpp src/dusk/extras.c src/dusk/file_select.cpp src/dusk/file_select.hpp src/dusk/frame_interpolation.cpp src/dusk/game_clock.cpp + src/dusk/gamepad_color.cpp src/dusk/globals.cpp src/dusk/gyro.cpp - include/dusk/menu_pointer.h - src/dusk/menu_pointer.cpp - src/dusk/mouse.cpp - src/dusk/gamepad_color.cpp - src/dusk/autosave.cpp src/dusk/http/http.hpp - src/dusk/io.cpp - src/dusk/layout.cpp - src/dusk/logging.cpp - src/dusk/settings.cpp - src/dusk/speedrun.cpp - src/dusk/string.cpp - src/dusk/stubs.cpp - include/dusk/texture_replacements.hpp - src/dusk/texture_replacements.cpp - src/dusk/touch_camera.cpp - src/dusk/update_check.cpp - src/dusk/update_check.hpp - #src/dusk/m_Do_ext_dusk.cpp - src/dusk/imgui/ImGuiConfig.hpp - src/dusk/imgui/ImGuiConsole.hpp - src/dusk/imgui/ImGuiConsole.cpp - src/dusk/imgui/ImGuiEngine.cpp - src/dusk/imgui/ImGuiEngine.hpp + src/dusk/imgui/ImGuiActorSpawner.cpp src/dusk/imgui/ImGuiBloomWindow.cpp src/dusk/imgui/ImGuiBloomWindow.hpp + src/dusk/imgui/ImGuiCameraOverlay.cpp + src/dusk/imgui/ImGuiConfig.hpp + src/dusk/imgui/ImGuiConsole.cpp + src/dusk/imgui/ImGuiConsole.hpp + src/dusk/imgui/ImGuiControllerOverlay.cpp + src/dusk/imgui/ImGuiEngine.cpp + src/dusk/imgui/ImGuiEngine.hpp + src/dusk/imgui/ImGuiHeapOverlay.cpp src/dusk/imgui/ImGuiMenuTools.cpp src/dusk/imgui/ImGuiMenuTools.hpp src/dusk/imgui/ImGuiMenuRandomizer.cpp src/dusk/imgui/ImGuiMenuRandomizer.hpp - src/dusk/imgui/ImGuiActorSpawner.cpp src/dusk/imgui/ImGuiProcessOverlay.cpp - src/dusk/imgui/ImGuiCameraOverlay.cpp - src/dusk/imgui/ImGuiHeapOverlay.cpp - src/dusk/imgui/ImGuiControllerOverlay.cpp - src/dusk/imgui/ImGuiStubLog.cpp src/dusk/imgui/ImGuiSaveEditor.cpp - src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStateShare.cpp + src/dusk/imgui/ImGuiStateShare.hpp + src/dusk/imgui/ImGuiStubLog.cpp + src/dusk/io.cpp + src/dusk/iso_validate.cpp + src/dusk/layout.cpp + src/dusk/livesplit.cpp + src/dusk/logging.cpp + src/dusk/menu_pointer.cpp + src/dusk/menu_pointer.h + src/dusk/mods/loader/bundle_disk.cpp + src/dusk/mods/loader/bundle_zip.cpp + src/dusk/mods/loader/context.cpp + src/dusk/mods/loader/depgraph.cpp + src/dusk/mods/loader/depgraph.hpp + src/dusk/mods/loader/loader.cpp + src/dusk/mods/loader/loader.hpp + src/dusk/mods/loader/native_module.cpp + src/dusk/mods/loader/native_module.hpp + src/dusk/mods/log_buffer.cpp + src/dusk/mods/log_buffer.hpp + src/dusk/mods/manifest.cpp + src/dusk/mods/manifest.hpp + src/dusk/mods/svc/camera.cpp + src/dusk/mods/svc/config.cpp + src/dusk/mods/svc/config.hpp + src/dusk/mods/svc/game.cpp + src/dusk/mods/svc/gfx.cpp + src/dusk/mods/svc/hook.cpp + src/dusk/mods/svc/host.cpp + src/dusk/mods/svc/log.cpp + src/dusk/mods/svc/overlay.cpp + src/dusk/mods/svc/registry.cpp + src/dusk/mods/svc/registry.hpp + src/dusk/mods/svc/resource.cpp + src/dusk/mods/svc/texture.cpp + src/dusk/mods/svc/ui.cpp + src/dusk/mods/svc/ui.hpp + src/dusk/mouse.cpp + src/dusk/scope_guard.hpp + src/dusk/settings.cpp + src/dusk/speedrun.cpp + src/dusk/stubs.cpp + src/dusk/texture_replacements.cpp + src/dusk/texture_replacements.hpp + src/dusk/touch_camera.cpp src/dusk/ui/achievements.cpp src/dusk/ui/achievements.hpp src/dusk/ui/bool_button.cpp @@ -1483,11 +1516,11 @@ set(DUSK_FILES src/dusk/ui/button.hpp src/dusk/ui/component.cpp src/dusk/ui/component.hpp - src/dusk/ui/controls.hpp src/dusk/ui/controller_config.cpp src/dusk/ui/controller_config.hpp src/dusk/ui/cosmetics.hpp src/dusk/ui/cosmetics.cpp + src/dusk/ui/controls.hpp src/dusk/ui/document.cpp src/dusk/ui/document.hpp src/dusk/ui/editor.cpp @@ -1496,18 +1529,22 @@ set(DUSK_FILES src/dusk/ui/event.hpp src/dusk/ui/graphics_tuner.cpp src/dusk/ui/graphics_tuner.hpp - src/dusk/ui/input.cpp - src/dusk/ui/input.hpp src/dusk/ui/icon_provider.cpp src/dusk/ui/icon_provider.hpp + src/dusk/ui/input.cpp + src/dusk/ui/input.hpp src/dusk/ui/logs_window.cpp src/dusk/ui/logs_window.hpp + src/dusk/ui/menu_bar.cpp + src/dusk/ui/menu_bar.hpp src/dusk/ui/mod_texture_provider.cpp src/dusk/ui/mod_texture_provider.hpp src/dusk/ui/mod_window.cpp src/dusk/ui/mod_window.hpp src/dusk/ui/modal.cpp src/dusk/ui/modal.hpp + src/dusk/ui/mods_window.cpp + src/dusk/ui/mods_window.hpp src/dusk/ui/nav_types.hpp src/dusk/ui/number_button.cpp src/dusk/ui/number_button.hpp @@ -1515,10 +1552,6 @@ set(DUSK_FILES src/dusk/ui/overlay.hpp src/dusk/ui/pane.cpp src/dusk/ui/pane.hpp - src/dusk/ui/menu_bar.cpp - src/dusk/ui/menu_bar.hpp - src/dusk/ui/mods_window.cpp - src/dusk/ui/mods_window.hpp src/dusk/ui/prelaunch.cpp src/dusk/ui/prelaunch.hpp src/dusk/ui/preset.cpp @@ -1533,10 +1566,10 @@ set(DUSK_FILES src/dusk/ui/string_button.hpp src/dusk/ui/tab_bar.cpp src/dusk/ui/tab_bar.hpp - src/dusk/ui/touch_controls_common.cpp - src/dusk/ui/touch_controls_common.hpp src/dusk/ui/touch_controls.cpp src/dusk/ui/touch_controls.hpp + src/dusk/ui/touch_controls_common.cpp + src/dusk/ui/touch_controls_common.hpp src/dusk/ui/touch_controls_editor.cpp src/dusk/ui/touch_controls_editor.hpp src/dusk/ui/ui.cpp @@ -1552,7 +1585,6 @@ set(DUSK_FILES src/dusk/achievements.cpp src/dusk/iso_validate.cpp src/dusk/livesplit.cpp - src/dusk/offset_ptr.cpp src/dusk/OSContext.cpp src/dusk/OSReport.cpp src/dusk/OSThread.cpp @@ -1590,6 +1622,9 @@ set(DUSK_FILES src/dusk/discord_presence.cpp src/dusk/version.cpp src/dusk/action_bindings.cpp + src/dusk/update_check.cpp + src/dusk/update_check.hpp + src/dusk/version.cpp src/dusk/cosmetics/color_utils.hpp src/dusk/cosmetics/color_utils.cpp src/dusk/cosmetics/midna_hair_color.hpp @@ -1684,6 +1719,10 @@ set(DUSK_FILES src/dusk/randomizer/generator/utility/time.cpp src/dusk/randomizer/generator/utility/time.hpp src/dusk/randomizer/generator/utility/yaml.hpp + src/helpers/batch.cpp + src/helpers/endian.cpp + src/helpers/offset_ptr.cpp + src/helpers/string.cpp ) set(DUSK_HTTP_BACKEND_FILES diff --git a/include/SSystem/SComponent/c_bg_s_chk.h b/include/SSystem/SComponent/c_bg_s_chk.h index adbc0c1807..6908ef80ed 100644 --- a/include/SSystem/SComponent/c_bg_s_chk.h +++ b/include/SSystem/SComponent/c_bg_s_chk.h @@ -5,7 +5,7 @@ #include "f_pc/f_pc_base.h" #include "SSystem/SComponent/c_bg_s_grp_pass_chk.h" #include "SSystem/SComponent/c_bg_s_poly_pass_chk.h" -#include "dusk/endian.h" +#include "helpers/endian.h" struct cBgD_Vtx_t : public Vec {}; diff --git a/include/SSystem/SComponent/c_m3d_g_tri.h b/include/SSystem/SComponent/c_m3d_g_tri.h index cac178e064..9947616d54 100644 --- a/include/SSystem/SComponent/c_m3d_g_tri.h +++ b/include/SSystem/SComponent/c_m3d_g_tri.h @@ -2,7 +2,7 @@ #define C_M3D_G_TRI_H_ #include "SSystem/SComponent/c_m3d_g_pla.h" -#include "dusk/endian.h" +#include "helpers/endian.h" class cM3dGCyl; diff --git a/include/d/actor/d_a_movie_player.h b/include/d/actor/d_a_movie_player.h index 9280c849d3..fbd8784d40 100644 --- a/include/d/actor/d_a_movie_player.h +++ b/include/d/actor/d_a_movie_player.h @@ -95,12 +95,6 @@ static void __THPAudioInitialize(THPAudioDecodeInfo* info, u8* ptr); #define THP_TEXTURE_SET_COUNT 3 #endif -#if TARGET_PC -namespace dusk { - void MoviePlayerShutdown(); -} -#endif - struct daMP_THPPlayer { /* 0x000 */ DVDFileInfo fileInfo; /* 0x03C */ THPHeader header; diff --git a/include/d/actor/d_flower.h b/include/d/actor/d_flower.h index cda0845867..db8661b92f 100644 --- a/include/d/actor/d_flower.h +++ b/include/d/actor/d_flower.h @@ -5,7 +5,7 @@ #include "SSystem/SComponent/c_xyz.h" #if TARGET_PC -#include "dusk/batch.hpp" +#include "helpers/batch.hpp" #endif class cCcD_Obj; @@ -112,11 +112,11 @@ public: TGXTexObj mTexObj_l_J_Ohana00_64TEX; TGXTexObj mTexObj_l_J_Ohana01_64128_0419TEX; - dusk::batch::LeafTemplate mTplHana00; // l_J_hana00DL - dusk::batch::LeafTemplate mTplHana00Cut; // l_J_hana00_cDL - dusk::batch::LeafTemplate mTplHana01; // l_J_hana01DL - dusk::batch::LeafTemplate mTplHana01Cut00; // l_J_hana01_c_00DL - dusk::batch::LeafTemplate mTplHana01Cut; // l_J_hana01_c_01DL + batch::LeafTemplate mTplHana00; // l_J_hana00DL + batch::LeafTemplate mTplHana00Cut; // l_J_hana00_cDL + batch::LeafTemplate mTplHana01; // l_J_hana01DL + batch::LeafTemplate mTplHana01Cut00; // l_J_hana01_c_00DL + batch::LeafTemplate mTplHana01Cut; // l_J_hana01_c_01DL #endif }; // Size: 0x12A54 diff --git a/include/d/actor/d_grass.h b/include/d/actor/d_grass.h index a7ed081198..4eee08c0e8 100644 --- a/include/d/actor/d_grass.h +++ b/include/d/actor/d_grass.h @@ -5,7 +5,7 @@ #include "SSystem/SComponent/c_xyz.h" #if TARGET_PC -#include "../../../src/dusk/batch.hpp" +#include #endif class cCcD_Obj; @@ -115,9 +115,9 @@ public: TGXTexObj mTexObj_l_M_Hijiki00TEX; TGXTexObj mTexObj_l_M_kusa05_RGBATEX; - dusk::batch::LeafTemplate mTplKusa9q; // l_M_Kusa_9qDL - dusk::batch::LeafTemplate mTplKusa9qCut; // l_M_Kusa_9q_cDL - dusk::batch::LeafTemplate mTplTengusa; // l_M_TenGusaDL + batch::LeafTemplate mTplKusa9q; // l_M_Kusa_9qDL + batch::LeafTemplate mTplKusa9qCut; // l_M_Kusa_9q_cDL + batch::LeafTemplate mTplTengusa; // l_M_TenGusaDL #endif }; // Size: 0x1D718 diff --git a/include/d/d_bg_w.h b/include/d/d_bg_w.h index 580e22d904..ae37d1c6d9 100644 --- a/include/d/d_bg_w.h +++ b/include/d/d_bg_w.h @@ -6,9 +6,9 @@ #include "d/d_bg_w_base.h" #include #include -#include "dusk/offset_ptr.h" -#include "dusk/endian.h" -#include "dusk/endian_ssystem.h" +#include "helpers/offset_ptr.h" +#include "helpers/endian.h" +#include "helpers/endian_ssystem.h" class cBgS_GrpPassChk; class cBgS_PolyPassChk; diff --git a/include/d/d_bg_w_kcol.h b/include/d/d_bg_w_kcol.h index 1fc812dd98..84b0da03f2 100644 --- a/include/d/d_bg_w_kcol.h +++ b/include/d/d_bg_w_kcol.h @@ -6,7 +6,7 @@ #include "d/d_bg_plc.h" #include "d/d_bg_s_sph_chk.h" #include "d/d_bg_w_base.h" -#include "dusk/offset_ptr.h" +#include "helpers/offset_ptr.h" class cBgS_GrpPassChk; class cBgS_PolyPassChk; diff --git a/include/d/d_com_inf_game.h b/include/d/d_com_inf_game.h index 8c81343566..9fb997d21e 100644 --- a/include/d/d_com_inf_game.h +++ b/include/d/d_com_inf_game.h @@ -17,8 +17,14 @@ #include "m_Do/m_Do_graphic.h" #include -#if TARGET_PC -#include "dusk/profiling.hpp" +#if defined(DUSK_BUILDING_GAME) +#include +#include "dusk/settings.h" +#else +#ifndef ZoneScoped +#define ZoneScoped +#define ZoneScopedN(name) +#endif #endif enum dComIfG_ButtonStatus { @@ -4907,27 +4913,23 @@ inline void dComIfGd_drawXluListDark() { g_dComIfG_gameInfo.drawlist.drawXluListDark(); } +#if TARGET_PC +void dComIfGd_drawXluListInvisible(); +#else inline void dComIfGd_drawXluListInvisible() { ZoneScoped; -#ifdef TARGET_PC - if (!dusk::getSettings().game.disableWaterRefraction) { -#endif - g_dComIfG_gameInfo.drawlist.drawXluListInvisible(); -#ifdef TARGET_PC - } -#endif + g_dComIfG_gameInfo.drawlist.drawXluListInvisible(); } +#endif +#if TARGET_PC +void dComIfGd_drawOpaListInvisible(); +#else inline void dComIfGd_drawOpaListInvisible() { ZoneScoped; -#ifdef TARGET_PC - if (!dusk::getSettings().game.disableWaterRefraction) { -#endif - g_dComIfG_gameInfo.drawlist.drawOpaListInvisible(); -#ifdef TARGET_PC - } -#endif + g_dComIfG_gameInfo.drawlist.drawOpaListInvisible(); } +#endif inline void dComIfGd_drawXluListZxlu() { ZoneScoped; diff --git a/include/d/d_drawlist.h b/include/d/d_drawlist.h index 907cf7b178..25d92aff54 100644 --- a/include/d/d_drawlist.h +++ b/include/d/d_drawlist.h @@ -5,7 +5,7 @@ #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J3DGraphBase/J3DSys.h" #include "SSystem/SComponent/c_m3d_g_pla.h" -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" #include "f_op/f_op_view.h" #include "global.h" #include "m_Do/m_Do_ext.h" diff --git a/include/d/d_event_data.h b/include/d/d_event_data.h index ee9b868af4..eef55b1164 100644 --- a/include/d/d_event_data.h +++ b/include/d/d_event_data.h @@ -3,7 +3,7 @@ #include "global.h" #include "f_pc/f_pc_base.h" -#include "dusk/endian.h" +#include "helpers/endian.h" struct msg_class; diff --git a/include/d/d_msg_class.h b/include/d/d_msg_class.h index 307d48f291..5292eed5d3 100644 --- a/include/d/d_msg_class.h +++ b/include/d/d_msg_class.h @@ -4,8 +4,8 @@ #include "JSystem/JMessage/control.h" #include "JSystem/JMessage/JMessage.h" #include "SSystem/SComponent/c_xyz.h" -#include "dusk/endian.h" -#include "dusk/string.hpp" +#include "helpers/endian.h" +#include "helpers/string.hpp" #if REGION_JPN #define D_MSG_CLASS_PAGE_CNT_MAX 30 diff --git a/include/d/d_msg_flow.h b/include/d/d_msg_flow.h index 354c278dbb..98bfb029d8 100644 --- a/include/d/d_msg_flow.h +++ b/include/d/d_msg_flow.h @@ -2,7 +2,7 @@ #define D_MSG_D_MSG_FLOW_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" enum { NODETYPE_MESSAGE_e = 1, diff --git a/include/d/d_save.h b/include/d/d_save.h index 59511dc523..94fd6ea3ba 100644 --- a/include/d/d_save.h +++ b/include/d/d_save.h @@ -8,7 +8,7 @@ #include "d/d_item_data.h" #include "JSystem/JUtility/JUTAssert.h" #include "JSystem/JHostIO/JORReflexible.h" -#include "dusk/endian.h" +#include "helpers/endian.h" #if TARGET_PC #include @@ -522,7 +522,7 @@ public: #endif void setPlayerName(const char* i_name) { #if AVOID_UB - dusk::SafeStringCopyTruncate(mPlayerName, i_name); + SafeStringCopyTruncate(mPlayerName, i_name); #else strcpy(mPlayerName, i_name); #endif @@ -534,7 +534,7 @@ public: #endif void setHorseName(const char* i_name) { #if AVOID_UB - dusk::SafeStringCopyTruncate(mHorseName, i_name); + SafeStringCopyTruncate(mHorseName, i_name); #else strcpy(mHorseName, i_name); #endif diff --git a/include/d/d_stage.h b/include/d/d_stage.h index 8c19a6f693..d59da1f513 100644 --- a/include/d/d_stage.h +++ b/include/d/d_stage.h @@ -4,7 +4,7 @@ #include "SSystem/SComponent/c_lib.h" #include "d/d_kankyo.h" #include "d/d_kankyo_data.h" -#include "dusk/offset_ptr.h" +#include "helpers/offset_ptr.h" #include "f_op/f_op_actor_mng.h" #include "global.h" #include "os_report.h" diff --git a/include/d/d_tresure.h b/include/d/d_tresure.h index 37ff4d84b9..e043dd94fb 100644 --- a/include/d/d_tresure.h +++ b/include/d/d_tresure.h @@ -2,7 +2,7 @@ #define D_D_TRESURE_H #include -#include "dusk/offset_ptr.h" +#include "helpers/offset_ptr.h" class dTres_c { public: diff --git a/include/d/dolzel.h b/include/d/dolzel.h index d51eb22372..d4f53c842d 100644 --- a/include/d/dolzel.h +++ b/include/d/dolzel.h @@ -8,7 +8,7 @@ #endif #ifndef __MWERKS__ -#include "dusk/math.h" +#include "helpers/math.h" #endif #endif // dolzel.h diff --git a/include/dusk/imgui.h b/include/dusk/imgui.h deleted file mode 100644 index 5c761fb026..0000000000 --- a/include/dusk/imgui.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef _SRC_IMGUI_H_ -#define _SRC_IMGUI_H_ - -#include - -#ifdef __cplusplus -extern "C" -{ -#endif - - void imgui_main(const AuroraInfo* info); - void frame_limiter(); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/include/dusk/profiling.hpp b/include/dusk/profiling.hpp deleted file mode 100644 index 82dab45216..0000000000 --- a/include/dusk/profiling.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#if defined(__has_include) -#if __has_include() -#include -#endif -#endif - -#ifndef ZoneScoped -#define ZoneScoped -#define ZoneScopedN(name) -#endif diff --git a/include/f_op/f_op_actor_mng.h b/include/f_op/f_op_actor_mng.h index 5d51b74aa2..6461c823d1 100644 --- a/include/f_op/f_op_actor_mng.h +++ b/include/f_op/f_op_actor_mng.h @@ -12,7 +12,7 @@ #include "f_pc/f_pc_manager.h" #include "m_Do/m_Do_hostIO.h" #include "SSystem/SComponent/c_phase.h" -#include "dusk/endian_ssystem.h" +#include "helpers/endian_ssystem.h" #if !__MWERKS__ // mwerks compiler makes value initialization act like default initialization so we need diff --git a/include/helpers/README.md b/include/helpers/README.md new file mode 100644 index 0000000000..d14f1e1b36 --- /dev/null +++ b/include/helpers/README.md @@ -0,0 +1,12 @@ +# Public game helpers + +Headers in this directory provide port-specific types and utilities used by ordinary game +headers. Their corresponding implementations live in `src/helpers/`. + +These helpers **must not** depend on internal `src/dusk/` declarations in their public interface. +Unlike the internal `dusk::` namespace, they are exposed to mods that use the `game` feature +and therefore must remain ABI stable within `GameService` major versions. + +APIs _specifically_ for mod use do not belong here; instead they belong in mod services, +which are individually versioned, can provide backwards compatibility, and are designed to +keep track of per-mod runtime state. diff --git a/src/dusk/batch.hpp b/include/helpers/batch.hpp similarity index 90% rename from src/dusk/batch.hpp rename to include/helpers/batch.hpp index 2569f27761..0eaeb17a2c 100644 --- a/src/dusk/batch.hpp +++ b/include/helpers/batch.hpp @@ -2,7 +2,7 @@ #include -namespace dusk::batch { +namespace batch { struct LeafTemplate { static constexpr u32 kMaxVtx = 192; @@ -22,4 +22,4 @@ struct LeafTemplate { void decode_leaf_template(const u8* dl, u32 size, LeafTemplate& out); -} // namespace dusk +} // namespace batch diff --git a/include/dusk/endian.h b/include/helpers/endian.h similarity index 98% rename from include/dusk/endian.h rename to include/helpers/endian.h index d16d0aa80c..689f9f0dab 100644 --- a/include/dusk/endian.h +++ b/include/helpers/endian.h @@ -1,5 +1,4 @@ -#ifndef DOLPHIN_ENDIAN_H -#define DOLPHIN_ENDIAN_H +#pragma once #include @@ -292,6 +291,3 @@ inline void be_swap(Mtx& val) { #define BE(T) T #define BE_HOST(T) (T) #endif - - -#endif // DOLPHIN_ENDIAN_H diff --git a/include/dusk/endian_gx.hpp b/include/helpers/endian_gx.hpp similarity index 100% rename from include/dusk/endian_gx.hpp rename to include/helpers/endian_gx.hpp diff --git a/include/dusk/endian_ssystem.h b/include/helpers/endian_ssystem.h similarity index 93% rename from include/dusk/endian_ssystem.h rename to include/helpers/endian_ssystem.h index fe9248ffa4..b383ef6e43 100644 --- a/include/dusk/endian_ssystem.h +++ b/include/helpers/endian_ssystem.h @@ -1,5 +1,4 @@ -#ifndef _DUSK_ENDIAN_SSYSTEM_H_ -#define _DUSK_ENDIAN_SSYSTEM_H_ +#pragma once #include "SSystem/SComponent/c_sxyz.h" #include "endian.h" @@ -61,5 +60,3 @@ struct BE { }; } }; - -#endif \ No newline at end of file diff --git a/include/dusk/gx_helper.h b/include/helpers/gx_helper.h similarity index 94% rename from include/dusk/gx_helper.h rename to include/helpers/gx_helper.h index 02a36fc6db..32cf340ed5 100644 --- a/include/dusk/gx_helper.h +++ b/include/helpers/gx_helper.h @@ -1,12 +1,17 @@ -#ifndef DUSK_GX_HELPER_H -#define DUSK_GX_HELPER_H +#pragma once #include #include #include -#include "profiling.hpp" +#if defined(DUSK_BUILDING_GAME) +#include +#else +#ifndef ZoneScopedN +#define ZoneScopedN(name) +#endif +#endif #if DUSK_GFX_DEBUG_GROUPS #define GX_DEBUG_GROUP(name, ...) \ @@ -79,5 +84,3 @@ struct GXScopedDebugGroup { }; #define GX_AND_TRACY_SCOPED(name) GXScopedDebugGroup scope(name); ZoneScopedN(name); - -#endif // DUSK_GX_HELPER_H diff --git a/include/dusk/math.h b/include/helpers/math.h similarity index 84% rename from include/dusk/math.h rename to include/helpers/math.h index 04f1087670..c1e966951a 100644 --- a/include/dusk/math.h +++ b/include/helpers/math.h @@ -1,5 +1,4 @@ -#ifndef _SRC_DUSK_MATH_H_ -#define _SRC_DUSK_MATH_H_ +#pragma once #include @@ -17,5 +16,3 @@ inline float i_tanf(float x) { return tan(x); } inline float i_acosf(float x) { return acos(x); } #include - -#endif // _SRC_DUSK_MATH_H_ diff --git a/include/dusk/offset_ptr.h b/include/helpers/offset_ptr.h similarity index 89% rename from include/dusk/offset_ptr.h rename to include/helpers/offset_ptr.h index 3a940804c2..924b840659 100644 --- a/include/dusk/offset_ptr.h +++ b/include/helpers/offset_ptr.h @@ -1,5 +1,4 @@ -#ifndef DUSK_OFFSET_PTR_H -#define DUSK_OFFSET_PTR_H +#pragma once #if TARGET_PC @@ -47,21 +46,17 @@ struct OffsetPtrT { } operator T*() const { - return (T*) value; - } + return (T*)value; } - template + template explicit operator TOther*() const { - return (TOther*) value; + return (TOther*)value; } }; - #define OFFSET_PTR(T) OffsetPtrT #define OFFSET_PTR_RAW OffsetPtr #else #define OFFSET_PTR(T) T* #define OFFSET_PTR_RAW u32 #endif - -#endif // DUSK_OFFSET_PTR_H diff --git a/include/dusk/string.hpp b/include/helpers/string.hpp similarity index 89% rename from include/dusk/string.hpp rename to include/helpers/string.hpp index af91ff8fe9..b7ba0ef3b7 100644 --- a/include/dusk/string.hpp +++ b/include/helpers/string.hpp @@ -1,8 +1,7 @@ -#ifndef DUSK_STRING_HPP -#define DUSK_STRING_HPP -#include +#pragma once -namespace dusk { +#include +#include struct TextSpan { char* buffer; @@ -44,7 +43,7 @@ private: }; #if TARGET_PC -#define TEXT_SPAN dusk::TextSpan +#define TEXT_SPAN TextSpan #else #define TEXT_SPAN char* #endif @@ -111,11 +110,11 @@ int SafeStringPrintf(char (&buffer)[BufSize], const char* format, ...) { } #if TARGET_PC -#define SAFE_STRCPY dusk::SafeStringCopy -#define SAFE_STRCAT dusk::SafeStringCat -#define SAFE_SPRINTF dusk::SafeStringPrintf -#define SAFE_STRCPY_BOUNDED dusk::SafeStringCopy -#define SAFE_STRCAT_BOUNDED dusk::SafeStringCat +#define SAFE_STRCPY SafeStringCopy +#define SAFE_STRCAT SafeStringCat +#define SAFE_SPRINTF SafeStringPrintf +#define SAFE_STRCPY_BOUNDED SafeStringCopy +#define SAFE_STRCAT_BOUNDED SafeStringCat #else #define SAFE_STRCPY strcpy #define SAFE_STRCAT strcat @@ -123,6 +122,3 @@ int SafeStringPrintf(char (&buffer)[BufSize], const char* format, ...) { #define SAFE_STRCPY_BOUNDED strcpy #define SAFE_STRCPY_BOUNDED strcat #endif -} - -#endif // DUSK_STRING_HPP diff --git a/include/m_Do/m_Do_audio.h b/include/m_Do/m_Do_audio.h index a2fcaca49a..769ad79860 100644 --- a/include/m_Do/m_Do_audio.h +++ b/include/m_Do/m_Do_audio.h @@ -4,8 +4,12 @@ #include "Z2AudioLib/Z2AudioMgr.h" #include "Z2AudioLib/Z2EnvSeMgr.h" #include "Z2AudioLib/Z2LinkMgr.h" +#if defined(DUSK_BUILDING_GAME) #include "dusk/audio.h" #include "dusk/settings.h" +#else +#define DUSK_AUDIO_SKIP(...) +#endif class mDoAud_zelAudio_c : public Z2AudioMgr { public: @@ -134,15 +138,7 @@ inline void mDoAud_seStart(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_re } #if TARGET_PC -inline void mDoAud_seStartMenu(u32 i_sfxID) { - if (!mDoAud_zelAudio_c::isInitFlag()) { - return; - } - if (!dusk::getSettings().audio.menuSounds.getValue()) { - return; - } - mDoAud_seStart(i_sfxID, nullptr, 0, 0); -} +void mDoAud_seStartMenu(u32 i_sfxID); #endif inline void mDoAud_seStartLevel(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_reverb) { diff --git a/include/m_Do/m_Do_controller_pad.h b/include/m_Do/m_Do_controller_pad.h index 3ee09cc728..f63c1c8d1c 100644 --- a/include/m_Do/m_Do_controller_pad.h +++ b/include/m_Do/m_Do_controller_pad.h @@ -3,7 +3,10 @@ #include "JSystem/JUtility/JUTGamePad.h" #include "SSystem/SComponent/c_API_controller_pad.h" + +#if defined(DUSK_BUILDING_GAME) #include "dusk/settings.h" +#endif // Controller Ports 1 - 4 enum { PAD_1, PAD_2, PAD_3, PAD_4 }; @@ -54,29 +57,21 @@ public: static f32 getStickValue(u32 pad) { return getCpadInfo(pad).mMainStickValue; } static s16 getStickAngle(u32 pad) { return getCpadInfo(pad).mMainStickAngle; } +#if TARGET_PC + static s16 getStickAngle3D(u32 pad); +#else static s16 getStickAngle3D(u32 pad) { - #if TARGET_PC - if (dusk::getSettings().game.enableMirrorMode) { - return -getCpadInfo(pad).mMainStickAngle; - } else { - return getCpadInfo(pad).mMainStickAngle; - } - #else return getCpadInfo(pad).mMainStickAngle; - #endif } +#endif +#if TARGET_PC + static f32 getSubStickX3D(u32 pad); +#else static f32 getSubStickX3D(u32 pad) { - #if TARGET_PC - if (dusk::getSettings().game.enableMirrorMode) { - return -getCpadInfo(pad).mCStickPosX; - } else { - return getCpadInfo(pad).mCStickPosX; - } - #else return getCpadInfo(pad).mCStickPosX; - #endif } +#endif static f32 getSubStickX(u32 pad) { return getCpadInfo(pad).mCStickPosX; } static f32 getSubStickY(u32 pad) { return getCpadInfo(pad).mCStickPosY; } diff --git a/include/m_Do/m_Do_lib.h b/include/m_Do/m_Do_lib.h index db3a1dbea4..6b85cc0660 100644 --- a/include/m_Do/m_Do_lib.h +++ b/include/m_Do/m_Do_lib.h @@ -8,7 +8,7 @@ #include "JSystem/JGeometry.h" #endif -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" typedef struct Vec Vec; struct ResTIMG; diff --git a/include/m_Do/m_Do_mtx.h b/include/m_Do/m_Do_mtx.h index dcd5cd10c7..89e7e8b7cf 100644 --- a/include/m_Do/m_Do_mtx.h +++ b/include/m_Do/m_Do_mtx.h @@ -6,7 +6,7 @@ #include #include "JSystem/JMath/JMath.h" -#include "dusk/endian.h" +#include "helpers/endian.h" extern u8 g_printCurrentHeapDebug; DUSK_GAME_EXTERN u8 g_printOtherHeapDebug; diff --git a/include/m_Do/m_Do_printf.h b/include/m_Do/m_Do_printf.h index 527be3d58a..a71669f432 100644 --- a/include/m_Do/m_Do_printf.h +++ b/include/m_Do/m_Do_printf.h @@ -2,7 +2,7 @@ #define M_DO_M_DO_PRINTF_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" void my_PutString(const char*); void mDoPrintf_vprintf_Interrupt(char const*, va_list); diff --git a/include/mods/hook.hpp b/include/mods/hook.hpp deleted file mode 100644 index c58bd719c9..0000000000 --- a/include/mods/hook.hpp +++ /dev/null @@ -1,464 +0,0 @@ -#pragma once - -#include "mods/svc/hook.h" - -#include -#include -#include -#include -#include -#include - -namespace dusk::mods { - -template -T arg(void* argsRaw, int n) noexcept { - void** args = static_cast(argsRaw); - return *static_cast > >(args[n]); -} - -template -std::remove_reference_t& arg_ref(void* argsRaw, int n) noexcept { - void** args = static_cast(argsRaw); - return *static_cast > >(args[n]); -} - -template -void* mfp_addr(F fn) noexcept { - void* p = nullptr; - static_assert(sizeof(fn) >= sizeof(void*), "unexpected function pointer size"); - std::memcpy(&p, &fn, sizeof(void*)); - return p; -} - -/* A string usable as a template argument: carries the hook target's symbol name and - * makes each NamedHook instantiation's static state unique. */ -template -struct FixedString { - char chars[N]{}; - constexpr FixedString(const char (&s)[N]) noexcept { - for (size_t i = 0; i < N; ++i) { - chars[i] = s[i]; - } - } -}; - -namespace detail { - -template -constexpr std::string_view class_name() { -#if defined(__clang__) || defined(__GNUC__) - // "... class_name() [T = daAlink_c]" / "... [with T = daAlink_c; ...]" - constexpr std::string_view fn = __PRETTY_FUNCTION__; - constexpr size_t start = fn.find("T = ") + 4; - return fn.substr(start, fn.find_first_of(";]", start) - start); -#elif defined(_MSC_VER) - // "... class_name(void)" - constexpr std::string_view fn = __FUNCSIG__; - constexpr size_t start = fn.find("class_name<") + 11; - constexpr std::string_view name = fn.substr(start, fn.rfind(">(") - start); - if constexpr (name.starts_with("class ")) { - return name.substr(6); - } else if constexpr (name.starts_with("struct ")) { - return name.substr(7); - } else { - return name; - } -#else -#error "unsupported compiler" -#endif -} - -/* The manifest name of C's vtable. Only unscoped, non-template class names are - * supported (an empty result fails resolution and the install reports it). */ -template -constexpr auto vtable_symbol() { - constexpr std::string_view name = class_name(); - constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos; - // "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated - std::array out{}; - if constexpr (!simple) { - return out; - } - size_t n = 0; -#if defined(_WIN32) - for (char c : {'?', '?', '_', '7'}) { - out[n++] = c; - } - for (char c : name) { - out[n++] = c; - } - for (char c : {'@', '@', '6', 'B', '@'}) { - out[n++] = c; - } -#else - for (char c : {'_', 'Z', 'T', 'V'}) { - out[n++] = c; - } - size_t len = name.size(); - char digits[8]{}; - size_t d = 0; - while (len != 0) { - digits[d++] = static_cast('0' + len % 10); - len /= 10; - } - while (d != 0) { - out[n++] = digits[--d]; - } - for (char c : name) { - out[n++] = c; - } -#endif - return out; -} - -#if defined(_WIN32) -/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at. - * Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */ -inline size_t vcall_slot_offset(const void*& fn) noexcept { - constexpr size_t npos = static_cast(-1); -#if defined(_M_X64) || defined(__x86_64__) - const auto* p = static_cast(fn); - for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs - int32_t rel; - std::memcpy(&rel, p + 1, 4); - p += 5 + rel; - } - fn = p; - // The vptr load. Unoptimized clang-cl thunks spill/reload rcx first - // (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window. - const uint8_t* q = nullptr; - for (int i = 0; i <= 12; ++i) { - if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx] - q = p + i + 3; - break; - } - } - if (q == nullptr) { - return npos; - } - if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC) - return 0; - } - if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8] - return static_cast(q[2]); - } - if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32] - int32_t off; - std::memcpy(&off, q + 2, 4); - return off; - } - // clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax - // distinguishes the thunk from an ordinary getter that begins the same way. - if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) { - size_t off = 0; - const uint8_t* r = q + 3; - if (q[2] == 0x40) { - off = static_cast(q[3]); - r = q + 4; - } else if (q[2] == 0x80) { - int32_t off32; - std::memcpy(&off32, q + 3, 4); - off = off32; - r = q + 7; - } - for (int i = 0; i <= 8; ++i) { - if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional) - return off; - } - } - } - return npos; -#elif defined(_M_ARM64) || defined(__aarch64__) - const auto* p = static_cast(fn); - uint32_t insn[3]; - for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs - std::memcpy(insn, p, 4); - if ((insn[0] & 0xFC000000u) != 0x14000000u) { - break; - } - const auto imm26 = static_cast(insn[0] << 6) >> 6; - p += static_cast(imm26) * 4; - } - fn = p; - std::memcpy(insn, p, 12); - // ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs - if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) { - return npos; - } - const uint32_t t = insn[0] & 0x1Fu; - if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) { - return npos; - } - const uint32_t s = insn[1] & 0x1Fu; - if (insn[2] != (0xD61F0000u | (s << 5))) { - return npos; - } - return ((insn[1] >> 10) & 0xFFFu) * 8; -#else - (void)fn; - return npos; -#endif -} -#endif - -/* Code address of the member function a mfp designates. Virtual mfps don't carry - * one; recover it from the class's vtable (resolved from the symbol manifest), so - * Hook works uniformly on virtual and non-virtual members. */ -template -ModResult member_target(const HookService* hooks, F mfp, void** out) { - *out = nullptr; - uintptr_t words[sizeof(F) > sizeof(uintptr_t) ? 2 : 1] = {}; - std::memcpy(words, &mfp, sizeof(words) < sizeof(F) ? sizeof(words) : sizeof(F)); - -#if defined(_WIN32) - const void* fn = reinterpret_cast(words[0]); - const size_t slot = vcall_slot_offset(fn); - if (slot == static_cast(-1)) { // not a vcall thunk: direct address - *out = const_cast(fn); - return MOD_OK; - } - void* vtable = nullptr; - const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol().data(), &vtable, nullptr); - if (resolved != MOD_OK) { - return resolved; - } - // ??_7 points at the first slot. - *out = *reinterpret_cast(static_cast(vtable) + slot); -#else -#if defined(__aarch64__) || defined(__arm__) - // AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function - // addresses can't spare their low bit), and ptr holds the slot offset directly. - const bool isVirtual = (words[1] & 1) != 0; - const uintptr_t thisAdjust = words[1] >> 1; - const uintptr_t slotOffset = words[0]; -#else - // Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1. - const bool isVirtual = (words[0] & 1) != 0; - const uintptr_t thisAdjust = words[1]; - const uintptr_t slotOffset = words[0] - 1; -#endif - if (!isVirtual) { // non-virtual: the address itself - *out = reinterpret_cast(words[0]); - return MOD_OK; - } - if (thisAdjust != 0) { - // this-adjusting mfp (member of a secondary base): the slot offset is - // relative to a vtable we can't locate. Hook the overrider by name instead. - return MOD_UNSUPPORTED; - } - void* vtable = nullptr; - const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol().data(), &vtable, nullptr); - if (resolved != MOD_OK) { - return resolved; - } - // _ZTV points at the offset-to-top slot; the address point mfps index from is - // two pointers in (past offset-to-top and the typeinfo pointer). - *out = *reinterpret_cast(static_cast(vtable) + 2 * sizeof(void*) + slotOffset); -#endif - return *out != nullptr ? MOD_OK : MOD_UNAVAILABLE; -} - -} // namespace detail - -/* Trampoline generator + per-target state shared by Hook and NamedHook. Tag makes - * each hooked target's statics distinct; the target address is filled in at install. */ -template -struct HookImpl { - static inline R (*g_orig)(A...) = nullptr; - static inline const HookService* hooks = nullptr; - static inline void* target = nullptr; - - static bool dispatch_pre(void* args, void* retval) { - if (hooks == nullptr) { - return false; - } - - int skipOriginal = 0; - const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal); - return result == MOD_OK && skipOriginal != 0; - } - - static void dispatch_post(void* args, void* retval) { - if (hooks != nullptr) { - hooks->dispatch_post(mod_ctx, target, args, retval); - } - } - - static R trampoline(A... args) { - if constexpr (sizeof...(A) == 0) { - if constexpr (std::is_void_v) { - const bool skipOriginal = dispatch_pre(nullptr, nullptr); - if (!skipOriginal) { - g_orig(args...); - } - dispatch_post(nullptr, nullptr); - } else { - R result{}; - const bool skipOriginal = - dispatch_pre(nullptr, static_cast(std::addressof(result))); - if (!skipOriginal) { - result = g_orig(args...); - } - dispatch_post(nullptr, static_cast(std::addressof(result))); - return result; - } - } else { - void* ptrs[] = {static_cast(std::addressof(args))...}; - if constexpr (std::is_void_v) { - const bool skipOriginal = dispatch_pre(static_cast(ptrs), nullptr); - if (!skipOriginal) { - g_orig(args...); - } - dispatch_post(static_cast(ptrs), nullptr); - } else { - R result{}; - const bool skipOriginal = dispatch_pre( - static_cast(ptrs), static_cast(std::addressof(result))); - if (!skipOriginal) { - result = g_orig(args...); - } - dispatch_post(static_cast(ptrs), static_cast(std::addressof(result))); - return result; - } - } - } -}; - -namespace detail { -template -using TargetTag = std::integral_constant; -template -struct NameTag {}; -} // namespace detail - -/* - * Typed hook on a function named at compile time (&daAlink_c::execute, &free_fn). - * Member functions may be virtual: the install decodes the member function pointer and hooks the - * class's own overrider. - */ -template -struct Hook; - -template -struct Hook : HookImpl, R, C*, A...> { - static ModResult resolve_target(const HookService* hooks, void** out) { - return detail::member_target(hooks, Target, out); - } -}; - -template -struct Hook : HookImpl, R, const C*, A...> { - static ModResult resolve_target(const HookService* hooks, void** out) { - return detail::member_target(hooks, Target, out); - } -}; - -template -struct Hook : HookImpl, R, A...> { - static ModResult resolve_target(const HookService*, void** out) { - *out = mfp_addr(Target); - return MOD_OK; - } -}; - -/* - * Typed hook on a function by its symbol name, for targets you can't name in C++: file-local - * statics, private members, or symbols without a header. The signature is written free-style with - * the receiver first and is *not* compiler-checked. - * - * using HookshotHit = dusk::mods::NamedHook< - * "daAlink_hookshotAtHitCallBack", - * void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>; - * dusk::mods::hook_add_pre(svc_hook, on_hookshot_hit); - */ -template -struct NamedHook; - -template -struct NamedHook : HookImpl, R, A...> { - static ModResult resolve_target(const HookService* hooks, void** out) { - HookSymbolFlags flags{}; - const ModResult resolved = hooks->resolve(mod_ctx, Name.chars, out, &flags); - if (resolved == MOD_OK && (flags & HOOK_SYMBOL_CODE) == 0) { - *out = nullptr; - return MOD_INVALID_ARGUMENT; - } - return resolved; - } -}; - -template -ModResult hook_install(const HookService* hooks) { - if (hooks == nullptr) { - return MOD_UNAVAILABLE; - } - - Entry::hooks = hooks; - if (Entry::target == nullptr) { - const ModResult resolved = Entry::resolve_target(hooks, &Entry::target); - if (resolved != MOD_OK) { - return resolved; - } - } - return hooks->install(mod_ctx, Entry::target, reinterpret_cast(Entry::trampoline), - reinterpret_cast(&Entry::g_orig)); -} - -template -ModResult hook_install(const HookService* hooks) { - return hook_install >(hooks); -} - -template -ModResult hook_add_pre( - const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { - const ModResult installed = hook_install(hooks); - if (installed != MOD_OK) { - return installed; - } - - return hooks->add_pre(mod_ctx, Entry::target, callback, options); -} - -template -ModResult hook_add_pre( - const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { - return hook_add_pre >(hooks, callback, options); -} - -template -ModResult hook_add_post( - const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { - const ModResult installed = hook_install(hooks); - if (installed != MOD_OK) { - return installed; - } - - return hooks->add_post(mod_ctx, Entry::target, callback, options); -} - -template -ModResult hook_add_post( - const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { - return hook_add_post >(hooks, callback, options); -} - -template -ModResult hook_replace( - const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { - const ModResult installed = hook_install(hooks); - if (installed != MOD_OK) { - return installed; - } - - return hooks->replace(mod_ctx, Entry::target, callback, options); -} - -template -ModResult hook_replace( - const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { - return hook_replace >(hooks, callback, options); -} - -} // namespace dusk::mods diff --git a/include/os_report.h b/include/os_report.h index c622b172b3..0831e45396 100644 --- a/include/os_report.h +++ b/include/os_report.h @@ -31,10 +31,4 @@ extern u8 __OSReport_Warning_disable; extern u8 __OSReport_System_disable; extern u8 __OSReport_enable; -#if TARGET_PC -namespace dusk { - DUSK_GAME_EXTERN bool OSReportReallyForceEnable; -} -#endif - #endif // _OS_REPORT_H diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DMaterialFactory.h b/libs/JSystem/include/JSystem/J2DGraph/J2DMaterialFactory.h index f82788d57c..1eddcb943a 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DMaterialFactory.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DMaterialFactory.h @@ -4,7 +4,7 @@ #include "JSystem/J2DGraph/J2DManage.h" #include "JSystem/J2DGraph/J2DMatBlock.h" -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-j2d diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DPane.h b/libs/JSystem/include/JSystem/J2DGraph/J2DPane.h index 510381ae10..a588b4e7f7 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DPane.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DPane.h @@ -5,7 +5,7 @@ #include "JSystem/JSupport/JSUList.h" #include #include -#include "dusk/endian.h" +#include "helpers/endian.h" class J2DAnmBase; class J2DAnmColor; diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DPicture.h b/libs/JSystem/include/JSystem/J2DGraph/J2DPicture.h index f738a96dd4..f311e191fa 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DPicture.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DPicture.h @@ -4,7 +4,7 @@ #include "JSystem/J2DGraph/J2DPane.h" #include "JSystem/JUtility/JUTTexture.h" #include "JSystem/JUtility/TColor.h" -#include "dusk/endian.h" +#include "helpers/endian.h" class J2DMaterial; class JUTPalette; diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DScreen.h b/libs/JSystem/include/JSystem/J2DGraph/J2DScreen.h index 9244198475..ffc7568846 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DScreen.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DScreen.h @@ -4,7 +4,7 @@ #include "JSystem/J2DGraph/J2DManage.h" #include "JSystem/J2DGraph/J2DPane.h" #include "JSystem/JUtility/TColor.h" -#include "dusk/endian.h" +#include "helpers/endian.h" class J2DMaterial; class JUTNameTab; diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h b/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h index 368325ff81..e515321188 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h @@ -4,7 +4,7 @@ #include #include #include "global.h" -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-j2d diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DTextBox.h b/libs/JSystem/include/JSystem/J2DGraph/J2DTextBox.h index 46495fac1b..bbbf2592bb 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DTextBox.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DTextBox.h @@ -3,8 +3,8 @@ #include "JSystem/J2DGraph/J2DMaterial.h" #include "JSystem/J2DGraph/J2DPane.h" -#include "dusk/endian.h" -#include "dusk/string.hpp" +#include "helpers/endian.h" +#include "helpers/string.hpp" class J2DMaterial; class JUTFont; @@ -100,7 +100,7 @@ public: J2DTextBoxVBinding); void private_readStream(J2DPane*, JSURandomInputStream*, JKRArchive*); TEXT_SPAN getStringPtr() const; - dusk::TextSpan getSpan() const; + TextSpan getSpan() const; s32 setString(s16, char const*, ...); s32 setString(char const*, ...); diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DAnimation.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DAnimation.h index 3c5133e880..f006183d07 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DAnimation.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DAnimation.h @@ -6,7 +6,7 @@ #include #include "global.h" -#include "dusk/endian.h" +#include "helpers/endian.h" #if TARGET_PC #define OFFSET_PTR_V0 BE(u32) diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DCluster.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DCluster.h index 2a71ada3dd..4704fe1ee1 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DCluster.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DCluster.h @@ -3,7 +3,7 @@ #include "JSystem/J3DAssert.h" #include "JSystem/J3DGraphLoader/J3DClusterLoader.h" -#include "dusk/endian.h" +#include "helpers/endian.h" class J3DDeformer; class J3DClusterKey; diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DJointTree.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DJointTree.h index 92b252fb7a..e4018f1ea2 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DJointTree.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DJointTree.h @@ -3,7 +3,7 @@ #include "JSystem/J3DAssert.h" #include "JSystem/J3DGraphBase/J3DTransform.h" -#include "dusk/endian.h" +#include "helpers/endian.h" class J3DJoint; class J3DMtxBuffer; diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h index 2e5b3bd38d..d55bb82465 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h @@ -3,7 +3,6 @@ #include "JSystem/J3DGraphAnimator/J3DSkinDeform.h" #include "JSystem/J3DGraphBase/J3DPacket.h" -#include "dusk/frame_interpolation.h" #include enum J3DMdlFlag { @@ -106,12 +105,13 @@ public: void setUserArea(uintptr_t area) { mUserArea = area; } uintptr_t getUserArea() const { return mUserArea; } Vec* getBaseScale() { return &mBaseScale; } +#if TARGET_PC + void setAnmMtx(int jointNo, Mtx m); +#else void setAnmMtx(int jointNo, Mtx m) { mMtxBuffer->setAnmMtx(jointNo, m); -#ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mMtxBuffer->getAnmMtx(jointNo)); -#endif } +#endif MtxP getAnmMtx(int jointNo) { return mMtxBuffer->getAnmMtx(jointNo); } MtxP getWeightAnmMtx(int i) { return mMtxBuffer->getWeightAnmMtx(i); } J3DSkinDeform* getSkinDeform() { return mSkinDeform; } diff --git a/libs/JSystem/include/JSystem/J3DGraphBase/J3DShape.h b/libs/JSystem/include/JSystem/J3DGraphBase/J3DShape.h index ac9d4558bc..4ba3cc8023 100644 --- a/libs/JSystem/include/JSystem/J3DGraphBase/J3DShape.h +++ b/libs/JSystem/include/JSystem/J3DGraphBase/J3DShape.h @@ -7,7 +7,7 @@ #include "JSystem/JMath/JMath.h" #include "global.h" #include -#include "dusk/endian_gx.hpp" +#include "helpers/endian_gx.hpp" class J3DShapeMtx; diff --git a/libs/JSystem/include/JSystem/J3DGraphBase/J3DShapeMtx.h b/libs/JSystem/include/JSystem/J3DGraphBase/J3DShapeMtx.h index 9edd5e6898..2b7ad8ad35 100644 --- a/libs/JSystem/include/JSystem/J3DGraphBase/J3DShapeMtx.h +++ b/libs/JSystem/include/JSystem/J3DGraphBase/J3DShapeMtx.h @@ -4,7 +4,7 @@ #include "JSystem/J3DGraphBase/J3DShape.h" #include "JSystem/J3DAssert.h" #include -#include "dusk/endian.h" +#include "helpers/endian.h" class J3DTexMtx; class J3DTexGenBlock; diff --git a/libs/JSystem/include/JSystem/J3DGraphBase/J3DStruct.h b/libs/JSystem/include/JSystem/J3DGraphBase/J3DStruct.h index 05663fd84e..0de93bcaf5 100644 --- a/libs/JSystem/include/JSystem/J3DGraphBase/J3DStruct.h +++ b/libs/JSystem/include/JSystem/J3DGraphBase/J3DStruct.h @@ -7,7 +7,7 @@ #include "global.h" #include "JSystem/JMath/JMath.h" -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-j3d diff --git a/libs/JSystem/include/JSystem/J3DGraphBase/J3DSys.h b/libs/JSystem/include/JSystem/J3DGraphBase/J3DSys.h index b97be2ec03..a4750e5d29 100644 --- a/libs/JSystem/include/JSystem/J3DGraphBase/J3DSys.h +++ b/libs/JSystem/include/JSystem/J3DGraphBase/J3DSys.h @@ -6,8 +6,7 @@ #include "JSystem/J3DAssert.h" #include "JSystem/JMath/JMath.h" -#include "dusk/frame_interpolation.h" -#include "dusk/endian.h" +#include "helpers/endian.h" #include "global.h" enum J3DSysDrawBuf { @@ -190,15 +189,13 @@ struct J3DSys { Mtx& getModelDrawMtx(u16 no) { return mModelDrawMtx[no]; } J3DShapePacket* getShapePacket() { return mShapePacket; } +#if TARGET_PC + void setViewMtx(const Mtx m); +#else void setViewMtx(const Mtx m) { -#ifdef TARGET_PC - Mtx patched; - if (dusk::frame_interp::lookup_replacement(m, patched)) { - m = patched; - } -#endif MTXCopy(m, mViewMtx); } +#endif J3DModel* getModel() { return mModel; } diff --git a/libs/JSystem/include/JSystem/J3DGraphBase/J3DTexture.h b/libs/JSystem/include/JSystem/J3DGraphBase/J3DTexture.h index 6e95222b6d..5b05b08bcc 100644 --- a/libs/JSystem/include/JSystem/J3DGraphBase/J3DTexture.h +++ b/libs/JSystem/include/JSystem/J3DGraphBase/J3DTexture.h @@ -7,7 +7,7 @@ #include "global.h" #include -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" /** * @ingroup jsystem-j3d diff --git a/libs/JSystem/include/JSystem/J3DGraphBase/J3DVertex.h b/libs/JSystem/include/JSystem/J3DGraphBase/J3DVertex.h index 1638e316e9..f7ad92afb8 100644 --- a/libs/JSystem/include/JSystem/J3DGraphBase/J3DVertex.h +++ b/libs/JSystem/include/JSystem/J3DGraphBase/J3DVertex.h @@ -3,7 +3,7 @@ #include #include -#include "dusk/endian_gx.hpp" +#include "helpers/endian_gx.hpp" class J3DModel; class J3DAnmVtxColor; diff --git a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DClusterLoader.h b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DClusterLoader.h index 2899065150..75ba644be8 100644 --- a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DClusterLoader.h +++ b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DClusterLoader.h @@ -4,7 +4,7 @@ #include "JSystem/J3DGraphAnimator/J3DAnimation.h" #include "JSystem/J3DGraphAnimator/J3DAnimation.h" -#include "dusk/endian.h" +#include "helpers/endian.h" #if TARGET_PC #define OFFSET_PTR_V0 BE(u32) diff --git a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DModelLoader.h b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DModelLoader.h index 6c856f1a3d..05fc99785b 100644 --- a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DModelLoader.h +++ b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DModelLoader.h @@ -4,7 +4,7 @@ #include "JSystem/J3DGraphBase/J3DSys.h" #include -#include "dusk/endian.h" +#include "helpers/endian.h" class J3DModelData; class J3DMaterialTable; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISound.h b/libs/JSystem/include/JSystem/JAudio2/JAISound.h index e96053fc9f..fa262a7426 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAISound.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAISound.h @@ -5,7 +5,7 @@ #include "JSystem/JAudio2/JAIAudible.h" #include "JSystem/JUtility/JUTAssert.h" #include "global.h" -#include "dusk/endian.h" +#include "helpers/endian.h" #include class JAISound; diff --git a/libs/JSystem/include/JSystem/JAudio2/JASAramStream.h b/libs/JSystem/include/JSystem/JAudio2/JASAramStream.h index 5128b9186a..46a30d2616 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASAramStream.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASAramStream.h @@ -4,7 +4,7 @@ #include "JSystem/JAudio2/JASTaskThread.h" #include "JSystem/JUtility/JUTAssert.h" #include -#include "dusk/endian.h" +#include "helpers/endian.h" class JASChannel; diff --git a/libs/JSystem/include/JSystem/JAudio2/JASOscillator.h b/libs/JSystem/include/JSystem/JAudio2/JASOscillator.h index 438bf9c70f..8ac20ce1ca 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASOscillator.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASOscillator.h @@ -2,7 +2,7 @@ #define JASOSCILLATOR_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-jaudio diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h b/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h index 44d4137d9b..cc5653fe25 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUAudibleParam.h @@ -2,7 +2,7 @@ #define JAUAUDIBLEPARAM_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-jaudio diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUAudioArcInterpreter.h b/libs/JSystem/include/JSystem/JAudio2/JAUAudioArcInterpreter.h index c36ad44b22..33a31adb9f 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUAudioArcInterpreter.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUAudioArcInterpreter.h @@ -2,7 +2,7 @@ #define JAUAUDIOARCINTERPRETER_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-jaudio diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUSoundAnimator.h b/libs/JSystem/include/JSystem/JAudio2/JAUSoundAnimator.h index e81cd70ee4..f590815af5 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUSoundAnimator.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUSoundAnimator.h @@ -2,7 +2,7 @@ #define JAUSOUNDANIMATOR_H #include "JSystem/JAudio2/JAISound.h" -#include "dusk/offset_ptr.h" +#include "helpers/offset_ptr.h" class JAUSoundAnimation; diff --git a/libs/JSystem/include/JSystem/JAudio2/JAUSoundTable.h b/libs/JSystem/include/JSystem/JAudio2/JAUSoundTable.h index 4cc48f2a0b..fbd3427649 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JAUSoundTable.h +++ b/libs/JSystem/include/JSystem/JAudio2/JAUSoundTable.h @@ -3,7 +3,7 @@ #include "JSystem/JAudio2/JAISound.h" #include "JSystem/JAudio2/JASGadget.h" -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-jaudio diff --git a/libs/JSystem/include/JSystem/JGadget/binary.h b/libs/JSystem/include/JSystem/JGadget/binary.h index f93e548f88..eb0ddfb1d0 100644 --- a/libs/JSystem/include/JSystem/JGadget/binary.h +++ b/libs/JSystem/include/JSystem/JGadget/binary.h @@ -3,7 +3,7 @@ #include "JSystem/JUtility/JUTAssert.h" #include "JSystem/JGadget/search.h" -#include "dusk/endian.h" +#include "helpers/endian.h" namespace JGadget { namespace binary { diff --git a/libs/JSystem/include/JSystem/JHostIO/JHICommonMem.h b/libs/JSystem/include/JSystem/JHostIO/JHICommonMem.h index 3d8c452c72..e5a3e11553 100644 --- a/libs/JSystem/include/JSystem/JHostIO/JHICommonMem.h +++ b/libs/JSystem/include/JSystem/JHostIO/JHICommonMem.h @@ -2,7 +2,7 @@ #define JHICOMMONMEM_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" inline u32 JHIhtonl(u32 v) { return BSWAP32(v); diff --git a/libs/JSystem/include/JSystem/JKernel/JKRArchive.h b/libs/JSystem/include/JSystem/JKernel/JKRArchive.h index 40fff2dd6d..4ac5de7a46 100644 --- a/libs/JSystem/include/JSystem/JKernel/JKRArchive.h +++ b/libs/JSystem/include/JSystem/JKernel/JKRArchive.h @@ -4,7 +4,7 @@ #include "JSystem/JKernel/JKRCompression.h" #include "JSystem/JKernel/JKRFileLoader.h" #include "global.h" -#include "dusk/endian.h" +#include "helpers/endian.h" class JKRHeap; diff --git a/libs/JSystem/include/JSystem/JMath/JMath.h b/libs/JSystem/include/JSystem/JMath/JMath.h index 5d0263d669..8a7efc3f32 100644 --- a/libs/JSystem/include/JSystem/JMath/JMath.h +++ b/libs/JSystem/include/JSystem/JMath/JMath.h @@ -4,7 +4,7 @@ #include #include -#include "dusk/math.h" +#include "helpers/math.h" typedef f32 Mtx33[3][3]; typedef f32 Mtx23[2][3]; diff --git a/libs/JSystem/include/JSystem/JMessage/JMessage.h b/libs/JSystem/include/JSystem/JMessage/JMessage.h index 0b3d1c64c8..7218c7f4a9 100644 --- a/libs/JSystem/include/JSystem/JMessage/JMessage.h +++ b/libs/JSystem/include/JSystem/JMessage/JMessage.h @@ -6,7 +6,7 @@ #else #include #endif -#include "dusk/endian.h" +#include "helpers/endian.h" // Struct definitions might be wrong typedef struct bmg_header_t { diff --git a/libs/JSystem/include/JSystem/JParticle/JPADynamicsBlock.h b/libs/JSystem/include/JSystem/JParticle/JPADynamicsBlock.h index c69ddc07cb..eb08e4ff9f 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPADynamicsBlock.h +++ b/libs/JSystem/include/JSystem/JParticle/JPADynamicsBlock.h @@ -4,7 +4,7 @@ #include "JSystem/JGeometry.h" #include -#include "dusk/endian.h" +#include "helpers/endian.h" struct JPAEmitterWorkData; diff --git a/libs/JSystem/include/JSystem/JParticle/JPAResource.h b/libs/JSystem/include/JSystem/JParticle/JPAResource.h index 07563fa73d..ba52564b95 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPAResource.h +++ b/libs/JSystem/include/JSystem/JParticle/JPAResource.h @@ -2,7 +2,7 @@ #define JPARESOURCE_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" class JKRHeap; struct JPAEmitterWorkData; diff --git a/libs/JSystem/include/JSystem/JStudio/JStudio/jstudio-math.h b/libs/JSystem/include/JSystem/JStudio/JStudio/jstudio-math.h index a23eed44e9..826e0948c1 100644 --- a/libs/JSystem/include/JSystem/JStudio/JStudio/jstudio-math.h +++ b/libs/JSystem/include/JSystem/JStudio/JStudio/jstudio-math.h @@ -6,7 +6,7 @@ #define m_PI_D 3.141592653589793 #ifndef __MWERKS__ -#include "dusk/math.h" +#include "helpers/math.h" #endif namespace JStudio { diff --git a/libs/JSystem/include/JSystem/JSupport/JSUInputStream.h b/libs/JSystem/include/JSystem/JSupport/JSUInputStream.h index 77c27d0530..07d5a29b69 100644 --- a/libs/JSystem/include/JSystem/JSupport/JSUInputStream.h +++ b/libs/JSystem/include/JSystem/JSupport/JSUInputStream.h @@ -2,7 +2,7 @@ #define JSUINPUTSTREAM_H #include "JSystem/JSupport/JSUIosBase.h" -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-jsupport diff --git a/libs/JSystem/include/JSystem/JUtility/JUTFont.h b/libs/JSystem/include/JSystem/JUtility/JUTFont.h index 3c1cd0bf1f..0b262c0ac4 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTFont.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTFont.h @@ -3,7 +3,7 @@ #include "JSystem/JUtility/TColor.h" #include -#include "dusk/endian.h" +#include "helpers/endian.h" #if TARGET_PC struct FontDrawContext { diff --git a/libs/JSystem/include/JSystem/JUtility/JUTNameTab.h b/libs/JSystem/include/JSystem/JUtility/JUTNameTab.h index 6affae8ecb..6d4994d3f3 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTNameTab.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTNameTab.h @@ -2,7 +2,7 @@ #define JUTNAMETAB_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" /** * @ingroup jsystem-jutility diff --git a/libs/JSystem/include/JSystem/JUtility/JUTPalette.h b/libs/JSystem/include/JSystem/JUtility/JUTPalette.h index 5d29005a92..8987c28f13 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTPalette.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTPalette.h @@ -3,7 +3,7 @@ #include -#include "dusk/endian.h" +#include "helpers/endian.h" enum JUTTransparency { UNK0, UNK1 }; diff --git a/libs/JSystem/include/JSystem/JUtility/JUTResFont.h b/libs/JSystem/include/JSystem/JUtility/JUTResFont.h index a333cb9939..5e1cfa0011 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTResFont.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTResFont.h @@ -2,7 +2,7 @@ #define JUTRESFONT_H #include "JSystem/JUtility/JUTFont.h" -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" class JKRHeap; diff --git a/libs/JSystem/include/JSystem/JUtility/JUTTexture.h b/libs/JSystem/include/JSystem/JUtility/JUTTexture.h index 6a7a8f9a0e..c4356b0bf9 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTTexture.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTTexture.h @@ -3,8 +3,8 @@ #include #include -#include "dusk/endian.h" -#include "dusk/gx_helper.h" +#include "helpers/endian.h" +#include "helpers/gx_helper.h" class JUTPalette; diff --git a/libs/JSystem/include/JSystem/JUtility/TColor.h b/libs/JSystem/include/JSystem/JUtility/TColor.h index d4a615b5b4..5ec7afa42a 100644 --- a/libs/JSystem/include/JSystem/JUtility/TColor.h +++ b/libs/JSystem/include/JSystem/JUtility/TColor.h @@ -2,7 +2,7 @@ #define TCOLOR_H #include -#include "dusk/endian.h" +#include "helpers/endian.h" namespace JUtility { diff --git a/libs/JSystem/src/J2DGraph/J2DMaterialFactory.cpp b/libs/JSystem/src/J2DGraph/J2DMaterialFactory.cpp index 2cede2c49a..34e33bc03d 100644 --- a/libs/JSystem/src/J2DGraph/J2DMaterialFactory.cpp +++ b/libs/JSystem/src/J2DGraph/J2DMaterialFactory.cpp @@ -8,7 +8,7 @@ #include #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" J2DMaterialFactory::J2DMaterialFactory(J2DMaterialBlock const& param_0) { mMaterialNum = param_0.field_0x8; diff --git a/libs/JSystem/src/J2DGraph/J2DPane.cpp b/libs/JSystem/src/J2DGraph/J2DPane.cpp index 55a1b27ae1..01ab0e5444 100644 --- a/libs/JSystem/src/J2DGraph/J2DPane.cpp +++ b/libs/JSystem/src/J2DGraph/J2DPane.cpp @@ -8,7 +8,7 @@ #include "JSystem/JSupport/JSURandomInputStream.h" #include "JSystem/JUtility/JUTResource.h" #ifndef __MWERKS__ -#include "dusk/math.h" +#include "helpers/math.h" #endif J2DPane::J2DPane() : mBounds(), mGlobalBounds(), mClipRect(), mPaneTree(this) { diff --git a/libs/JSystem/src/J2DGraph/J2DTevs.cpp b/libs/JSystem/src/J2DGraph/J2DTevs.cpp index bb49596d85..bda5576cee 100644 --- a/libs/JSystem/src/J2DGraph/J2DTevs.cpp +++ b/libs/JSystem/src/J2DGraph/J2DTevs.cpp @@ -9,7 +9,7 @@ #ifdef __MWERKS__ #include #else -#include +#include #endif #include diff --git a/libs/JSystem/src/J2DGraph/J2DWindowEx.cpp b/libs/JSystem/src/J2DGraph/J2DWindowEx.cpp index fe4c5df9e6..284d7ea220 100644 --- a/libs/JSystem/src/J2DGraph/J2DWindowEx.cpp +++ b/libs/JSystem/src/J2DGraph/J2DWindowEx.cpp @@ -3,7 +3,7 @@ #include "JSystem/J2DGraph/J2DWindowEx.h" #include "JSystem/JUtility/JUTTexture.h" #include "JSystem/JSupport/JSURandomInputStream.h" -#include "dusk/endian.h" +#include "helpers/endian.h" struct J2DWindowExDef { BE(u32) field_0x0[4]; diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp index 8e111428cc..51f3951e38 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp @@ -8,7 +8,10 @@ #include "JSystem/J3DGraphBase/J3DShapeMtx.h" #include "JSystem/J3DGraphBase/J3DSys.h" #include "JSystem/JKernel/JKRHeap.h" + +#if TARGET_PC #include "dusk/frame_interpolation.h" +#endif #define J3D_ASSERTMSG(LINE, COND, MSG) JUT_ASSERT_MSG(LINE, (COND) != 0, MSG) #define J3D_WARN1(LINE, MSG, ARG1) JUT_WARN(LINE, MSG, ARG1) @@ -105,6 +108,11 @@ void J3DModel::interp_callback(bool isSimFrame, void* pUserWork) { i_this->diff(); } } + +void J3DModel::setAnmMtx(int jointNo, Mtx m) { + mMtxBuffer->setAnmMtx(jointNo, m); + dusk::frame_interp::record_final_mtx(mMtxBuffer->getAnmMtx(jointNo)); +} #endif s32 J3DModel::createShapePacket(J3DModelData* pModelData) { diff --git a/libs/JSystem/src/J3DGraphBase/J3DSys.cpp b/libs/JSystem/src/J3DGraphBase/J3DSys.cpp index 653b6e08f1..d37b4608bf 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DSys.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DSys.cpp @@ -4,10 +4,14 @@ #include "JSystem/J3DGraphBase/J3DSys.h" #include "JSystem/J3DGraphBase/J3DTevs.h" #include "JSystem/J3DGraphBase/J3DTexture.h" -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" #include "global.h" #include "tracy/Tracy.hpp" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + DUSK_GAME_DATA J3DSys j3dSys; DUSK_GAME_DATA Mtx J3DSys::mCurrentMtx; @@ -370,3 +374,13 @@ void J3DSys::reinitPixelProc() { GXSetZMode(GX_TRUE, GX_LEQUAL, GX_TRUE); GXSetZCompLoc(GX_TRUE); } + +#if TARGET_PC +void J3DSys::setViewMtx(const Mtx m) { + Mtx patched; + if (dusk::frame_interp::lookup_replacement(m, patched)) { + m = patched; + } + MTXCopy(m, mViewMtx); +} +#endif diff --git a/libs/JSystem/src/JAHostIO/JAHVirtualNode.cpp b/libs/JSystem/src/JAHostIO/JAHVirtualNode.cpp index 6bd9bf225a..1459a705cd 100644 --- a/libs/JSystem/src/JAHostIO/JAHVirtualNode.cpp +++ b/libs/JSystem/src/JAHostIO/JAHVirtualNode.cpp @@ -2,7 +2,7 @@ #include #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" DUSK_GAME_DATA u32 JAHVirtualNode::smVirNodeNum; diff --git a/libs/JSystem/src/JAHostIO/JAHioNode.cpp b/libs/JSystem/src/JAHostIO/JAHioNode.cpp index d3b45a4b5d..6e6ea2a320 100644 --- a/libs/JSystem/src/JAHostIO/JAHioNode.cpp +++ b/libs/JSystem/src/JAHostIO/JAHioNode.cpp @@ -6,7 +6,7 @@ #include "JSystem/JAHostIO/JAHioNode.h" #include "JSystem/JHostIO/JORServer.h" -#include "dusk/string.hpp" +#include "helpers/string.hpp" DUSK_GAME_DATA JAHioNode* JAHioNode::smCurrentNode; diff --git a/libs/JSystem/src/JAudio2/JASWaveArcLoader.cpp b/libs/JSystem/src/JAudio2/JASWaveArcLoader.cpp index 3848a64d3c..02730f46b4 100644 --- a/libs/JSystem/src/JAudio2/JASWaveArcLoader.cpp +++ b/libs/JSystem/src/JAudio2/JASWaveArcLoader.cpp @@ -9,7 +9,7 @@ #include #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" DUSK_GAME_DATA JASHeap* JASWaveArcLoader::sAramHeap; diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index aef89f309b..99b1e63364 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -14,11 +14,12 @@ #ifdef TARGET_PC #include "dusk/dusk.h" -#include "dusk/gx_helper.h" +#include "dusk/frame_interpolation.h" #include "dusk/logging.h" #include "dusk/settings.h" #include "dusk/time.h" #include "f_op/f_op_overlap_mng.h" +#include "helpers/gx_helper.h" #include "SDL3/SDL_timer.h" #include "tracy/Tracy.hpp" diff --git a/libs/JSystem/src/JKernel/JKRFileCache.cpp b/libs/JSystem/src/JKernel/JKRFileCache.cpp index 1a97ddefa3..a4795602c0 100644 --- a/libs/JSystem/src/JKernel/JKRFileCache.cpp +++ b/libs/JSystem/src/JKernel/JKRFileCache.cpp @@ -8,7 +8,7 @@ #include #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" #include "global.h" JKRFileCache* JKRFileCache::mount(const char* path, JKRHeap* heap, const char* param_3) { diff --git a/libs/JSystem/src/JKernel/JKRFileLoader.cpp b/libs/JSystem/src/JKernel/JKRFileLoader.cpp index c3c5ac1903..0ff00f5f84 100644 --- a/libs/JSystem/src/JKernel/JKRFileLoader.cpp +++ b/libs/JSystem/src/JKernel/JKRFileLoader.cpp @@ -8,7 +8,7 @@ #include #include #include "JSystem/JKernel/JKRHeap.h" -#include "dusk/string.hpp" +#include "helpers/string.hpp" #include "global.h" DUSK_GAME_DATA JKRFileLoader* JKRFileLoader::sCurrentVolume; diff --git a/libs/JSystem/src/JKernel/JKRHeap.cpp b/libs/JSystem/src/JKernel/JKRHeap.cpp index 34b3e7c619..3c329e0a43 100644 --- a/libs/JSystem/src/JKernel/JKRHeap.cpp +++ b/libs/JSystem/src/JKernel/JKRHeap.cpp @@ -15,7 +15,7 @@ #include "JSystem/JUtility/JUTAssert.h" #include "JSystem/JUtility/JUTException.h" -#include "dusk/string.hpp" +#include "helpers/string.hpp" #ifdef __MWERKS__ #include #else @@ -702,7 +702,7 @@ JKRHeap* JKRHeap::getCurrentHeap() { } void JKRHeap::setName(const char* name) { - dusk::SafeStringCopyTruncate(mName, name); + SafeStringCopyTruncate(mName, name); } void JKRHeap::setNamef(const char* fmt, ...) { diff --git a/libs/JSystem/src/JKernel/JKRThread.cpp b/libs/JSystem/src/JKernel/JKRThread.cpp index 8203248339..73135381eb 100644 --- a/libs/JSystem/src/JKernel/JKRThread.cpp +++ b/libs/JSystem/src/JKernel/JKRThread.cpp @@ -7,7 +7,7 @@ #include "global.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" #if TARGET_PC #include "dusk/os.h" diff --git a/libs/JSystem/src/JParticle/JPAParticle.cpp b/libs/JSystem/src/JParticle/JPAParticle.cpp index eb2395e346..99ba95453c 100644 --- a/libs/JSystem/src/JParticle/JPAParticle.cpp +++ b/libs/JSystem/src/JParticle/JPAParticle.cpp @@ -7,6 +7,10 @@ #include "JSystem/JParticle/JPAEmitterManager.h" #include "JSystem/JParticle/JPAExtraShape.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + JPAParticleCallBack::~JPAParticleCallBack() { /* empty function */ } diff --git a/libs/JSystem/src/JParticle/JPAResource.cpp b/libs/JSystem/src/JParticle/JPAResource.cpp index 2d5d872bac..9385b258e7 100644 --- a/libs/JSystem/src/JParticle/JPAResource.cpp +++ b/libs/JSystem/src/JParticle/JPAResource.cpp @@ -19,6 +19,8 @@ #include "tracy/Tracy.hpp" #if TARGET_PC +#include "dusk/frame_interpolation.h" + #define JPA_DRAW_CTX_ARG , &ctx #else #define JPA_DRAW_CTX_ARG diff --git a/libs/JSystem/src/JStudio/JStudio/stb-data.cpp b/libs/JSystem/src/JStudio/JStudio/stb-data.cpp index 95b8d7dafd..5de2b8f7bf 100644 --- a/libs/JSystem/src/JStudio/JStudio/stb-data.cpp +++ b/libs/JSystem/src/JStudio/JStudio/stb-data.cpp @@ -1,5 +1,5 @@ #include "JSystem/JSystem.h" // IWYU pragma: keep -#include "dusk/endian.h" +#include "helpers/endian.h" #include "JSystem/JStudio/JStudio/stb-data.h" DUSK_GAME_DATA const s32 JStudio::stb::data::gauDataSize_TEParagraph_data[8] = {0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40}; diff --git a/libs/JSystem/src/JUtility/JUTConsole.cpp b/libs/JSystem/src/JUtility/JUTConsole.cpp index 2899e3f0e4..75150cb498 100644 --- a/libs/JSystem/src/JUtility/JUTConsole.cpp +++ b/libs/JSystem/src/JUtility/JUTConsole.cpp @@ -8,7 +8,7 @@ #include "JSystem/JUtility/JUTConsole.h" #include "JSystem/JUtility/JUTDirectPrint.h" #include "JSystem/JUtility/JUTVideo.h" -#include "dusk/string.hpp" +#include "helpers/string.hpp" #include "global.h" DUSK_GAME_DATA JUTConsoleManager* JUTConsoleManager::sManager; diff --git a/libs/JSystem/src/JUtility/JUTException.cpp b/libs/JSystem/src/JUtility/JUTException.cpp index 4b87dd50f8..9e00bce739 100644 --- a/libs/JSystem/src/JUtility/JUTException.cpp +++ b/libs/JSystem/src/JUtility/JUTException.cpp @@ -10,7 +10,7 @@ #include #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" #ifdef __REVOLUTION_SDK__ #include #else diff --git a/libs/JSystem/src/JUtility/JUTFader.cpp b/libs/JSystem/src/JUtility/JUTFader.cpp index e7d33454b7..a8d9fe6028 100644 --- a/libs/JSystem/src/JUtility/JUTFader.cpp +++ b/libs/JSystem/src/JUtility/JUTFader.cpp @@ -10,6 +10,7 @@ #ifdef TARGET_PC #include +#include "dusk/frame_interpolation.h" #endif JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor) diff --git a/libs/freeverb/CMakeLists.txt b/libs/freeverb/CMakeLists.txt index c0c12da293..d0275f62ac 100644 --- a/libs/freeverb/CMakeLists.txt +++ b/libs/freeverb/CMakeLists.txt @@ -7,3 +7,5 @@ add_library(freeverb allpass.cpp revmodel.cpp ) +target_include_directories(freeverb PRIVATE include/freeverb) +target_include_directories(freeverb INTERFACE include) diff --git a/libs/freeverb/allpass.hpp b/libs/freeverb/include/freeverb/allpass.hpp similarity index 100% rename from libs/freeverb/allpass.hpp rename to libs/freeverb/include/freeverb/allpass.hpp diff --git a/libs/freeverb/comb.hpp b/libs/freeverb/include/freeverb/comb.hpp similarity index 100% rename from libs/freeverb/comb.hpp rename to libs/freeverb/include/freeverb/comb.hpp diff --git a/libs/freeverb/denormals.h b/libs/freeverb/include/freeverb/denormals.h similarity index 100% rename from libs/freeverb/denormals.h rename to libs/freeverb/include/freeverb/denormals.h diff --git a/libs/freeverb/revmodel.hpp b/libs/freeverb/include/freeverb/revmodel.hpp similarity index 100% rename from libs/freeverb/revmodel.hpp rename to libs/freeverb/include/freeverb/revmodel.hpp diff --git a/libs/freeverb/tuning.h b/libs/freeverb/include/freeverb/tuning.h similarity index 100% rename from libs/freeverb/tuning.h rename to libs/freeverb/include/freeverb/tuning.h diff --git a/mods/ao_mod/CMakeLists.txt b/mods/ao_mod/CMakeLists.txt index 13590923f0..723ed55006 100644 --- a/mods/ao_mod/CMakeLists.txt +++ b/mods/ao_mod/CMakeLists.txt @@ -13,6 +13,7 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) endif () add_mod(ao_mod + FEATURES webgpu SOURCES src/mod.cpp MOD_JSON mod.json RES_DIR res diff --git a/mods/shadow_mod/CMakeLists.txt b/mods/shadow_mod/CMakeLists.txt index 4479d0fcc4..ae44eca49c 100644 --- a/mods/shadow_mod/CMakeLists.txt +++ b/mods/shadow_mod/CMakeLists.txt @@ -13,6 +13,7 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) endif () add_mod(shadow_mod + FEATURES game webgpu SOURCES src/mod.cpp MOD_JSON mod.json RES_DIR res diff --git a/mods/shadow_mod/src/mod.cpp b/mods/shadow_mod/src/mod.cpp index 0eec4fecef..0319dab3dc 100644 --- a/mods/shadow_mod/src/mod.cpp +++ b/mods/shadow_mod/src/mod.cpp @@ -24,7 +24,6 @@ #include "mods/service.hpp" #include "mods/svc/camera.h" #include "mods/svc/config.h" -#include "mods/svc/game.h" #include "mods/svc/gfx.h" #include "mods/svc/hook.h" #include "mods/svc/log.h" @@ -45,7 +44,6 @@ IMPORT_SERVICE(UiService, svc_ui); IMPORT_SERVICE(GfxService, svc_gfx); IMPORT_SERVICE(CameraService, svc_camera); IMPORT_SERVICE(HookService, svc_hook); -IMPORT_SERVICE(GameService, svc_game); IMPORT_SERVICE(LogService, svc_log); namespace { @@ -93,10 +91,15 @@ constexpr float kMaxLightLookahead = 10000.0f; constexpr float kSunMoonDistance = 80000.0f; constexpr float kSunMoonZDistance = -48000.0f; -using ClipperSphereClip = int (J3DUClipper::*)(f32 const (*)[4], Vec, f32) const; -using ClipperBoxClip = int (J3DUClipper::*)(f32 const (*)[4], Vec*, Vec*) const; -constexpr ClipperSphereClip kClipperSphereClip = static_cast(&J3DUClipper::clip); -constexpr ClipperBoxClip kClipperBoxClip = static_cast(&J3DUClipper::clip); +DEFINE_HOOK(&dDlst_shadowControl_c::imageDraw, GameShadowImageDraw); +DEFINE_HOOK(&dDlst_shadowControl_c::draw, GameShadowDraw); +DEFINE_HOOK(&drawCloudShadow, CloudShadowDraw); +DEFINE_HOOK(static_cast(&J3DUClipper::clip), + ClipperSphereClip); +DEFINE_HOOK( + static_cast(&J3DUClipper::clip), + ClipperBoxClip); +DEFINE_HOOK(GXCopyTex, CopyTex); // Mirror of the WGSL Uniforms struct (keep in sync with res/shadow.wgsl). struct ShadowUniforms { @@ -1048,20 +1051,18 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { // Skip the game's own shadow rendering while the dynamic pass is active: the // shadowControl pair covers the actor real/blob shadows, drawCloudShadow the weather // cloud shadows. - if (dusk::mods::hook_add_pre<&dDlst_shadowControl_c::imageDraw>(svc_hook, on_game_shadow_pre) != - MOD_OK || - dusk::mods::hook_add_pre<&dDlst_shadowControl_c::draw>(svc_hook, on_game_shadow_pre) != - MOD_OK || - dusk::mods::hook_add_pre<&drawCloudShadow>(svc_hook, on_game_shadow_pre) != MOD_OK) + if (dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || + dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || + dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering"); } - if (dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK || - dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK) + if (dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK || + dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping"); } - if (dusk::mods::hook_add_pre(svc_hook, on_copy_tex_pre) != MOD_OK) { + if (dusk::mods::hook_add_pre(svc_hook, on_copy_tex_pre) != MOD_OK) { return dusk::mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex"); } UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT; diff --git a/platforms/android/.gitignore b/platforms/android/.gitignore index 7775e3c7f4..002aaa86aa 100644 --- a/platforms/android/.gitignore +++ b/platforms/android/.gitignore @@ -3,3 +3,4 @@ build/ app/build/ local.properties app/src/main/jniLibs/*/*.so +app/src/main/bundled_mods/ diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle index 8527cd1f4d..c7a25e732b 100644 --- a/platforms/android/app/build.gradle +++ b/platforms/android/app/build.gradle @@ -12,6 +12,11 @@ def syncDuskAssets = tasks.register('syncDuskAssets', Sync) { into 'res' exclude '**/.DS_Store' } + // Staged by platforms/android/scripts/stage-jni-libs.sh + from(new File(projectDir, 'src/main/bundled_mods')) { + into 'mods' + include '*.dusk' + } into duskGeneratedAssetsDir } diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java index 96fc302d9e..2fa6cbacb3 100644 --- a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java @@ -27,6 +27,10 @@ import org.libsdl.app.SDLActivity; import org.libsdl.app.SDLSurface; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; @@ -90,10 +94,55 @@ public class DuskActivity extends SDLActivity { @Override protected void onCreate(Bundle savedInstanceState) { + extractBundledMods(); super.onCreate(savedInstanceState); hideSystemBars(); } + // Bundled mod packages ship as APK assets, which the native loader cannot read directly; + // mirror them into internal storage (the loader's CachePath/bundled_mods search dir) + // before SDL_main starts. + private void extractBundledMods() { + File outDir = new File(getFilesDir(), "bundled_mods"); + try { + deleteRecursively(outDir); // drop packages removed by an app update + String[] names = getAssets().list("mods"); + if (names == null || names.length == 0) { + return; + } + if (!outDir.mkdirs()) { + Log.w(TAG, "Unable to create " + outDir); + return; + } + byte[] buffer = new byte[65536]; + for (String name : names) { + if (!name.endsWith(".dusk")) { + continue; + } + try (InputStream in = getAssets().open("mods/" + name); + OutputStream out = new FileOutputStream(new File(outDir, name))) + { + int count; + while ((count = in.read(buffer)) > 0) { + out.write(buffer, 0, count); + } + } + } + } catch (IOException e) { + Log.w(TAG, "Failed to extract bundled mods", e); + } + } + + private static void deleteRecursively(File file) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + file.delete(); + } + @Override protected SDLSurface createSDLSurface(Context context) { return new DuskSurface(context); diff --git a/platforms/android/scripts/stage-jni-libs.sh b/platforms/android/scripts/stage-jni-libs.sh index 42b08c13e6..3984cf03ee 100755 --- a/platforms/android/scripts/stage-jni-libs.sh +++ b/platforms/android/scripts/stage-jni-libs.sh @@ -61,12 +61,55 @@ rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64" for abi in $ANDROID_STAGE_ABIS; do case "$abi" in - arm64-v8a) src="$ROOT_DIR/build/android-arm64/libmain.so" ;; - x86_64) src="$ROOT_DIR/build/android-x86_64/libmain.so" ;; + arm64-v8a) + src="$ROOT_DIR/build/android-arm64/libmain.so" + triple="aarch64-linux-android" + ;; + x86_64) + src="$ROOT_DIR/build/android-x86_64/libmain.so" + triple="x86_64-linux-android" + ;; *) echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2 exit 1 ;; esac copy_lib "$abi" "$src" + if [[ -n "$STRIP_TOOL" ]]; then + stl="$(dirname "$STRIP_TOOL")/../sysroot/usr/lib/$triple/libc++_shared.so" + if [[ -f "$stl" ]]; then + cp -f "$stl" "$APP_DIR/$abi/libc++_shared.so" + echo "Staged $stl -> $APP_DIR/$abi/libc++_shared.so" + else + echo "Missing libc++_shared.so for $abi at $stl" >&2 + exit 1 + fi + else + echo "Cannot stage libc++_shared.so for $abi (NDK not found)" >&2 + exit 1 + fi +done + +# Stage bundled mod packages into the app's assets source dir. +MODS_STAGING_DIR="$ROOT_DIR/platforms/android/app/src/main/bundled_mods" +rm -rf "$MODS_STAGING_DIR" +mkdir -p "$MODS_STAGING_DIR" +for abi in $ANDROID_STAGE_ABIS; do + case "$abi" in + arm64-v8a) build_dir="$ROOT_DIR/build/android-arm64" ;; + x86_64) build_dir="$ROOT_DIR/build/android-x86_64" ;; + esac + [[ -d "$build_dir/bundled_mods" ]] || continue + for pkg in "$build_dir/bundled_mods"/*.dusk; do + [[ -f "$pkg" ]] || continue + name="$(basename "$pkg")" + if [[ ! -f "$MODS_STAGING_DIR/$name" ]]; then + cp -f "$pkg" "$MODS_STAGING_DIR/$name" + echo "Staged bundled mod $pkg" + else + stage_dir="$build_dir/mods/${name%.dusk}/${name%.dusk}_stage" + (cd "$stage_dir" && zip -q -r "$MODS_STAGING_DIR/$name" lib) + echo "Appended $abi libraries to bundled mod $name" + fi + done done diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt index 8670603866..2f25c28efe 100644 --- a/sdk/CMakeLists.txt +++ b/sdk/CMakeLists.txt @@ -1,18 +1,17 @@ # Dusklight Mod SDK entry point # -# Provides game/service headers, compile definitions, version.h and WebGPU -# headers without configuring the full game tree. +# Provides selectable mod service, game ABI, and WebGPU header surfaces without configuring the +# full game tree. # # Usage (from a mod project): # add_subdirectory(/sdk dusk-sdk EXCLUDE_FROM_ALL) -# add_mod(my_mod SOURCES ... MOD_JSON mod.json) +# add_mod(my_mod FEATURES game webgpu SOURCES ... MOD_JSON mod.json) # -# On Windows, pass -DDUSK_GAME_IMPLIB= from the -# matching game release. TODO: auto-download from tag +# TODO: auto-download link targets from tag cmake_minimum_required(VERSION 3.25) -if (TARGET dusklight_game_headers) +if (TARGET dusklight_mod_api) message(FATAL_ERROR "Mod SDK already configured") endif () @@ -30,18 +29,13 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/DetectVersion.cmake") detect_version() configure_version_header() -# Provides dawn::webgpu_dawn and dawn::dawncpp_headers for public gfx service headers. -include("${CMAKE_CURRENT_SOURCE_DIR}/../extern/aurora/cmake/AuroraDependencyVersions.cmake") -set(AURORA_DAWN_PROVIDER "package" CACHE STRING "How to provide Dawn for the mod SDK") -include("${CMAKE_CURRENT_SOURCE_DIR}/../extern/aurora/cmake/AuroraDawnProvider.cmake") - -# Game ABI headers & compile definitions +# Mod API and optional feature header surfaces include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GameABIConfig.cmake") -if (WIN32) - set(DUSK_GAME_IMPLIB "" CACHE FILEPATH "Path to the dusklight import library (sdk/dusklight.lib)") - if (DUSK_GAME_IMPLIB AND NOT EXISTS "${DUSK_GAME_IMPLIB}") - message(FATAL_ERROR "Mod SDK: DUSK_GAME_IMPLIB does not exist: ${DUSK_GAME_IMPLIB}") +if (WIN32 OR APPLE OR ANDROID) + set(DUSK_GAME_EXE "" CACHE FILEPATH "Game binary or link stub mods link against") + if (DUSK_GAME_EXE AND NOT EXISTS "${DUSK_GAME_EXE}") + message(FATAL_ERROR "Mod SDK: DUSK_GAME_EXE does not exist: ${DUSK_GAME_EXE}") endif () endif () diff --git a/include/mods/api.h b/sdk/include/mods/api.h similarity index 51% rename from include/mods/api.h rename to sdk/include/mods/api.h index 85e2f76545..c63d08cfe2 100644 --- a/include/mods/api.h +++ b/sdk/include/mods/api.h @@ -23,7 +23,7 @@ extern "C" { #define MOD_EXTERN_C #endif -#define MOD_ABI_VERSION 5u +#define MOD_ABI_VERSION 1u #define MOD_ERROR_MESSAGE_SIZE 512u typedef struct ModContext ModContext; @@ -37,7 +37,8 @@ typedef enum ModResult { MOD_INVALID_ARGUMENT = 5, } ModResult; -static_assert(sizeof(ModResult) == 4, "mod SDK enums must be int-sized; do not build mods with -fshort-enums"); +static_assert(sizeof(ModResult) == 4, + "mod SDK enums must be int-sized; do not build mods with -fshort-enums"); typedef struct ModError { uint32_t struct_size; @@ -108,41 +109,134 @@ typedef enum ServiceExportFlags { SERVICE_EXPORT_DEFERRED = 1u << 0u, } ServiceExportFlags; -typedef struct ServiceImport { - uint32_t struct_size; - const char* service_id; +/* + * Mod metadata records. + * + * A mod's manifest is a sequence of records in a dedicated section of the native library ("modmeta" + * on ELF, "__DATA,__modmeta" on Mach-O, "modmeta$a/$d/$z" on PE). + * + * The records are pure data. Every string is inline and NUL-terminated. Fields documented as + * runtime-only hold relocated pointers that are meaningless on disk; static parsers recover their + * targets from the file's relocation/bind entries instead. + * + * Layout rules: + * - Little-endian, 8-byte aligned; every record size is a multiple of 8. + * - Parsers must skip all-zero 8-byte units (linker padding) and records of unknown kind. + * - Exactly one MOD_META_HEADER record per library. + * + * The IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK macros emit these records; mods do not construct + * them by hand. + */ + +#define MOD_META_SERVICE_ID_SIZE 64u + +/* Records are 8-byte aligned so the linker packs them without padding; most are naturally + * aligned by their pointer fields, the alignment below covers the rest. */ +#if defined(__cplusplus) +#define MOD_META_ALIGN alignas(8) +#elif defined(_MSC_VER) +#define MOD_META_ALIGN __declspec(align(8)) +#else +#define MOD_META_ALIGN __attribute__((aligned(8))) +#endif + +typedef enum ModMetaKind { + MOD_META_PAD = 0, + MOD_META_HEADER = 1, + MOD_META_IMPORT = 2, + MOD_META_EXPORT = 3, + MOD_META_HOOK_FN = 4, + MOD_META_HOOK_MEM = 5, + MOD_META_HOOK_NAME = 6, +} ModMetaKind; + +typedef struct ModMetaRecord { + uint16_t size; /* total record size in bytes, a multiple of 8 */ + uint8_t kind; /* ModMetaKind */ + uint8_t flags; /* ServiceImportFlags / ServiceExportFlags for imports/exports */ +} ModMetaRecord; + +typedef struct ModMetaServiceId { + char chars[MOD_META_SERVICE_ID_SIZE]; /* NUL-terminated */ +} ModMetaServiceId; + +typedef struct MOD_META_ALIGN ModMetaHeader { + ModMetaRecord rec; + uint32_t abi_version; +} ModMetaHeader; + +static_assert(sizeof(ModMetaHeader) == 8); + +typedef struct MOD_META_ALIGN ModMetaImport { + ModMetaRecord rec; uint16_t major_version; uint16_t min_minor_version; - uint32_t flags; - void* slot; -} ServiceImport; + void* slot; /* runtime only */ + ModMetaServiceId service_id; +} ModMetaImport; -typedef struct ServiceExport { - uint32_t struct_size; - const char* service_id; +static_assert(sizeof(ModMetaImport) == 16 + MOD_META_SERVICE_ID_SIZE); + +typedef struct MOD_META_ALIGN ModMetaExport { + ModMetaRecord rec; uint16_t major_version; uint16_t minor_version; - uint32_t flags; - const void* service; -} ServiceExport; + const void* service; /* runtime only */ + ModMetaServiceId service_id; +} ModMetaExport; -typedef struct ModManifest { +static_assert(sizeof(ModMetaExport) == 16 + MOD_META_SERVICE_ID_SIZE); + +/* Hook on a function named at link time: `target` carries the &fn relocation. */ +typedef struct MOD_META_ALIGN ModMetaHookFn { + ModMetaRecord rec; + uint32_t reserved; + void* target; /* runtime only */ + void* resolved; /* runtime only */ +} ModMetaHookFn; + +static_assert(sizeof(ModMetaHookFn) == 24); + +/* + * Hook on a member function: `pmf` holds the compiler's pointer-to-member representation + * (non-virtual: a function address relocation; virtual Itanium/AAPCS: literal slot words). Two + * NUL-terminated strings follow `resolved`: the class vtable symbol (empty if the class name is not + * representable), then the stringified target for tooling display. + */ +typedef struct MOD_META_ALIGN ModMetaHookMem { + ModMetaRecord rec; + uint32_t reserved; + unsigned char pmf[16]; + void* resolved; /* runtime only */ +} ModMetaHookMem; + +static_assert(sizeof(ModMetaHookMem) == 32); + +/* + * Hook on a function by symbol name, for targets that cannot be named in C++ (file-local statics, + * private members). One NUL-terminated string follows `resolved`; it may be either the platform + * mangled name or the demangled qualified display name. + */ +typedef struct MOD_META_ALIGN ModMetaHookName { + ModMetaRecord rec; + uint32_t reserved; + void* resolved; /* runtime only */ +} ModMetaHookName; + +static_assert(sizeof(ModMetaHookName) == 16); + +typedef struct ModMeta { uint32_t struct_size; - uint32_t abi_version; - const ServiceImport* imports; - size_t import_count; - const ServiceExport* exports; - size_t export_count; -} ModManifest; + const void* records_begin; + const void* records_end; +} ModMeta; -typedef const ModManifest* (*ModGetManifestFn)(void); +MOD_EXPORT extern const ModMeta mod_meta; typedef ModResult (*ModInitializeFn)(ModError* out_error); typedef ModResult (*ModUpdateFn)(ModError* out_error); typedef ModResult (*ModShutdownFn)(ModError* out_error); -MOD_EXPORT const ModManifest* mod_get_manifest(void); - MOD_EXPORT ModResult mod_initialize(ModError* out_error); MOD_EXPORT ModResult mod_update(ModError* out_error); MOD_EXPORT ModResult mod_shutdown(ModError* out_error); diff --git a/sdk/include/mods/hook.hpp b/sdk/include/mods/hook.hpp new file mode 100644 index 0000000000..d48e9bff9f --- /dev/null +++ b/sdk/include/mods/hook.hpp @@ -0,0 +1,211 @@ +#pragma once + +#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_GAME) +#error "DEFINE_HOOK requires add_mod(... FEATURES game)" +#endif + +#include + +#include +#include + +namespace dusk::mods { + +template +T arg(void* argsRaw, int n) noexcept { + void** args = static_cast(argsRaw); + return *static_cast>>(args[n]); +} + +template +std::remove_reference_t& arg_ref(void* argsRaw, int n) noexcept { + void** args = static_cast(argsRaw); + return *static_cast>>(args[n]); +} + +/* + * Trampoline generator + per-target state. Tag makes each hooked target's statics distinct; the + * target address comes from the declaration's metadata record, resolved by the host at mod + * initialization. + */ +template +struct HookImpl { + static inline R (*g_orig)(A...) = nullptr; + static inline const HookService* hooks = nullptr; + static inline void* target = nullptr; + + static bool dispatch_pre(void* args, void* retval) { + if (hooks == nullptr) { + return false; + } + + int skipOriginal = 0; + const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal); + return result == MOD_OK && skipOriginal != 0; + } + + static void dispatch_post(void* args, void* retval) { + if (hooks != nullptr) { + hooks->dispatch_post(mod_ctx, target, args, retval); + } + } + + static R trampoline(A... args) { + if constexpr (sizeof...(A) == 0) { + if constexpr (std::is_void_v) { + const bool skipOriginal = dispatch_pre(nullptr, nullptr); + if (!skipOriginal) { + g_orig(args...); + } + dispatch_post(nullptr, nullptr); + } else { + R result{}; + const bool skipOriginal = + dispatch_pre(nullptr, static_cast(std::addressof(result))); + if (!skipOriginal) { + result = g_orig(args...); + } + dispatch_post(nullptr, static_cast(std::addressof(result))); + return result; + } + } else { + void* ptrs[] = {static_cast(std::addressof(args))...}; + if constexpr (std::is_void_v) { + const bool skipOriginal = dispatch_pre(static_cast(ptrs), nullptr); + if (!skipOriginal) { + g_orig(args...); + } + dispatch_post(static_cast(ptrs), nullptr); + } else { + R result{}; + const bool skipOriginal = dispatch_pre( + static_cast(ptrs), static_cast(std::addressof(result))); + if (!skipOriginal) { + result = g_orig(args...); + } + dispatch_post(static_cast(ptrs), static_cast(std::addressof(result))); + return result; + } + } + } +}; + +namespace detail { +template +using TargetTag = std::integral_constant; +template +struct NameTag {}; +} // namespace detail + +/* + * Typed base for a hook on a function named at compile time (&daAlink_c::execute, &free_fn). + * Instantiate through DEFINE_HOOK, which pairs it with the metadata record the host resolves. + */ +template +struct Hook; + +template +struct Hook : HookImpl, R, C*, A...> {}; + +template +struct Hook : HookImpl, R, const C*, A...> {}; + +template +struct Hook : HookImpl, R, A...> {}; + +/* + * Typed base for a hook on a function by its symbol name, for targets you can't name in C++: + * file-local statics, private members, or symbols without a header. The signature is written + * free-style with the receiver first and is *not* compiler-checked. Instantiate through + * DEFINE_HOOK_SYMBOL. + */ +template +struct NamedHook; + +template +struct NamedHook : HookImpl, R, A...> {}; + +/* + * Declare a hook target. The declaration emits a metadata record that the host resolves at mod + * initialization. Every hook target must be declared. + * + * DEFINE_HOOK(&daAlink_c::execute, LinkExecute); + * DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack", + * void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit); + * + * dusk::mods::hook_add_pre(svc_hook, on_link_execute); + * + * DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O + * leading underscore) or the demangled qualified display name; overloaded display names are + * ambiguous and need the mangled form. + */ +#define DEFINE_HOOK(target, alias) \ + [[maybe_unused]] static const void* const mod_meta_hook_##alias = \ + &::dusk::mods::detail::HookRecordFor<(target), \ + ::dusk::mods::FixedString{#target}>::Holder::record; \ + struct alias : ::dusk::mods::Hook<(target)> { \ + static void* resolved_target() { \ + return ::dusk::mods::detail::HookRecordFor<(target), \ + ::dusk::mods::FixedString{#target}>::Holder::record.resolved; \ + } \ + } + +#define DEFINE_HOOK_SYMBOL(name, sig, alias) \ + MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \ + ::dusk::mods::detail::make_hook_name_record<::dusk::mods::FixedString{name}>(); \ + struct alias : ::dusk::mods::NamedHook<::dusk::mods::FixedString{name}, sig> { \ + static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \ + } + +template +ModResult hook_install(const HookService* hooks) { + if (hooks == nullptr) { + return MOD_UNAVAILABLE; + } + + Entry::hooks = hooks; + if (Entry::target == nullptr) { + void* resolved = Entry::resolved_target(); + if (resolved == nullptr) { + return MOD_UNAVAILABLE; + } + Entry::target = resolved; + } + return hooks->install(mod_ctx, Entry::target, reinterpret_cast(Entry::trampoline), + reinterpret_cast(&Entry::g_orig)); +} + +template +ModResult hook_add_pre( + const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) { + const ModResult installed = hook_install(hooks); + if (installed != MOD_OK) { + return installed; + } + + return hooks->add_pre(mod_ctx, Entry::target, callback, options); +} + +template +ModResult hook_add_post( + const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) { + const ModResult installed = hook_install(hooks); + if (installed != MOD_OK) { + return installed; + } + + return hooks->add_post(mod_ctx, Entry::target, callback, options); +} + +template +ModResult hook_replace( + const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) { + const ModResult installed = hook_install(hooks); + if (installed != MOD_OK) { + return installed; + } + + return hooks->replace(mod_ctx, Entry::target, callback, options); +} + +} // namespace dusk::mods diff --git a/sdk/include/mods/meta.hpp b/sdk/include/mods/meta.hpp new file mode 100644 index 0000000000..7d5d1e5eec --- /dev/null +++ b/sdk/include/mods/meta.hpp @@ -0,0 +1,287 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +/* + * modmeta records. Each IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK use places one + * constant-initialized record object in the metadata section. + */ +#if defined(_WIN32) +#pragma section("modmeta$a", read, write) +#pragma section("modmeta$d", read, write) +#pragma section("modmeta$z", read, write) +#if defined(__clang__) +#define MOD_META_RECORD __declspec(allocate("modmeta$d")) __attribute__((used)) +#else +#define MOD_META_RECORD __declspec(allocate("modmeta$d")) +#endif +#elif defined(__APPLE__) +#define MOD_META_RECORD __attribute__((section("__DATA,__modmeta"), used)) +#elif defined(__has_attribute) && __has_attribute(retain) +#define MOD_META_RECORD __attribute__((section("modmeta"), used, retain)) +#else +#define MOD_META_RECORD __attribute__((section("modmeta"), used)) +#endif + +/* Section bounds for the mod_meta descriptor */ +#if defined(_WIN32) +#define MOD_META_BOUNDS_DEFN \ + extern "C" { \ + __declspec(allocate("modmeta$a")) constinit unsigned long long mod_meta_bounds_begin = 0; \ + __declspec(allocate("modmeta$z")) constinit unsigned long long mod_meta_bounds_end = 0; \ + } +#define MOD_META_BOUNDS_BEGIN (&mod_meta_bounds_begin) +#define MOD_META_BOUNDS_END (&mod_meta_bounds_end) +#elif defined(__APPLE__) +extern "C" const unsigned char mod_meta_bounds_begin[] __asm("section$start$__DATA$__modmeta"); +extern "C" const unsigned char mod_meta_bounds_end[] __asm("section$end$__DATA$__modmeta"); +#define MOD_META_BOUNDS_DEFN +#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[]; +#define MOD_META_BOUNDS_DEFN +#define MOD_META_BOUNDS_BEGIN (__start_modmeta) +#define MOD_META_BOUNDS_END (__stop_modmeta) +#endif + +namespace dusk::mods { + +/* A string usable as a template argument: carries a symbol/target name into record builders + * and makes each hook declaration's static state unique. */ +template +struct FixedString { + char chars[N]{}; + constexpr FixedString(const char (&s)[N]) noexcept { + for (size_t i = 0; i < N; ++i) { + chars[i] = s[i]; + } + } +}; + +namespace detail { + +template +constexpr std::string_view class_name() { +#if defined(__clang__) || defined(__GNUC__) + // "... class_name() [T = daAlink_c]" / "... [with T = daAlink_c; ...]" + constexpr std::string_view fn = __PRETTY_FUNCTION__; + constexpr size_t start = fn.find("T = ") + 4; + return fn.substr(start, fn.find_first_of(";]", start) - start); +#elif defined(_MSC_VER) + // "... class_name(void)" + constexpr std::string_view fn = __FUNCSIG__; + constexpr size_t start = fn.find("class_name<") + 11; + constexpr std::string_view name = fn.substr(start, fn.rfind(">(") - start); + if constexpr (name.starts_with("class ")) { + return name.substr(6); + } else if constexpr (name.starts_with("struct ")) { + return name.substr(7); + } else { + return name; + } +#else +#error "unsupported compiler" +#endif +} + +/* The symbol name of C's vtable. Only unscoped, non-template class names are supported (an + * empty result makes hooks on virtual members of C fail resolution, which is reported). */ +template +constexpr auto vtable_symbol() { + constexpr std::string_view name = class_name(); + constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos; + // "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated + std::array out{}; + if constexpr (!simple) { + return out; + } + size_t n = 0; +#if defined(_WIN32) + for (char c : {'?', '?', '_', '7'}) { + out[n++] = c; + } + for (char c : name) { + out[n++] = c; + } + for (char c : {'@', '@', '6', 'B', '@'}) { + out[n++] = c; + } +#else + for (char c : {'_', 'Z', 'T', 'V'}) { + out[n++] = c; + } + size_t len = name.size(); + char digits[8]{}; + size_t d = 0; + while (len != 0) { + digits[d++] = static_cast('0' + len % 10); + len /= 10; + } + while (d != 0) { + out[n++] = digits[--d]; + } + for (char c : name) { + out[n++] = c; + } +#endif + return out; +} + +template +struct member_traits; +template +struct member_traits { + using Class = C; +}; +template +struct member_traits { + using Class = C; +}; + +consteval ModMetaServiceId make_service_id(const char* id) { + ModMetaServiceId out{}; + size_t n = 0; + for (; id[n] != '\0'; ++n) { + if (n + 1 >= MOD_META_SERVICE_ID_SIZE) { + throw "service id exceeds MOD_META_SERVICE_ID_SIZE"; + } + out.chars[n] = id[n]; + } + return out; +} + +consteval ModMetaHeader make_header() { + ModMetaHeader r{}; + r.rec = {sizeof(ModMetaHeader), MOD_META_HEADER, 0}; + r.abi_version = MOD_ABI_VERSION; + return r; +} + +/* + * Typed record variants: embedding the target as its native type makes the compiler emit the + * on-disk representation (relocations, PMF slot words) that static parsers read; the layouts + * match the byte-view structs in api.h. + */ + +template +struct HookFnRecord { + ModMetaRecord rec; + uint32_t reserved; + F target; + void* resolved; +}; + +template +struct HookMemRecord { + ModMetaRecord rec; + uint32_t reserved; + union { + F fn; + unsigned char raw[16]; + } pmf; + void* resolved; + char names[N]; +}; + +template +struct HookNameRecord { + ModMetaRecord rec; + uint32_t reserved; + void* resolved; + char name[N]; +}; + +template +constexpr size_t align_up(size_t n) { + return (n + (N - 1)) & ~(N - 1); +} + +template +struct HookMemNames { + char chars[N]{}; + size_t len{}; +}; + +template +consteval auto make_hook_mem_names() { + using C = member_traits::Class; + // Strip the leading '&' of the stringified target expression for display. + constexpr size_t dispFrom = Disp.chars[0] == '&' ? 1 : 0; + constexpr size_t dispLen = sizeof(Disp.chars) - 1 - dispFrom; + constexpr auto vtbl = vtable_symbol(); + constexpr size_t vtblLen = std::string_view{vtbl.data()}.size(); + HookMemNames(vtblLen + 1 + dispLen + 1)> r{}; + size_t at = 0; + for (size_t i = 0; i < vtblLen; ++i) { + r.chars[at++] = vtbl[i]; + } + r.chars[at++] = '\0'; + for (size_t i = 0; i < dispLen; ++i) { + r.chars[at++] = Disp.chars[dispFrom + i]; + } + r.len = sizeof(r.chars); + return r; +} + +/* + * MSVC constant-evaluates a pointer-to-member only when every other operand in the + * initializer is a literal: no consteval calls, constexpr-object copies, or default + * member initializers. + */ +template +struct HookMemHolder { + using F = decltype(Target); + static_assert(sizeof(F) <= 16, "unsupported pointer-to-member representation"); + MOD_META_RECORD static constinit inline HookMemRecord record = { + {sizeof(HookMemRecord), MOD_META_HOOK_MEM, 0}, 0, {Target}, nullptr, + {Cs...}}; +}; + +template +struct HookFnHolder { + using F = decltype(Target); + static_assert(std::is_pointer_v && std::is_function_v>, + "hook target must be a function or member function"); + MOD_META_RECORD static constinit inline HookFnRecord record = { + {sizeof(HookFnRecord), MOD_META_HOOK_FN, 0}, 0, Target, nullptr}; +}; + +template > +struct HookRecordFor { + using Holder = HookFnHolder; +}; + +template +struct HookRecordFor { + template + struct Bind; + template + struct Bind> { + using Type = HookMemHolder().chars[Is]...>; + }; + using Holder = + Bind().len>>::Type; +}; + +template +consteval auto make_hook_name_record() { + constexpr size_t len = sizeof(Name.chars) - 1; + HookNameRecord(len + 1)> r{}; + r.rec = {sizeof(r), MOD_META_HOOK_NAME, 0}; + for (size_t i = 0; i < len; ++i) { + r.name[i] = Name.chars[i]; + } + return r; +} + +} // namespace detail +} // namespace dusk::mods diff --git a/include/mods/service.hpp b/sdk/include/mods/service.hpp similarity index 51% rename from include/mods/service.hpp rename to sdk/include/mods/service.hpp index d79b53f24d..d97d8def31 100644 --- a/include/mods/service.hpp +++ b/sdk/include/mods/service.hpp @@ -1,60 +1,17 @@ #pragma once -#include "mods/api.h" +#include +#include #include #include #include -#include namespace dusk::mods { template struct ServiceTraits; -namespace detail { - -inline std::vector& imports() { - static std::vector entries; - return entries; -} - -inline std::vector& exports() { - static std::vector entries; - return entries; -} - -inline int register_import(ServiceImport entry) { - imports().push_back(entry); - return 0; -} - -inline int register_export(ServiceExport entry) { - exports().push_back(entry); - return 0; -} - -inline const ModManifest* manifest() { - static ModManifest manifest{ - sizeof(ModManifest), - MOD_ABI_VERSION, - nullptr, - 0, - nullptr, - 0, - }; - - auto& importEntries = imports(); - auto& exportEntries = exports(); - manifest.imports = importEntries.data(); - manifest.import_count = importEntries.size(); - manifest.exports = exportEntries.data(); - manifest.export_count = exportEntries.size(); - return &manifest; -} - -} // namespace detail - inline ModResult set_error(ModError* outError, ModResult code, const char* message) { if (outError != nullptr && outError->struct_size >= sizeof(ModError)) { outError->code = code; @@ -71,33 +28,41 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa #define DEFINE_MOD() \ extern "C" { \ MOD_EXPORT ModContext* mod_ctx = nullptr; \ - MOD_EXPORT const ModManifest* mod_get_manifest(void) { \ - return ::dusk::mods::detail::manifest(); \ } \ + MOD_META_RECORD static constinit ModMetaHeader mod_meta_header_record = \ + ::dusk::mods::detail::make_header(); \ + MOD_META_BOUNDS_DEFN \ + extern "C" { \ + MOD_EXPORT constinit const ModMeta mod_meta = { \ + sizeof(ModMeta), \ + MOD_META_BOUNDS_BEGIN, \ + MOD_META_BOUNDS_END, \ + }; \ } // Declares `static const service_type* variable`, filled in by the host before mod_initialize. // Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports -// must be checked against nullptr before use. +// must be checked against nullptr before use. The unversioned macros use the latest minor version; +// set an explicit version to target an older minor version for backwards compatibility. #define IMPORT_SERVICE_EX( \ service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \ static const service_type* variable = nullptr; \ - [[maybe_unused]] static const int mod_import_registration_##variable = \ - ::dusk::mods::detail::register_import(ServiceImport{ \ - sizeof(ServiceImport), \ - (service_id_value), \ - static_cast(major_value), \ - static_cast(min_minor_value), \ - static_cast(flags_value), \ - &(variable), \ - }) + MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = { \ + {sizeof(ModMetaImport), MOD_META_IMPORT, static_cast(flags_value)}, \ + static_cast(major_value), \ + static_cast(min_minor_value), \ + &(variable), \ + ::dusk::mods::detail::make_service_id(service_id_value), \ + } #define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \ IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits::id, \ ::dusk::mods::ServiceTraits::major_version, min_minor_value, \ SERVICE_IMPORT_REQUIRED) -#define IMPORT_SERVICE(service_type, variable) IMPORT_SERVICE_VERSION(service_type, variable, 0) +#define IMPORT_SERVICE(service_type, variable) \ + IMPORT_SERVICE_VERSION( \ + service_type, variable, ::dusk::mods::ServiceTraits::minor_version) #define IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, min_minor_value) \ IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits::id, \ @@ -105,34 +70,27 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa SERVICE_IMPORT_OPTIONAL) #define IMPORT_OPTIONAL_SERVICE(service_type, variable) \ - IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, 0) + IMPORT_OPTIONAL_SERVICE_VERSION( \ + service_type, variable, ::dusk::mods::ServiceTraits::minor_version) #define EXPORT_SERVICE_AS(instance, service_id_value) \ - namespace { \ - const int mod_export_registration_##instance = \ - ::dusk::mods::detail::register_export(ServiceExport{ \ - sizeof(ServiceExport), \ - (service_id_value), \ - (instance).header.major_version, \ - (instance).header.minor_version, \ - SERVICE_EXPORT_STATIC, \ - &(instance), \ - }); \ + MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##instance = { \ + {sizeof(ModMetaExport), MOD_META_EXPORT, SERVICE_EXPORT_STATIC}, \ + (instance).header.major_version, \ + (instance).header.minor_version, \ + &(instance), \ + ::dusk::mods::detail::make_service_id(service_id_value), \ } #define EXPORT_SERVICE(instance) \ EXPORT_SERVICE_AS( \ - instance, ::dusk::mods::ServiceTraits >::id) + instance, ::dusk::mods::ServiceTraits>::id) #define EXPORT_DEFERRED_SERVICE(token, service_id_value, major_value, minor_value) \ - namespace { \ - const int mod_deferred_export_registration_##token = \ - ::dusk::mods::detail::register_export(ServiceExport{ \ - sizeof(ServiceExport), \ - (service_id_value), \ - static_cast(major_value), \ - static_cast(minor_value), \ - SERVICE_EXPORT_DEFERRED, \ - nullptr, \ - }); \ + MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##token = { \ + {sizeof(ModMetaExport), MOD_META_EXPORT, SERVICE_EXPORT_DEFERRED}, \ + static_cast(major_value), \ + static_cast(minor_value), \ + nullptr, \ + ::dusk::mods::detail::make_service_id(service_id_value), \ } diff --git a/include/mods/svc/camera.h b/sdk/include/mods/svc/camera.h similarity index 96% rename from include/mods/svc/camera.h rename to sdk/include/mods/svc/camera.h index f974cec152..ec7724a1de 100644 --- a/include/mods/svc/camera.h +++ b/sdk/include/mods/svc/camera.h @@ -1,6 +1,6 @@ #pragma once -#include "mods/api.h" +#include #define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera" #define CAMERA_SERVICE_MAJOR 1u @@ -60,5 +60,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = CAMERA_SERVICE_ID; static constexpr uint16_t major_version = CAMERA_SERVICE_MAJOR; + static constexpr uint16_t minor_version = CAMERA_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/config.h b/sdk/include/mods/svc/config.h similarity index 98% rename from include/mods/svc/config.h rename to sdk/include/mods/svc/config.h index 2a26210295..50c518bd40 100644 --- a/include/mods/svc/config.h +++ b/sdk/include/mods/svc/config.h @@ -1,6 +1,6 @@ #pragma once -#include "mods/api.h" +#include #define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config" #define CONFIG_SERVICE_MAJOR 1u @@ -103,5 +103,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = CONFIG_SERVICE_ID; static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR; + static constexpr uint16_t minor_version = CONFIG_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/game.h b/sdk/include/mods/svc/game.h similarity index 74% rename from include/mods/svc/game.h rename to sdk/include/mods/svc/game.h index fbd914d4bc..bdcf2d71f0 100644 --- a/include/mods/svc/game.h +++ b/sdk/include/mods/svc/game.h @@ -1,10 +1,10 @@ #pragma once -#include "mods/api.h" +#include /* - * Mods that link or hook game code directly must import this service; service-only and asset-only - * mods must not. + * The mod SDK imports this service automatically for mods built with FEATURES game; service-only + * and asset-only mods do not require it. * * Major version is the game-code ABI epoch: it is bumped when game-visible struct or vtable layouts * change incompatibly (e.g. a TARGET_PC field added to an existing game struct). The loader's @@ -20,11 +20,12 @@ typedef struct GameService { } GameService; #ifdef __cplusplus -#include "mods/service.hpp" +#include template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = GAME_SERVICE_ID; static constexpr uint16_t major_version = GAME_SERVICE_MAJOR; + static constexpr uint16_t minor_version = GAME_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/gfx.h b/sdk/include/mods/svc/gfx.h similarity index 97% rename from include/mods/svc/gfx.h rename to sdk/include/mods/svc/gfx.h index a9812fd044..fad1b0bf9a 100644 --- a/include/mods/svc/gfx.h +++ b/sdk/include/mods/svc/gfx.h @@ -1,6 +1,10 @@ #pragma once -#include "mods/api.h" +#include + +#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_WEBGPU) +#error "mods/svc/gfx.h requires add_mod(... FEATURES webgpu)" +#endif #include @@ -205,5 +209,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = GFX_SERVICE_ID; static constexpr uint16_t major_version = GFX_SERVICE_MAJOR; + static constexpr uint16_t minor_version = GFX_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/hook.h b/sdk/include/mods/svc/hook.h similarity index 98% rename from include/mods/svc/hook.h rename to sdk/include/mods/svc/hook.h index d1a75fa6cc..987c438fa0 100644 --- a/include/mods/svc/hook.h +++ b/sdk/include/mods/svc/hook.h @@ -1,6 +1,6 @@ #pragma once -#include "mods/api.h" +#include /* * Intercept game functions by address. Prefer the typed helpers in mods/hook.hpp @@ -121,5 +121,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = HOOK_SERVICE_ID; static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR; + static constexpr uint16_t minor_version = HOOK_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/host.h b/sdk/include/mods/svc/host.h similarity index 88% rename from include/mods/svc/host.h rename to sdk/include/mods/svc/host.h index 43c19cadfb..0f7851c7e1 100644 --- a/include/mods/svc/host.h +++ b/sdk/include/mods/svc/host.h @@ -1,6 +1,6 @@ #pragma once -#include "mods/api.h" +#include /* * The host service: the calling mod's identity and its runtime interface to the loader. @@ -9,7 +9,7 @@ #define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host" #define HOST_SERVICE_MAJOR 2u -#define HOST_SERVICE_MINOR 0u +#define HOST_SERVICE_MINOR 1u /* * Ignore unknown values: later service minors may add events. @@ -91,6 +91,16 @@ typedef struct HostService { ModResult (*watch_mod_lifecycle)( ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle); ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle); + + /* + * Read-only directory containing this platform's packaged native runtime: the mod module + * and any RUNTIME_LIBRARIES. The path is absolute and remains valid until mod_shutdown + * returns. Libraries loaded dynamically from here are owned by the mod and must be unloaded + * during mod_shutdown. + * + * Added in minor version 1. + */ + const char* (*native_dir)(ModContext* ctx); } HostService; #ifdef __cplusplus @@ -100,5 +110,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = HOST_SERVICE_ID; static constexpr uint16_t major_version = HOST_SERVICE_MAJOR; + static constexpr uint16_t minor_version = HOST_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/log.h b/sdk/include/mods/svc/log.h similarity index 93% rename from include/mods/svc/log.h rename to sdk/include/mods/svc/log.h index bad94f9503..0dbf4ec48e 100644 --- a/include/mods/svc/log.h +++ b/sdk/include/mods/svc/log.h @@ -1,6 +1,6 @@ #pragma once -#include "mods/api.h" +#include /* * Logging into the game's console and log files. Messages are attributed to the calling mod @@ -43,5 +43,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = LOG_SERVICE_ID; static constexpr uint16_t major_version = LOG_SERVICE_MAJOR; + static constexpr uint16_t minor_version = LOG_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/overlay.h b/sdk/include/mods/svc/overlay.h similarity index 95% rename from include/mods/svc/overlay.h rename to sdk/include/mods/svc/overlay.h index 9fc86fc850..02f60bd727 100644 --- a/include/mods/svc/overlay.h +++ b/sdk/include/mods/svc/overlay.h @@ -1,6 +1,6 @@ #pragma once -#include "mods/api.h" +#include #define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay" #define OVERLAY_SERVICE_MAJOR 1u @@ -53,5 +53,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = OVERLAY_SERVICE_ID; static constexpr uint16_t major_version = OVERLAY_SERVICE_MAJOR; + static constexpr uint16_t minor_version = OVERLAY_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/resource.h b/sdk/include/mods/svc/resource.h similarity index 94% rename from include/mods/svc/resource.h rename to sdk/include/mods/svc/resource.h index 1173b2ab1d..1a8251d83a 100644 --- a/include/mods/svc/resource.h +++ b/sdk/include/mods/svc/resource.h @@ -1,6 +1,6 @@ #pragma once -#include "mods/api.h" +#include /* * Read-only access to the res/ tree of the calling mod's own bundle. Reload serves the new @@ -48,5 +48,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = RESOURCE_SERVICE_ID; static constexpr uint16_t major_version = RESOURCE_SERVICE_MAJOR; + static constexpr uint16_t minor_version = RESOURCE_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/texture.h b/sdk/include/mods/svc/texture.h similarity index 97% rename from include/mods/svc/texture.h rename to sdk/include/mods/svc/texture.h index d664c5f99d..bb648cf860 100644 --- a/include/mods/svc/texture.h +++ b/sdk/include/mods/svc/texture.h @@ -1,6 +1,6 @@ #pragma once -#include "mods/api.h" +#include #define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture" #define TEXTURE_SERVICE_MAJOR 1u @@ -84,5 +84,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = TEXTURE_SERVICE_ID; static constexpr uint16_t major_version = TEXTURE_SERVICE_MAJOR; + static constexpr uint16_t minor_version = TEXTURE_SERVICE_MINOR; }; #endif diff --git a/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h similarity index 99% rename from include/mods/svc/ui.h rename to sdk/include/mods/svc/ui.h index d9f2ac669e..73df25cb47 100644 --- a/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -1,7 +1,7 @@ #pragma once -#include "mods/api.h" -#include "mods/svc/config.h" +#include +#include #define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui" #define UI_SERVICE_MAJOR 1u @@ -280,5 +280,6 @@ template <> struct dusk::mods::ServiceTraits { static constexpr const char* id = UI_SERVICE_ID; static constexpr uint16_t major_version = UI_SERVICE_MAJOR; + static constexpr uint16_t minor_version = UI_SERVICE_MINOR; }; #endif diff --git a/sdk/pseudo_reloc.cpp b/sdk/pseudo_reloc.cpp deleted file mode 100644 index f86d783f2d..0000000000 --- a/sdk/pseudo_reloc.cpp +++ /dev/null @@ -1,139 +0,0 @@ -// Mod SDK runtime (Windows): applies lld's MinGW-style runtime pseudo-relocations on -// the plain MSVC CRT. This is what lets mods reference game data (`extern` globals) with no -// __declspec(dllimport) annotations: `lld-link -lldmingw` auto-imports the data through IAT -// slots and records fixups, and this module applies them at load time. -// -// The fixup pass runs as an early CRT initializer, and the IAT slots it reads were already -// bound by the OS loader, so even mod static initializers observe patched references. - -#include - -#include -#include -#include - -extern "C" char __RUNTIME_PSEUDO_RELOC_LIST__; -extern "C" char __RUNTIME_PSEUDO_RELOC_LIST_END__; -extern "C" IMAGE_DOS_HEADER __ImageBase; - -namespace { - -struct HdrV2 { - uint32_t magic1; - uint32_t magic2; - uint32_t version; -}; -struct ItemV2 { - uint32_t sym; // RVA of the __imp_ slot (OS-bound IAT entry) - uint32_t target; // RVA of the reference to patch - uint32_t flags; // low 8 bits: bit width of the reference -}; - -bool g_relocsFailed = false; - -void report(const char* fmt, ...) { - char buf[512]; - va_list args; - va_start(args, fmt); - vsnprintf(buf, sizeof(buf), fmt, args); - va_end(args); - fprintf(stderr, "%s\n", buf); - OutputDebugStringA(buf); -} - -bool compute_fixup(const ItemV2& item, char* base, intptr_t* out) { - char* impSlot = base + item.sym; - const intptr_t real = *reinterpret_cast(impSlot); - char* target = base + item.target; - const int bits = static_cast(item.flags & 0xff); - - intptr_t reldata; - switch (bits) { - case 8: - reldata = *reinterpret_cast(target); - break; - case 16: - reldata = *reinterpret_cast(target); - break; - case 32: - reldata = *reinterpret_cast(target); - break; - case 64: - reldata = *reinterpret_cast(target); - break; - default: - report("unsupported %d-bit pseudo-relocation at RVA 0x%x", bits, item.target); - return false; - } - reldata -= reinterpret_cast(impSlot); - reldata += real; - - if (bits < 64) { - const intptr_t maxUnsigned = (intptr_t{1} << bits) - 1; - const intptr_t minSigned = -(intptr_t{1} << (bits - 1)); - if (reldata > maxUnsigned || reldata < minSigned) { - report("%d-bit data fixup at RVA 0x%x is out of range (delta %+lld)", bits, item.target, - static_cast(reldata)); - return false; - } - } - *out = reldata; - return true; -} - -} // namespace - -// lld refuses to emit runtime pseudo-relocs unless a function with exactly this (mingw CRT) -// name exists in the image. It is also our actual entry point. -extern "C" void _pei386_runtime_relocator() { - char* base = reinterpret_cast(&__ImageBase); - char* start = &__RUNTIME_PSEUDO_RELOC_LIST__; - char* end = &__RUNTIME_PSEUDO_RELOC_LIST_END__; - if (end - start < static_cast(sizeof(HdrV2))) { - return; // no data auto-imports in this mod - } - const HdrV2* hdr = reinterpret_cast(start); - if (hdr->magic1 != 0 || hdr->magic2 != 0 || hdr->version != 1) { - report("unexpected pseudo-relocation list format"); - g_relocsFailed = true; - return; - } - const ItemV2* items = reinterpret_cast(hdr + 1); - const ItemV2* itemsEnd = reinterpret_cast(end); - - // Validate everything before writing anything, so a bad fixup can't leave the image - // half-patched. - intptr_t scratch; - for (const ItemV2* it = items; it < itemsEnd; ++it) { - if (!compute_fixup(*it, base, &scratch)) { - g_relocsFailed = true; - return; - } - } - for (const ItemV2* it = items; it < itemsEnd; ++it) { - intptr_t reldata; - compute_fixup(*it, base, &reldata); - char* target = base + it->target; - const size_t len = static_cast(it->flags & 0xff) / 8; - DWORD old = 0; - if (!VirtualProtect(target, len, PAGE_EXECUTE_READWRITE, &old)) { - report("VirtualProtect failed at RVA 0x%x", it->target); - g_relocsFailed = true; - return; - } - std::memcpy(target, &reldata, len); - VirtualProtect(target, len, old, &old); - } -} - -using PVFV = void (*)(); -#pragma section(".CRT$XCB", read) -extern "C" __declspec(allocate(".CRT$XCB")) PVFV dusk_mod_pseudo_reloc_init = - _pei386_runtime_relocator; - -BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID) { - if (reason == DLL_PROCESS_ATTACH && g_relocsFailed) { - return FALSE; - } - return TRUE; -} diff --git a/sdk/src/game_feature.cpp b/sdk/src/game_feature.cpp new file mode 100644 index 0000000000..15fa16c443 --- /dev/null +++ b/sdk/src/game_feature.cpp @@ -0,0 +1,5 @@ +#include +#include + +// Linking the game feature makes the loader enforce the game ABI epoch. +IMPORT_SERVICE(GameService, svc_game); diff --git a/src/Z2AudioCS/SpkSystem.cpp b/src/Z2AudioCS/SpkSystem.cpp index ebe8c0a88c..0e91ca599c 100644 --- a/src/Z2AudioCS/SpkSystem.cpp +++ b/src/Z2AudioCS/SpkSystem.cpp @@ -1,6 +1,5 @@ #include "Z2AudioCS/SpkSystem.h" -#include "../../libs/JSystem/include/JSystem/JKernel/JKRHeap.h" #include "JSystem/JAudio2/JASGadget.h" #include "JSystem/JAudio2/JASHeapCtrl.h" #include "JSystem/JKernel/JKRHeap.h" diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 02e23fe4d2..f8b0533b3e 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -57,12 +57,12 @@ #include "dusk/settings.h" #include "res/Object/Alink.h" #include -#include #include "dusk/cosmetics/color_utils.hpp" #include "dusk/randomizer/game/flags.h" #include "dusk/randomizer/game/stages.h" #include "dusk/randomizer/game/tools.h" #endif +#include static int daAlink_Create(fopAc_ac_c* i_this); static int daAlink_Delete(daAlink_c* i_this); diff --git a/src/d/actor/d_a_alink_effect.inc b/src/d/actor/d_a_alink_effect.inc index 3f9a60855a..bc6796e78f 100644 --- a/src/d/actor/d_a_alink_effect.inc +++ b/src/d/actor/d_a_alink_effect.inc @@ -8,7 +8,7 @@ #include "d/actor/d_a_alink.h" #include "d/d_com_inf_game.h" #include "d/d_k_wmark.h" -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" DUSK_GAME_DATA const EffParamProc daAlink_c::m_fEffParamProc[] = { &daAlink_c::setEffectFrontRollParam, diff --git a/src/d/actor/d_a_bg_obj.cpp b/src/d/actor/d_a_bg_obj.cpp index 85af9a406c..da18ce4501 100644 --- a/src/d/actor/d_a_bg_obj.cpp +++ b/src/d/actor/d_a_bg_obj.cpp @@ -13,7 +13,7 @@ #include "d/actor/d_a_bg_obj.h" #include "d/actor/d_a_set_bgobj.h" #include "d/d_s_play.h" -#include "dusk/string.hpp" +#include "helpers/string.hpp" static const char* getBmdName(int param_0, int param_1) { static char l_bmdName[16]; diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index c405645320..486a035590 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -18,7 +18,7 @@ #endif #ifndef __MWERKS__ -#include "dusk/math.h" +#include "helpers/math.h" #endif static BOOL daMirror_c_createHeap(fopAc_ac_c* i_this) { diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index 43117615e5..a830f1440f 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -29,7 +29,7 @@ #include "JSystem/JAudio2/JASCriticalSection.h" #if TARGET_PC -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" #include "dusk/os.h" #include "dusk/layout.hpp" #if MOVIE_SUPPORT @@ -4592,12 +4592,3 @@ DUSK_PROFILE actor_process_profile_definition DUSK_CONST g_profile_MOVIE_PLAYER }; AUDIO_INSTANCES; - -#if TARGET_PC -void dusk::MoviePlayerShutdown() { - // We need to cleanly shut down the threads to avoid crashes on shutdown. - if (daMP_c::m_myObj) { - daMP_c::m_myObj->daMP_c_Finish(); - } -} -#endif \ No newline at end of file diff --git a/src/d/actor/d_a_npc_hanjo.cpp b/src/d/actor/d_a_npc_hanjo.cpp index ede59f65b5..cbd3d9dcf9 100644 --- a/src/d/actor/d_a_npc_hanjo.cpp +++ b/src/d/actor/d_a_npc_hanjo.cpp @@ -15,7 +15,7 @@ #include "Z2AudioLib/Z2Instances.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" static DUSK_CONSTEXPR int l_bmdData[4][2] = { {14, 1}, {26, 2}, diff --git a/src/d/actor/d_a_npc_saru.cpp b/src/d/actor/d_a_npc_saru.cpp index d3690b833a..7f3f9e1126 100644 --- a/src/d/actor/d_a_npc_saru.cpp +++ b/src/d/actor/d_a_npc_saru.cpp @@ -11,7 +11,7 @@ #include "d/actor/d_a_e_ym.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" enum saru_TW_RES_File_ID { /* BMDR */ diff --git a/src/d/actor/d_a_npc_shop0.cpp b/src/d/actor/d_a_npc_shop0.cpp index b24e372aaf..e61ea44f4e 100644 --- a/src/d/actor/d_a_npc_shop0.cpp +++ b/src/d/actor/d_a_npc_shop0.cpp @@ -8,7 +8,7 @@ #include "d/actor/d_a_npc_shop0.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" static int createHeapCallBack(fopAc_ac_c* i_this) { return static_cast(i_this)->createHeap(); diff --git a/src/d/actor/d_a_npc_yelia.cpp b/src/d/actor/d_a_npc_yelia.cpp index 84fa2b3ec9..8321fb7492 100644 --- a/src/d/actor/d_a_npc_yelia.cpp +++ b/src/d/actor/d_a_npc_yelia.cpp @@ -10,10 +10,10 @@ #include #if TARGET_PC -#include "dusk/string.hpp" #include "dusk/randomizer/game/randomizer_context.hpp" #include "dusk/randomizer/game/tools.h" #endif +#include "helpers/string.hpp" static DUSK_CONSTEXPR daNpc_GetParam1 l_bmdData[3] = { {3, 1}, diff --git a/src/d/actor/d_a_npc_ykw.cpp b/src/d/actor/d_a_npc_ykw.cpp index 155c9e61aa..4e3573687c 100644 --- a/src/d/actor/d_a_npc_ykw.cpp +++ b/src/d/actor/d_a_npc_ykw.cpp @@ -21,10 +21,10 @@ #include #if TARGET_PC -#include "dusk/string.hpp" #include "dusk/randomizer/game/randomizer_context.hpp" #include "dusk/randomizer/game/verify_item_functions.h" #endif +#include "helpers/string.hpp" #if DEBUG class daNpc_ykW_HIO_c : public mDoHIO_entry_c { diff --git a/src/d/actor/d_a_obj_gra2.cpp b/src/d/actor/d_a_obj_gra2.cpp index faef741c27..a5fa226ee7 100644 --- a/src/d/actor/d_a_obj_gra2.cpp +++ b/src/d/actor/d_a_obj_gra2.cpp @@ -12,7 +12,7 @@ #include "d/d_cc_uty.h" #include "d/d_com_inf_actor.h" #include "d/d_com_inf_game.h" -#include "dusk/string.hpp" +#include "helpers/string.hpp" #if DEBUG #include "d/d_debug_viewer.h" #endif diff --git a/src/d/actor/d_a_obj_lv4chandelier.cpp b/src/d/actor/d_a_obj_lv4chandelier.cpp index 9ab47345d4..7847b37358 100644 --- a/src/d/actor/d_a_obj_lv4chandelier.cpp +++ b/src/d/actor/d_a_obj_lv4chandelier.cpp @@ -13,7 +13,7 @@ #include "d/d_cc_uty.h" #ifndef __MWERKS__ -#include "dusk/math.h" +#include "helpers/math.h" #endif #if DEBUG diff --git a/src/d/actor/d_a_obj_sekizoa.cpp b/src/d/actor/d_a_obj_sekizoa.cpp index 5170fbca4e..f8493ec385 100644 --- a/src/d/actor/d_a_obj_sekizoa.cpp +++ b/src/d/actor/d_a_obj_sekizoa.cpp @@ -9,7 +9,7 @@ #include #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" #include "f_op/f_op_actor_mng.h" #include "f_op/f_op_msg.h" diff --git a/src/d/actor/d_a_tag_evt.cpp b/src/d/actor/d_a_tag_evt.cpp index 6f1de5a71c..780fc1d38c 100644 --- a/src/d/actor/d_a_tag_evt.cpp +++ b/src/d/actor/d_a_tag_evt.cpp @@ -8,7 +8,7 @@ #include "f_op/f_op_actor_mng.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" static DUSK_CONST char* l_evtNameList[] = { NULL, diff --git a/src/d/actor/d_a_tag_msg.cpp b/src/d/actor/d_a_tag_msg.cpp index cf8db83f8f..e023319a9f 100644 --- a/src/d/actor/d_a_tag_msg.cpp +++ b/src/d/actor/d_a_tag_msg.cpp @@ -11,7 +11,7 @@ #include "d/d_debug_viewer.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" static int createHeapCallBack(fopAc_ac_c* i_this) { daTag_Msg_c* msg = (daTag_Msg_c*)i_this; diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 54b1e022cc..bef7848c24 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -19,7 +19,7 @@ static u8* l_J_Ohana00_64TEX_get() { static u8 buf[0x800]; static bool _ = (dusk // from d_grass.inc static MtxP get_model_mtx(Mtx modelMtx, Mtx storage); static void transform_positions( - const dusk::batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos); + const batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos); static void split_batch(u32& emitted, u32 vtxCount); #else #include "assets/l_J_Ohana00_64TEX.h" @@ -595,11 +595,11 @@ dFlower_packet_c::dFlower_packet_c() { l_J_Ohana01_64128_0419TEX__width + 1, l_J_Ohana01_64128_0419TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE ); - dusk::batch::decode_leaf_template(l_J_hana00DL, 0x140, mTplHana00); - dusk::batch::decode_leaf_template(l_J_hana00_cDL, 0xC0, mTplHana00Cut); - dusk::batch::decode_leaf_template(l_J_hana01DL, 0x120, mTplHana01); - dusk::batch::decode_leaf_template(l_J_hana01_c_00DL, 0xC0, mTplHana01Cut00); - dusk::batch::decode_leaf_template(l_J_hana01_c_01DL, 0x120, mTplHana01Cut); + batch::decode_leaf_template(l_J_hana00DL, 0x140, mTplHana00); + batch::decode_leaf_template(l_J_hana00_cDL, 0xC0, mTplHana00Cut); + batch::decode_leaf_template(l_J_hana01DL, 0x120, mTplHana01); + batch::decode_leaf_template(l_J_hana01_c_00DL, 0xC0, mTplHana01Cut00); + batch::decode_leaf_template(l_J_hana01_c_01DL, 0x120, mTplHana01Cut); #endif m_deleteRoom = &dFlower_packet_c::deleteRoom; @@ -757,9 +757,9 @@ static GXColor hana01_amb_color(int idx, const dKy_tevstr_c* tevstr) { return amb; } -static void flower_emit(const dusk::batch::LeafTemplate& tpl, const Vec* xformedPos, GXColor amb) { +static void flower_emit(const batch::LeafTemplate& tpl, const Vec* xformedPos, GXColor amb) { for (u32 i = 0; i < tpl.vtxCount; i++) { - const dusk::batch::LeafTemplate::Vtx& v = tpl.vtx[i]; + const batch::LeafTemplate::Vtx& v = tpl.vtx[i]; const Vec& p = xformedPos[v.pos]; GXPosition3f32(p.x, p.y, p.z); GXNormal1x8(v.nrm); @@ -852,7 +852,7 @@ void dFlower_packet_c::draw() { for (int bucket = 0; bucket < 2; bucket++) { const bool cut = bucket != 0; - const dusk::batch::LeafTemplate& tpl = cut ? mTplHana00Cut : mTplHana00; + const batch::LeafTemplate& tpl = cut ? mTplHana00Cut : mTplHana00; bool open = false; u32 emitted = 0; @@ -931,10 +931,10 @@ void dFlower_packet_c::draw() { GXLoadPosMtxImm(identity, GX_PNMTX0); GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); - const dusk::batch::LeafTemplate* const buckets[3] = { + const batch::LeafTemplate* const buckets[3] = { &mTplHana01, &mTplHana01Cut00, &mTplHana01Cut}; for (int bucket = 0; bucket < 3; bucket++) { - const dusk::batch::LeafTemplate& tpl = *buckets[bucket]; + const batch::LeafTemplate& tpl = *buckets[bucket]; bool open = false; u32 emitted = 0; diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index 9e8c360518..d2c212f043 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -513,9 +513,9 @@ dGrass_packet_c::dGrass_packet_c() { field_0x1d714 = 0; #if TARGET_PC - dusk::batch::decode_leaf_template(mp_Mkusa_9q_DL, m_Mkusa_9q_DL_size, mTplKusa9q); - dusk::batch::decode_leaf_template(mp_Mkusa_9q_cDL, m_Mkusa_9q_cDL_size, mTplKusa9qCut); - dusk::batch::decode_leaf_template(l_M_TenGusaDL, 0xC0, mTplTengusa); + batch::decode_leaf_template(mp_Mkusa_9q_DL, m_Mkusa_9q_DL_size, mTplKusa9q); + batch::decode_leaf_template(mp_Mkusa_9q_cDL, m_Mkusa_9q_cDL_size, mTplKusa9qCut); + batch::decode_leaf_template(l_M_TenGusaDL, 0xC0, mTplTengusa); #endif OS_REPORT("草群メモリ=%fK\n", 117.7734375f); @@ -533,7 +533,7 @@ static MtxP get_model_mtx(Mtx modelMtx, Mtx storage) { } static void transform_positions( - const dusk::batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos) { + const batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos) { for (u32 i = 0; i < tpl.posRefCount; i++) { const u8 idx = tpl.posRefs[i]; MTXMultVec(mtx, &posArray[idx], &xfPos[idx]); @@ -609,10 +609,10 @@ static GXColor blade_amb_color(const dGrass_data_c* blade, const dKy_tevstr_c* t return amb; } -static void blade_emit(const dusk::batch::LeafTemplate& tpl, const Vec* xformedPos, +static void blade_emit(const batch::LeafTemplate& tpl, const Vec* xformedPos, const GXColor* colors, GXColor amb) { for (u32 i = 0; i < tpl.vtxCount; i++) { - const dusk::batch::LeafTemplate::Vtx& v = tpl.vtx[i]; + const batch::LeafTemplate::Vtx& v = tpl.vtx[i]; const Vec& p = xformedPos[v.pos]; GXPosition3f32(p.x, p.y, p.z); GXNormal1x8(v.nrm); @@ -765,7 +765,7 @@ void dGrass_packet_c::draw() { for (int bucket = 0; bucket < 4; bucket++) { const bool kusaTex = bucket < 2; const bool cut = (bucket & 1) != 0; - const dusk::batch::LeafTemplate& tpl = + const batch::LeafTemplate& tpl = cut ? mTplKusa9qCut : (kusaTex ? mTplKusa9q : mTplTengusa); bool open = false; diff --git a/src/d/d_bg_parts.cpp b/src/d/d_bg_parts.cpp index c7d94551b9..07c91f8807 100644 --- a/src/d/d_bg_parts.cpp +++ b/src/d/d_bg_parts.cpp @@ -8,7 +8,7 @@ #include "JSystem/JKernel/JKRSolidHeap.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" void dBgp_c::material_c::draw() { material_c* material = this; diff --git a/src/d/d_bg_s.cpp b/src/d/d_bg_s.cpp index f48c58fead..51811d9538 100644 --- a/src/d/d_bg_s.cpp +++ b/src/d/d_bg_s.cpp @@ -15,7 +15,7 @@ #include "d/d_bg_s_capt_poly.h" #if TARGET_PC -#include "dusk/offset_ptr.h" +#include "helpers/offset_ptr.h" #include "dusk/settings.h" #endif diff --git a/src/d/d_com_inf_game.cpp b/src/d/d_com_inf_game.cpp index cf8a5eeb78..ff3f24816a 100644 --- a/src/d/d_com_inf_game.cpp +++ b/src/d/d_com_inf_game.cpp @@ -26,8 +26,10 @@ #include #include +#include "helpers/string.hpp" + #if TARGET_PC -#include "dusk/string.hpp" +#include "dusk/settings.h" #include "dusk/logging.h" #include "dusk/randomizer/game/tools.h" #include "dusk/randomizer/game/stages.h" @@ -35,7 +37,6 @@ #include "dusk/randomizer/game/randomizer_context.hpp" #endif - void dComIfG_play_c::ct() { mWindowNum = 0; mParticle = NULL; @@ -3727,3 +3728,19 @@ u8 dComIfGs_staffroll_next_go_check() { DUSK_GAME_DATA GXColor g_whiteColor = {255, 255, 255, 255}; DUSK_GAME_DATA GXColor g_saftyWhiteColor = {160, 160, 160, 255}; + +#if TARGET_PC +void dComIfGd_drawXluListInvisible() { + ZoneScoped; + if (!dusk::getSettings().game.disableWaterRefraction) { + g_dComIfG_gameInfo.drawlist.drawXluListInvisible(); + } +} + +void dComIfGd_drawOpaListInvisible() { + ZoneScoped; + if (!dusk::getSettings().game.disableWaterRefraction) { + g_dComIfG_gameInfo.drawlist.drawOpaListInvisible(); + } +} +#endif diff --git a/src/d/d_cursor_mng.cpp b/src/d/d_cursor_mng.cpp index 230b4e740e..b99ec8f4db 100644 --- a/src/d/d_cursor_mng.cpp +++ b/src/d/d_cursor_mng.cpp @@ -1,10 +1,7 @@ #include "JSystem/JKernel/JKRHeap.h" #include "d/dolzel.h" // IWYU pragma: keep - #include "d/d_cursor_mng.h" - -#include "../../libs/JSystem/include/JSystem/JKernel/JKRHeap.h" #include "d/d_com_inf_game.h" dCsr_mng_c* dCsr_mng_c::m_myObj; diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index 438e910bff..8552ead16e 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -20,7 +20,7 @@ #include "absl/container/flat_hash_map.h" #include "client/TracyScoped.hpp" #include "dusk/frame_interpolation.h" -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" #include "dusk/logging.h" static const void* getInterpKey(const void* base, int idx) { diff --git a/src/d/d_ev_camera.cpp b/src/d/d_ev_camera.cpp index 67cda5863b..2650c2d05e 100644 --- a/src/d/d_ev_camera.cpp +++ b/src/d/d_ev_camera.cpp @@ -13,7 +13,7 @@ #include "d/actor/d_a_alink.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" #ifdef __MWERKS__ #define LOAD_4BYTE_STRING_LITERAL(x) (*(u32*)(x)) diff --git a/src/d/d_event.cpp b/src/d/d_event.cpp index fbe3aa882c..f22c945d3b 100644 --- a/src/d/d_event.cpp +++ b/src/d/d_event.cpp @@ -15,8 +15,8 @@ #if TARGET_PC #include "dusk/randomizer/game/randomizer_context.hpp" -#include "dusk/string.hpp" #endif +#include "helpers/string.hpp" namespace { static u8 event_debug_evnt() { diff --git a/src/d/d_event_manager.cpp b/src/d/d_event_manager.cpp index 4e377e79d5..c363192813 100644 --- a/src/d/d_event_manager.cpp +++ b/src/d/d_event_manager.cpp @@ -17,10 +17,11 @@ #if TARGET_PC #include "dusk/randomizer/game/verify_item_functions.h" #include "dusk/randomizer/game/randomizer_context.hpp" -#include "dusk/string.hpp" #endif +#include "helpers/string.hpp" + #if DEBUG static dEvM_HIO_c l_HIO; #endif diff --git a/src/d/d_file_sel_info.cpp b/src/d/d_file_sel_info.cpp index f99ff7ef4c..9952e27b36 100644 --- a/src/d/d_file_sel_info.cpp +++ b/src/d/d_file_sel_info.cpp @@ -14,7 +14,7 @@ #include #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" #include "dusk/version.hpp" dFile_info_c::dFile_info_c(JKRArchive* i_archive, u8 param_1) { @@ -122,12 +122,12 @@ int dFile_info_c::setSaveData(dSv_save_c* i_savedata, BOOL i_validChksum, u8 i_d if (!curFileSeedHash.empty()) { // Overwrite "Save time" text with "Randomizer" auto saveTimeText = (J2DTextBox*)mFileInfo.Scr->search(MULTI_CHAR('f_s_t_02')); - dusk::SafeStringCopy(saveTimeText->getStringPtr(), "Randomizer"); + SafeStringCopy(saveTimeText->getStringPtr(), "Randomizer"); saveTimeText->setHBinding(J2DTextBoxHBinding::HBIND_LEFT); // Overwrite the "Total play time" text with the seed hash auto playTimeText = (J2DTextBox*)mFileInfo.Scr->search(MULTI_CHAR('f_p_t_02')); - dusk::SafeStringCopy(playTimeText->getStringPtr(), curFileSeedHash.c_str()); + SafeStringCopy(playTimeText->getStringPtr(), curFileSeedHash.c_str()); // Give the text double the space on the menu incase the seed hash is long playTimeText->setHBinding(J2DTextBoxHBinding::HBIND_LEFT); diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index dbacbdcc77..b77bb8c0de 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -27,10 +27,10 @@ #include "dusk/config.hpp" #include "dusk/menu_pointer.h" #include "dusk/randomizer/game/randomizer_context.hpp" -#include "dusk/string.hpp" #include "dusk/ui/modal.hpp" #include "dusk/ui/rando_config.hpp" #include "dusk/ui/ui.hpp" +#include "helpers/string.hpp" namespace { constexpr u8 pointer_target(u8 group, u8 index) noexcept { diff --git a/src/d/d_gameover.cpp b/src/d/d_gameover.cpp index f4ae6c7457..1cdaccac5d 100644 --- a/src/d/d_gameover.cpp +++ b/src/d/d_gameover.cpp @@ -15,9 +15,9 @@ #include #if TARGET_PC -#include "dusk/gx_helper.h" #include "dusk/randomizer/game/randomizer_context.hpp" #endif +#include "helpers/gx_helper.h" class dGov_HIO_c : public mDoHIO_entry_c { public: diff --git a/src/d/d_menu_calibration.cpp b/src/d/d_menu_calibration.cpp index bc256547d7..23f81121bc 100644 --- a/src/d/d_menu_calibration.cpp +++ b/src/d/d_menu_calibration.cpp @@ -14,7 +14,7 @@ #include "m_Do/m_Do_controller_pad.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" // Need 0xC bytes of padding with no symbol between dMenu_Calibration_c::__vtable and the end of // .data diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index 17d581eeab..36348b4387 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -26,7 +26,11 @@ #include "m_Do/m_Do_graphic.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" + +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif #if (PLATFORM_WII || PLATFORM_SHIELD) #define POINTER_OPT dComIfGs_getOptPointer() diff --git a/src/d/d_menu_fishing.cpp b/src/d/d_menu_fishing.cpp index 75ba679df7..2af4291525 100644 --- a/src/d/d_menu_fishing.cpp +++ b/src/d/d_menu_fishing.cpp @@ -16,7 +16,7 @@ #include "m_Do/m_Do_graphic.h" #include -#include "dusk/string.hpp" +#include "helpers/string.hpp" #include "dusk/version.hpp" typedef void (dMenu_Fishing_c::*initFunc)(); diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp index 7b9558c1ce..7db489e51b 100644 --- a/src/d/d_menu_fmap.cpp +++ b/src/d/d_menu_fmap.cpp @@ -21,10 +21,14 @@ #include "d/d_msg_object.h" #include "d/d_msg_scrn_explain.h" #include "d/d_stage.h" -#include "dusk/memory.h" -#include "dusk/string.hpp" +#include "helpers/string.hpp" #include "f_op/f_op_msg_mng.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#include "dusk/memory.h" +#endif + static dMf_HIO_c g_fmHIO; static dMenu_Fmap_c::process init_process[30] = { diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index bbff0fb113..f78acb913b 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -29,6 +29,7 @@ #include #if TARGET_PC +#include "dusk/frame_interpolation.h" #include "dusk/game_clock.h" #include "dusk/menu_pointer.h" #include "dusk/settings.h" diff --git a/src/d/d_meter2_info.cpp b/src/d/d_meter2_info.cpp index 6ae13be1a0..38af28a058 100644 --- a/src/d/d_meter2_info.cpp +++ b/src/d/d_meter2_info.cpp @@ -17,9 +17,9 @@ #if TARGET_PC #include "dusk/randomizer/game/randomizer_context.hpp" -#include "dusk/string.hpp" #include "battery/embed.hpp" #endif +#include "helpers/string.hpp" enum ITEMICON_RES_FILE_ID { ITEMICON_BTI_ARI_MESU_00=0x3, diff --git a/src/d/d_meter_button.cpp b/src/d/d_meter_button.cpp index 1f47969f47..0e3354e76e 100644 --- a/src/d/d_meter_button.cpp +++ b/src/d/d_meter_button.cpp @@ -18,11 +18,10 @@ #include "d/d_pane_class.h" #include "dusk/frame_interpolation.h" #include -#if TARGET_PC -#include "dusk/string.hpp" -#endif -#include "dusk/string.hpp" +#if TARGET_PC +#include "helpers/string.hpp" +#endif #if VERSION == VERSION_GCN_JPN #define STR_BUF_LEN 528 diff --git a/src/d/d_model.cpp b/src/d/d_model.cpp index f1e58f2961..bdff472367 100644 --- a/src/d/d_model.cpp +++ b/src/d/d_model.cpp @@ -5,6 +5,10 @@ #include "JSystem/J3DGraphBase/J3DMaterial.h" #include "d/d_com_inf_game.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + void dMdl_c::draw() { j3dSys.setVtxPos(mpModelData->getVtxPosArray(), mpModelData->getVtxNum()); j3dSys.setVtxNrm(mpModelData->getVtxNrmArray(), mpModelData->getNrmNum()); diff --git a/src/d/d_particle.cpp b/src/d/d_particle.cpp index 4bcdc7af5b..e33de068db 100644 --- a/src/d/d_particle.cpp +++ b/src/d/d_particle.cpp @@ -26,8 +26,12 @@ #include "m_Do/m_Do_lib.h" #include "tracy/Tracy.hpp" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + #ifndef __MWERKS__ -#include "dusk/math.h" +#include "helpers/math.h" #endif #if DEBUG diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index dd08dbc3cb..b96164674f 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -41,8 +41,8 @@ #include "dusk/settings.h" #include -#include "dusk/string.hpp" -#define strcpy dusk::SafeStringCopy +#include "helpers/string.hpp" +#define strcpy SafeStringCopy #endif static u8 dSv_item_rename(u8 i_itemNo) { diff --git a/src/d/d_stage.cpp b/src/d/d_stage.cpp index 9f128963c8..2e22b5e3b4 100644 --- a/src/d/d_stage.cpp +++ b/src/d/d_stage.cpp @@ -25,11 +25,13 @@ #if TARGET_PC #include "dusk/logging.h" -#include "dusk/string.hpp" #include "dusk/randomizer/game/randomizer_context.hpp" #include "dusk/randomizer/game/flags.h" #include "dusk/randomizer/game/stages.h" #include "dusk/randomizer/game/tools.h" +#endif +#include "helpers/string.hpp" +#if TARGET_PC #include #include #endif @@ -162,7 +164,7 @@ void dStage_startStage_c::set(const char* i_Name, s8 i_RoomNo, s16 i_Point, s8 i #if TARGET_PC // UB fix. if (mName != i_Name) { - dusk::SafeStringCopy(mName, i_Name); + SafeStringCopy(mName, i_Name); } #else strcpy(mName, i_Name); diff --git a/src/dusk/OSReport.cpp b/src/dusk/OSReport.cpp index 422b88f03c..78bfd652c9 100644 --- a/src/dusk/OSReport.cpp +++ b/src/dusk/OSReport.cpp @@ -1,6 +1,7 @@ #include #include "aurora/lib/logging.hpp" +#include "dusk/os.h" #include "os_report.h" aurora::Module Log("dusk::osReport"); @@ -143,4 +144,4 @@ void OSAttention(const char* fmt, ...) { va_start(args, fmt); OSVAttention(fmt, args); va_end(args); -} \ No newline at end of file +} diff --git a/include/dusk/achievements.h b/src/dusk/achievements.h similarity index 100% rename from include/dusk/achievements.h rename to src/dusk/achievements.h diff --git a/include/dusk/action_bindings.h b/src/dusk/action_bindings.h similarity index 100% rename from include/dusk/action_bindings.h rename to src/dusk/action_bindings.h diff --git a/include/dusk/app_info.hpp b/src/dusk/app_info.hpp similarity index 86% rename from include/dusk/app_info.hpp rename to src/dusk/app_info.hpp index e08fe680d9..bb8c15856e 100644 --- a/include/dusk/app_info.hpp +++ b/src/dusk/app_info.hpp @@ -1,5 +1,4 @@ -#ifndef DUSK_APPNAME_HPP -#define DUSK_APPNAME_HPP +#pragma once namespace dusk { /** @@ -21,5 +20,3 @@ namespace dusk { */ constexpr auto OrgName = "TwilitRealm"; } - -#endif // DUSK_APPNAME_HPP diff --git a/include/dusk/audio.h b/src/dusk/audio.h similarity index 72% rename from include/dusk/audio.h rename to src/dusk/audio.h index e3dfda9f52..0f247689f6 100644 --- a/include/dusk/audio.h +++ b/src/dusk/audio.h @@ -1,5 +1,4 @@ -#ifndef DUSK_AUDIO_H -#define DUSK_AUDIO_H +#pragma once #if TARGET_PC #define DUSK_AUDIO_DISABLED 0 @@ -12,5 +11,3 @@ #else #define DUSK_AUDIO_SKIP(...) #endif - -#endif // DUSK_AUDIO_H diff --git a/include/dusk/audio/DuskAudioSystem.h b/src/dusk/audio/DuskAudioSystem.h similarity index 100% rename from include/dusk/audio/DuskAudioSystem.h rename to src/dusk/audio/DuskAudioSystem.h diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index ca4d079e92..1f304d4b3e 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -10,11 +10,11 @@ #include #include "Adpcm.hpp" -#include "freeverb/revmodel.hpp" #include "dusk/audio/DuskAudioSystem.h" -#include "dusk/endian.h" #include "dusk/logging.h" +#include "freeverb/revmodel.hpp" #include "global.h" +#include "helpers/endian.h" #include "tracy/Tracy.hpp" using namespace dusk::audio; diff --git a/include/dusk/autosave.h b/src/dusk/autosave.h similarity index 86% rename from include/dusk/autosave.h rename to src/dusk/autosave.h index c32cd1e22b..2ab0887295 100644 --- a/include/dusk/autosave.h +++ b/src/dusk/autosave.h @@ -1,8 +1,5 @@ #pragma once -#ifndef AUTOSAVE_H -#define AUTOSAVE_H - #include #include #include @@ -15,5 +12,3 @@ void autoSaving(); void waitingForWrite(); void endAutoSave(); void toggleAutoSave(bool enabled); - -#endif \ No newline at end of file diff --git a/include/dusk/config.hpp b/src/dusk/config.hpp similarity index 99% rename from include/dusk/config.hpp rename to src/dusk/config.hpp index 6d36dff1f3..53591437db 100644 --- a/include/dusk/config.hpp +++ b/src/dusk/config.hpp @@ -1,5 +1,4 @@ -#ifndef DUSK_CONFIG_HPP -#define DUSK_CONFIG_HPP +#pragma once #include #include @@ -193,5 +192,3 @@ const ConfigImplBase* GetConfigImpl() { } } // namespace dusk::config - -#endif diff --git a/include/dusk/config_var.hpp b/src/dusk/config_var.hpp similarity index 99% rename from include/dusk/config_var.hpp rename to src/dusk/config_var.hpp index 52e62172cb..c4d3ac1d6b 100644 --- a/include/dusk/config_var.hpp +++ b/src/dusk/config_var.hpp @@ -1,5 +1,4 @@ -#ifndef DUSK_CONFIG_VAR_HPP -#define DUSK_CONFIG_VAR_HPP +#pragma once #include "dolphin/types.h" #include @@ -386,5 +385,3 @@ private: using ActionBindConfigVar = ConfigVar; } - -#endif // DUSK_CONFIG_VAR_HPP diff --git a/include/dusk/crash_handler.h b/src/dusk/crash_handler.h similarity index 100% rename from include/dusk/crash_handler.h rename to src/dusk/crash_handler.h diff --git a/include/dusk/crash_reporting.h b/src/dusk/crash_reporting.h similarity index 100% rename from include/dusk/crash_reporting.h rename to src/dusk/crash_reporting.h diff --git a/src/dusk/data.cpp b/src/dusk/data.cpp index 69ee5bebc8..48517b7308 100644 --- a/src/dusk/data.cpp +++ b/src/dusk/data.cpp @@ -1005,4 +1005,50 @@ Paths initialize_data() { }; } +std::filesystem::path user_home_path() { + const char* homePath = SDL_GetUserFolder(SDL_FOLDER_HOME); + if (homePath == nullptr || homePath[0] == '\0') { + return {}; + } + return std::filesystem::path{reinterpret_cast(homePath)}; +} + +std::filesystem::path normalized_display_path(const std::filesystem::path& path) { + std::error_code ec; + auto normalized = std::filesystem::weakly_canonical(path, ec); + if (!ec) { + return normalized; + } + + normalized = std::filesystem::absolute(path, ec); + if (!ec) { + return normalized.lexically_normal(); + } + + return path.lexically_normal(); +} + +std::string abbreviated_path_string(const std::filesystem::path& path) { + const auto homePath = user_home_path(); + if (path.empty() || homePath.empty()) { + return io::fs_path_to_string(path); + } + + const auto normalizedPath = normalized_display_path(path); + const auto normalizedHome = normalized_display_path(homePath); + if (normalizedPath == normalizedHome) { + return "~"; + } + + const auto relativePath = normalizedPath.lexically_relative(normalizedHome); + if (!relativePath.empty() && !relativePath.is_absolute()) { + const auto it = relativePath.begin(); + if (it == relativePath.end() || *it != "..") { + return io::fs_path_to_string(std::filesystem::path{"~"} / relativePath); + } + } + + return io::fs_path_to_string(path); +} + } // namespace dusk::data diff --git a/src/dusk/data.hpp b/src/dusk/data.hpp index 7e7861dbf0..a2f9313852 100644 --- a/src/dusk/data.hpp +++ b/src/dusk/data.hpp @@ -39,5 +39,8 @@ bool set_portable_data_path(); bool reset_data_path(); bool is_default_data_path(); bool is_data_path_restart_pending(); +std::filesystem::path user_home_path(); +std::filesystem::path normalized_display_path(const std::filesystem::path& path); +std::string abbreviated_path_string(const std::filesystem::path& path); } // namespace dusk::data diff --git a/include/dusk/discord_presence.hpp b/src/dusk/discord_presence.hpp similarity index 100% rename from include/dusk/discord_presence.hpp rename to src/dusk/discord_presence.hpp diff --git a/include/dusk/dusk.h b/src/dusk/dusk.h similarity index 87% rename from include/dusk/dusk.h rename to src/dusk/dusk.h index 911ddbb535..b95b33515e 100644 --- a/include/dusk/dusk.h +++ b/src/dusk/dusk.h @@ -1,5 +1,4 @@ -#ifndef DUSK_DUSK_H -#define DUSK_DUSK_H +#pragma once #include @@ -19,5 +18,3 @@ constexpr u32 defaultAspectRatioW = 19; constexpr u32 defaultAspectRatioH = 14; static_assert(defaultWindowWidth / defaultAspectRatioW == defaultWindowHeight / defaultAspectRatioH); - -#endif // DUSK_DUSK_H diff --git a/src/dusk/dvd_asset.cpp b/src/dusk/dvd_asset.cpp index 75c82107cc..f5c8e73d4d 100644 --- a/src/dusk/dvd_asset.cpp +++ b/src/dusk/dvd_asset.cpp @@ -1,6 +1,6 @@ #include "dusk/dvd_asset.hpp" #include "dusk/logging.h" -#include "dusk/endian.h" +#include "helpers/endian.h" #include "dolphin/dvd.h" #include "DynamicLink.h" #include "JSystem/JKernel/JKRArchive.h" diff --git a/include/dusk/dvd_asset.hpp b/src/dusk/dvd_asset.hpp similarity index 100% rename from include/dusk/dvd_asset.hpp rename to src/dusk/dvd_asset.hpp diff --git a/include/dusk/extras.h b/src/dusk/extras.h similarity index 73% rename from include/dusk/extras.h rename to src/dusk/extras.h index 562ca051c1..8a3d633a3f 100644 --- a/include/dusk/extras.h +++ b/src/dusk/extras.h @@ -1,5 +1,4 @@ -#ifndef _SRC_EXTRAS_H_ -#define _SRC_EXTRAS_H_ +#pragma once #ifdef __cplusplus extern "C" { @@ -13,5 +12,3 @@ int stricmp(const char* str1, const char* str2); #ifdef __cplusplus } #endif - -#endif // _SRC_EXTRAS_H_ diff --git a/include/dusk/frame_interpolation.h b/src/dusk/frame_interpolation.h similarity index 100% rename from include/dusk/frame_interpolation.h rename to src/dusk/frame_interpolation.h diff --git a/include/dusk/game_clock.h b/src/dusk/game_clock.h similarity index 100% rename from include/dusk/game_clock.h rename to src/dusk/game_clock.h diff --git a/include/dusk/gamepad_color.h b/src/dusk/gamepad_color.h similarity index 66% rename from include/dusk/gamepad_color.h rename to src/dusk/gamepad_color.h index 0524b0b98d..9e3e35a84e 100644 --- a/include/dusk/gamepad_color.h +++ b/src/dusk/gamepad_color.h @@ -1,11 +1,6 @@ #pragma once -#ifndef GAMEPAD_COLOR_H -#define GAMEPAD_COLOR_H - namespace dusk::input { void handleGamepadColor(); bool pad_has_led(int port) noexcept; } - -#endif \ No newline at end of file diff --git a/include/dusk/gfx.hpp b/src/dusk/gfx.hpp similarity index 100% rename from include/dusk/gfx.hpp rename to src/dusk/gfx.hpp diff --git a/src/dusk/globals.cpp b/src/dusk/globals.cpp index 49ab54ccbe..591ece679f 100644 --- a/src/dusk/globals.cpp +++ b/src/dusk/globals.cpp @@ -1,6 +1,25 @@ #include #include #include +#include "dusk/dusk.h" +#include "dusk/main.h" + +bool dusk::IsRunning = true; +bool dusk::IsShuttingDown = false; +bool dusk::IsGameLaunched = false; +bool dusk::RestartRequested = false; +uint8_t dusk::SaveRequested = 0; +dusk::StageRequest dusk::StageRequested{"", false}; +std::filesystem::path dusk::ConfigPath; +std::filesystem::path dusk::CachePath; +AuroraStats dusk::lastFrameAuroraStats; +float dusk::frameUsagePct = 0.0f; + +void dusk::RequestRestart() noexcept { + RestartRequested = SupportsProcessRestart; + IsRunning = false; +} + u8 g_printOtherHeapDebug; dKankyo_HIO_c g_kankyoHIO; diff --git a/include/dusk/gyro.h b/src/dusk/gyro.h similarity index 100% rename from include/dusk/gyro.h rename to src/dusk/gyro.h diff --git a/include/dusk/hotkeys.h b/src/dusk/hotkeys.h similarity index 89% rename from include/dusk/hotkeys.h rename to src/dusk/hotkeys.h index 7a774bde80..bdf1778c3f 100644 --- a/include/dusk/hotkeys.h +++ b/src/dusk/hotkeys.h @@ -1,5 +1,4 @@ -#ifndef DUSK_HOTKEYS_H -#define DUSK_HOTKEYS_H +#pragma once namespace dusk::hotkeys { @@ -23,5 +22,3 @@ constexpr const char* TOGGLE_FULLSCREEN = "F11"; constexpr const char* TURBO = "Tab"; } - -#endif // DUSK_HOTKEYS_H diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index cf5b22a270..b9ad5eb021 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -15,6 +15,7 @@ #include "dusk/data.hpp" #include "dusk/dusk.h" #include "dusk/main.h" +#include "dusk/os.h" #include "m_Do/m_Do_main.h" #include diff --git a/src/dusk/io.cpp b/src/dusk/io.cpp index 88bd04ae8f..7638194d35 100644 --- a/src/dusk/io.cpp +++ b/src/dusk/io.cpp @@ -5,8 +5,6 @@ #include "dusk/io.hpp" -using namespace dusk::io; - #if _WIN32 #define MODE_TYPE wchar_t #define MODE(val) L##val @@ -23,7 +21,10 @@ using namespace dusk::io; #define _SH_DENYWR 0 #endif -static FILE* ThrowIfNotOpen(const FileStream& file) { +namespace dusk::io { +namespace { + +FILE* ThrowIfNotOpen(const FileStream& file) { if (!file.GetFileHandle()) { throw std::runtime_error("Invalid file handle!"); } @@ -31,14 +32,14 @@ static FILE* ThrowIfNotOpen(const FileStream& file) { return static_cast(file.GetFileHandle()); } -[[noreturn]] static void ThrowForError(int code) { +[[noreturn]] void ThrowForError(int code) { if (code == 0) { throw std::system_error(std::make_error_code(std::errc::io_error)); } throw std::system_error(std::make_error_code(static_cast(code))); } -static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, int shareFlag) { +FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, int shareFlag) { FILE* file; int err; @@ -59,12 +60,13 @@ static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, return file; } -static FILE* OpenCore(const char* path, const MODE_TYPE* mode, int shareFlag) { +FILE* OpenCore(const char* path, const MODE_TYPE* mode, int shareFlag) { return OpenCore(reinterpret_cast(path), mode, shareFlag); } -FileStream::FileStream() noexcept : file(nullptr) { -} +} // namespace + +FileStream::FileStream() noexcept : file(nullptr) {} FileStream::FileStream(FILE* file) : file(file) { if (!file) { @@ -184,3 +186,5 @@ FILE* FileStream::ToInner() { file = nullptr; return handle; } + +} // namespace dusk::io diff --git a/include/dusk/io.hpp b/src/dusk/io.hpp similarity index 97% rename from include/dusk/io.hpp rename to src/dusk/io.hpp index aba6bf8a48..c8f9ae771b 100644 --- a/include/dusk/io.hpp +++ b/src/dusk/io.hpp @@ -1,5 +1,4 @@ -#ifndef DUSK_IO_HPP -#define DUSK_IO_HPP +#pragma once #include #include @@ -106,5 +105,3 @@ inline std::string fs_path_to_string(const std::filesystem::path& path) { } } // namespace dusk::io - -#endif // DUSK_IO_HPP diff --git a/include/dusk/layout.hpp b/src/dusk/layout.hpp similarity index 90% rename from include/dusk/layout.hpp rename to src/dusk/layout.hpp index 78316d2e09..bb207c2000 100644 --- a/include/dusk/layout.hpp +++ b/src/dusk/layout.hpp @@ -1,5 +1,4 @@ -#ifndef DUSK_LAYOUT_H -#define DUSK_LAYOUT_H +#pragma once #include "dolphin/types.h" @@ -33,5 +32,3 @@ struct LayoutRect { f32 heightInner); }; } - -#endif // DUSK_LAYOUT_H diff --git a/include/dusk/livesplit.h b/src/dusk/livesplit.h similarity index 100% rename from include/dusk/livesplit.h rename to src/dusk/livesplit.h diff --git a/include/dusk/logging.h b/src/dusk/logging.h similarity index 93% rename from include/dusk/logging.h rename to src/dusk/logging.h index 54350af4bf..fdf315752e 100644 --- a/include/dusk/logging.h +++ b/src/dusk/logging.h @@ -1,5 +1,4 @@ -#ifndef DUSK_LOGGING_H -#define DUSK_LOGGING_H +#pragma once #include #include @@ -34,5 +33,3 @@ extern aurora::Module DuskLog; #else #define STUB_RET() (void)0 #endif - -#endif diff --git a/src/dusk/main.cpp b/src/dusk/main.cpp index a35f695b50..c1003a7718 100644 --- a/src/dusk/main.cpp +++ b/src/dusk/main.cpp @@ -5,6 +5,7 @@ #endif #include +#include "d/actor/d_a_movie_player.h" #include "dusk/main.h" #include "dusk/io.hpp" diff --git a/include/dusk/main.h b/src/dusk/main.h similarity index 91% rename from include/dusk/main.h rename to src/dusk/main.h index a16291a8a7..1378e48a1b 100644 --- a/include/dusk/main.h +++ b/src/dusk/main.h @@ -1,5 +1,4 @@ -#ifndef DUSK_MAIN_H -#define DUSK_MAIN_H +#pragma once #include @@ -34,5 +33,3 @@ inline constexpr bool SupportsProcessRestart = true; void RequestRestart() noexcept; } // namespace dusk - -#endif // DUSK_MAIN_H diff --git a/include/dusk/map_loader_definitions.h b/src/dusk/map_loader_definitions.h similarity index 100% rename from include/dusk/map_loader_definitions.h rename to src/dusk/map_loader_definitions.h diff --git a/include/dusk/memory.h b/src/dusk/memory.h similarity index 68% rename from include/dusk/memory.h rename to src/dusk/memory.h index dd55181170..2d24e5b957 100644 --- a/include/dusk/memory.h +++ b/src/dusk/memory.h @@ -1,10 +1,7 @@ -#ifndef DUSK_MEMORY_H -#define DUSK_MEMORY_H +#pragma once #if TARGET_PC #define HEAP_SIZE(original, dusk) (dusk) #else #define HEAP_SIZE(original, dusk) (original) #endif - -#endif diff --git a/include/dusk/menu_pointer.h b/src/dusk/menu_pointer.h similarity index 100% rename from include/dusk/menu_pointer.h rename to src/dusk/menu_pointer.h diff --git a/include/dusk/mod_loader.hpp b/src/dusk/mod_loader.hpp similarity index 82% rename from include/dusk/mod_loader.hpp rename to src/dusk/mod_loader.hpp index 9d77f6984a..34e17274bc 100644 --- a/include/dusk/mod_loader.hpp +++ b/src/dusk/mod_loader.hpp @@ -67,9 +67,32 @@ struct ModSearchDir { std::filesystem::path nativeLibDir; }; +struct ModMetaParsed { + uint32_t abiVersion = 0; + std::vector imports; + std::vector exports; + std::vector hookFns; + std::vector hookMems; + std::vector hookNames; +}; + +inline const char* hook_mem_vtable_symbol(const ModMetaHookMem& rec) { + return reinterpret_cast(&rec) + sizeof(ModMetaHookMem); +} + +inline const char* hook_mem_display_name(const ModMetaHookMem& rec) { + const char* vtable = hook_mem_vtable_symbol(rec); + return vtable + std::char_traits::length(vtable) + 1; +} + +inline const char* hook_name_symbol(const ModMetaHookName& rec) { + return reinterpret_cast(&rec) + sizeof(ModMetaHookName); +} + struct NativeMod { std::unique_ptr handle; - const ModManifest* manifest = nullptr; + const ModMeta* meta = nullptr; + ModMetaParsed parsed; ModContext** contextSymbol = nullptr; ModInitializeFn fn_initialize = nullptr; @@ -111,6 +134,16 @@ enum class NativeModStatus : u8 { */ MissingExport, + /** + * Mod's metadata record section is malformed. + */ + InvalidMetadata, + + /** + * Mod bundle contains files in an invalid location. + */ + InvalidBundle, + /** * Unknown error loading the native mod. */ @@ -119,14 +152,16 @@ enum class NativeModStatus : u8 { struct LoadedMod { ModMetadata metadata; - std::string modPath; - std::string dir; + std::filesystem::path modPath; + std::filesystem::path dir; + // Stable UTF-8 storage for HostService::mod_dir. + std::string dirUtf8; uint32_t searchDirIndex = 0; // Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported. bool inPlace = false; - std::unique_ptr > cvarIsEnabled; + std::unique_ptr> cvarIsEnabled; config::Subscription enabledSubscription = 0; bool active = false; @@ -147,7 +182,11 @@ struct LoadedMod { // asset-only reloads, so it doubles as a generation for anything caching per-mod content. uint32_t cacheGeneration = 0; // Currently extracted native library, empty if none. - std::string nativePath; + std::filesystem::path nativePath; + // Read-only directory containing the current platform's main module and runtime libraries. + std::filesystem::path nativeDir; + // Stable UTF-8 storage for HostService::native_dir. + std::string nativeDirUtf8; NativeModStatus nativeStatus = NativeModStatus::None; std::unique_ptr native; @@ -197,10 +236,10 @@ private: // the next tick, by which point every per-frame entry into the mod should have returned. struct RetiredNative { std::unique_ptr native; - std::string path; + std::filesystem::path directory; }; - std::vector > m_mods; + std::vector> m_mods; std::vector m_searchDirs; std::filesystem::path m_cacheDir; std::vector m_pendingRequests; @@ -210,7 +249,9 @@ private: bool m_startupComplete = false; void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex); - void load_native(LoadedMod& mod, const std::string& dllEntry); + void load_native(LoadedMod& mod, const std::string& dllEntry, + const std::vector& runtimeEntries); + bool load_native_if_present(LoadedMod& mod); // Resolved / if it exists on disk, empty otherwise. [[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const; void unload_native(LoadedMod& mod); diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index e1bb2797c8..353a2ab596 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -4,18 +4,22 @@ #include #include +#include #include #include #include #include #include +#include #include "../manifest.hpp" #include "depgraph.hpp" #include "dusk/config.hpp" +#include "dusk/data.hpp" #include "dusk/io.hpp" #include "dusk/mods/log_buffer.hpp" #include "dusk/mods/svc/config.hpp" +#include "dusk/mods/svc/hook.hpp" #include "dusk/mods/svc/registry.hpp" #include "dusk/ui/mods_window.hpp" #include "dusk/ui/ui.hpp" @@ -28,46 +32,51 @@ using namespace std::string_view_literals; #if defined(_WIN32) #if defined(_M_ARM64) -static constexpr std::string_view k_nativeLibName = "windows-arm64.dll"sv; +static constexpr std::string_view k_nativePlatform = "windows-arm64"sv; #elif defined(_M_X64) -static constexpr std::string_view k_nativeLibName = "windows-amd64.dll"sv; +static constexpr std::string_view k_nativePlatform = "windows-amd64"sv; #elif defined(_M_IX86) -static constexpr std::string_view k_nativeLibName = "windows-x86.dll"sv; +static constexpr std::string_view k_nativePlatform = "windows-x86"sv; #else -static constexpr std::string_view k_nativeLibName = ""sv; +static constexpr std::string_view k_nativePlatform = ""sv; #endif +static constexpr std::string_view k_nativeLibName = "mod.dll"sv; #elif defined(__ANDROID__) #if defined(__aarch64__) -static constexpr std::string_view k_nativeLibName = "android-aarch64.so"sv; +static constexpr std::string_view k_nativePlatform = "android-aarch64"sv; #elif defined(__x86_64__) -static constexpr std::string_view k_nativeLibName = "android-x86_64.so"sv; +static constexpr std::string_view k_nativePlatform = "android-x86_64"sv; #else -static constexpr std::string_view k_nativeLibName = ""sv; +static constexpr std::string_view k_nativePlatform = ""sv; #endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; #elif defined(__APPLE__) #include #if TARGET_OS_IOS -static constexpr std::string_view k_nativeLibName = "ios-arm64.dylib"sv; +static constexpr std::string_view k_nativePlatform = "ios-arm64"sv; #elif TARGET_OS_TV -static constexpr std::string_view k_nativeLibName = "tvos-arm64.dylib"sv; +static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv; #elif defined(__aarch64__) -static constexpr std::string_view k_nativeLibName = "darwin-arm64.dylib"sv; +static constexpr std::string_view k_nativePlatform = "macos-arm64"sv; #elif defined(__x86_64__) -static constexpr std::string_view k_nativeLibName = "darwin-x86_64.dylib"sv; +static constexpr std::string_view k_nativePlatform = "macos-x86_64"sv; #else -static constexpr std::string_view k_nativeLibName = ""sv; +static constexpr std::string_view k_nativePlatform = ""sv; #endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; #elif defined(__linux__) #if defined(__aarch64__) -static constexpr std::string_view k_nativeLibName = "linux-aarch64.so"sv; +static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv; #elif defined(__x86_64__) -static constexpr std::string_view k_nativeLibName = "linux-x86_64.so"sv; +static constexpr std::string_view k_nativePlatform = "linux-x86_64"sv; #elif defined(__i386__) -static constexpr std::string_view k_nativeLibName = "linux-x86.so"sv; +static constexpr std::string_view k_nativePlatform = "linux-x86"sv; #else -static constexpr std::string_view k_nativeLibName = ""sv; +static constexpr std::string_view k_nativePlatform = ""sv; #endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; #else +static constexpr std::string_view k_nativePlatform = ""sv; static constexpr std::string_view k_nativeLibName = ""sv; #endif @@ -75,6 +84,23 @@ namespace dusk::mods { namespace { aurora::Module Log{"dusk::mods::loader"}; ModLoader g_modLoader; +constexpr std::string_view k_nativeLibDir = "lib/"sv; + +class DirectoryRollback { +public: + ~DirectoryRollback() { + if (!mPath.empty()) { + std::error_code ec; + std::filesystem::remove_all(mPath, ec); + } + } + + void set_path(std::filesystem::path path) { mPath = std::move(path); } + void release() { mPath.clear(); } + +private: + std::filesystem::path mPath; +}; std::unique_ptr load_bundle(const std::filesystem::path& modPath, bool fromDir) { if (fromDir) { @@ -85,24 +111,81 @@ std::unique_ptr load_bundle(const std::filesystem::path& modPath, boo } } -struct DllLocateResult { +struct NativeRuntimeLocation { std::string entry; + std::vector runtimeEntries; bool anyLibs = false; }; -DllLocateResult locate_dll_in_bundle(ModBundle& bundle) { - DllLocateResult result; +struct NativeLocateFailure { + NativeModStatus status; + std::string logMessage; +}; + +using NativeLocateResult = std::variant; + +bool has_native_library_extension(std::string_view name) { + const auto endsWith = [name](std::string_view extension) { + if (name.size() < extension.size()) { + return false; + } + const auto suffix = name.substr(name.size() - extension.size()); + return std::ranges::equal(suffix, extension, [](char lhs, char rhs) { + const auto lower = [](char value) { + return value >= 'A' && value <= 'Z' ? static_cast(value + ('a' - 'A')) : + value; + }; + return lower(lhs) == lower(rhs); + }); + }; + return endsWith(".dll"sv) || endsWith(".so"sv) || endsWith(".dylib"sv); +} + +NativeLocateResult locate_native_runtime(ModBundle& bundle) { + NativeRuntimeLocation result; + const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); + const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName}; for (const auto& name : bundle.getFileNames()) { - if (name.find('/') != std::string::npos || - (!name.ends_with(".dll"sv) && !name.ends_with(".dylib"sv) && !name.ends_with(".so"sv))) - { + if (name.find('/') == std::string::npos && has_native_library_extension(name)) { + return NativeLocateFailure{ + NativeModStatus::InvalidBundle, + fmt::format( + "native library '{}' found at the root (natives go in /lib/{{platform}})", + name), + }; + } + if (!name.starts_with(k_nativeLibDir)) { continue; } - result.anyLibs = true; - if (name == k_nativeLibName) { + + const std::string_view libPath{ + name.data() + k_nativeLibDir.size(), name.size() - k_nativeLibDir.size()}; + const auto platformEnd = libPath.find('/'); + if (platformEnd != std::string_view::npos) { + const auto entryName = libPath.substr(platformEnd + 1); + if (entryName.find('/') == std::string_view::npos && + (entryName == "mod.dll"sv || entryName == "mod.so"sv)) + { + result.anyLibs = true; + } + } + + if (!k_nativePlatform.empty() && name.starts_with(platformPrefix)) { + const std::string_view relativeName{ + name.data() + platformPrefix.size(), name.size() - platformPrefix.size()}; + if (!is_safe_resource_path(relativeName)) { + continue; + } + result.runtimeEntries.push_back(name); + } + if (name == nativeEntry) { result.entry = name; } } + std::ranges::sort(result.runtimeEntries); + result.runtimeEntries.erase( + std::unique(result.runtimeEntries.begin(), result.runtimeEntries.end()), + result.runtimeEntries.end()); return result; } } // namespace @@ -220,40 +303,149 @@ static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle }; } -static bool validate_manifest(const ModManifest* manifest, LoadedMod& mod) { - if (manifest == nullptr) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "returned a null mod manifest"); - mod.nativeStatus = NativeModStatus::MissingExport; - return false; - } - if (manifest->struct_size != sizeof(ModManifest)) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid size {} (expected {})", - manifest->struct_size, sizeof(ModManifest)); - mod.nativeStatus = NativeModStatus::ApiVersionMismatch; - return false; - } - if (manifest->abi_version != MOD_ABI_VERSION) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", - manifest->abi_version, MOD_ABI_VERSION); - mod.nativeStatus = NativeModStatus::ApiVersionMismatch; - return false; - } - if ((manifest->import_count > 0 && manifest->imports == nullptr) || - (manifest->export_count > 0 && manifest->exports == nullptr)) - { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid import/export arrays"); - mod.nativeStatus = NativeModStatus::MissingExport; - return false; - } - return true; +// True if the first `capacity` bytes of `str` contain a NUL. +static bool terminated_within(const char* str, size_t capacity) { + return std::memchr(str, '\0', capacity) != nullptr; } -static bool validate_context_symbol(ModContext** contextSymbol, LoadedMod& mod) { - if (contextSymbol == nullptr) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); - mod.nativeStatus = NativeModStatus::MissingExport; +static bool parse_meta(NativeMod& native, LoadedMod& mod) { + const ModMeta* meta = native.meta; + if (meta->struct_size < sizeof(ModMeta)) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}", + meta->struct_size); + mod.nativeStatus = NativeModStatus::InvalidMetadata; return false; } + const auto* cursor = static_cast(meta->records_begin); + const auto* end = static_cast(meta->records_end); + if (cursor == nullptr || end == nullptr || cursor > end || + (reinterpret_cast(cursor) & 7) != 0) + { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid"); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + + ModMetaParsed parsed; + size_t headerCount = 0; + const auto invalid = [&](std::string_view why) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}", + cursor - static_cast(meta->records_begin), why); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + }; + + while (cursor < end) { + if (end - cursor < 8) { + return invalid("trailing bytes"); + } + uint64_t first = 0; + std::memcpy(&first, cursor, sizeof(first)); + if (first == 0) { // linker padding / bounds sentinel + cursor += 8; + continue; + } + + const auto* rec = reinterpret_cast(cursor); + const size_t size = rec->size; + if (size < 8 || size % 8 != 0 || size > static_cast(end - cursor)) { + return invalid("bad record size"); + } + + switch (rec->kind) { + case MOD_META_PAD: + break; + case MOD_META_HEADER: { + if (size < sizeof(ModMetaHeader)) { + return invalid("truncated header record"); + } + const auto* header = reinterpret_cast(rec); + ++headerCount; + parsed.abiVersion = header->abi_version; + break; + } + case MOD_META_IMPORT: { + if (size < sizeof(ModMetaImport)) { + return invalid("truncated import record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { + return invalid("unterminated import service id"); + } + parsed.imports.push_back(record); + break; + } + case MOD_META_EXPORT: { + if (size < sizeof(ModMetaExport)) { + return invalid("truncated export record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { + return invalid("unterminated export service id"); + } + parsed.exports.push_back(record); + break; + } + case MOD_META_HOOK_FN: { + if (size < sizeof(ModMetaHookFn)) { + return invalid("truncated hook record"); + } + parsed.hookFns.push_back( + reinterpret_cast(const_cast(cursor))); + break; + } + case MOD_META_HOOK_MEM: { + if (size <= sizeof(ModMetaHookMem)) { + return invalid("truncated hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMem); + const size_t capacity = size - sizeof(ModMetaHookMem); + if (!terminated_within(strings, capacity)) { + return invalid("unterminated hook vtable symbol"); + } + const size_t vtableLen = std::char_traits::length(strings); + if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { + return invalid("unterminated hook display name"); + } + parsed.hookMems.push_back(record); + break; + } + case MOD_META_HOOK_NAME: { + if (size <= sizeof(ModMetaHookName)) { + return invalid("truncated hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + const char* name = reinterpret_cast(cursor) + sizeof(ModMetaHookName); + if (!terminated_within(name, size - sizeof(ModMetaHookName))) { + return invalid("unterminated hook symbol name"); + } + parsed.hookNames.push_back(record); + break; + } + default: + // Additive record kinds may appear within a format version; skip them. + log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}", + rec->kind); + break; + } + cursor += size; + } + + if (headerCount != 1) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}", + headerCount); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + if (parsed.abiVersion != MOD_ABI_VERSION) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", + parsed.abiVersion, MOD_ABI_VERSION); + mod.nativeStatus = NativeModStatus::ApiVersionMismatch; + return false; + } + + native.parsed = std::move(parsed); return true; } @@ -270,12 +462,16 @@ static std::string native_status_message(const NativeModStatus status) { case NativeModStatus::BuildDisabled: return "Code mods are disabled on this Dusklight build"; case NativeModStatus::ModMissingPlatform: - return fmt::format("Mod not supported on this platform ({})", k_nativeLibName); + return fmt::format("Mod not supported on this platform ({})", k_nativePlatform); case NativeModStatus::ApiVersionMismatch: // TODO: differentiate whether mod or Dusklight is out of date return "Mod ABI version mismatch"; case NativeModStatus::MissingExport: return "Missing required mod API exports"; + case NativeModStatus::InvalidMetadata: + return "Invalid mod metadata records"; + case NativeModStatus::InvalidBundle: + return "Invalid mod bundle layout (old mod?)"; case NativeModStatus::Unknown: return "Unknown mod load failure"; case NativeModStatus::None: @@ -303,7 +499,8 @@ std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod) return path; } -void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { +void ModLoader::load_native( + LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries) { if (!EnableCodeMods) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build"); mod.nativeStatus = NativeModStatus::BuildDisabled; @@ -313,13 +510,23 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { namespace fs = std::filesystem; const fs::path cacheDir = m_cacheDir / mod.metadata.id; + const fs::path scratchDir = cacheDir / "data"; std::error_code ec; - fs::create_directories(cacheDir, ec); + fs::create_directories(scratchDir, ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}", + data::abbreviated_path_string(scratchDir), ec.message()); + return; + } + mod.dir = fs::absolute(scratchDir); + mod.dirUtf8 = io::fs_path_to_string(mod.dir); fs::path libPath; + fs::path runtimeDir; + DirectoryRollback runtimeDirRollback; if (mod.inPlace) { if (!dllEntry.empty()) { - libPath = fs::path(mod.modPath) / dllEntry; + libPath = mod.modPath / dllEntry; } else if (auto external = external_native_lib_path(mod); !external.empty()) { libPath = std::move(external); } else { @@ -328,6 +535,7 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { mod.nativeStatus = NativeModStatus::ModMissingPlatform; return; } + runtimeDir = libPath.parent_path(); } else { if (dllEntry.empty()) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, @@ -336,34 +544,63 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { return; } - // Generation-versioned filename: every dlopen gets a path it has never seen, so a reload - // always yields a fresh image with fresh statics even if the previous dlclose did not - // fully unmap the old one (TLS/ObjC pinning). The .cache dir is wiped on startup. - const fs::path dllCachePath = - cacheDir / fmt::format("{}.g{}{}", mod.metadata.id, ++mod.cacheGeneration, - io::fs_path_to_string(fs::path(dllEntry).extension())); - - std::vector dllData; - try { - dllData = mod.bundle->readFile(dllEntry); - } catch (const std::exception& e) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}", dllEntry); + // Every generation gets a new directory. The main module and all of its runtime + // libraries therefore have fresh paths and can coexist with a previous generation + // that is still unwinding after a reload. + runtimeDir = cacheDir / fmt::format("g{}", ++mod.cacheGeneration); + runtimeDirRollback.set_path(runtimeDir); + fs::create_directories(runtimeDir, ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "failed to create native runtime directory {}: {}", + data::abbreviated_path_string(runtimeDir), ec.message()); return; } - { - std::ofstream out(dllCachePath, std::ios::binary | std::ios::out); - if (!out) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", - io::fs_path_to_string(dllCachePath)); + const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); + for (const auto& entry : runtimeEntries) { + if (!entry.starts_with(platformPrefix)) { + continue; + } + const std::string_view relativeName{ + entry.data() + platformPrefix.size(), entry.size() - platformPrefix.size()}; + if (!is_safe_resource_path(relativeName)) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "unsafe native runtime path '{}'; skipping", entry); return; } - out.write(reinterpret_cast(dllData.data()), - static_cast(dllData.size())); + const fs::path outputPath = runtimeDir / fs::path{relativeName}; + fs::create_directories(outputPath.parent_path(), ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "failed to create directory for {}: {}", entry, ec.message()); + return; + } + + std::vector data; + try { + data = mod.bundle->readFile(entry); + } catch (const std::exception& e) { + log::write( + mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}: {}", entry, e.what()); + return; + } + + std::ofstream out(outputPath, std::ios::binary | std::ios::out); + if (!out) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); + return; + } + out.write(reinterpret_cast(data.data()), + static_cast(data.size())); + if (!out) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); + return; + } } - libPath = dllCachePath; + libPath = runtimeDir / fs::path{dllEntry}.filename(); } auto nativeMod = std::make_unique(); @@ -371,40 +608,67 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) { nativeMod->handle = std::make_unique(libPath); } catch (const std::runtime_error& e) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}", - io::fs_path_to_string(libPath), e.what()); + data::abbreviated_path_string(libPath), e.what()); return; } - const auto getManifest = nativeMod->handle->LookupSymbol("mod_get_manifest"); + nativeMod->meta = nativeMod->handle->LookupSymbol("mod_meta"); nativeMod->contextSymbol = nativeMod->handle->LookupSymbol("mod_ctx"); nativeMod->fn_initialize = nativeMod->handle->LookupSymbol("mod_initialize"); nativeMod->fn_update = nativeMod->handle->LookupSymbol("mod_update"); nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol("mod_shutdown"); - if (!getManifest || !nativeMod->contextSymbol || !nativeMod->fn_initialize || + if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize || !nativeMod->fn_update || !nativeMod->fn_shutdown) { log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{} missing required mod API exports; skipping", - io::fs_path_to_string(libPath.filename())); + data::abbreviated_path_string(libPath)); mod.nativeStatus = NativeModStatus::MissingExport; return; } - nativeMod->manifest = getManifest(); - if (!validate_manifest(nativeMod->manifest, mod)) { + if (!parse_meta(*nativeMod, mod)) { return; } - if (!validate_context_symbol(nativeMod->contextSymbol, mod)) { + if (nativeMod->contextSymbol == nullptr) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); + mod.nativeStatus = NativeModStatus::MissingExport; return; } *nativeMod->contextSymbol = mod.context.get(); - mod.dir = io::fs_path_to_string(fs::absolute(cacheDir)); - mod.nativePath = io::fs_path_to_string(fs::absolute(libPath)); + mod.nativePath = fs::absolute(libPath); + mod.nativeDir = fs::absolute(runtimeDir); + mod.nativeDirUtf8 = io::fs_path_to_string(mod.nativeDir); mod.native = std::move(nativeMod); mod.nativeStatus = NativeModStatus::Loaded; + runtimeDirRollback.release(); +} + +bool ModLoader::load_native_if_present(LoadedMod& mod) { + const auto result = locate_native_runtime(*mod.bundle); + if (const auto* failure = std::get_if(&result)) { + mod.nativeStatus = failure->status; + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{}", failure->logMessage); + fail_mod(mod, MOD_ERROR, native_status_message(failure->status)); + return false; + } + + const auto& native = std::get(result); + if (!native.anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) { + mod.nativeStatus = NativeModStatus::None; + return true; + } + + mod.nativeStatus = NativeModStatus::Unknown; + load_native(mod, native.entry, native.runtimeEntries); + if (mod.nativeStatus != NativeModStatus::Loaded) { + fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); + return false; + } + return true; } void ModLoader::unload_native(LoadedMod& mod) { @@ -412,43 +676,39 @@ void ModLoader::unload_native(LoadedMod& mod) { return; } // Deferred dlclose: this mod's code may still be on the stack below the current tick - m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativePath)}); + m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativeDir)}); mod.nativePath.clear(); + mod.nativeDir.clear(); + mod.nativeDirUtf8.clear(); } void ModLoader::drain_retired_natives() { for (auto& retired : m_retiredNatives) { retired.native.reset(); - if (!retired.path.empty()) { + if (!retired.directory.empty()) { std::error_code ec; - std::filesystem::remove(retired.path, ec); + std::filesystem::remove_all(retired.directory, ec); } } m_retiredNatives.clear(); } -static ModManifestInfo build_manifest_info(const ModManifest& manifest) { +static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) { ModManifestInfo info; - info.imports.reserve(manifest.import_count); - for (size_t i = 0; i < manifest.import_count; ++i) { - const auto& serviceImport = manifest.imports[i]; - if (serviceImport.struct_size != sizeof(ServiceImport) || - !svc::valid_service_id(serviceImport.service_id)) - { + info.imports.reserve(parsed.imports.size()); + for (const auto* record : parsed.imports) { + if (!svc::valid_service_id(record->service_id.chars)) { continue; } - info.imports.push_back({serviceImport.service_id, serviceImport.major_version, - (serviceImport.flags & SERVICE_IMPORT_OPTIONAL) == 0}); + info.imports.push_back({record->service_id.chars, record->major_version, + (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0}); } - info.exports.reserve(manifest.export_count); - for (size_t i = 0; i < manifest.export_count; ++i) { - const auto& serviceExport = manifest.exports[i]; - if (serviceExport.struct_size != sizeof(ServiceExport) || - !svc::valid_service_id(serviceExport.service_id)) - { + info.exports.reserve(parsed.exports.size()); + for (const auto* record : parsed.exports) { + if (!svc::valid_service_id(record->service_id.chars)) { continue; } - info.exports.push_back({serviceExport.service_id, serviceExport.major_version}); + info.exports.push_back({record->service_id.chars, record->major_version}); } return info; } @@ -484,24 +744,20 @@ static bool required_deps_active(const LoadedMod& mod) { // A deferred export that was not published by the end of the provider's initialization can // never resolve, which is almost certainly a bug in the provider. static void warn_unpublished_deferred_exports(const LoadedMod& mod) { - if (!mod.active || !mod.native || mod.native->manifest == nullptr) { + if (!mod.active || !mod.native) { return; } - const auto& manifest = *mod.native->manifest; - for (size_t i = 0; i < manifest.export_count; ++i) { - const auto& serviceExport = manifest.exports[i]; - if (serviceExport.struct_size != sizeof(ServiceExport) || - (serviceExport.flags & SERVICE_EXPORT_DEFERRED) == 0) - { + for (const auto* serviceExport : mod.native->parsed.exports) { + if ((serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) == 0) { continue; } const auto* record = - svc::find_service_record(serviceExport.service_id, serviceExport.major_version); + svc::find_service_record(serviceExport->service_id.chars, serviceExport->major_version); if (record != nullptr && record->service == nullptr) { log::write(mod.metadata.id, LOG_LEVEL_WARN, "declared deferred service '{}@{}' but never published it during initialization", - serviceExport.service_id, serviceExport.major_version); + serviceExport->service_id.chars, serviceExport->major_version); } } } @@ -514,8 +770,7 @@ void ModLoader::try_load_mod( try { bundle = load_bundle(modPath, fromDir); } catch (const std::exception& e) { - Log.error( - "Failed to open {} bundle: {}", io::fs_path_to_string(modPath.filename()), e.what()); + Log.error("Failed to open {} bundle: {}", data::abbreviated_path_string(modPath), e.what()); return; } @@ -523,26 +778,26 @@ void ModLoader::try_load_mod( try { metadata = load_metadata(modPath, *bundle); } catch (const std::exception& e) { - Log.error("bad mod.json in {}: {}", io::fs_path_to_string(modPath.filename()), e.what()); + Log.error("bad mod.json in {}: {}", data::abbreviated_path_string(modPath), e.what()); return; } if (const auto* existing = find_mod(metadata.id)) { if (existing->searchDirIndex < searchDirIndex) { - log::write(metadata.id, LOG_LEVEL_INFO, "{} shadowed by higher-priority copy {}", - io::fs_path_to_string(modPath.filename()), existing->modPath); + log::write(metadata.id, LOG_LEVEL_INFO, "{} shadowed by higher-priority duplicate {}", + data::abbreviated_path_string(modPath), + data::abbreviated_path_string(existing->modPath)); } else { log::write(metadata.id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}", - io::fs_path_to_string(modPath.filename())); + data::abbreviated_path_string(modPath)); } return; } const auto& inserted = m_mods.emplace_back(std::make_unique()); - auto& mod = *inserted; mod.active = true; - mod.modPath = io::fs_path_to_string(fs::absolute(modPath)); + mod.modPath = fs::absolute(modPath); mod.searchDirIndex = searchDirIndex; mod.inPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; mod.metadata = std::move(metadata); @@ -551,21 +806,12 @@ void ModLoader::try_load_mod( mod.context->mod = &mod; mod.cvarIsEnabled = std::make_unique>(mod_enabled_cvar_name(mod.metadata.id), true); - - const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle); - if (anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty())) { - mod.nativeStatus = NativeModStatus::Unknown; - load_native(mod, dllEntry); - if (mod.nativeStatus != NativeModStatus::Loaded) { - Log.error("Native mod '{}' failed to load, disabling", mod.metadata.id); - fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); - } else { - mod.manifestInfo = build_manifest_info(*mod.native->manifest); - } + if (load_native_if_present(mod) && mod.native) { + mod.manifestInfo = build_manifest_info(mod.native->parsed); } log::write(mod.metadata.id, LOG_LEVEL_INFO, "found '{}' v{} by {} ({})", mod.metadata.name, - mod.metadata.version, mod.metadata.author, io::fs_path_to_string(modPath.filename())); + mod.metadata.version, mod.metadata.author, data::abbreviated_path_string(modPath)); } bool ModLoader::activate_mod(LoadedMod& mod) { @@ -593,6 +839,8 @@ bool ModLoader::activate_mod(LoadedMod& mod) { return false; } + svc::hook_resolve_mod_records(mod); + *mod.native->contextSymbol = mod.context.get(); log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_initialize"); @@ -688,9 +936,11 @@ void ModLoader::init() { if (!fs::is_directory(searchDir.path)) { if (dirIndex == 0) { - Log.info("mods directory '{}' not found", io::fs_path_to_string(searchDir.path)); + Log.info( + "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); } else { - Log.debug("mods directory '{}' not found", io::fs_path_to_string(searchDir.path)); + Log.debug( + "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); } continue; } @@ -878,25 +1128,13 @@ bool ModLoader::ensure_native_loaded(LoadedMod& mod) { if (mod.native || mod.nativeStatus == NativeModStatus::None) { return true; } - - const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle); - if (!anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) { - mod.nativeStatus = NativeModStatus::None; - return true; - } - - mod.nativeStatus = NativeModStatus::Unknown; - load_native(mod, dllEntry); - if (mod.nativeStatus != NativeModStatus::Loaded) { - fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); - return false; - } - return true; + return load_native_if_present(mod); } bool ModLoader::reload_bundle(LoadedMod& mod) { namespace fs = std::filesystem; - log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", mod.modPath); + log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", + data::abbreviated_path_string(mod.modPath)); std::shared_ptr newBundle; ModMetadata newMetadata; @@ -922,16 +1160,12 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { mod.failureReason.clear(); ModManifestInfo newInfo; - if (const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle); anyLibs) { - mod.nativeStatus = NativeModStatus::Unknown; - load_native(mod, dllEntry); - if (mod.nativeStatus != NativeModStatus::Loaded) { - fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); - return false; - } - newInfo = build_manifest_info(*mod.native->manifest); + if (!load_native_if_present(mod)) { + return false; + } + if (mod.native) { + newInfo = build_manifest_info(mod.native->parsed); } else { - mod.nativeStatus = NativeModStatus::None; ++mod.cacheGeneration; } @@ -998,8 +1232,7 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { if (register_static_service_exports(*mod)) { mod->servicesRegistered = true; } else { - log::write( - mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); + log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); deactivate_mod(*mod); } } diff --git a/src/dusk/mods/loader/native_module.cpp b/src/dusk/mods/loader/native_module.cpp index edd3addb4f..f6adf20c60 100644 --- a/src/dusk/mods/loader/native_module.cpp +++ b/src/dusk/mods/loader/native_module.cpp @@ -17,7 +17,8 @@ namespace { #if defined(_WIN32) void* pl_dlopen(const std::filesystem::path& p) { - return LoadLibraryW(p.wstring().c_str()); + return LoadLibraryExW(p.wstring().c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); } void* pl_dlsym(void* h, const char* name) { return reinterpret_cast(GetProcAddress(static_cast(h), name)); @@ -55,11 +56,10 @@ std::string pl_dlerror() { return e ? e : "(unknown error)"; } #endif -} +} // namespace namespace dusk::mods::loader { -NativeModule::NativeModule() noexcept : handle(nullptr) { -} +NativeModule::NativeModule() noexcept : handle(nullptr) {} NativeModule::NativeModule(NativeModule&& other) noexcept { handle = other.handle; diff --git a/src/dusk/mods/loader/native_module.hpp b/src/dusk/mods/loader/native_module.hpp index ff2acf9632..2632496f96 100644 --- a/src/dusk/mods/loader/native_module.hpp +++ b/src/dusk/mods/loader/native_module.hpp @@ -22,8 +22,6 @@ public: #if defined(_WIN32) static constexpr auto LibraryExtension = ".dll"; -#elif defined(__APPLE__) - static constexpr auto LibraryExtension = ".dylib"; #else static constexpr auto LibraryExtension = ".so"; #endif diff --git a/src/dusk/mods/manifest.cpp b/src/dusk/mods/manifest.cpp index 7e83dfb8f6..17097a518b 100644 --- a/src/dusk/mods/manifest.cpp +++ b/src/dusk/mods/manifest.cpp @@ -6,18 +6,14 @@ #include #include -#include #include #include #include -#include #include #include "aurora/lib/logging.hpp" -#include "dusk/io.hpp" - #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #include @@ -63,6 +59,31 @@ struct Entry { }; static_assert(sizeof(Entry) == 24); +/* + * `symgen manifest --embed` appends the symbol manifest to the linked executable as an added + * section and patches this descriptor with its location. + */ +struct SymdbDescriptor { + volatile uint64_t magic; + volatile uint64_t rva; + volatile uint64_t size; +}; +constexpr uint64_t kDescriptorMagic = 0x52444842444d5953ull; // "SYMDBHDR" +#if defined(_WIN32) +#pragma section(".symdbh", read) +__declspec(allocate(".symdbh")) +#if defined(__clang__) +__attribute__((used)) +#endif +constinit const SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0}; +#elif defined(__APPLE__) +__attribute__((section("__DATA,__symdbh"), used)) constinit const SymdbDescriptor + s_symdbDescriptor{kDescriptorMagic, 0, 0}; +#else +__attribute__((section("symdbh"), used)) constinit const SymdbDescriptor s_symdbDescriptor{ + kDescriptorMagic, 0, 0}; +#endif + struct State { std::vector data; const Entry* entries = nullptr; @@ -149,13 +170,28 @@ bool running_image_identity(std::vector& outId, uintptr_t& outBase) { #elif defined(__linux__) struct Ctx { std::vector* id; + uintptr_t probe; uintptr_t base = 0; bool found = false; - } ctx{&outId}; + } ctx{&outId, reinterpret_cast(&s_symdbDescriptor)}; dl_iterate_phdr( [](dl_phdr_info* info, size_t, void* data) -> int { auto* ctx = static_cast(data); - // The first callback is the main executable. + // Select the image containing our descriptor: the game is not always the + // first entry (on Android it is libmain.so, behind the app process). + bool contains = false; + for (int i = 0; i < info->dlpi_phnum; ++i) { + const auto& phdr = info->dlpi_phdr[i]; + if (phdr.p_type == PT_LOAD && ctx->probe >= info->dlpi_addr + phdr.p_vaddr && + ctx->probe - (info->dlpi_addr + phdr.p_vaddr) < phdr.p_memsz) + { + contains = true; + break; + } + } + if (!contains) { + return 0; + } ctx->base = info->dlpi_addr; for (int i = 0; i < info->dlpi_phnum; ++i) { const auto& phdr = info->dlpi_phdr[i]; @@ -178,7 +214,7 @@ bool running_image_identity(std::vector& outId, uintptr_t& outBase) { p = desc + ((note->n_descsz + 3) & ~3u); } } - return 1; // only inspect the main executable + return 1; // matched our image; stop either way }, &ctx); outBase = ctx.base; @@ -190,13 +226,6 @@ bool running_image_identity(std::vector& outId, uintptr_t& outBase) { #endif } -std::filesystem::path manifest_path() { - const char* basePath = SDL_GetBasePath(); - std::filesystem::path dir = - basePath != nullptr ? std::filesystem::path{basePath} : std::filesystem::current_path(); - return dir / "dusklight.symdb"; -} - std::string hex_string(const uint8_t* data, size_t len) { std::string out; out.reserve(len * 2); @@ -216,40 +245,12 @@ void initialize() { } s_state.initialized = true; - const auto path = manifest_path(); - std::error_code ec; - if (!std::filesystem::exists(path, ec)) { - Log.info("no symbol manifest at {}; by-name resolution unavailable", - io::fs_path_to_string(path)); + if (s_symdbDescriptor.magic != kDescriptorMagic) { + Log.error("symbol manifest descriptor is corrupt"); return; } - std::vector data; - try { - data = io::FileStream::ReadAllBytes(path); - } catch (const std::exception& e) { - Log.error("failed to read symbol manifest {}: {}", io::fs_path_to_string(path), e.what()); - return; - } - if (data.size() < sizeof(Header)) { - Log.error( - "symbol manifest {} is truncated ({} bytes)", io::fs_path_to_string(path), data.size()); - return; - } - - Header header{}; - std::memcpy(&header, data.data(), sizeof(header)); - if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) { - Log.error("symbol manifest {} has wrong magic/version", io::fs_path_to_string(path)); - return; - } - const auto compression = static_cast(header.compression); - if ((compression != Compression::None && compression != Compression::Zstd) || - header.buildIdLen > sizeof(header.buildId) || - header.compressedLen > data.size() - sizeof(Header) || - header.uncompressedLen > std::numeric_limits::max() || - (compression == Compression::None && header.compressedLen != header.uncompressedLen)) - { - Log.error("symbol manifest {} is malformed", io::fs_path_to_string(path)); + if (s_symdbDescriptor.rva == 0) { + Log.info("no symbol manifest embedded; by-name resolution unavailable"); return; } @@ -259,41 +260,67 @@ void initialize() { Log.error("cannot determine the running image's build id; ignoring symbol manifest"); return; } + + const auto* blob = reinterpret_cast(imageBase + s_symdbDescriptor.rva); + const auto blobLen = static_cast(s_symdbDescriptor.size); + if (blobLen < sizeof(Header)) { + Log.error("embedded symbol manifest is truncated ({} bytes)", blobLen); + return; + } + + Header header{}; + std::memcpy(&header, blob, sizeof(header)); + if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) { + Log.error("embedded symbol manifest has wrong magic/version"); + return; + } + const auto compression = static_cast(header.compression); + if ((compression != Compression::None && compression != Compression::Zstd) || + header.buildIdLen > sizeof(header.buildId) || + header.compressedLen > blobLen - sizeof(Header) || + header.uncompressedLen > std::numeric_limits::max() || + (compression == Compression::None && header.compressedLen != header.uncompressedLen)) + { + Log.error("embedded symbol manifest is malformed"); + return; + } + + // The manifest travels inside the image it describes, so a mismatch here means broken + // build tooling rather than a stale file — but it still guards resolved addresses. if (imageId.size() != header.buildIdLen || std::memcmp(imageId.data(), header.buildId, imageId.size()) != 0) { - Log.error("symbol manifest {} is stale: built for {}, running image is {}", - io::fs_path_to_string(path), hex_string(header.buildId, header.buildIdLen), + Log.error("embedded symbol manifest is stale: built for {}, running image is {}", + hex_string(header.buildId, header.buildIdLen), hex_string(imageId.data(), imageId.size())); return; } const auto compressedLen = static_cast(header.compressedLen); const auto uncompressedLen = static_cast(header.uncompressedLen); - std::vector payload; - const auto* storedPayload = data.data() + sizeof(Header); + std::vector data; + const auto* storedPayload = blob + sizeof(Header); if (compression == Compression::None) { - payload.assign(storedPayload, storedPayload + compressedLen); + data.assign(storedPayload, storedPayload + compressedLen); } else { - payload.resize(uncompressedLen); + data.resize(uncompressedLen); const size_t decompressedLen = - ZSTD_decompress(payload.data(), payload.size(), storedPayload, compressedLen); + ZSTD_decompress(data.data(), data.size(), storedPayload, compressedLen); if (ZSTD_isError(decompressedLen)) { - Log.error("failed to decompress symbol manifest {}: {}", io::fs_path_to_string(path), + Log.error("failed to decompress embedded symbol manifest: {}", ZSTD_getErrorName(decompressedLen)); return; } - if (decompressedLen != payload.size()) { - Log.error("symbol manifest {} decompressed to {} bytes, expected {}", - io::fs_path_to_string(path), decompressedLen, payload.size()); + if (decompressedLen != data.size()) { + Log.error("embedded symbol manifest decompressed to {} bytes, expected {}", + decompressedLen, data.size()); return; } } - data = std::move(payload); const uint64_t entriesEnd = uint64_t{header.entryCount} * sizeof(Entry); if (entriesEnd > data.size()) { - Log.error("decompressed symbol manifest {} is malformed", io::fs_path_to_string(path)); + Log.error("decompressed embedded symbol manifest is malformed"); return; } diff --git a/src/dusk/mods/svc/hook.cpp b/src/dusk/mods/svc/hook.cpp index 41399bf97d..34d7d54aa7 100644 --- a/src/dusk/mods/svc/hook.cpp +++ b/src/dusk/mods/svc/hook.cpp @@ -6,6 +6,7 @@ #if DUSK_CODE_MODS #include "dusk/logging.h" +#include "dusk/mods/log_buffer.hpp" #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #endif @@ -64,8 +66,29 @@ struct InstalledHook { std::unordered_map s_registry; std::unordered_map s_installed; +std::unordered_map> s_declaredTargets; uint64_t s_nextOrder = 0; +bool declared_target(const ModContext* context, void* fnAddr) { + if (context == nullptr) { + return false; + } + const auto it = s_declaredTargets.find(context); + if (it == s_declaredTargets.end()) { + return false; + } + const auto addr = reinterpret_cast(fnAddr); + return std::ranges::find(it->second, addr) != it->second.end(); +} + +ModResult reject_undeclared(ModContext* context, void* fnAddr) { + log::write(mod_id_from_context(context), LOG_LEVEL_ERROR, + "tried to hook undeclared target {:p}; hook targets must be declared with " + "DEFINE_HOOK/DEFINE_HOOK_SYMBOL", + fnAddr); + return MOD_INVALID_ARGUMENT; +} + HookOptions normalize_options(const HookOptions* options) { if (options == nullptr || options->struct_size < sizeof(HookOptions)) { return HOOK_OPTIONS_INIT; @@ -95,10 +118,19 @@ void sort_hooks(std::vector& hooks) { }); } +// Once a hook is installed, funchook has patched the target's entry: its bytes lead to the +// detour, not the function, so canonicalization must stop there. +[[maybe_unused]] bool installed_target(void* addr) { + return s_installed.contains(reinterpret_cast(addr)); +} + // Follow E9/FF25 chains to skip MSVC incremental-link and import stubs. void* resolve_import_thunk(void* addr) { #if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__)) for (int i = 0; i < 8; ++i) { + if (installed_target(addr)) { + break; + } const auto* p = static_cast(addr); if (p[0] == 0x48 && p[1] == 0xFF && p[2] == 0x25) { // lld emits a REX.W prefix ++p; @@ -122,6 +154,9 @@ void* resolve_import_thunk(void* addr) { // incremental-link stubs are a plain `b`, or `adrp x16; add x16, x16, #off; br x16` // range-extension thunks when the target is out of B range. for (int i = 0; i < 8; ++i) { + if (installed_target(addr)) { + break; + } const auto* p = static_cast(addr); uint32_t insn0, insn1, insn2; std::memcpy(&insn0, p, 4); @@ -158,6 +193,18 @@ void* resolve_import_thunk(void* addr) { return addr; } +// Resolve thunks recursively (max of 8 steps) until we find our target. +void* resolve_target(void* addr) { + for (int i = 0; i < 8; ++i) { + void* next = resolve_import_thunk(addr); + if (next == addr) { + break; + } + addr = next; + } + return addr; +} + funchook_t* install_trampoline(void* fnAddr, void* trampoline, void** outOriginal) { funchook_t* fh = funchook_create(); if (fh == nullptr) { @@ -199,7 +246,10 @@ ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, vo return MOD_INVALID_ARGUMENT; } - fnAddr = resolve_import_thunk(fnAddr); + fnAddr = resolve_target(fnAddr); + if (!declared_target(context, fnAddr)) { + return reject_undeclared(context, fnAddr); + } const auto key = reinterpret_cast(fnAddr); if (const auto it = s_installed.find(key); it != s_installed.end()) { auto& entry = it->second; @@ -242,7 +292,10 @@ ModResult hook_add_pre( if (fnAddr == nullptr || context == nullptr || callback == nullptr) { return MOD_INVALID_ARGUMENT; } - fnAddr = resolve_import_thunk(fnAddr); + fnAddr = resolve_target(fnAddr); + if (!declared_target(context, fnAddr)) { + return reject_undeclared(context, fnAddr); + } auto& hooks = s_registry[reinterpret_cast(fnAddr)].pre; hooks.push_back({context, callback, normalize_options(options), s_nextOrder++}); sort_hooks(hooks); @@ -254,7 +307,10 @@ ModResult hook_add_post( if (fnAddr == nullptr || context == nullptr || callback == nullptr) { return MOD_INVALID_ARGUMENT; } - fnAddr = resolve_import_thunk(fnAddr); + fnAddr = resolve_target(fnAddr); + if (!declared_target(context, fnAddr)) { + return reject_undeclared(context, fnAddr); + } auto& hooks = s_registry[reinterpret_cast(fnAddr)].post; hooks.push_back({context, nullptr, callback, normalize_options(options), s_nextOrder++}); sort_hooks(hooks); @@ -268,7 +324,10 @@ ModResult hook_replace( } const HookOptions normalized = normalize_options(options); - fnAddr = resolve_import_thunk(fnAddr); + fnAddr = resolve_target(fnAddr); + if (!declared_target(context, fnAddr)) { + return reject_undeclared(context, fnAddr); + } auto& slot = s_registry[reinterpret_cast(fnAddr)]; if (slot.replace.replaceCallback == nullptr) { slot.replace = {context, callback, nullptr, normalized, s_nextOrder++}; @@ -302,7 +361,7 @@ ModResult hook_dispatch_pre( return MOD_INVALID_ARGUMENT; } - fnAddr = resolve_import_thunk(fnAddr); + fnAddr = resolve_target(fnAddr); const auto it = s_registry.find(reinterpret_cast(fnAddr)); if (it == s_registry.end()) { return MOD_OK; @@ -352,7 +411,7 @@ ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval return MOD_INVALID_ARGUMENT; } - fnAddr = resolve_import_thunk(fnAddr); + fnAddr = resolve_target(fnAddr); const auto it = s_registry.find(reinterpret_cast(fnAddr)); if (it == s_registry.end()) { return MOD_OK; @@ -371,8 +430,187 @@ ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval return MOD_OK; } +#if defined(_WIN32) +/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at. + * Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */ +size_t vcall_slot_offset(const void*& fn) noexcept { + constexpr size_t npos = static_cast(-1); +#if defined(_M_X64) || defined(__x86_64__) + const auto* p = static_cast(fn); + for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs + int32_t rel; + std::memcpy(&rel, p + 1, 4); + p += 5 + rel; + } + fn = p; + // The vptr load. Unoptimized clang-cl thunks spill/reload rcx first + // (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window. + const uint8_t* q = nullptr; + for (int i = 0; i <= 12; ++i) { + if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx] + q = p + i + 3; + break; + } + } + if (q == nullptr) { + return npos; + } + if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC) + return 0; + } + if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8] + return static_cast(q[2]); + } + if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32] + int32_t off; + std::memcpy(&off, q + 2, 4); + return off; + } + // clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax + // distinguishes the thunk from an ordinary getter that begins the same way. + if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) { + size_t off = 0; + const uint8_t* r = q + 3; + if (q[2] == 0x40) { + off = static_cast(q[3]); + r = q + 4; + } else if (q[2] == 0x80) { + int32_t off32; + std::memcpy(&off32, q + 3, 4); + off = off32; + r = q + 7; + } + for (int i = 0; i <= 8; ++i) { + if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional) + return off; + } + } + } + return npos; +#elif defined(_M_ARM64) || defined(__aarch64__) + const auto* p = static_cast(fn); + uint32_t insn[3]; + for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs + std::memcpy(insn, p, 4); + if ((insn[0] & 0xFC000000u) != 0x14000000u) { + break; + } + const auto imm26 = static_cast(insn[0] << 6) >> 6; + p += static_cast(imm26) * 4; + } + fn = p; + std::memcpy(insn, p, 12); + // ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs + if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) { + return npos; + } + const uint32_t t = insn[0] & 0x1Fu; + if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) { + return npos; + } + const uint32_t s = insn[1] & 0x1Fu; + if (insn[2] != (0xD61F0000u | (s << 5))) { + return npos; + } + return ((insn[1] >> 10) & 0xFFFu) * 8; +#else + (void)fn; + return npos; +#endif +} +#endif // _WIN32 + +bool resolve_symbol_checked(const char* symbol, bool requireCode, void** out, std::string& why) { + HookSymbolFlags flags{}; + switch (manifest::resolve(symbol, out, &flags)) { + case manifest::ResolveStatus::Ok: + if (requireCode && (flags & HOOK_SYMBOL_CODE) == 0) { + why = fmt::format("'{}' is not a code symbol", symbol); + return false; + } + return true; + case manifest::ResolveStatus::Unavailable: + why = "no symbol manifest for this build"; + return false; + case manifest::ResolveStatus::NotFound: + why = fmt::format("symbol '{}' not found", symbol); + return false; + case manifest::ResolveStatus::Ambiguous: + why = fmt::format("'{}' maps to more than one address; use the mangled name", symbol); + return false; + } + why = "unexpected resolve failure"; + return false; +} + +/* Decode a HOOK_MEM record's pointer-to-member representation into the target code address, + * mirroring what calling through the mfp would invoke. Virtual members hook the class's own + * overrider, read from its vtable (resolved from the symbol manifest). */ +void* resolve_member_record( + const ModMetaHookMem& record, const char* vtableSymbol, std::string& why) { + uintptr_t words[2]; + std::memcpy(words, record.pmf, sizeof(words)); + +#if defined(_WIN32) + const void* fn = reinterpret_cast(words[0]); + const size_t slot = vcall_slot_offset(fn); + if (slot == static_cast(-1)) { // not a vcall thunk: direct address + return const_cast(fn); + } + if (vtableSymbol[0] == '\0') { + why = "class name is not representable as a vtable symbol"; + return nullptr; + } + void* vtable = nullptr; + if (!resolve_symbol_checked(vtableSymbol, false, &vtable, why)) { + return nullptr; + } + // ??_7 points at the first slot. + return *reinterpret_cast(static_cast(vtable) + slot); +#else +#if defined(__aarch64__) || defined(__arm__) + // AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function + // addresses can't spare their low bit), and ptr holds the slot offset directly. + const bool isVirtual = (words[1] & 1) != 0; + const uintptr_t thisAdjust = words[1] >> 1; + const uintptr_t slotOffset = words[0]; +#else + // Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1. + const bool isVirtual = (words[0] & 1) != 0; + const uintptr_t thisAdjust = words[1]; + const uintptr_t slotOffset = words[0] - 1; +#endif + if (!isVirtual) { // non-virtual: the address itself + return reinterpret_cast(words[0]); + } + if (thisAdjust != 0) { + // this-adjusting mfp (member of a secondary base): the slot offset is relative to a + // vtable we can't locate. Hook the overrider by name instead. + why = "virtual member of a secondary base; hook the overrider by name"; + return nullptr; + } + if (vtableSymbol[0] == '\0') { + why = "class name is not representable as a vtable symbol"; + return nullptr; + } + void* vtable = nullptr; + if (!resolve_symbol_checked(vtableSymbol, false, &vtable, why)) { + return nullptr; + } + // _ZTV points at the offset-to-top slot; the address point mfps index from is + // two pointers in (past offset-to-top and the typeinfo pointer). + void* target = + *reinterpret_cast(static_cast(vtable) + 2 * sizeof(void*) + slotOffset); + if (target == nullptr) { + why = "vtable slot is empty"; + } + return target; +#endif +} + void hook_remove_mod(LoadedMod& mod) { ModContext* context = mod.context.get(); + s_declaredTargets.erase(context); for (auto it = s_registry.begin(); it != s_registry.end();) { auto& slot = it->second; @@ -508,6 +746,56 @@ constexpr HookService s_hookService{ } // namespace +#if DUSK_CODE_MODS +void hook_resolve_mod_records(LoadedMod& mod) { + auto& declared = s_declaredTargets[mod.context.get()]; + declared.clear(); + if (!mod.native) { + return; + } + + const auto resolved = [&](void* target, void** slot) { + target = resolve_target(target); + *slot = target; + declared.push_back(reinterpret_cast(target)); + }; + const auto unresolved = [&](const char* what, std::string_view why, void** slot) { + *slot = nullptr; + log::write(mod.metadata.id, LOG_LEVEL_WARN, + "hook target '{}' did not resolve ({}); installing this hook will fail", what, why); + }; + + for (auto* record : mod.native->parsed.hookFns) { + if (record->target != nullptr) { + resolved(record->target, &record->resolved); + } else { + unresolved("", "null link-time target", &record->resolved); + } + } + for (auto* record : mod.native->parsed.hookMems) { + std::string why; + void* target = resolve_member_record(*record, hook_mem_vtable_symbol(*record), why); + if (target != nullptr) { + resolved(target, &record->resolved); + } else { + unresolved(hook_mem_display_name(*record), why, &record->resolved); + } + } + for (auto* record : mod.native->parsed.hookNames) { + const char* name = hook_name_symbol(*record); + std::string why; + void* target = nullptr; + if (resolve_symbol_checked(name, true, &target, why)) { + resolved(target, &record->resolved); + } else { + unresolved(name, why, &record->resolved); + } + } +} +#else +void hook_resolve_mod_records(LoadedMod&) {} +#endif // DUSK_CODE_MODS + constinit const ServiceModule g_hookModule{ .id = HOOK_SERVICE_ID, .majorVersion = HOOK_SERVICE_MAJOR, diff --git a/src/dusk/mods/svc/hook.hpp b/src/dusk/mods/svc/hook.hpp new file mode 100644 index 0000000000..d712a8054a --- /dev/null +++ b/src/dusk/mods/svc/hook.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace dusk::mods { +struct LoadedMod; +} + +namespace dusk::mods::svc { + +void hook_resolve_mod_records(LoadedMod& mod); + +} // namespace dusk::mods::svc diff --git a/src/dusk/mods/svc/host.cpp b/src/dusk/mods/svc/host.cpp index 0199a7865a..5deccc7c9e 100644 --- a/src/dusk/mods/svc/host.cpp +++ b/src/dusk/mods/svc/host.cpp @@ -60,7 +60,12 @@ const char* host_mod_version(ModContext* context) { const char* host_mod_dir(ModContext* context) { const auto* mod = mod_from_context(context); - return mod != nullptr ? mod->dir.c_str() : ""; + return mod != nullptr ? mod->dirUtf8.c_str() : ""; +} + +const char* host_native_dir(ModContext* context) { + const auto* mod = mod_from_context(context); + return mod != nullptr ? mod->nativeDirUtf8.c_str() : ""; } struct LifecycleWatcher { @@ -142,6 +147,7 @@ constinit HostService s_hostService{ .mod_dir = host_mod_dir, .watch_mod_lifecycle = host_watch_mod_lifecycle, .unwatch_mod_lifecycle = host_unwatch_mod_lifecycle, + .native_dir = host_native_dir, }; } // namespace diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 338625230a..2033d47475 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -217,29 +217,25 @@ void ModLoader::init_services() { } bool ModLoader::register_static_service_exports(LoadedMod& mod) { - if (!mod.native || mod.native->manifest == nullptr) { + if (!mod.native) { return true; } - const auto& manifest = *mod.native->manifest; - for (size_t i = 0; i < manifest.export_count; ++i) { - const auto& serviceExport = manifest.exports[i]; - if (serviceExport.struct_size != sizeof(ServiceExport) || - !svc::valid_service_id(serviceExport.service_id)) - { + for (const auto* serviceExport : mod.native->parsed.exports) { + if (!svc::valid_service_id(serviceExport->service_id.chars)) { fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service export descriptor"); return false; } - const bool deferred = (serviceExport.flags & SERVICE_EXPORT_DEFERRED) != 0; - if (!deferred && serviceExport.service == nullptr) { + const bool deferred = (serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) != 0; + if (!deferred && serviceExport->service == nullptr) { fail_mod(mod, MOD_INVALID_ARGUMENT, "Static service export has null service pointer"); return false; } const auto result = - svc::register_service(serviceExport.service_id, serviceExport.major_version, - serviceExport.minor_version, serviceExport.service, &mod, deferred); + svc::register_service(serviceExport->service_id.chars, serviceExport->major_version, + serviceExport->minor_version, serviceExport->service, &mod, deferred); if (result != MOD_OK) { fail_mod(mod, result, "Service export registration failed"); return false; @@ -262,18 +258,13 @@ std::string ModLoader::describe_missing_import( // No record can also mean the provider failed or is disabled and its services were removed. for (const auto& other : mods()) { - if ((other.active && !other.loadFailed) || !other.native || - other.native->manifest == nullptr) - { + if ((other.active && !other.loadFailed) || !other.native) { continue; } - const auto& manifest = *other.native->manifest; - for (size_t i = 0; i < manifest.export_count; ++i) { - const auto& serviceExport = manifest.exports[i]; - if (serviceExport.struct_size == sizeof(ServiceExport) && - svc::valid_service_id(serviceExport.service_id) && - std::string_view{serviceExport.service_id} == serviceId && - serviceExport.major_version == majorVersion) + for (const auto* serviceExport : other.native->parsed.exports) { + if (svc::valid_service_id(serviceExport->service_id.chars) && + std::string_view{serviceExport->service_id.chars} == serviceId && + serviceExport->major_version == majorVersion) { return fmt::format("Required service {}@{} unavailable: provider '{}' {}", serviceId, majorVersion, other.metadata.id, @@ -286,35 +277,33 @@ std::string ModLoader::describe_missing_import( } bool ModLoader::resolve_service_imports(LoadedMod& mod) { - if (!mod.native || mod.native->manifest == nullptr) { + if (!mod.native) { return true; } - const auto& manifest = *mod.native->manifest; - for (size_t i = 0; i < manifest.import_count; ++i) { - const auto& serviceImport = manifest.imports[i]; - if (serviceImport.struct_size != sizeof(ServiceImport) || - !svc::valid_service_id(serviceImport.service_id) || serviceImport.slot == nullptr) + for (const auto* serviceImport : mod.native->parsed.imports) { + if (!svc::valid_service_id(serviceImport->service_id.chars) || + serviceImport->slot == nullptr) { fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service import descriptor"); return false; } - const auto* service = svc::find_service( - serviceImport.service_id, serviceImport.major_version, serviceImport.min_minor_version); + const auto* service = svc::find_service(serviceImport->service_id.chars, + serviceImport->major_version, serviceImport->min_minor_version); if (service == nullptr) { - *static_cast(serviceImport.slot) = nullptr; - if ((serviceImport.flags & SERVICE_IMPORT_OPTIONAL) != 0) { + *static_cast(serviceImport->slot) = nullptr; + if ((serviceImport->rec.flags & SERVICE_IMPORT_OPTIONAL) != 0) { continue; } fail_mod(mod, MOD_UNAVAILABLE, - describe_missing_import(serviceImport.service_id, serviceImport.major_version, - serviceImport.min_minor_version)); + describe_missing_import(serviceImport->service_id.chars, + serviceImport->major_version, serviceImport->min_minor_version)); return false; } - *static_cast(serviceImport.slot) = service->service; + *static_cast(serviceImport->slot) = service->service; } return true; diff --git a/include/dusk/mouse.h b/src/dusk/mouse.h similarity index 100% rename from include/dusk/mouse.h rename to src/dusk/mouse.h diff --git a/include/dusk/os.h b/src/dusk/os.h similarity index 62% rename from include/dusk/os.h rename to src/dusk/os.h index 180bdb9b1a..46529d1e61 100644 --- a/include/dusk/os.h +++ b/src/dusk/os.h @@ -1,5 +1,4 @@ -#ifndef DUSK_OS_H -#define DUSK_OS_H +#pragma once #ifdef __cplusplus extern "C" { @@ -9,6 +8,8 @@ void OSSetCurrentThreadName(const char* name); #ifdef __cplusplus } -#endif -#endif // DUSK_OS_H \ No newline at end of file +namespace dusk { +extern bool OSReportReallyForceEnable; +} +#endif diff --git a/include/dusk/scope_guard.hpp b/src/dusk/scope_guard.hpp similarity index 79% rename from include/dusk/scope_guard.hpp rename to src/dusk/scope_guard.hpp index 281d23434d..ac35334d5d 100644 --- a/include/dusk/scope_guard.hpp +++ b/src/dusk/scope_guard.hpp @@ -1,5 +1,4 @@ -#ifndef DUSK_SCOPE_GUARD_HPP -#define DUSK_SCOPE_GUARD_HPP +#pragma once #include @@ -16,5 +15,3 @@ public: private: std::function m_func; }; - -#endif //DUSK_SCOPE_GUARD_HPP diff --git a/include/dusk/settings.h b/src/dusk/settings.h similarity index 100% rename from include/dusk/settings.h rename to src/dusk/settings.h diff --git a/include/dusk/speedrun.h b/src/dusk/speedrun.h similarity index 100% rename from include/dusk/speedrun.h rename to src/dusk/speedrun.h diff --git a/include/dusk/texture_replacements.hpp b/src/dusk/texture_replacements.hpp similarity index 78% rename from include/dusk/texture_replacements.hpp rename to src/dusk/texture_replacements.hpp index 85cb59b001..1a633d424d 100644 --- a/include/dusk/texture_replacements.hpp +++ b/src/dusk/texture_replacements.hpp @@ -1,5 +1,4 @@ -#ifndef DUSK_TEXTURE_REPLACEMENTS_HPP -#define DUSK_TEXTURE_REPLACEMENTS_HPP +#pragma once #include @@ -13,5 +12,3 @@ void set_enabled(bool enabled); void shutdown(); } - -#endif diff --git a/include/dusk/time.h b/src/dusk/time.h similarity index 98% rename from include/dusk/time.h rename to src/dusk/time.h index ead946188b..fad3186042 100644 --- a/include/dusk/time.h +++ b/src/dusk/time.h @@ -1,5 +1,4 @@ -#ifndef DUSK_TIME_H -#define DUSK_TIME_H +#pragma once #include #include @@ -138,5 +137,3 @@ private: void NanoSleep(const duration_t duration) { SDL_DelayPrecise(duration); } #endif }; - -#endif diff --git a/include/dusk/touch_camera.h b/src/dusk/touch_camera.h similarity index 100% rename from include/dusk/touch_camera.h rename to src/dusk/touch_camera.h diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index e2ee60729a..f6cfe0320f 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -219,55 +219,8 @@ AuroraBackend configured_backend() { return configuredBackend; } -std::filesystem::path normalized_display_path(const std::filesystem::path& path) { - std::error_code ec; - auto normalized = std::filesystem::weakly_canonical(path, ec); - if (!ec) { - return normalized; - } - - normalized = std::filesystem::absolute(path, ec); - if (!ec) { - return normalized.lexically_normal(); - } - - return path.lexically_normal(); -} - -std::filesystem::path user_home_path() { - const char* homePath = SDL_GetUserFolder(SDL_FOLDER_HOME); - if (homePath == nullptr || homePath[0] == '\0') { - return {}; - } - return std::filesystem::path{reinterpret_cast(homePath)}; -} - -Rml::String abbreviated_data_path_string() { - const auto path = data::configured_data_path(); - const auto homePath = user_home_path(); - if (path.empty() || homePath.empty()) { - return io::fs_path_to_string(path); - } - - const auto normalizedPath = normalized_display_path(path); - const auto normalizedHome = normalized_display_path(homePath); - if (normalizedPath == normalizedHome) { - return "~"; - } - - const auto relativePath = normalizedPath.lexically_relative(normalizedHome); - if (!relativePath.empty() && !relativePath.is_absolute()) { - const auto it = relativePath.begin(); - if (it == relativePath.end() || *it != "..") { - return io::fs_path_to_string(std::filesystem::path{"~"} / relativePath); - } - } - - return io::fs_path_to_string(path); -} - Rml::String configured_data_path_display_name() { - const auto path = abbreviated_data_path_string(); + const auto path = data::abbreviated_path_string(data::configured_data_path()); if (path.empty()) { return "(none)"; } @@ -284,8 +237,9 @@ public: explicit DataFolderPathText(Rml::Element* parent) : Component(append(parent, "div")) {} void update() override { - const Rml::String rml = "Current data folder:
" + - escape(abbreviated_data_path_string()) + "
"; + const Rml::String rml = + "Current data folder:
" + + escape(data::abbreviated_path_string(data::configured_data_path())) + "
"; if (rml != mCurrentRml) { mRoot->SetInnerRML(rml); mCurrentRml = rml; diff --git a/include/dusk/version.hpp b/src/dusk/version.hpp similarity index 95% rename from include/dusk/version.hpp rename to src/dusk/version.hpp index fe393b049b..4fd6395059 100644 --- a/include/dusk/version.hpp +++ b/src/dusk/version.hpp @@ -1,5 +1,4 @@ -#ifndef DUSK_VERSION_HPP -#define DUSK_VERSION_HPP +#pragma once /** * Functionality for switching game behavior based on the loaded game version (e.g. PAL/JPN, GC/Wii) @@ -63,5 +62,3 @@ namespace dusk::version { return defaultValue; } } // namespace dusk::version - -#endif // DUSK_VERSION_HPP diff --git a/src/f_pc/f_pc_manager.cpp b/src/f_pc/f_pc_manager.cpp index f095597b57..4a1f72a19b 100644 --- a/src/f_pc/f_pc_manager.cpp +++ b/src/f_pc/f_pc_manager.cpp @@ -21,6 +21,10 @@ #include "f_pc/f_pc_priority.h" #include "m_Do/m_Do_controller_pad.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + #include "tracy/Tracy.hpp" #if TARGET_PC diff --git a/src/dusk/batch.cpp b/src/helpers/batch.cpp similarity index 97% rename from src/dusk/batch.cpp rename to src/helpers/batch.cpp index 04cba52b0d..a614eb8b1c 100644 --- a/src/dusk/batch.cpp +++ b/src/helpers/batch.cpp @@ -1,10 +1,10 @@ -#include "dusk/batch.hpp" +#include "helpers/batch.hpp" #include "dusk/logging.h" #include #include -namespace dusk::batch { +namespace batch { void decode_leaf_template(const u8* dl, u32 size, LeafTemplate& out) { out.vtxCount = 0; @@ -69,4 +69,4 @@ void decode_leaf_template(const u8* dl, u32 size, LeafTemplate& out) { } } -} // namespace dusk::batch +} // namespace batch diff --git a/src/dusk/endian.cpp b/src/helpers/endian.cpp similarity index 94% rename from src/dusk/endian.cpp rename to src/helpers/endian.cpp index 777ed8ae69..a4de822f07 100644 --- a/src/dusk/endian.cpp +++ b/src/helpers/endian.cpp @@ -1,7 +1,7 @@ -#include "dusk/endian.h" +#include "helpers/endian.h" #include #include "SSystem/SComponent/c_xyz.h" -#include "dusk/endian_gx.hpp" +#include "helpers/endian_gx.hpp" #define IMPL_ENUM(type) \ template <> \ diff --git a/src/dusk/offset_ptr.cpp b/src/helpers/offset_ptr.cpp similarity index 92% rename from src/dusk/offset_ptr.cpp rename to src/helpers/offset_ptr.cpp index f26fae3db5..59ca70a4de 100644 --- a/src/dusk/offset_ptr.cpp +++ b/src/helpers/offset_ptr.cpp @@ -1,8 +1,7 @@ -#include "dusk/offset_ptr.h" +#include "helpers/offset_ptr.h" #include "JSystem/JUtility/JUTAssert.h" -#if TARGET_PC bool OffsetPtr::isRelocated() { return value & 0x8000'0000; } @@ -25,4 +24,3 @@ bool OffsetPtr::setBase(void* base) { value = newDiff | 0x8000'0000; return true; } -#endif diff --git a/src/dusk/string.cpp b/src/helpers/string.cpp similarity index 96% rename from src/dusk/string.cpp rename to src/helpers/string.cpp index 7666f9d9ae..23fdbfb8db 100644 --- a/src/dusk/string.cpp +++ b/src/helpers/string.cpp @@ -1,4 +1,4 @@ -#include "dusk/string.hpp" +#include "helpers/string.hpp" #include "fmt/format.h" namespace { @@ -14,8 +14,6 @@ void strncpyProxy(char* dst, const char* src, size_t count) { } } // namespace -namespace dusk { - void TextSpan::CrashSpawnEmpty() { CRASH("Span is empty!"); } @@ -81,5 +79,3 @@ int SafeStringVPrintf(char* buffer, size_t bufSize, const char* src, std::va_lis return vsnprintf(buffer, bufSize, src, args); } - -} // namespace dusk \ No newline at end of file diff --git a/src/m_Do/m_Do_audio.cpp b/src/m_Do/m_Do_audio.cpp index 0409d9f1e5..37c6318bce 100644 --- a/src/m_Do/m_Do_audio.cpp +++ b/src/m_Do/m_Do_audio.cpp @@ -14,6 +14,10 @@ #include "m_Do/m_Do_dvd_thread.h" #include "tracy/Tracy.hpp" +#if TARGET_PC +#include "dusk/settings.h" +#endif + #if PLATFORM_WII || PLATFORM_SHIELD #include "Z2AudioCS/Z2AudioCS.h" #include @@ -25,6 +29,18 @@ DUSK_GAME_DATA u8 mDoAud_zelAudio_c::mResetFlag; DUSK_GAME_DATA u8 mDoAud_zelAudio_c::mBgmSet; +#if TARGET_PC +void mDoAud_seStartMenu(u32 i_sfxID) { + if (!mDoAud_zelAudio_c::isInitFlag()) { + return; + } + if (!dusk::getSettings().audio.menuSounds.getValue()) { + return; + } + mDoAud_seStart(i_sfxID, nullptr, 0, 0); +} +#endif + void mDoAud_zelAudio_c::reset() { mBgmSet = false; } diff --git a/src/m_Do/m_Do_controller_pad.cpp b/src/m_Do/m_Do_controller_pad.cpp index fc1ebf7b8c..9751bf4f27 100644 --- a/src/m_Do/m_Do_controller_pad.cpp +++ b/src/m_Do/m_Do_controller_pad.cpp @@ -14,6 +14,7 @@ #if TARGET_PC #include "dusk/menu_pointer.h" +#include "dusk/settings.h" #include "dusk/ui/touch_controls.hpp" #endif @@ -22,6 +23,22 @@ DUSK_GAME_DATA JUTGamePad* mDoCPd_c::m_gamePad[4]; DUSK_GAME_DATA interface_of_controller_pad mDoCPd_c::m_cpadInfo[4]; DUSK_GAME_DATA interface_of_controller_pad mDoCPd_c::m_debugCpadInfo[4]; +#if TARGET_PC +s16 mDoCPd_c::getStickAngle3D(u32 pad) { + if (dusk::getSettings().game.enableMirrorMode) { + return -getCpadInfo(pad).mMainStickAngle; + } + return getCpadInfo(pad).mMainStickAngle; +} + +f32 mDoCPd_c::getSubStickX3D(u32 pad) { + if (dusk::getSettings().game.enableMirrorMode) { + return -getCpadInfo(pad).mCStickPosX; + } + return getCpadInfo(pad).mCStickPosX; +} +#endif + void mDoCPd_c::create() { #if PLATFORM_GCN || PLATFORM_SHIELD m_gamePad[0] = JKR_NEW JUTGamePad(JUTGamePad::EPort1); diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index 1a4bc19098..0d0f9014dc 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -51,10 +51,10 @@ #include "aurora/lib/window.hpp" #include "d/actor/d_a_horse.h" #include "dusk/dusk.h" -#include "dusk/endian.h" +#include "helpers/endian.h" #include "dusk/frame_interpolation.h" #include "dusk/gfx.hpp" -#include "dusk/gx_helper.h" +#include "helpers/gx_helper.h" #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/logging.h" #include "dusk/settings.h" diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 353f7cc1be..5c0d0e1615 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -65,6 +65,7 @@ #include "dusk/mod_loader.hpp" #include "dusk/logging.h" #include "dusk/main.h" +#include "dusk/os.h" #include "dusk/ui/menu_bar.hpp" #include "dusk/ui/overlay.hpp" #include "dusk/ui/prelaunch.hpp" @@ -128,22 +129,6 @@ const int audioHeapSize = 0x14D800; // ========================================================================= #define COPYDATE_PATH "/str/Final/Release/COPYDATE" -#if TARGET_PC -DUSK_GAME_DATA bool dusk::IsRunning = true; -DUSK_GAME_DATA bool dusk::IsShuttingDown = false; -DUSK_GAME_DATA bool dusk::IsGameLaunched = false; -DUSK_GAME_DATA bool dusk::RestartRequested = false; -DUSK_GAME_DATA uint8_t dusk::SaveRequested = 0; -DUSK_GAME_DATA dusk::StageRequest dusk::StageRequested = {"",false}; -DUSK_GAME_DATA std::filesystem::path dusk::ConfigPath; -DUSK_GAME_DATA std::filesystem::path dusk::CachePath; -#endif - -void dusk::RequestRestart() noexcept { - RestartRequested = SupportsProcessRestart; - IsRunning = false; -} - s32 LOAD_COPYDATE(void*) { char buffer[32]; memset(buffer, 0, sizeof(buffer)); @@ -171,9 +156,7 @@ s32 LOAD_COPYDATE(void*) { return 1; } -DUSK_GAME_DATA AuroraInfo auroraInfo; -DUSK_GAME_DATA AuroraStats dusk::lastFrameAuroraStats; -DUSK_GAME_DATA float dusk::frameUsagePct = 0.0f; +AuroraInfo auroraInfo; bool launchUILoop() { while (dusk::IsRunning && !dusk::IsGameLaunched) { @@ -930,7 +913,10 @@ int game_main(int argc, char* argv[]) { main01(); - dusk::MoviePlayerShutdown(); + // We need to cleanly shut down the threads to avoid crashes on shutdown. + if (daMP_c::m_myObj) { + daMP_c::m_myObj->daMP_c_Finish(); + } dusk::crash_reporting::shutdown(); dusk::ShutdownFileLogging(); diff --git a/src/m_Do/m_Do_printf.cpp b/src/m_Do/m_Do_printf.cpp index 3cacece77d..70cde36c32 100644 --- a/src/m_Do/m_Do_printf.cpp +++ b/src/m_Do/m_Do_printf.cpp @@ -9,6 +9,7 @@ #include "m_Do/m_Do_ext.h" #if TARGET_PC #include +#include "dusk/os.h" #endif u8 __OSReport_disable;