mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-10 02:35:17 -04:00
Merge pull request #2211 from encounter/modmeta
Mod API: Refactor mods to have static metadata for parsing
This commit is contained in:
@@ -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}})
|
||||
@@ -279,5 +290,8 @@ jobs:
|
||||
path: |
|
||||
build/install/*.exe
|
||||
build/install/*.dll
|
||||
build/install/*.symdb
|
||||
build/install/res/
|
||||
build/install/mods/
|
||||
build/install/debug.7z
|
||||
build/install/sdk/
|
||||
|
||||
+6
-3
@@ -491,9 +491,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 ()
|
||||
|
||||
|
||||
+4
-21
@@ -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": {
|
||||
|
||||
@@ -19,6 +19,9 @@ for install_path in build/install/*; do
|
||||
[[ "$(basename "$install_path")" == *.* ]] && continue
|
||||
cp -r "$install_path" build/appdir/usr/bin
|
||||
done
|
||||
if [[ -f build/install/dusklight.symdb ]]; then
|
||||
cp build/install/dusklight.symdb build/appdir/usr/bin
|
||||
fi
|
||||
cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,1024x1024} build/appdir/usr/share/icons/hicolor
|
||||
cp platforms/freedesktop/dev.twilitrealm.dusk.desktop build/appdir/usr/share/applications
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
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 "$<TARGET_OBJECTS:${target}>")
|
||||
foreach (_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||
list(APPEND _rsp_lines "$<TARGET_FILE:${_lib}>")
|
||||
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 "$<TARGET_FILE:${_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 "$<TARGET_FILE:dawn::webgpu_dawn>")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
set(_vscript "${CMAKE_BINARY_DIR}/dusklight_exports.ver")
|
||||
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_EXE}" exports
|
||||
"@${_rsp}"
|
||||
--out "${_vscript}"
|
||||
--format version-script
|
||||
--exclude cmake_pch
|
||||
--exclude miniz
|
||||
--exclude asan_options
|
||||
# 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 "$<TARGET_FILE:${target}>" -o "${_stub}"
|
||||
--soname "$<TARGET_FILE_NAME:${target}>" --arch "${_arch}"
|
||||
BYPRODUCTS "${_stub}"
|
||||
COMMENT "Generating dusklight link stub"
|
||||
VERBATIM)
|
||||
install(FILES "${_stub}" DESTINATION sdk)
|
||||
endfunction()
|
||||
@@ -0,0 +1,91 @@
|
||||
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 "$<CONFIG>/")
|
||||
endif ()
|
||||
|
||||
set(_rsp_lines "$<TARGET_OBJECTS:${target}>")
|
||||
foreach (_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||
list(APPEND _rsp_lines "$<TARGET_FILE:${_lib}>")
|
||||
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 "$<TARGET_FILE:${_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 "$<TARGET_FILE:dawn::webgpu_dawn>")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
set(_exp "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.exp")
|
||||
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}" exports
|
||||
"@${_rsp}"
|
||||
--out "${_exp}"
|
||||
--exclude cmake_pch
|
||||
--exclude miniz
|
||||
--exclude asan_options
|
||||
${_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()
|
||||
+104
-49
@@ -1,8 +1,9 @@
|
||||
# add_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>] [OVERLAY_DIR <overlay>]
|
||||
# add_mod(<target> SOURCES <file>... MOD_JSON <mod.json>
|
||||
# [RUNTIME_LIBRARIES <file>...] [RES_DIR <res>] [OVERLAY_DIR <overlay>]
|
||||
# [TEXTURES_DIR <textures>] [OUTPUT_DIR <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)
|
||||
@@ -45,7 +48,8 @@ function(_mod_collect_assets out_var dir)
|
||||
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" ${ARGN})
|
||||
if (NOT ARG_MOD_JSON)
|
||||
message(FATAL_ERROR "add_mod: MOD_JSON is required")
|
||||
endif ()
|
||||
@@ -55,11 +59,12 @@ function(add_mod target_name)
|
||||
endif ()
|
||||
|
||||
set(_has_lib 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
|
||||
@@ -90,50 +95,65 @@ 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 (TARGET dusklight)
|
||||
set(_game_exe "$<TARGET_FILE:dusklight>")
|
||||
add_dependencies(${target_name} dusklight)
|
||||
elseif (DUSK_GAME_EXE)
|
||||
_mod_resolve_source_path(_game_exe "${DUSK_GAME_EXE}")
|
||||
else ()
|
||||
message(FATAL_ERROR "add_mod: DUSK_GAME_EXE is not set (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}")
|
||||
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}")
|
||||
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: DUSK_GAME_SOLIB is not set (libmain.so)")
|
||||
message(FATAL_ERROR "add_mod: DUSK_GAME_EXE is not set (libmain.so or stub)")
|
||||
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)
|
||||
set_target_properties(${target_name} PROPERTIES
|
||||
BUILD_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "$ORIGIN")
|
||||
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.")
|
||||
# Mods link against the game's import library (sdk/windows-<arch>.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 (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-<arch>.lib)")
|
||||
endif ()
|
||||
else ()
|
||||
message(FATAL_ERROR "add_mod: DUSK_GAME_EXE is not set (import library)")
|
||||
endif ()
|
||||
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_IMPLIB}")
|
||||
target_link_libraries(${target_name} PRIVATE "${_game_lib}")
|
||||
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 "$<$<COMPILE_LANGUAGE:C,CXX>:/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 +162,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
|
||||
"$<TARGET_FILE:${target_name}>" "${_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
|
||||
"$<TARGET_FILE:${target_name}>" "${_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 +231,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 +270,10 @@ endfunction()
|
||||
# user cache).
|
||||
# - Linux: pre-extracted stage dirs into <install>/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 <app>/mods/<id> and the dylib into Frameworks/<id>.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 <app>/mods/<id>, the mod library into Frameworks/<id>.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 +282,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 +298,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 "$<TARGET_FILE:${_target}>"
|
||||
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/$<TARGET_FILE_NAME:crashpad_handler>\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||
|
||||
@@ -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.0")
|
||||
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)
|
||||
@@ -122,4 +122,7 @@ function(setup_symbol_manifest target)
|
||||
COMMAND "${SYMGEN_EXE}" manifest ${_input} --out "${_out}"
|
||||
COMMENT "Generating symbol manifest"
|
||||
VERBATIM)
|
||||
if (NOT APPLE)
|
||||
install(FILES "${_out}" DESTINATION .)
|
||||
endif ()
|
||||
endfunction()
|
||||
|
||||
+16
-12
@@ -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")
|
||||
@@ -53,7 +60,7 @@ function(setup_windows_exports target)
|
||||
# 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
|
||||
@@ -70,11 +77,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 +89,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()
|
||||
|
||||
+74
-53
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
@@ -131,26 +131,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.
|
||||
|
||||
`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 `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 +204,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) {
|
||||
@@ -469,8 +473,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 +494,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<LinkPosMove>(svc_hook, on_pos_move_pre);
|
||||
```
|
||||
|
||||
### Post-hooks
|
||||
@@ -495,24 +505,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<LinkPosMove>(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<daAlink_c*>(args, 0));
|
||||
int result = LinkExecute::g_orig(dusk::mods::arg<daAlink_c*>(args, 0));
|
||||
if (retval != nullptr) {
|
||||
*static_cast<int*>(retval) = result;
|
||||
}
|
||||
}
|
||||
|
||||
dusk::mods::hook_replace<&daAlink_c::execute>(svc_hook, on_execute_replace);
|
||||
dusk::mods::hook_replace<LinkExecute>(svc_hook, on_execute_replace);
|
||||
```
|
||||
|
||||
By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`,
|
||||
@@ -525,10 +533,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<HookshotHit>(svc_hook, on_hookshot_hit_pre);
|
||||
...
|
||||
@@ -537,9 +543,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 +565,8 @@ T& ref = dusk::mods::arg_ref<T>(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<int>(args, 1);
|
||||
@@ -561,36 +576,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<CreateItem>(svc_hook, on_create_item_pre);
|
||||
```
|
||||
|
||||
For reference parameters (e.g. `const cXyz& pos`), `arg_ref<cXyz>` 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
|
||||
@@ -680,7 +670,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 +696,7 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<MyModService> {
|
||||
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 +725,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 +755,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/<platform>/`. 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.
|
||||
|
||||
@@ -67,9 +67,32 @@ struct ModSearchDir {
|
||||
std::filesystem::path nativeLibDir;
|
||||
};
|
||||
|
||||
struct ModMetaParsed {
|
||||
uint32_t abiVersion = 0;
|
||||
std::vector<ModMetaImport*> imports;
|
||||
std::vector<ModMetaExport*> exports;
|
||||
std::vector<ModMetaHookFn*> hookFns;
|
||||
std::vector<ModMetaHookMem*> hookMems;
|
||||
std::vector<ModMetaHookName*> hookNames;
|
||||
};
|
||||
|
||||
inline const char* hook_mem_vtable_symbol(const ModMetaHookMem& rec) {
|
||||
return reinterpret_cast<const char*>(&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<char>::length(vtable) + 1;
|
||||
}
|
||||
|
||||
inline const char* hook_name_symbol(const ModMetaHookName& rec) {
|
||||
return reinterpret_cast<const char*>(&rec) + sizeof(ModMetaHookName);
|
||||
}
|
||||
|
||||
struct NativeMod {
|
||||
std::unique_ptr<loader::NativeModule> handle;
|
||||
const ModManifest* manifest = nullptr;
|
||||
const ModMeta* meta = nullptr;
|
||||
ModMetaParsed parsed;
|
||||
ModContext** contextSymbol = nullptr;
|
||||
|
||||
ModInitializeFn fn_initialize = nullptr;
|
||||
@@ -111,6 +134,11 @@ enum class NativeModStatus : u8 {
|
||||
*/
|
||||
MissingExport,
|
||||
|
||||
/**
|
||||
* Mod's metadata record section is malformed.
|
||||
*/
|
||||
InvalidMetadata,
|
||||
|
||||
/**
|
||||
* Unknown error loading the native mod.
|
||||
*/
|
||||
@@ -126,7 +154,7 @@ struct LoadedMod {
|
||||
// Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported.
|
||||
bool inPlace = false;
|
||||
|
||||
std::unique_ptr<ConfigVar<bool> > cvarIsEnabled;
|
||||
std::unique_ptr<ConfigVar<bool>> cvarIsEnabled;
|
||||
config::Subscription enabledSubscription = 0;
|
||||
|
||||
bool active = false;
|
||||
@@ -148,6 +176,8 @@ struct LoadedMod {
|
||||
uint32_t cacheGeneration = 0;
|
||||
// Currently extracted native library, empty if none.
|
||||
std::string nativePath;
|
||||
// Read-only directory containing the current platform's main module and runtime libraries.
|
||||
std::string nativeDir;
|
||||
|
||||
NativeModStatus nativeStatus = NativeModStatus::None;
|
||||
std::unique_ptr<NativeMod> native;
|
||||
@@ -197,10 +227,10 @@ private:
|
||||
// the next tick, by which point every per-frame entry into the mod should have returned.
|
||||
struct RetiredNative {
|
||||
std::unique_ptr<NativeMod> native;
|
||||
std::string path;
|
||||
std::string directory;
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<LoadedMod> > m_mods;
|
||||
std::vector<std::unique_ptr<LoadedMod>> m_mods;
|
||||
std::vector<ModSearchDir> m_searchDirs;
|
||||
std::filesystem::path m_cacheDir;
|
||||
std::vector<Request> m_pendingRequests;
|
||||
@@ -210,7 +240,8 @@ 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<std::string>& runtimeEntries);
|
||||
// Resolved <nativeLibDir>/<mod id><ext> if it exists on disk, empty otherwise.
|
||||
[[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const;
|
||||
void unload_native(LoadedMod& mod);
|
||||
|
||||
+118
-24
@@ -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);
|
||||
|
||||
+52
-309
@@ -2,11 +2,7 @@
|
||||
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace dusk::mods {
|
||||
@@ -14,255 +10,20 @@ namespace dusk::mods {
|
||||
template <class T>
|
||||
T arg(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T> > >(args[n]);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T> > >(args[n]);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
template <class F>
|
||||
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 <size_t N>
|
||||
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 <class T>
|
||||
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<class daAlink_c>(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 <class C>
|
||||
constexpr auto vtable_symbol() {
|
||||
constexpr std::string_view name = class_name<C>();
|
||||
constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos;
|
||||
// "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated
|
||||
std::array<char, name.size() + 12> 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<char>('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<size_t>(-1);
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
const auto* p = static_cast<const uint8_t*>(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<int8_t>(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<int8_t>(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<const uint8_t*>(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<int32_t>(insn[0] << 6) >> 6;
|
||||
p += static_cast<intptr_t>(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 <class C, class F>
|
||||
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<const void*>(words[0]);
|
||||
const size_t slot = vcall_slot_offset(fn);
|
||||
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
|
||||
*out = const_cast<void*>(fn);
|
||||
return MOD_OK;
|
||||
}
|
||||
void* vtable = nullptr;
|
||||
const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol<C>().data(), &vtable, nullptr);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
}
|
||||
// ??_7 points at the first slot.
|
||||
*out = *reinterpret_cast<void**>(static_cast<char*>(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<void*>(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<C>().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<void**>(static_cast<char*>(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. */
|
||||
/*
|
||||
* 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 <class Tag, class R, class... A>
|
||||
struct HookImpl {
|
||||
static inline R (*g_orig)(A...) = nullptr;
|
||||
@@ -333,60 +94,64 @@ 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.
|
||||
* 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 <auto Target>
|
||||
struct Hook;
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {
|
||||
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||
return detail::member_target<C>(hooks, Target, out);
|
||||
}
|
||||
};
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {};
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...) const>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {
|
||||
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||
return detail::member_target<C>(hooks, Target, out);
|
||||
}
|
||||
};
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {};
|
||||
|
||||
template <class R, class... A, R (*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {
|
||||
static ModResult resolve_target(const HookService*, void** out) {
|
||||
*out = mfp_addr(Target);
|
||||
return MOD_OK;
|
||||
}
|
||||
};
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {};
|
||||
|
||||
/*
|
||||
* 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<HookshotHit>(svc_hook, on_hookshot_hit);
|
||||
* 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 <FixedString Name, class Sig>
|
||||
struct NamedHook;
|
||||
|
||||
template <FixedString Name, class R, class... A>
|
||||
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, 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;
|
||||
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, 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<LinkExecute>(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 <class Entry>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
@@ -396,20 +161,16 @@ ModResult hook_install(const HookService* hooks) {
|
||||
|
||||
Entry::hooks = hooks;
|
||||
if (Entry::target == nullptr) {
|
||||
const ModResult resolved = Entry::resolve_target(hooks, &Entry::target);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
void* resolved = Entry::resolved_target();
|
||||
if (resolved == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
Entry::target = resolved;
|
||||
}
|
||||
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
|
||||
reinterpret_cast<void**>(&Entry::g_orig));
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
return hook_install<Hook<Target> >(hooks);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
@@ -421,12 +182,6 @@ ModResult hook_add_pre(
|
||||
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_add_pre<Hook<Target> >(hooks, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
@@ -438,12 +193,6 @@ ModResult hook_add_post(
|
||||
return hooks->add_post(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_add_post<Hook<Target> >(hooks, callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
@@ -455,10 +204,4 @@ ModResult hook_replace(
|
||||
return hooks->replace(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_replace<Hook<Target> >(hooks, callback, options);
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
/*
|
||||
* 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 <size_t N>
|
||||
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 <class T>
|
||||
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<class daAlink_c>(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 <class C>
|
||||
constexpr auto vtable_symbol() {
|
||||
constexpr std::string_view name = class_name<C>();
|
||||
constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos;
|
||||
// "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated
|
||||
std::array<char, name.size() + 12> 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<char>('0' + len % 10);
|
||||
len /= 10;
|
||||
}
|
||||
while (d != 0) {
|
||||
out[n++] = digits[--d];
|
||||
}
|
||||
for (char c : name) {
|
||||
out[n++] = c;
|
||||
}
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
template <class F>
|
||||
struct member_traits;
|
||||
template <class C, class R, class... A>
|
||||
struct member_traits<R (C::*)(A...)> {
|
||||
using Class = C;
|
||||
};
|
||||
template <class C, class R, class... A>
|
||||
struct member_traits<R (C::*)(A...) const> {
|
||||
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 <class F>
|
||||
struct HookFnRecord {
|
||||
ModMetaRecord rec;
|
||||
uint32_t reserved;
|
||||
F target;
|
||||
void* resolved;
|
||||
};
|
||||
|
||||
template <class F, size_t N>
|
||||
struct HookMemRecord {
|
||||
ModMetaRecord rec;
|
||||
uint32_t reserved;
|
||||
union {
|
||||
F fn;
|
||||
unsigned char raw[16];
|
||||
} pmf;
|
||||
void* resolved;
|
||||
char names[N];
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
struct HookNameRecord {
|
||||
ModMetaRecord rec;
|
||||
uint32_t reserved;
|
||||
void* resolved;
|
||||
char name[N];
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
constexpr size_t align_up(size_t n) {
|
||||
return (n + (N - 1)) & ~(N - 1);
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
struct HookMemNames {
|
||||
char chars[N]{};
|
||||
size_t len{};
|
||||
};
|
||||
|
||||
template <auto Target, FixedString Disp>
|
||||
consteval auto make_hook_mem_names() {
|
||||
using C = member_traits<decltype(Target)>::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<C>();
|
||||
constexpr size_t vtblLen = std::string_view{vtbl.data()}.size();
|
||||
HookMemNames<align_up<8>(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 <auto Target, char... Cs>
|
||||
struct HookMemHolder {
|
||||
using F = decltype(Target);
|
||||
static_assert(sizeof(F) <= 16, "unsupported pointer-to-member representation");
|
||||
MOD_META_RECORD static constinit inline HookMemRecord<F, sizeof...(Cs)> record = {
|
||||
{sizeof(HookMemRecord<F, sizeof...(Cs)>), MOD_META_HOOK_MEM, 0}, 0, {Target}, nullptr,
|
||||
{Cs...}};
|
||||
};
|
||||
|
||||
template <auto Target>
|
||||
struct HookFnHolder {
|
||||
using F = decltype(Target);
|
||||
static_assert(std::is_pointer_v<F> && std::is_function_v<std::remove_pointer_t<F>>,
|
||||
"hook target must be a function or member function");
|
||||
MOD_META_RECORD static constinit inline HookFnRecord<F> record = {
|
||||
{sizeof(HookFnRecord<F>), MOD_META_HOOK_FN, 0}, 0, Target, nullptr};
|
||||
};
|
||||
|
||||
template <auto Target, FixedString Disp,
|
||||
bool = std::is_member_function_pointer_v<decltype(Target)>>
|
||||
struct HookRecordFor {
|
||||
using Holder = HookFnHolder<Target>;
|
||||
};
|
||||
|
||||
template <auto Target, FixedString Disp>
|
||||
struct HookRecordFor<Target, Disp, true> {
|
||||
template <class Seq>
|
||||
struct Bind;
|
||||
template <size_t... Is>
|
||||
struct Bind<std::index_sequence<Is...>> {
|
||||
using Type = HookMemHolder<Target, make_hook_mem_names<Target, Disp>().chars[Is]...>;
|
||||
};
|
||||
using Holder =
|
||||
Bind<std::make_index_sequence<make_hook_mem_names<Target, Disp>().len>>::Type;
|
||||
};
|
||||
|
||||
template <FixedString Name>
|
||||
consteval auto make_hook_name_record() {
|
||||
constexpr size_t len = sizeof(Name.chars) - 1;
|
||||
HookNameRecord<align_up<8>(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
|
||||
+37
-79
@@ -1,60 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
#include "mods/meta.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
template <class Service>
|
||||
struct ServiceTraits;
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline std::vector<ServiceImport>& imports() {
|
||||
static std::vector<ServiceImport> entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
inline std::vector<ServiceExport>& exports() {
|
||||
static std::vector<ServiceExport> 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<uint16_t>(major_value), \
|
||||
static_cast<uint16_t>(min_minor_value), \
|
||||
static_cast<uint32_t>(flags_value), \
|
||||
&(variable), \
|
||||
})
|
||||
MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = { \
|
||||
{sizeof(ModMetaImport), MOD_META_IMPORT, static_cast<uint8_t>(flags_value)}, \
|
||||
static_cast<uint16_t>(major_value), \
|
||||
static_cast<uint16_t>(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<service_type>::id, \
|
||||
::dusk::mods::ServiceTraits<service_type>::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<service_type>::minor_version)
|
||||
|
||||
#define IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, min_minor_value) \
|
||||
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::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<service_type>::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<std::remove_cv_t<decltype(instance)> >::id)
|
||||
instance, ::dusk::mods::ServiceTraits<std::remove_cv_t<decltype(instance)>>::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<uint16_t>(major_value), \
|
||||
static_cast<uint16_t>(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<uint16_t>(major_value), \
|
||||
static_cast<uint16_t>(minor_value), \
|
||||
nullptr, \
|
||||
::dusk::mods::detail::make_service_id(service_id_value), \
|
||||
}
|
||||
|
||||
@@ -60,5 +60,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<CameraService> {
|
||||
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
|
||||
|
||||
@@ -103,5 +103,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<ConfigService> {
|
||||
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
|
||||
|
||||
@@ -26,5 +26,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<GameService> {
|
||||
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
|
||||
|
||||
@@ -205,5 +205,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<GfxService> {
|
||||
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
|
||||
|
||||
@@ -121,5 +121,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<HookService> {
|
||||
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
|
||||
|
||||
+12
-1
@@ -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<HostService> {
|
||||
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
|
||||
|
||||
@@ -43,5 +43,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<LogService> {
|
||||
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
|
||||
|
||||
@@ -53,5 +53,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<OverlayService> {
|
||||
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
|
||||
|
||||
@@ -48,5 +48,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<ResourceService> {
|
||||
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
|
||||
|
||||
@@ -84,5 +84,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<TextureService> {
|
||||
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
|
||||
|
||||
@@ -280,5 +280,6 @@ template <>
|
||||
struct dusk::mods::ServiceTraits<UiService> {
|
||||
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
|
||||
|
||||
+15
-12
@@ -93,10 +93,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<ClipperSphereClip>(&J3DUClipper::clip);
|
||||
constexpr ClipperBoxClip kClipperBoxClip = static_cast<ClipperBoxClip>(&J3DUClipper::clip);
|
||||
DEFINE_HOOK(&dDlst_shadowControl_c::imageDraw, GameShadowImageDraw);
|
||||
DEFINE_HOOK(&dDlst_shadowControl_c::draw, GameShadowDraw);
|
||||
DEFINE_HOOK(&drawCloudShadow, CloudShadowDraw);
|
||||
DEFINE_HOOK(static_cast<int (J3DUClipper::*)(f32 const (*)[4], Vec, f32) const>(&J3DUClipper::clip),
|
||||
ClipperSphereClip);
|
||||
DEFINE_HOOK(
|
||||
static_cast<int (J3DUClipper::*)(f32 const (*)[4], Vec*, Vec*) const>(&J3DUClipper::clip),
|
||||
ClipperBoxClip);
|
||||
DEFINE_HOOK(GXCopyTex, CopyTex);
|
||||
|
||||
// Mirror of the WGSL Uniforms struct (keep in sync with res/shadow.wgsl).
|
||||
struct ShadowUniforms {
|
||||
@@ -1048,20 +1053,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<GameShadowImageDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
|
||||
dusk::mods::hook_add_pre<GameShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
|
||||
dusk::mods::hook_add_pre<CloudShadowDraw>(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<kClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
|
||||
dusk::mods::hook_add_pre<kClipperBoxClip>(svc_hook, on_frustum_clip_pre) != MOD_OK)
|
||||
if (dusk::mods::hook_add_pre<ClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
|
||||
dusk::mods::hook_add_pre<ClipperBoxClip>(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<GXCopyTex>(svc_hook, on_copy_tex_pre) != MOD_OK) {
|
||||
if (dusk::mods::hook_add_pre<CopyTex>(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;
|
||||
|
||||
@@ -3,3 +3,4 @@ build/
|
||||
app/build/
|
||||
local.properties
|
||||
app/src/main/jniLibs/*/*.so
|
||||
app/src/main/bundled_mods/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -70,3 +70,27 @@ for abi in $ANDROID_STAGE_ABIS; do
|
||||
esac
|
||||
copy_lib "$abi" "$src"
|
||||
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
|
||||
|
||||
+5
-6
@@ -7,8 +7,7 @@
|
||||
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
|
||||
# add_mod(my_mod SOURCES ... MOD_JSON mod.json)
|
||||
#
|
||||
# On Windows, pass -DDUSK_GAME_IMPLIB=<path to sdk/dusklight.lib> from the
|
||||
# matching game release. TODO: auto-download from tag
|
||||
# TODO: auto-download link targets from tag
|
||||
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
|
||||
@@ -38,10 +37,10 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/../extern/aurora/cmake/AuroraDawnProvider.c
|
||||
# Game ABI headers & compile definitions
|
||||
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 ()
|
||||
|
||||
|
||||
@@ -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 <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
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<intptr_t*>(impSlot);
|
||||
char* target = base + item.target;
|
||||
const int bits = static_cast<int>(item.flags & 0xff);
|
||||
|
||||
intptr_t reldata;
|
||||
switch (bits) {
|
||||
case 8:
|
||||
reldata = *reinterpret_cast<int8_t*>(target);
|
||||
break;
|
||||
case 16:
|
||||
reldata = *reinterpret_cast<int16_t*>(target);
|
||||
break;
|
||||
case 32:
|
||||
reldata = *reinterpret_cast<int32_t*>(target);
|
||||
break;
|
||||
case 64:
|
||||
reldata = *reinterpret_cast<int64_t*>(target);
|
||||
break;
|
||||
default:
|
||||
report("unsupported %d-bit pseudo-relocation at RVA 0x%x", bits, item.target);
|
||||
return false;
|
||||
}
|
||||
reldata -= reinterpret_cast<intptr_t>(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<long long>(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<char*>(&__ImageBase);
|
||||
char* start = &__RUNTIME_PSEUDO_RELOC_LIST__;
|
||||
char* end = &__RUNTIME_PSEUDO_RELOC_LIST_END__;
|
||||
if (end - start < static_cast<ptrdiff_t>(sizeof(HdrV2))) {
|
||||
return; // no data auto-imports in this mod
|
||||
}
|
||||
const HdrV2* hdr = reinterpret_cast<const HdrV2*>(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<const ItemV2*>(hdr + 1);
|
||||
const ItemV2* itemsEnd = reinterpret_cast<const ItemV2*>(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<size_t>(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;
|
||||
}
|
||||
+313
-110
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
@@ -16,6 +17,7 @@
|
||||
#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 +30,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 <TargetConditionals.h>
|
||||
#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 +82,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<ModBundle> load_bundle(const std::filesystem::path& modPath, bool fromDir) {
|
||||
if (fromDir) {
|
||||
@@ -85,24 +109,49 @@ std::unique_ptr<ModBundle> load_bundle(const std::filesystem::path& modPath, boo
|
||||
}
|
||||
}
|
||||
|
||||
struct DllLocateResult {
|
||||
struct NativeLocateResult {
|
||||
std::string entry;
|
||||
std::vector<std::string> runtimeEntries;
|
||||
bool anyLibs = false;
|
||||
};
|
||||
|
||||
DllLocateResult locate_dll_in_bundle(ModBundle& bundle) {
|
||||
DllLocateResult result;
|
||||
NativeLocateResult locate_native_runtime(ModBundle& bundle) {
|
||||
NativeLocateResult 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.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,31 +269,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;
|
||||
// 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 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;
|
||||
}
|
||||
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))
|
||||
const auto* cursor = static_cast<const uint8_t*>(meta->records_begin);
|
||||
const auto* end = static_cast<const uint8_t*>(meta->records_end);
|
||||
if (cursor == nullptr || end == nullptr || cursor > end ||
|
||||
(reinterpret_cast<uintptr_t>(cursor) & 7) != 0)
|
||||
{
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid import/export arrays");
|
||||
mod.nativeStatus = NativeModStatus::MissingExport;
|
||||
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<const uint8_t*>(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<const ModMetaRecord*>(cursor);
|
||||
const size_t size = rec->size;
|
||||
if (size < 8 || size % 8 != 0 || size > static_cast<size_t>(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<const ModMetaHeader*>(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<ModMetaImport*>(const_cast<uint8_t*>(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<ModMetaExport*>(const_cast<uint8_t*>(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<ModMetaHookFn*>(const_cast<uint8_t*>(cursor)));
|
||||
break;
|
||||
}
|
||||
case MOD_META_HOOK_MEM: {
|
||||
if (size <= sizeof(ModMetaHookMem)) {
|
||||
return invalid("truncated hook record");
|
||||
}
|
||||
auto* record = reinterpret_cast<ModMetaHookMem*>(const_cast<uint8_t*>(cursor));
|
||||
const char* strings = reinterpret_cast<const char*>(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<char>::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<ModMetaHookName*>(const_cast<uint8_t*>(cursor));
|
||||
const char* name = reinterpret_cast<const char*>(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 +437,14 @@ 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::Unknown:
|
||||
return "Unknown mod load failure";
|
||||
case NativeModStatus::None:
|
||||
@@ -303,7 +472,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<std::string>& runtimeEntries) {
|
||||
if (!EnableCodeMods) {
|
||||
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build");
|
||||
mod.nativeStatus = NativeModStatus::BuildDisabled;
|
||||
@@ -313,10 +483,19 @@ 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 {}: {}",
|
||||
io::fs_path_to_string(scratchDir), ec.message());
|
||||
return;
|
||||
}
|
||||
mod.dir = io::fs_path_to_string(fs::absolute(scratchDir));
|
||||
|
||||
fs::path libPath;
|
||||
fs::path runtimeDir;
|
||||
DirectoryRollback runtimeDirRollback;
|
||||
if (mod.inPlace) {
|
||||
if (!dllEntry.empty()) {
|
||||
libPath = fs::path(mod.modPath) / dllEntry;
|
||||
@@ -328,6 +507,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 +516,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<u8> 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 {}: {}",
|
||||
io::fs_path_to_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<const char*>(dllData.data()),
|
||||
static_cast<std::streamsize>(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<u8> 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<const char*>(data.data()),
|
||||
static_cast<std::streamsize>(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<NativeMod>();
|
||||
@@ -375,13 +584,13 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto getManifest = nativeMod->handle->LookupSymbol<ModGetManifestFn>("mod_get_manifest");
|
||||
nativeMod->meta = nativeMod->handle->LookupSymbol<const ModMeta*>("mod_meta");
|
||||
nativeMod->contextSymbol = nativeMod->handle->LookupSymbol<ModContext**>("mod_ctx");
|
||||
nativeMod->fn_initialize = nativeMod->handle->LookupSymbol<ModInitializeFn>("mod_initialize");
|
||||
nativeMod->fn_update = nativeMod->handle->LookupSymbol<ModUpdateFn>("mod_update");
|
||||
nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol<ModShutdownFn>("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,
|
||||
@@ -391,8 +600,7 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
nativeMod->manifest = getManifest();
|
||||
if (!validate_manifest(nativeMod->manifest, mod)) {
|
||||
if (!parse_meta(*nativeMod, mod)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -401,10 +609,11 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
|
||||
}
|
||||
*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.nativeDir = io::fs_path_to_string(fs::absolute(runtimeDir));
|
||||
mod.native = std::move(nativeMod);
|
||||
mod.nativeStatus = NativeModStatus::Loaded;
|
||||
runtimeDirRollback.release();
|
||||
}
|
||||
|
||||
void ModLoader::unload_native(LoadedMod& mod) {
|
||||
@@ -412,43 +621,38 @@ 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();
|
||||
}
|
||||
|
||||
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 +688,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,15 +752,15 @@ void ModLoader::try_load_mod(
|
||||
mod.cvarIsEnabled =
|
||||
std::make_unique<ConfigVar<bool>>(mod_enabled_cvar_name(mod.metadata.id), true);
|
||||
|
||||
const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle);
|
||||
const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*mod.bundle);
|
||||
if (anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty())) {
|
||||
mod.nativeStatus = NativeModStatus::Unknown;
|
||||
load_native(mod, dllEntry);
|
||||
load_native(mod, dllEntry, runtimeEntries);
|
||||
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);
|
||||
mod.manifestInfo = build_manifest_info(mod.native->parsed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,6 +793,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");
|
||||
@@ -879,14 +1081,14 @@ bool ModLoader::ensure_native_loaded(LoadedMod& mod) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle);
|
||||
const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*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);
|
||||
load_native(mod, dllEntry, runtimeEntries);
|
||||
if (mod.nativeStatus != NativeModStatus::Loaded) {
|
||||
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
|
||||
return false;
|
||||
@@ -922,14 +1124,16 @@ bool ModLoader::reload_bundle(LoadedMod& mod) {
|
||||
mod.failureReason.clear();
|
||||
|
||||
ModManifestInfo newInfo;
|
||||
if (const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle); anyLibs) {
|
||||
if (const auto [dllEntry, runtimeEntries, anyLibs] = locate_native_runtime(*mod.bundle);
|
||||
anyLibs)
|
||||
{
|
||||
mod.nativeStatus = NativeModStatus::Unknown;
|
||||
load_native(mod, dllEntry);
|
||||
load_native(mod, dllEntry, runtimeEntries);
|
||||
if (mod.nativeStatus != NativeModStatus::Loaded) {
|
||||
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
|
||||
return false;
|
||||
}
|
||||
newInfo = build_manifest_info(*mod.native->manifest);
|
||||
newInfo = build_manifest_info(mod.native->parsed);
|
||||
} else {
|
||||
mod.nativeStatus = NativeModStatus::None;
|
||||
++mod.cacheGeneration;
|
||||
@@ -998,8 +1202,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void*>(GetProcAddress(static_cast<HMODULE>(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;
|
||||
|
||||
@@ -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
|
||||
|
||||
+282
-6
@@ -6,6 +6,7 @@
|
||||
|
||||
#if DUSK_CODE_MODS
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -13,6 +14,7 @@
|
||||
#include <exception>
|
||||
#include <fmt/format.h>
|
||||
#include <funchook.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#endif
|
||||
@@ -64,8 +66,29 @@ struct InstalledHook {
|
||||
|
||||
std::unordered_map<uintptr_t, HookSlot> s_registry;
|
||||
std::unordered_map<uintptr_t, InstalledHook> s_installed;
|
||||
std::unordered_map<const ModContext*, std::vector<uintptr_t>> 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<uintptr_t>(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;
|
||||
@@ -158,6 +181,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 +234,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<uintptr_t>(fnAddr);
|
||||
if (const auto it = s_installed.find(key); it != s_installed.end()) {
|
||||
auto& entry = it->second;
|
||||
@@ -242,7 +280,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<uintptr_t>(fnAddr)].pre;
|
||||
hooks.push_back({context, callback, normalize_options(options), s_nextOrder++});
|
||||
sort_hooks(hooks);
|
||||
@@ -254,7 +295,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<uintptr_t>(fnAddr)].post;
|
||||
hooks.push_back({context, nullptr, callback, normalize_options(options), s_nextOrder++});
|
||||
sort_hooks(hooks);
|
||||
@@ -268,7 +312,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<uintptr_t>(fnAddr)];
|
||||
if (slot.replace.replaceCallback == nullptr) {
|
||||
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
|
||||
@@ -302,7 +349,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<uintptr_t>(fnAddr));
|
||||
if (it == s_registry.end()) {
|
||||
return MOD_OK;
|
||||
@@ -352,7 +399,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<uintptr_t>(fnAddr));
|
||||
if (it == s_registry.end()) {
|
||||
return MOD_OK;
|
||||
@@ -371,8 +418,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<size_t>(-1);
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
const auto* p = static_cast<const uint8_t*>(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<int8_t>(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<int8_t>(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<const uint8_t*>(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<int32_t>(insn[0] << 6) >> 6;
|
||||
p += static_cast<intptr_t>(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<const void*>(words[0]);
|
||||
const size_t slot = vcall_slot_offset(fn);
|
||||
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
|
||||
return const_cast<void*>(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<void**>(static_cast<char*>(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<void*>(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<void**>(static_cast<char*>(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 +734,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<uintptr_t>(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("<fn>", "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,
|
||||
|
||||
@@ -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
|
||||
@@ -63,6 +63,11 @@ const char* host_mod_dir(ModContext* context) {
|
||||
return mod != nullptr ? mod->dir.c_str() : "";
|
||||
}
|
||||
|
||||
const char* host_native_dir(ModContext* context) {
|
||||
const auto* mod = mod_from_context(context);
|
||||
return mod != nullptr ? mod->nativeDir.c_str() : "";
|
||||
}
|
||||
|
||||
struct LifecycleWatcher {
|
||||
ModLifecycleFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
@@ -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
|
||||
|
||||
@@ -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<const void**>(serviceImport.slot) = nullptr;
|
||||
if ((serviceImport.flags & SERVICE_IMPORT_OPTIONAL) != 0) {
|
||||
*static_cast<const void**>(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<const void**>(serviceImport.slot) = service->service;
|
||||
*static_cast<const void**>(serviceImport->slot) = service->service;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user