mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-10 10:43:41 -04:00
Hook service, symgen manifest & data linking
This commit is contained in:
+84
-1
@@ -217,7 +217,41 @@ FetchContent_Declare(miniz
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
FetchContent_MakeAvailable(cxxopts json miniz)
|
||||
|
||||
set(_fetch_content_deps cxxopts json miniz)
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
message(STATUS "dusklight: Fetching funchook")
|
||||
# cmake/PatchFunchook.cmake patches funchook's cmake/capstone.cmake.in to inject a
|
||||
# PATCH_COMMAND into capstone's inner ExternalProject. That PATCH_COMMAND runs
|
||||
# cmake/PatchCapstone.cmake after capstone is cloned, which removes the
|
||||
# cmake_policy(SET CMP0048 OLD) line that CMake >= 3.27 rejects.
|
||||
# This is incredibly scuffed and we should probably think of a better way to do this
|
||||
set(CAPSTONE_FIX_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/PatchCapstone.cmake")
|
||||
FetchContent_Declare(funchook
|
||||
GIT_REPOSITORY https://github.com/kubo/funchook.git
|
||||
GIT_TAG v1.1.3
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
PATCH_COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=<SOURCE_DIR> -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/PatchFunchook.cmake
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
set(FUNCHOOK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(FUNCHOOK_BUILD_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(FUNCHOOK_INSTALL OFF CACHE BOOL "" FORCE)
|
||||
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||
list(LENGTH CMAKE_OSX_ARCHITECTURES _osx_arch_count)
|
||||
if (_osx_arch_count EQUAL 1)
|
||||
list(GET CMAKE_OSX_ARCHITECTURES 0 _osx_arch)
|
||||
if (_osx_arch MATCHES "^(arm64|aarch64|ARM64)$")
|
||||
set(FUNCHOOK_CPU arm64 CACHE STRING "" FORCE)
|
||||
elseif (_osx_arch MATCHES "^(x86_64|AMD64|amd64|i[3-6]86|x86)$")
|
||||
set(FUNCHOOK_CPU x86 CACHE STRING "" FORCE)
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
list(APPEND _fetch_content_deps funchook)
|
||||
endif ()
|
||||
FetchContent_MakeAvailable(${_fetch_content_deps})
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
message(STATUS "dusklight: Fetching sentry-native")
|
||||
@@ -273,6 +307,9 @@ find_package(Threads REQUIRED)
|
||||
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
|
||||
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
|
||||
Threads::Threads zstd::libzstd dusklight_game_headers)
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
list(APPEND GAME_LIBS funchook-static)
|
||||
endif ()
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
list(APPEND GAME_LIBS sentry)
|
||||
@@ -413,6 +450,52 @@ target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
|
||||
target_include_directories(dusklight PRIVATE ${miniz_SOURCE_DIR})
|
||||
target_link_libraries(dusklight PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
|
||||
target_precompile_headers(dusklight PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
|
||||
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
include(cmake/SymbolManifest.cmake)
|
||||
if (WIN32)
|
||||
# Game ABI exports & import library for mod linking
|
||||
include(cmake/WindowsExports.cmake)
|
||||
setup_windows_exports(dusklight)
|
||||
endif ()
|
||||
# Post-link symbol manifest: hookable-surface name->address map keyed to the build.
|
||||
setup_symbol_manifest(dusklight)
|
||||
endif ()
|
||||
|
||||
# Hook reliability: guaranteed patchable entries on the game ABI surface, and no identical-code folding.
|
||||
if (MSVC)
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:/hotpatch>)
|
||||
endif ()
|
||||
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
|
||||
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN:16 /OPT:NOICF)
|
||||
else ()
|
||||
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN /OPT:NOICF)
|
||||
endif ()
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
|
||||
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=2,1>)
|
||||
else ()
|
||||
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=10,5>)
|
||||
endif ()
|
||||
endif ()
|
||||
if (DEFINED DUSK_PATCHABLE_ENTRY_FLAG)
|
||||
target_compile_options(dusklight PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||
endforeach()
|
||||
endif ()
|
||||
|
||||
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)
|
||||
target_link_options(dusklight PRIVATE -rdynamic)
|
||||
endif ()
|
||||
|
||||
if (TARGET crashpad_handler)
|
||||
add_dependencies(dusklight crashpad_handler)
|
||||
add_custom_command(TARGET dusklight POST_BUILD
|
||||
|
||||
@@ -85,6 +85,51 @@ function(add_mod target_name)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
# Game symbols resolve against the host executable at dlopen time.
|
||||
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
|
||||
elseif (ANDROID)
|
||||
if (TARGET dusklight)
|
||||
target_link_libraries(${target_name} PRIVATE dusklight)
|
||||
elseif (DUSK_GAME_SOLIB)
|
||||
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_SOLIB}")
|
||||
else ()
|
||||
message(FATAL_ERROR "add_mod: DUSK_GAME_SOLIB is not set (libmain.so)")
|
||||
endif ()
|
||||
elseif (UNIX)
|
||||
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
|
||||
elseif (WIN32)
|
||||
# Link against the generated import library (game ABI surface). Function calls
|
||||
# resolve through import thunks. Data is toolchain dependent:
|
||||
# - clang-cl: lld's mingw mode auto-imports data references, fixed up at load by
|
||||
# the mod SDK's pseudo-relocation runtime (pseudo_reloc.cpp).
|
||||
# - cl (MSVC): only DUSK_GAME_DATA-annotated data is reachable. Un-annotated
|
||||
# references fail to link.
|
||||
if (NOT DUSK_GAME_IMPLIB)
|
||||
message(FATAL_ERROR "add_mod: DUSK_GAME_IMPLIB is not set.")
|
||||
endif ()
|
||||
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_IMPLIB}")
|
||||
set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||
target_compile_definitions(${target_name} PRIVATE _ITERATOR_DEBUG_LEVEL=0)
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
target_compile_options(${target_name} PRIVATE "$<$<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 ()
|
||||
|
||||
set(_output_dir "${DUSK_MODS_OUTPUT_DIR}")
|
||||
if (ARG_OUTPUT_DIR)
|
||||
set(_output_dir "${ARG_OUTPUT_DIR}")
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Patches capstone's CMakeLists.txt for compatibility with CMake >= 4.0:
|
||||
# - Bumps cmake_minimum_required to 3.10 (CMake >= 4.0 dropped < 3.5 support; < 3.10 warns)
|
||||
# - Removes cmake_policy(SET CMP0048 OLD) (rejected by CMake >= 3.27)
|
||||
file(READ "${DIR}/CMakeLists.txt" _content)
|
||||
string(REGEX REPLACE
|
||||
"cmake_minimum_required[ \t]*\\([ \t]*VERSION[ \t]+[0-9]+\\.[0-9]+(\\.[0-9]+)?[ \t]*\\)"
|
||||
"cmake_minimum_required(VERSION 3.10)"
|
||||
_content "${_content}")
|
||||
string(REGEX REPLACE
|
||||
"cmake_policy[ \t]*\\([ \t]*SET[ \t]+CMP0048[ \t]+OLD[ \t]*\\)"
|
||||
"# cmake_policy(SET CMP0048 OLD)"
|
||||
_content "${_content}")
|
||||
file(WRITE "${DIR}/CMakeLists.txt" "${_content}")
|
||||
@@ -0,0 +1,60 @@
|
||||
file(READ "${SOURCE_DIR}/cmake/capstone.cmake.in" _content)
|
||||
|
||||
# Insert PATCH_COMMAND before CONFIGURE_COMMAND in the ExternalProject_Add.
|
||||
# Bracket args prevent cmake from substituting ${...} while writing this file.
|
||||
string(REPLACE
|
||||
" CONFIGURE_COMMAND \"\""
|
||||
[=[ PATCH_COMMAND "${CMAKE_COMMAND}" -DDIR=${CMAKE_CURRENT_BINARY_DIR}/capstone-src -P "${CAPSTONE_FIX_SCRIPT}"
|
||||
CONFIGURE_COMMAND ""]=]
|
||||
_content "${_content}")
|
||||
|
||||
file(WRITE "${SOURCE_DIR}/cmake/capstone.cmake.in" "${_content}")
|
||||
|
||||
file(READ "${SOURCE_DIR}/src/funchook_unix.c" _unix_content)
|
||||
|
||||
# macOS rejects the POSIX mprotect RWX/RW transition for executable image pages on arm64.
|
||||
# Use Mach VM_PROT_COPY for the short patch window, then restore RX permissions.
|
||||
if (NOT _unix_content MATCHES "VM_PROT_READ \\| VM_PROT_WRITE \\| VM_PROT_COPY")
|
||||
string(REPLACE
|
||||
[=[ rv = mprotect(mstate->addr, mstate->size, prot);]=]
|
||||
[=[#ifdef __APPLE__
|
||||
kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr,
|
||||
(vm_size_t)mstate->size, FALSE,
|
||||
VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY);
|
||||
if (kr == KERN_SUCCESS) {
|
||||
funchook_log(funchook, " unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR")\n",
|
||||
mstate->addr, mstate->size, start, len);
|
||||
return 0;
|
||||
}
|
||||
funchook_set_error_message(funchook, "Failed to unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR", error=%s)",
|
||||
mstate->addr, mstate->size, start, len,
|
||||
mach_error_string(kr));
|
||||
return FUNCHOOK_ERROR_MEMORY_FUNCTION;
|
||||
#endif
|
||||
rv = mprotect(mstate->addr, mstate->size, prot);]=]
|
||||
_unix_content "${_unix_content}")
|
||||
|
||||
string(REPLACE
|
||||
[=[ char errbuf[128];
|
||||
int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=]
|
||||
[=[ char errbuf[128];
|
||||
#ifdef __APPLE__
|
||||
kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr,
|
||||
(vm_size_t)mstate->size, FALSE,
|
||||
VM_PROT_READ | VM_PROT_EXECUTE);
|
||||
|
||||
if (kr == KERN_SUCCESS) {
|
||||
funchook_log(funchook, " protect memory %p (size=%"PRIuPTR", prot=read,exec)\n",
|
||||
mstate->addr, mstate->size);
|
||||
return 0;
|
||||
}
|
||||
funchook_set_error_message(funchook, "Failed to protect memory %p (size=%"PRIuPTR", prot=read,exec, error=%s)",
|
||||
mstate->addr, mstate->size,
|
||||
mach_error_string(kr));
|
||||
return FUNCHOOK_ERROR_MEMORY_FUNCTION;
|
||||
#endif
|
||||
int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=]
|
||||
_unix_content "${_unix_content}")
|
||||
endif ()
|
||||
|
||||
file(WRITE "${SOURCE_DIR}/src/funchook_unix.c" "${_unix_content}")
|
||||
@@ -0,0 +1,125 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||
|
||||
set(_SYMGEN_VERSION "1.1.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)
|
||||
|
||||
function(symgen_host_asset out_name)
|
||||
string(TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" _host_processor)
|
||||
set(_asset "")
|
||||
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
|
||||
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||
set(_asset "symgen-macos-arm64")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-macos-x86_64")
|
||||
endif ()
|
||||
elseif (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||
if (_host_processor MATCHES "^(aarch64|arm64)$")
|
||||
set(_asset "symgen-linux-aarch64")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-linux-x86_64")
|
||||
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_asset "symgen-linux-i686")
|
||||
endif ()
|
||||
elseif (CMAKE_HOST_WIN32)
|
||||
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||
set(_asset "symgen-windows-arm64.exe")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-windows-x86_64.exe")
|
||||
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_asset "symgen-windows-x86.exe")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
set(${out_name} "${_asset}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(ensure_symgen required)
|
||||
if (TARGET symgen)
|
||||
return()
|
||||
endif ()
|
||||
|
||||
if (SYMGEN_PATH)
|
||||
get_filename_component(_symgen "${SYMGEN_PATH}" ABSOLUTE)
|
||||
if (NOT EXISTS "${_symgen}")
|
||||
if (required)
|
||||
message(FATAL_ERROR "symgen: SYMGEN_PATH does not exist: ${_symgen}")
|
||||
endif ()
|
||||
message(STATUS "symgen: SYMGEN_PATH does not exist, symbol manifest generation "
|
||||
"skipped (by-name hook resolution will be unavailable)")
|
||||
return()
|
||||
endif ()
|
||||
else ()
|
||||
symgen_host_asset(_asset)
|
||||
if (_asset STREQUAL "")
|
||||
if (required)
|
||||
message(FATAL_ERROR "symgen: no prebuilt binary for host "
|
||||
"${CMAKE_HOST_SYSTEM_NAME}/${CMAKE_HOST_SYSTEM_PROCESSOR} "
|
||||
"(configure with -DDUSK_ENABLE_CODE_MODS=OFF)")
|
||||
endif ()
|
||||
message(STATUS "symgen: no prebuilt binary for host "
|
||||
"${CMAKE_HOST_SYSTEM_NAME}/${CMAKE_HOST_SYSTEM_PROCESSOR}; "
|
||||
"symbol manifest generation skipped (by-name hook resolution will be unavailable)")
|
||||
return()
|
||||
endif ()
|
||||
|
||||
set(_symgen_dir "${CMAKE_BINARY_DIR}/_deps/symgen")
|
||||
set(_symgen "${_symgen_dir}/${_asset}")
|
||||
set(_url "${_SYMGEN_RELEASE_BASE_URL}/${_asset}")
|
||||
message(STATUS "dusklight: Fetching symgen ${_SYMGEN_VERSION} (${_asset})")
|
||||
file(MAKE_DIRECTORY "${_symgen_dir}")
|
||||
file(DOWNLOAD "${_url}" "${_symgen}"
|
||||
TLS_VERIFY ON
|
||||
STATUS _download_status
|
||||
SHOW_PROGRESS)
|
||||
list(GET _download_status 0 _download_code)
|
||||
if (NOT _download_code EQUAL 0)
|
||||
list(GET _download_status 1 _download_message)
|
||||
file(REMOVE "${_symgen}")
|
||||
if (required)
|
||||
message(FATAL_ERROR "symgen: failed to download ${_url}: ${_download_message}")
|
||||
endif ()
|
||||
message(STATUS "symgen: failed to download ${_url}: ${_download_message}; "
|
||||
"symbol manifest generation skipped (by-name hook resolution will be unavailable)")
|
||||
return()
|
||||
endif ()
|
||||
if (NOT CMAKE_HOST_WIN32)
|
||||
file(CHMOD "${_symgen}" PERMISSIONS
|
||||
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||
GROUP_READ GROUP_EXECUTE
|
||||
WORLD_READ WORLD_EXECUTE)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_custom_target(symgen DEPENDS "${_symgen}")
|
||||
set(SYMGEN_EXE "${_symgen}" CACHE INTERNAL "symgen executable" FORCE)
|
||||
endfunction()
|
||||
|
||||
function(setup_symbol_manifest target)
|
||||
ensure_symgen(TRUE)
|
||||
if (NOT TARGET symgen)
|
||||
return()
|
||||
endif ()
|
||||
add_dependencies(${target} symgen)
|
||||
|
||||
if (WIN32)
|
||||
set(_input --pdb "$<TARGET_PDB_FILE:${target}>")
|
||||
set(_out "$<TARGET_FILE_DIR:${target}>/dusklight.symdb")
|
||||
else ()
|
||||
set(_input --binary "$<TARGET_FILE:${target}>")
|
||||
if (APPLE)
|
||||
set(_out "$<TARGET_BUNDLE_CONTENT_DIR:${target}>/Resources/dusklight.symdb")
|
||||
else ()
|
||||
set(_out "$<TARGET_FILE_DIR:${target}>/dusklight.symdb")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND "${SYMGEN_EXE}" manifest ${_input} --out "${_out}"
|
||||
COMMENT "Generating symbol manifest"
|
||||
VERBATIM)
|
||||
endfunction()
|
||||
@@ -0,0 +1,76 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_DUSK_WINDOWS_EXPORTS_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||
|
||||
# Windows mod linking: generate the curated export surface for the game executable and the
|
||||
# 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()
|
||||
endif ()
|
||||
|
||||
include("${_DUSK_WINDOWS_EXPORTS_CMAKE_DIR}/SymbolManifest.cmake")
|
||||
ensure_symgen(TRUE)
|
||||
set(_symgen "${SYMGEN_EXE}")
|
||||
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 ()
|
||||
|
||||
# Generate curated exports list from the main binary
|
||||
set(_def "${CMAKE_BINARY_DIR}/dusklight_exports.def")
|
||||
add_custom_command(TARGET ${target} PRE_LINK
|
||||
# TODO: src/dusk/ is NOT excluded: inline code in game headers
|
||||
# currently call into it (e.g. dusk::frame_interp::lookup_replacement).
|
||||
COMMAND "${_symgen}" def
|
||||
--rsp "${_rsp}"
|
||||
--out "${_def}"
|
||||
--exclude cmake_pch
|
||||
--exclude miniz
|
||||
--exclude asan_options
|
||||
--max-exports 58000
|
||||
${_sdk_args}
|
||||
COMMENT "Generating dusklight exports"
|
||||
VERBATIM)
|
||||
target_link_options(${target} PRIVATE "/DEF:${_def}")
|
||||
|
||||
# Generate import library for mods to link against.
|
||||
set(_implib "${CMAKE_BINARY_DIR}/dusklight_imports.lib")
|
||||
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}")
|
||||
else ()
|
||||
set(_implib_cmd "${CMAKE_AR}" /nologo "/def:${_def}" /machine:x64 /name:dusklight.exe
|
||||
"/out:${_implib}")
|
||||
endif ()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${_implib_cmd}
|
||||
BYPRODUCTS "${_implib}"
|
||||
COMMENT "Generating dusklight import library"
|
||||
VERBATIM)
|
||||
if ("$CACHE{DUSK_GAME_IMPLIB}" STREQUAL "")
|
||||
set(DUSK_GAME_IMPLIB "${_implib}" CACHE INTERNAL "Import library for Windows mod linking")
|
||||
endif ()
|
||||
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)
|
||||
endfunction()
|
||||
+165
-7
@@ -3,8 +3,9 @@
|
||||
Mods are distributed as `.dusk` files: zip archives containing a `mod.json` manifest and, optionally, compiled code
|
||||
libraries and resources.
|
||||
|
||||
Everything a mod does goes through **services**: small, versioned C APIs. Dusklight provides built-in services, and mods
|
||||
can define their own to talk to each other.
|
||||
Most things a mod does goes through **services**: small, versioned C APIs. Dusklight provides built-in services, and
|
||||
mods can define their own to talk to each other. Mods also link against the game itself: include game headers and call
|
||||
game functions directly.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -13,10 +14,11 @@ can define their own to talk to each other.
|
||||
3. [Anatomy of a Code Mod](#anatomy-of-a-code-mod)
|
||||
4. [Services](#services)
|
||||
5. [Built-in Services](#built-in-services)
|
||||
6. [Asset Overlays](#asset-overlays)
|
||||
7. [Runtime Lifecycle](#runtime-lifecycle)
|
||||
8. [Error Handling](#error-handling)
|
||||
9. [Advanced: Exporting Services](#advanced-exporting-services)
|
||||
6. [Hooking Game Functions](#hooking-game-functions)
|
||||
7. [Asset Overlays](#asset-overlays)
|
||||
8. [Runtime Lifecycle](#runtime-lifecycle)
|
||||
9. [Error Handling](#error-handling)
|
||||
10. [Advanced: Exporting Services](#advanced-exporting-services)
|
||||
|
||||
---
|
||||
|
||||
@@ -211,6 +213,11 @@ svc_host->watch_mod_lifecycle(mod_ctx, on_mod_lifecycle, nullptr, &watch);
|
||||
`MOD_LIFECYCLE_DETACHED` fires on the game thread at a lifecycle safe point, after the subject's `mod_shutdown` ran and
|
||||
every service dropped its state. For your own mod's teardown, use `mod_shutdown` instead.
|
||||
|
||||
### HookService (`mods/svc/hook.h`)
|
||||
|
||||
Install hooks on game functions. You'll rarely call it directly; use the typed helpers in `mods/hook.hpp` described
|
||||
below.
|
||||
|
||||
### OverlayService (`mods/svc/overlay.h`)
|
||||
|
||||
Registers DVD file overlays at runtime. The dynamic counterpart to the static `overlay/` directory (
|
||||
@@ -302,6 +309,153 @@ Writes that store the same value are silent. Values applied from `config.json` o
|
||||
|
||||
---
|
||||
|
||||
## Hooking Game Functions
|
||||
|
||||
`mods/hook.hpp` provides typed helpers over the hook service:
|
||||
|
||||
```cpp
|
||||
#include "mods/hook.hpp"
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
```
|
||||
|
||||
### Pre-hooks
|
||||
|
||||
Run before the original. Return `HOOK_SKIP_ORIGINAL` to cancel it (post-hooks still run).
|
||||
|
||||
```cpp
|
||||
HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata) {
|
||||
daAlink_c* link = dusk::mods::arg<daAlink_c*>(args, 0); // arg 0 is `this`
|
||||
if (link->shape_angle.y > 10000) {
|
||||
return HOOK_SKIP_ORIGINAL;
|
||||
}
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
dusk::mods::hook_add_pre<&daAlink_c::posMove>(svc_hook, on_pos_move_pre);
|
||||
```
|
||||
|
||||
### Post-hooks
|
||||
|
||||
Run after the original (or after a replace-hook, or after a cancelled original). `retval` points to the return value, 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);
|
||||
```
|
||||
|
||||
### Replace-hooks
|
||||
|
||||
Substitute the original entirely. Call through to it via `Hook<...>::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));
|
||||
if (retval != nullptr) {
|
||||
*static_cast<int*>(retval) = result;
|
||||
}
|
||||
}
|
||||
|
||||
dusk::mods::hook_replace<&daAlink_c::execute>(svc_hook, on_execute_replace);
|
||||
```
|
||||
|
||||
By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`,
|
||||
`userdata`) controls this and callback ordering. Multiple mods can attach pre/post hooks to the same function
|
||||
independently.
|
||||
|
||||
### Hooking by name
|
||||
|
||||
Functions you can't name in C++ (file-local statics, private class members, anything not in a header) can be hooked by
|
||||
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*)>;
|
||||
|
||||
dusk::mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit_pre);
|
||||
...
|
||||
HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the original
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Reading and writing arguments
|
||||
|
||||
`args` is an array of pointers to the arguments. For member functions, index 0 is `this`; parameters follow in
|
||||
declaration order.
|
||||
|
||||
```cpp
|
||||
T value = dusk::mods::arg<T>(args, n); // copy
|
||||
T& ref = dusk::mods::arg_ref<T>(args, n); // read/write reference
|
||||
```
|
||||
|
||||
```cpp
|
||||
// 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);
|
||||
if (itemNo == dItemNo_HEART_e) {
|
||||
itemNo = dItemNo_GREEN_RUPEE_e;
|
||||
}
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
dusk::mods::hook_add_pre<&fopAcM_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
|
||||
|
||||
If your mod calls or hooks game code directly (anything beyond the service APIs), import `GameService` (
|
||||
`mods/svc/game.h`):
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(GameService, svc_game);
|
||||
```
|
||||
|
||||
Its major version is the game code ABI epoch: it's bumped when game struct or vtable layouts change incompatibly, and
|
||||
the ordinary service version check then rejects your mod with a clear error instead of letting it corrupt memory in a
|
||||
version it wasn't built for. Service-only and asset-only mods should *not* import it; they stay compatible across game
|
||||
ABI changes.
|
||||
|
||||
---
|
||||
|
||||
## Asset Overlays
|
||||
|
||||
Files placed under `overlay/` in the `.dusk` archive override game files at the corresponding path. For example,
|
||||
@@ -331,7 +485,7 @@ and [TextureService](#textureservice-modssvctextureh).
|
||||
Mods can be disabled, re-enabled, and reloaded at runtime without restarting the game (the enabled state persists as the
|
||||
`mod.<escaped id>.enabled` config var). Write your mod assuming this happens:
|
||||
|
||||
- **Disable** calls `mod_shutdown`, removes your services, overlays, and texture replacements (both static and
|
||||
- **Disable** calls `mod_shutdown`, removes your hooks, services, overlays, and texture replacements (both static and
|
||||
runtime-registered), and unloads your library.
|
||||
- **Enable** and **Reload** load a *fresh copy* of your library, imports are re-resolved, and `mod_initialize` runs
|
||||
again. You never see a second `mod_initialize` on the same image, so just make `mod_shutdown` release anything the
|
||||
@@ -344,6 +498,10 @@ first (in reverse dependency order) and brings them back afterward. A mod whose
|
||||
suspended and resumes automatically when the provider returns. Mods with an *optional* import of a disabled provider
|
||||
restart with that import null.
|
||||
|
||||
**One caution for hooks:** lifecycle changes are applied between frames, which is safe for hooks on functions
|
||||
that return every frame (effectively everything you'd normally hook). Avoid hooking a function that stays on
|
||||
the stack for the whole session (e.g. the outermost main loop); a mod that does cannot be safely unloaded.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
@@ -1550,8 +1550,12 @@ set(DUSK_FILES
|
||||
src/dusk/mods/loader/loader.hpp
|
||||
src/dusk/mods/loader/native_module.cpp
|
||||
src/dusk/mods/loader/native_module.hpp
|
||||
src/dusk/mods/loader/manifest.cpp
|
||||
src/dusk/mods/loader/manifest.hpp
|
||||
src/dusk/mods/svc/config.cpp
|
||||
src/dusk/mods/svc/config.hpp
|
||||
src/dusk/mods/svc/game.cpp
|
||||
src/dusk/mods/svc/hook.cpp
|
||||
src/dusk/mods/svc/host.cpp
|
||||
src/dusk/mods/svc/log.cpp
|
||||
src/dusk/mods/svc/overlay.cpp
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
#include <cstring>
|
||||
|
||||
#include "tracy/Tracy.hpp"
|
||||
#include "dusk/profiling.hpp"
|
||||
|
||||
enum dComIfG_ButtonStatus {
|
||||
/* 0x00 */ BUTTON_STATUS_NONE,
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <dolphin/gx/GXExtra.h>
|
||||
#include "tracy/Tracy.hpp"
|
||||
|
||||
#include "profiling.hpp"
|
||||
|
||||
#if DUSK_GFX_DEBUG_GROUPS
|
||||
#define GX_DEBUG_GROUP(name, ...) \
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<tracy/Tracy.hpp>)
|
||||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef ZoneScoped
|
||||
#define ZoneScoped
|
||||
#define ZoneScopedN(name)
|
||||
#endif
|
||||
@@ -114,6 +114,19 @@ inline int __builtin_clz(unsigned int v) {
|
||||
|
||||
#endif
|
||||
|
||||
// Data symbols exported from the main exe need dllimport on the mod side.
|
||||
// DUSK_BUILDING_GAME is defined for the game build so the same headers work in both.
|
||||
#if defined(TARGET_PC) && defined(_WIN32) && !defined(DUSK_BUILDING_GAME)
|
||||
#define DUSK_GAME_EXTERN extern __declspec(dllimport)
|
||||
#define DUSK_GAME_DATA __declspec(dllimport)
|
||||
#elif defined(TARGET_PC) && defined(_WIN32) && defined(DUSK_BUILDING_GAME)
|
||||
#define DUSK_GAME_EXTERN extern __declspec(dllexport)
|
||||
#define DUSK_GAME_DATA __declspec(dllexport)
|
||||
#else
|
||||
#define DUSK_GAME_EXTERN extern
|
||||
#define DUSK_GAME_DATA
|
||||
#endif
|
||||
|
||||
#define FAST_DIV(x, n) (x >> (n / 2))
|
||||
|
||||
#define SQUARE(x) ((x) * (x))
|
||||
|
||||
@@ -0,0 +1,464 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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. */
|
||||
template <class Tag, class R, class... A>
|
||||
struct HookImpl {
|
||||
static inline R (*g_orig)(A...) = nullptr;
|
||||
static inline const HookService* hooks = nullptr;
|
||||
static inline void* target = nullptr;
|
||||
|
||||
static bool dispatch_pre(void* args, void* retval) {
|
||||
if (hooks == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int skipOriginal = 0;
|
||||
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
|
||||
return result == MOD_OK && skipOriginal != 0;
|
||||
}
|
||||
|
||||
static void dispatch_post(void* args, void* retval) {
|
||||
if (hooks != nullptr) {
|
||||
hooks->dispatch_post(mod_ctx, target, args, retval);
|
||||
}
|
||||
}
|
||||
|
||||
static R trampoline(A... args) {
|
||||
if constexpr (sizeof...(A) == 0) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal =
|
||||
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal = dispatch_pre(
|
||||
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <auto Target>
|
||||
using TargetTag = std::integral_constant<decltype(Target), Target>;
|
||||
template <FixedString Name>
|
||||
struct NameTag {};
|
||||
} // namespace detail
|
||||
|
||||
/*
|
||||
* Typed hook on a function named at compile time (&daAlink_c::execute, &free_fn).
|
||||
* Member functions may be virtual: the install decodes the member function pointer and hooks the
|
||||
* class's own overrider.
|
||||
*/
|
||||
template <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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* 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);
|
||||
*/
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
if (hooks == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
Entry::hooks = hooks;
|
||||
if (Entry::target == nullptr) {
|
||||
const ModResult resolved = Entry::resolve_target(hooks, &Entry::target);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<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) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
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) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
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) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
|
||||
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,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* Mods that link or hook game code directly must import this service; service-only and asset-only
|
||||
* mods must not.
|
||||
*
|
||||
* Major version is the game-code ABI epoch: it is bumped when game-visible struct or vtable layouts
|
||||
* change incompatibly (e.g. a TARGET_PC field added to an existing game struct). The loader's
|
||||
* ordinary version check then fails mods built against the old epoch with a clear message instead
|
||||
* of letting them corrupt memory.
|
||||
*/
|
||||
#define GAME_SERVICE_ID "dev.twilitrealm.dusklight.game"
|
||||
#define GAME_SERVICE_MAJOR 1u
|
||||
#define GAME_SERVICE_MINOR 0u
|
||||
|
||||
typedef struct GameService {
|
||||
ServiceHeader header;
|
||||
} GameService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<GameService> {
|
||||
static constexpr const char* id = GAME_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* Intercept game functions by address. Prefer the typed helpers in mods/hook.hpp
|
||||
* (hook_add_pre/hook_add_post/hook_replace over a &Class::method): they generate the
|
||||
* trampoline and hide install/dispatch, which are the low-level primitives those helpers
|
||||
* build. resolve() maps a symbol name to an address for targets you can't name at compile time
|
||||
* (file-local statics included).
|
||||
*
|
||||
* Every call is game-thread-only. Install and removal must run with no hooked function on the
|
||||
* stack; the loader guarantees this by applying mod lifecycle changes between frames, which is
|
||||
* why hooking a function that never returns (the outermost loop) makes a mod un-unloadable.
|
||||
*/
|
||||
|
||||
#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook"
|
||||
#define HOOK_SERVICE_MAJOR 1u
|
||||
#define HOOK_SERVICE_MINOR 0u
|
||||
|
||||
/* Symbol flags reported by resolve() */
|
||||
typedef enum HookSymbolFlags {
|
||||
HOOK_SYMBOL_CODE = 1u << 0u,
|
||||
HOOK_SYMBOL_DATA = 1u << 1u,
|
||||
/* Not exported/dynamically visible: hookable, but never linkable. */
|
||||
HOOK_SYMBOL_LOCAL = 1u << 2u,
|
||||
/* Other names share this address (ICF fold/alias): a hook intercepts them all. */
|
||||
HOOK_SYMBOL_MULTI_NAME = 1u << 3u,
|
||||
/* Resolved through a demangled display-name alias rather than the real symbol. */
|
||||
HOOK_SYMBOL_DISPLAY = 1u << 6u,
|
||||
} HookSymbolFlags;
|
||||
|
||||
/* A pre-hook's return value: whether to run the original function. */
|
||||
typedef enum HookAction {
|
||||
HOOK_CONTINUE = 0, /* run the original (and any lower-priority pre-hooks) */
|
||||
HOOK_SKIP_ORIGINAL = 1, /* cancel the original and remaining pre-hooks; post-hooks still run */
|
||||
} HookAction;
|
||||
|
||||
/* How replace resolves a second replace-hook on a target that already has one. */
|
||||
typedef enum HookReplacePolicy {
|
||||
HOOK_REPLACE_CONFLICT = 0, /* refuse with MOD_CONFLICT (the default) */
|
||||
HOOK_REPLACE_PRIORITY = 1, /* take over only if this options.priority is strictly higher */
|
||||
HOOK_REPLACE_OVERRIDE = 2, /* take over unconditionally */
|
||||
} HookReplacePolicy;
|
||||
|
||||
typedef enum HookFlags {
|
||||
HOOK_FLAG_NONE = 0u,
|
||||
HOOK_FLAG_FAIL_ON_CONFLICT = 1u << 0u,
|
||||
} HookFlags;
|
||||
|
||||
/*
|
||||
* Hook callbacks. `args` is an array of pointers to the call's arguments (index 0 is `this`
|
||||
* for member functions); `retval` points at the return slot (NULL for void). Read and write
|
||||
* them through dusk::mods::arg<T> / arg_ref<T> from mods/hook.hpp. `userdata` is the pointer
|
||||
* from HookOptions. All run on the game thread, in the hooked call's own stack frame.
|
||||
*/
|
||||
typedef HookAction (*HookPreFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
typedef void (*HookPostFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
typedef void (*HookReplaceFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
|
||||
typedef struct HookOptions {
|
||||
uint32_t struct_size;
|
||||
/* Higher runs first; ties break by registration order. Applies to pre/post ordering and,
|
||||
* with HOOK_REPLACE_PRIORITY, to replace-hook takeover. */
|
||||
int32_t priority;
|
||||
uint32_t flags; /* HookFlags */
|
||||
HookReplacePolicy replace_policy;
|
||||
void* userdata; /* passed back to the callback */
|
||||
} HookOptions;
|
||||
|
||||
#define HOOK_OPTIONS_INIT {sizeof(HookOptions), 0, HOOK_FLAG_NONE, HOOK_REPLACE_CONFLICT, NULL}
|
||||
|
||||
typedef struct HookService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Install a trampoline detour on fn_addr and return the address to call the original through in
|
||||
* *out_original_fn. The typed helpers generate the trampoline and call this; mods normally
|
||||
* don't. The first mod to install a given target owns the live detour; later mods register as
|
||||
* candidates so a hook survives the owner unloading (the detour is handed off and every
|
||||
* original pointer is rewritten). Idempotent per (mod, out slot).
|
||||
*/
|
||||
ModResult (*install)(
|
||||
ModContext* ctx, void* fn_addr, void* trampoline_fn, void** out_original_fn);
|
||||
|
||||
/*
|
||||
* Register a callback on an already-installed target. Pre runs before the original (and can
|
||||
* cancel it), post runs after (even if cancelled). Any number of mods may add pre/post to the
|
||||
* same target; they run in priority then registration order. replace installs a single
|
||||
* substitute for the original, managed by options.replace_policy, MOD_CONFLICT if refused.
|
||||
*/
|
||||
ModResult (*add_pre)(
|
||||
ModContext* ctx, void* fn_addr, HookPreFn callback, const HookOptions* options);
|
||||
ModResult (*add_post)(
|
||||
ModContext* ctx, void* fn_addr, HookPostFn callback, const HookOptions* options);
|
||||
ModResult (*replace)(
|
||||
ModContext* ctx, void* fn_addr, HookReplaceFn callback, const HookOptions* options);
|
||||
|
||||
/*
|
||||
* Run the registered callbacks for a target. The generated trampoline calls these; they
|
||||
* are not a mod-facing entry point. dispatch_pre reports through *out_skip_original
|
||||
* whether the original should be skipped (a pre-hook returned HOOK_SKIP_ORIGINAL, or a
|
||||
* replace-hook ran).
|
||||
*/
|
||||
ModResult (*dispatch_pre)(
|
||||
ModContext* ctx, void* fn_addr, void* args, void* retval, int* out_skip_original);
|
||||
ModResult (*dispatch_post)(ModContext* ctx, void* fn_addr, void* args, void* retval);
|
||||
|
||||
/*
|
||||
* Resolve a game symbol by name from the symbol manifest, including non-exported (static)
|
||||
* functions. Names can be either the platform's mangled name (i.e. the name passed to dlopen;
|
||||
* no Mach-O leading underscore) or the qualified function name without parameters (e.g.
|
||||
* "daAlink_c::execute"). out_flags (optional) receives HookSymbolFlags.
|
||||
*
|
||||
* Results: MOD_OK; MOD_UNSUPPORTED (no manifest for this build, missing or stale);
|
||||
* MOD_UNAVAILABLE (symbol not found); MOD_CONFLICT (name maps to more than one address: C++
|
||||
* overloads or per-TU statics; use the mangled name).
|
||||
*/
|
||||
ModResult (*resolve)(
|
||||
ModContext* ctx, const char* symbol, void** out_addr, HookSymbolFlags* out_flags);
|
||||
} HookService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<HookService> {
|
||||
static constexpr const char* id = HOOK_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
+10
-2
@@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host"
|
||||
#define HOST_SERVICE_MAJOR 1u
|
||||
#define HOST_SERVICE_MINOR 1u
|
||||
#define HOST_SERVICE_MAJOR 2u
|
||||
#define HOST_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* Ignore unknown values: later service minors may add events.
|
||||
@@ -35,6 +35,14 @@ typedef void (*ModLifecycleFn)(ModContext* ctx, ModContext* subject, const char*
|
||||
typedef struct HostService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Version string of the current Dusklight build. (e.g. "1.4.2") */
|
||||
const char* version;
|
||||
|
||||
/* Build id of the running game binary: PDB GUID+age on Windows, LC_UUID on macOS, GNU build-id
|
||||
* on Linux. May be empty (len 0) if the identity could not be determined. */
|
||||
const uint8_t* build_id;
|
||||
uint32_t build_id_len;
|
||||
|
||||
/*
|
||||
* Look up a service by id at call time. Unlike a manifest import, this sees whatever is
|
||||
* currently published and carries no initialization-order guarantee (see mods/api.h).
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
# Usage (from a mod project):
|
||||
# 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
|
||||
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
|
||||
@@ -30,4 +33,11 @@ configure_version_header()
|
||||
# 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}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/ModSDK.cmake")
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -11,9 +11,10 @@
|
||||
|
||||
#include "depgraph.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/mods/svc/config.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/mods/svc/config.hpp"
|
||||
#include "dusk/mods/svc/registry.hpp"
|
||||
#include "manifest.hpp"
|
||||
#include "miniz.h"
|
||||
#include "native_module.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
@@ -639,6 +640,8 @@ void ModLoader::init() {
|
||||
}
|
||||
m_initialized = true;
|
||||
|
||||
manifest::initialize();
|
||||
|
||||
if (m_searchDirs.empty()) {
|
||||
Log.warn("no mod search directories configured; mod loading skipped");
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include "manifest.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
|
||||
#include "dusk/io.hpp"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <mach-o/loader.h>
|
||||
#elif defined(__linux__)
|
||||
#include <elf.h>
|
||||
#include <link.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::mods::manifest {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::manifest");
|
||||
|
||||
constexpr char kMagic[8] = {'S', 'Y', 'M', 'G', 'E', 'N', '\0', '\0'};
|
||||
constexpr uint32_t kVersion = 2;
|
||||
|
||||
enum class Compression : uint32_t {
|
||||
None = 0,
|
||||
Zstd = 1,
|
||||
};
|
||||
|
||||
// Mirrors the symgen manifest writer.
|
||||
struct Header {
|
||||
char magic[8];
|
||||
uint32_t version;
|
||||
uint32_t compression;
|
||||
uint64_t uncompressedLen;
|
||||
uint64_t compressedLen;
|
||||
uint32_t buildIdLen;
|
||||
uint8_t buildId[32];
|
||||
uint32_t entryCount;
|
||||
};
|
||||
static_assert(sizeof(Header) == 72);
|
||||
|
||||
struct Entry {
|
||||
uint64_t hash;
|
||||
uint64_t rva;
|
||||
uint32_t nameOff;
|
||||
HookSymbolFlags flags;
|
||||
};
|
||||
static_assert(sizeof(Entry) == 24);
|
||||
|
||||
struct State {
|
||||
std::vector<uint8_t> data;
|
||||
const Entry* entries = nullptr;
|
||||
uint32_t entryCount = 0;
|
||||
const char* strings = nullptr;
|
||||
uint64_t stringsLen = 0;
|
||||
uintptr_t imageBase = 0;
|
||||
// (rva, nameOff) of entries flagged kFlagInlineSites, sorted by rva
|
||||
std::vector<std::pair<uint64_t, uint32_t> > inlineSites;
|
||||
bool loaded = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
State s_state;
|
||||
|
||||
uint64_t fnv1a64(const char* str) {
|
||||
uint64_t hash = 0xcbf29ce484222325ull;
|
||||
for (const char* p = str; *p != '\0'; ++p) {
|
||||
hash ^= static_cast<uint8_t>(*p);
|
||||
hash *= 0x100000001b3ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// Build id of the running executable image, matching what symgen recorded:
|
||||
// PDB GUID (RFC 4122 byte order) + age on Windows, LC_UUID on Mach-O, GNU
|
||||
// build-id on ELF. Also reports the address RVAs are relative to.
|
||||
bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
|
||||
#if defined(_WIN32)
|
||||
auto* base = reinterpret_cast<uint8_t*>(GetModuleHandleW(nullptr));
|
||||
outBase = reinterpret_cast<uintptr_t>(base);
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
|
||||
if (dir.VirtualAddress == 0) {
|
||||
return false;
|
||||
}
|
||||
const auto* entries = reinterpret_cast<const IMAGE_DEBUG_DIRECTORY*>(base + dir.VirtualAddress);
|
||||
for (size_t i = 0; i < dir.Size / sizeof(IMAGE_DEBUG_DIRECTORY); ++i) {
|
||||
if (entries[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW) {
|
||||
continue;
|
||||
}
|
||||
struct CvInfo {
|
||||
uint32_t signature; // 'RSDS'
|
||||
uint8_t guid[16];
|
||||
uint32_t age;
|
||||
};
|
||||
if (entries[i].SizeOfData < sizeof(CvInfo)) {
|
||||
continue;
|
||||
}
|
||||
const auto* cv = reinterpret_cast<const CvInfo*>(base + entries[i].AddressOfRawData);
|
||||
if (cv->signature != 0x53445352) { // "RSDS"
|
||||
continue;
|
||||
}
|
||||
// The GUID struct stores Data1..Data3 little-endian in memory; the manifest
|
||||
// stores RFC 4122 (big-endian) order, so swap them here.
|
||||
outId.assign(cv->guid, cv->guid + 16);
|
||||
std::swap(outId[0], outId[3]);
|
||||
std::swap(outId[1], outId[2]);
|
||||
std::swap(outId[4], outId[5]);
|
||||
std::swap(outId[6], outId[7]);
|
||||
for (int b = 0; b < 4; ++b) {
|
||||
outId.push_back(static_cast<uint8_t>(cv->age >> (8 * b)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
#elif defined(__APPLE__)
|
||||
// Image 0 is the main executable. The manifest stores link-time vmaddrs
|
||||
// (nm convention, __TEXT vmaddr included).
|
||||
const auto* header = _dyld_get_image_header(0);
|
||||
outBase = static_cast<uintptr_t>(_dyld_get_image_vmaddr_slide(0));
|
||||
const auto* header64 = reinterpret_cast<const mach_header_64*>(header);
|
||||
const auto* cmd = reinterpret_cast<const load_command*>(header64 + 1);
|
||||
for (uint32_t i = 0; i < header64->ncmds; ++i) {
|
||||
if (cmd->cmd == LC_UUID) {
|
||||
const auto* uuidCmd = reinterpret_cast<const uuid_command*>(cmd);
|
||||
outId.assign(uuidCmd->uuid, uuidCmd->uuid + 16);
|
||||
return true;
|
||||
}
|
||||
cmd = reinterpret_cast<const load_command*>(
|
||||
reinterpret_cast<const uint8_t*>(cmd) + cmd->cmdsize);
|
||||
}
|
||||
return false;
|
||||
#elif defined(__linux__)
|
||||
struct Ctx {
|
||||
std::vector<uint8_t>* id;
|
||||
uintptr_t base = 0;
|
||||
bool found = false;
|
||||
} ctx{&outId};
|
||||
dl_iterate_phdr(
|
||||
[](dl_phdr_info* info, size_t, void* data) -> int {
|
||||
auto* ctx = static_cast<Ctx*>(data);
|
||||
// The first callback is the main executable.
|
||||
ctx->base = info->dlpi_addr;
|
||||
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
||||
const auto& phdr = info->dlpi_phdr[i];
|
||||
if (phdr.p_type != PT_NOTE) {
|
||||
continue;
|
||||
}
|
||||
const auto* p = reinterpret_cast<const uint8_t*>(info->dlpi_addr + phdr.p_vaddr);
|
||||
const auto* end = p + phdr.p_memsz;
|
||||
while (p + sizeof(ElfW(Nhdr)) <= end) {
|
||||
const auto* note = reinterpret_cast<const ElfW(Nhdr)*>(p);
|
||||
const auto* name = p + sizeof(ElfW(Nhdr));
|
||||
const auto* desc = name + ((note->n_namesz + 3) & ~3u);
|
||||
if (note->n_type == NT_GNU_BUILD_ID && note->n_namesz == 4 &&
|
||||
std::memcmp(name, "GNU", 4) == 0)
|
||||
{
|
||||
ctx->id->assign(desc, desc + note->n_descsz);
|
||||
ctx->found = true;
|
||||
return 1;
|
||||
}
|
||||
p = desc + ((note->n_descsz + 3) & ~3u);
|
||||
}
|
||||
}
|
||||
return 1; // only inspect the main executable
|
||||
},
|
||||
&ctx);
|
||||
outBase = ctx.base;
|
||||
return ctx.found;
|
||||
#else
|
||||
(void)outId;
|
||||
(void)outBase;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path manifest_path() {
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
std::filesystem::path dir =
|
||||
basePath != nullptr ? std::filesystem::path{basePath} : std::filesystem::current_path();
|
||||
return dir / "dusklight.symdb";
|
||||
}
|
||||
|
||||
std::string hex_string(const uint8_t* data, size_t len) {
|
||||
std::string out;
|
||||
out.reserve(len * 2);
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
constexpr char kHex[] = "0123456789abcdef";
|
||||
out.push_back(kHex[data[i] >> 4]);
|
||||
out.push_back(kHex[data[i] & 0xF]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void initialize() {
|
||||
if (s_state.initialized) {
|
||||
return;
|
||||
}
|
||||
s_state.initialized = true;
|
||||
|
||||
const auto path = manifest_path();
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(path, ec)) {
|
||||
Log.info("no symbol manifest at {}; by-name resolution unavailable",
|
||||
io::fs_path_to_string(path));
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> data;
|
||||
try {
|
||||
data = io::FileStream::ReadAllBytes(path);
|
||||
} catch (const std::exception& e) {
|
||||
Log.error("failed to read symbol manifest {}: {}", io::fs_path_to_string(path), e.what());
|
||||
return;
|
||||
}
|
||||
if (data.size() < sizeof(Header)) {
|
||||
Log.error(
|
||||
"symbol manifest {} is truncated ({} bytes)", io::fs_path_to_string(path), data.size());
|
||||
return;
|
||||
}
|
||||
|
||||
Header header{};
|
||||
std::memcpy(&header, data.data(), sizeof(header));
|
||||
if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) {
|
||||
Log.error("symbol manifest {} has wrong magic/version", io::fs_path_to_string(path));
|
||||
return;
|
||||
}
|
||||
const auto compression = static_cast<Compression>(header.compression);
|
||||
if ((compression != Compression::None && compression != Compression::Zstd) ||
|
||||
header.buildIdLen > sizeof(header.buildId) ||
|
||||
header.compressedLen > data.size() - sizeof(Header) ||
|
||||
header.uncompressedLen > std::numeric_limits<size_t>::max() ||
|
||||
(compression == Compression::None && header.compressedLen != header.uncompressedLen))
|
||||
{
|
||||
Log.error("symbol manifest {} is malformed", io::fs_path_to_string(path));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> imageId;
|
||||
uintptr_t imageBase = 0;
|
||||
if (!running_image_identity(imageId, imageBase)) {
|
||||
Log.error("cannot determine the running image's build id; ignoring symbol manifest");
|
||||
return;
|
||||
}
|
||||
if (imageId.size() != header.buildIdLen ||
|
||||
std::memcmp(imageId.data(), header.buildId, imageId.size()) != 0)
|
||||
{
|
||||
Log.error("symbol manifest {} is stale: built for {}, running image is {}",
|
||||
io::fs_path_to_string(path), hex_string(header.buildId, header.buildIdLen),
|
||||
hex_string(imageId.data(), imageId.size()));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto compressedLen = static_cast<size_t>(header.compressedLen);
|
||||
const auto uncompressedLen = static_cast<size_t>(header.uncompressedLen);
|
||||
std::vector<uint8_t> payload;
|
||||
const auto* storedPayload = data.data() + sizeof(Header);
|
||||
if (compression == Compression::None) {
|
||||
payload.assign(storedPayload, storedPayload + compressedLen);
|
||||
} else {
|
||||
payload.resize(uncompressedLen);
|
||||
const size_t decompressedLen =
|
||||
ZSTD_decompress(payload.data(), payload.size(), storedPayload, compressedLen);
|
||||
if (ZSTD_isError(decompressedLen)) {
|
||||
Log.error("failed to decompress symbol manifest {}: {}", io::fs_path_to_string(path),
|
||||
ZSTD_getErrorName(decompressedLen));
|
||||
return;
|
||||
}
|
||||
if (decompressedLen != payload.size()) {
|
||||
Log.error("symbol manifest {} decompressed to {} bytes, expected {}",
|
||||
io::fs_path_to_string(path), decompressedLen, payload.size());
|
||||
return;
|
||||
}
|
||||
}
|
||||
data = std::move(payload);
|
||||
|
||||
const uint64_t entriesEnd = uint64_t{header.entryCount} * sizeof(Entry);
|
||||
if (entriesEnd > data.size()) {
|
||||
Log.error("decompressed symbol manifest {} is malformed", io::fs_path_to_string(path));
|
||||
return;
|
||||
}
|
||||
|
||||
s_state.data = std::move(data);
|
||||
s_state.entries = reinterpret_cast<const Entry*>(s_state.data.data());
|
||||
s_state.entryCount = header.entryCount;
|
||||
s_state.strings = reinterpret_cast<const char*>(s_state.data.data() + entriesEnd);
|
||||
s_state.stringsLen = s_state.data.size() - entriesEnd;
|
||||
s_state.imageBase = imageBase;
|
||||
for (uint32_t i = 0; i < s_state.entryCount; ++i) {
|
||||
const Entry& entry = s_state.entries[i];
|
||||
if ((entry.flags & kFlagInlineSites) != 0 && entry.nameOff < s_state.stringsLen) {
|
||||
s_state.inlineSites.emplace_back(entry.rva, entry.nameOff);
|
||||
}
|
||||
}
|
||||
std::sort(s_state.inlineSites.begin(), s_state.inlineSites.end());
|
||||
s_state.inlineSites.erase(std::unique(s_state.inlineSites.begin(), s_state.inlineSites.end(),
|
||||
[](const auto& a, const auto& b) { return a.first == b.first; }),
|
||||
s_state.inlineSites.end());
|
||||
s_state.loaded = true;
|
||||
Log.info("symbol manifest loaded: {} symbols, build id {}", s_state.entryCount,
|
||||
hex_string(header.buildId, header.buildIdLen));
|
||||
}
|
||||
|
||||
bool available() {
|
||||
return s_state.loaded;
|
||||
}
|
||||
|
||||
const std::vector<uint8_t>& image_build_id() {
|
||||
static const std::vector<uint8_t> s_id = [] {
|
||||
std::vector<uint8_t> id;
|
||||
uintptr_t base = 0;
|
||||
running_image_identity(id, base);
|
||||
return id;
|
||||
}();
|
||||
return s_id;
|
||||
}
|
||||
|
||||
ResolveStatus resolve(const char* name, void** outAddr, HookSymbolFlags* outFlags) {
|
||||
if (!s_state.loaded) {
|
||||
return ResolveStatus::Unavailable;
|
||||
}
|
||||
const uint64_t hash = fnv1a64(name);
|
||||
const Entry* begin = s_state.entries;
|
||||
const Entry* end = begin + s_state.entryCount;
|
||||
size_t lo = 0;
|
||||
size_t hi = s_state.entryCount;
|
||||
while (lo < hi) {
|
||||
const size_t mid = lo + (hi - lo) / 2;
|
||||
if (begin[mid].hash < hash) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
for (const Entry* entry = begin + lo; entry != end && entry->hash == hash; ++entry) {
|
||||
if (entry->nameOff >= s_state.stringsLen ||
|
||||
std::strcmp(s_state.strings + entry->nameOff, name) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ((entry->flags & kFlagDupName) != 0) {
|
||||
return ResolveStatus::Ambiguous;
|
||||
}
|
||||
*outAddr = reinterpret_cast<void*>(s_state.imageBase + entry->rva);
|
||||
if (outFlags != nullptr) {
|
||||
*outFlags = entry->flags;
|
||||
}
|
||||
return ResolveStatus::Ok;
|
||||
}
|
||||
return ResolveStatus::NotFound;
|
||||
}
|
||||
|
||||
bool has_inline_sites(const void* addr, const char** outName) {
|
||||
if (!s_state.loaded || s_state.inlineSites.empty()) {
|
||||
return false;
|
||||
}
|
||||
const auto rva = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(addr) - s_state.imageBase);
|
||||
const auto it = std::lower_bound(s_state.inlineSites.begin(), s_state.inlineSites.end(),
|
||||
std::pair<uint64_t, uint32_t>{rva, 0});
|
||||
if (it == s_state.inlineSites.end() || it->first != rva) {
|
||||
return false;
|
||||
}
|
||||
if (outName != nullptr) {
|
||||
*outName = s_state.strings + it->second;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::manifest
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
namespace dusk::mods::manifest {
|
||||
|
||||
// Symbol flags mirrored from symgen.
|
||||
constexpr uint32_t kFlagCode = 1u << 0;
|
||||
constexpr uint32_t kFlagData = 1u << 1;
|
||||
constexpr uint32_t kFlagLocal = 1u << 2;
|
||||
constexpr uint32_t kFlagMultiName = 1u << 3;
|
||||
constexpr uint32_t kFlagDupName = 1u << 4;
|
||||
constexpr uint32_t kFlagInlineSites = 1u << 5;
|
||||
constexpr uint32_t kFlagDisplay = 1u << 6;
|
||||
|
||||
enum class ResolveStatus {
|
||||
Ok,
|
||||
Unavailable, // no manifest loaded (missing, stale, or malformed)
|
||||
NotFound,
|
||||
Ambiguous, // name maps to multiple addresses (overloads / per-TU statics)
|
||||
};
|
||||
|
||||
// Maps the symbol manifest next to the game binary and validates it against the
|
||||
// running image's build id (PDB GUID+age / Mach-O UUID / GNU build-id). A missing or
|
||||
// stale manifest logs and leaves by-name resolution unavailable; hooks by address are
|
||||
// unaffected. Safe to call more than once.
|
||||
void initialize();
|
||||
|
||||
bool available();
|
||||
|
||||
// Build id of the running executable image (PDB GUID+age / Mach-O UUID / GNU
|
||||
// build-id), computed once on first use; empty if it couldn't be determined.
|
||||
// Independent of whether a manifest file was loaded.
|
||||
const std::vector<uint8_t>& image_build_id();
|
||||
|
||||
// Resolve a symbol name to its address in the running image. Names can be either the platform's
|
||||
// mangled name (i.e. the name passed to dlopen; no Mach-O leading underscore) or the function name
|
||||
// without parameters (e.g. "daAlink_c::execute").
|
||||
ResolveStatus resolve(const char* name, void** outAddr, HookSymbolFlags* outFlags = nullptr);
|
||||
|
||||
// True if the manifest records that the function at this code address was inlined into
|
||||
// at least one caller in this build. An entry hook on it only intercepts the calls
|
||||
// that were not inlined. outName receives the symbol name (valid for the process lifetime)
|
||||
// when known. False when no manifest is loaded.
|
||||
bool has_inline_sites(const void* addr, const char** outName = nullptr);
|
||||
|
||||
} // namespace dusk::mods::manifest
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "mods/svc/game.h"
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr GameService s_gameService{
|
||||
.header = SERVICE_HEADER(GameService, GAME_SERVICE_MAJOR, GAME_SERVICE_MINOR),
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_gameModule{
|
||||
.id = GAME_SERVICE_ID,
|
||||
.majorVersion = GAME_SERVICE_MAJOR,
|
||||
.minorVersion = GAME_SERVICE_MINOR,
|
||||
.service = &s_gameService,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,480 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/loader/manifest.hpp"
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
#if DUSK_CODE_MODS
|
||||
#include "dusk/logging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <funchook.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#endif
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
#if DUSK_CODE_MODS
|
||||
|
||||
struct PreHookFn {
|
||||
ModContext* context = nullptr;
|
||||
HookPreFn callback = nullptr;
|
||||
HookOptions options = HOOK_OPTIONS_INIT;
|
||||
uint64_t order = 0;
|
||||
};
|
||||
|
||||
struct VoidHookFn {
|
||||
ModContext* context = nullptr;
|
||||
HookReplaceFn replaceCallback = nullptr;
|
||||
HookPostFn postCallback = nullptr;
|
||||
HookOptions options = HOOK_OPTIONS_INIT;
|
||||
uint64_t order = 0;
|
||||
};
|
||||
|
||||
struct HookSlot {
|
||||
std::vector<PreHookFn> pre;
|
||||
VoidHookFn replace{};
|
||||
std::vector<VoidHookFn> post;
|
||||
};
|
||||
|
||||
// One per mod that requested a hook on a target: its template-generated trampoline and the
|
||||
// address of its Hook::g_orig, both living in the mod's dylib. Any candidate's trampoline
|
||||
// is interchangeable (dispatch walks the shared HookSlot), so when the active installer's mod
|
||||
// unloads, the funchook detour is handed off to a surviving candidate and every candidate's
|
||||
// *orig_store is rewritten to the new original pointer.
|
||||
struct HookCandidate {
|
||||
ModContext* context = nullptr;
|
||||
void* trampoline = nullptr;
|
||||
void** origStore = nullptr;
|
||||
uint64_t order = 0;
|
||||
};
|
||||
|
||||
struct InstalledHook {
|
||||
funchook_t* handle = nullptr;
|
||||
void* original = nullptr;
|
||||
ModContext* active = nullptr;
|
||||
std::vector<HookCandidate> candidates;
|
||||
};
|
||||
|
||||
std::unordered_map<uintptr_t, HookSlot> s_registry;
|
||||
std::unordered_map<uintptr_t, InstalledHook> s_installed;
|
||||
uint64_t s_nextOrder = 0;
|
||||
|
||||
HookOptions normalize_options(const HookOptions* options) {
|
||||
if (options == nullptr || options->struct_size < sizeof(HookOptions)) {
|
||||
return HOOK_OPTIONS_INIT;
|
||||
}
|
||||
return *options;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void sort_hooks(std::vector<T>& hooks) {
|
||||
std::ranges::stable_sort(hooks, [](const T& a, const T& b) {
|
||||
if (a.options.priority != b.options.priority) {
|
||||
return a.options.priority > b.options.priority;
|
||||
}
|
||||
return a.order < b.order;
|
||||
});
|
||||
}
|
||||
|
||||
// Follow E9/FF25 chains to skip MSVC incremental-link and import stubs.
|
||||
void* resolve_import_thunk(void* addr) {
|
||||
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const auto* p = static_cast<const uint8_t*>(addr);
|
||||
if (p[0] == 0x48 && p[1] == 0xFF && p[2] == 0x25) { // lld emits a REX.W prefix
|
||||
++p;
|
||||
}
|
||||
if (p[0] == 0xFF && p[1] == 0x25) {
|
||||
int32_t offset;
|
||||
std::memcpy(&offset, p + 2, 4);
|
||||
addr = const_cast<void*>(*reinterpret_cast<const void* const*>(p + 6 + offset));
|
||||
break;
|
||||
}
|
||||
if (p[0] == 0xE9) {
|
||||
int32_t offset;
|
||||
std::memcpy(&offset, p + 1, 4);
|
||||
addr = const_cast<uint8_t*>(p) + 5 + offset;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
#elif defined(_WIN32) && (defined(_M_ARM64) || defined(__aarch64__))
|
||||
// Import thunks are `adrp x16; ldr x16, [x16, #off]; br x16` (deref the IAT slot);
|
||||
// incremental-link stubs are a plain `b`, or `adrp x16; add x16, x16, #off; br x16`
|
||||
// range-extension thunks when the target is out of B range.
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const auto* p = static_cast<const uint8_t*>(addr);
|
||||
uint32_t insn0, insn1, insn2;
|
||||
std::memcpy(&insn0, p, 4);
|
||||
if ((insn0 & 0xFC000000u) == 0x14000000u) { // b imm26
|
||||
auto imm26 = static_cast<int32_t>(insn0 << 6) >> 6;
|
||||
addr = const_cast<uint8_t*>(p) + static_cast<intptr_t>(imm26) * 4;
|
||||
continue;
|
||||
}
|
||||
if ((insn0 & 0x9F00001Fu) != 0x90000010u) { // adrp x16, page
|
||||
break;
|
||||
}
|
||||
std::memcpy(&insn1, p + 4, 4);
|
||||
std::memcpy(&insn2, p + 8, 4);
|
||||
if (insn2 != 0xD61F0200u) { // br x16
|
||||
break;
|
||||
}
|
||||
auto immhi = static_cast<int64_t>(static_cast<int32_t>(insn0 << 8) >> 13); // bits 23:5
|
||||
auto immlo = static_cast<int64_t>((insn0 >> 29) & 3);
|
||||
auto page = (reinterpret_cast<uintptr_t>(p) & ~uintptr_t{0xFFF}) +
|
||||
(static_cast<intptr_t>((immhi << 2) | immlo) << 12);
|
||||
if ((insn1 & 0xFFC003FFu) == 0xF9400210u) { // ldr x16, [x16, #imm12*8]
|
||||
auto slot = page + ((insn1 >> 10) & 0xFFF) * 8;
|
||||
addr = *reinterpret_cast<void**>(slot);
|
||||
break;
|
||||
}
|
||||
if ((insn1 & 0xFF8003FFu) == 0x91000210u) { // add x16, x16, #imm12{, lsl #12}
|
||||
auto imm = static_cast<uintptr_t>((insn1 >> 10) & 0xFFF);
|
||||
addr = reinterpret_cast<void*>(page + (((insn1 >> 22) & 1) != 0 ? imm << 12 : imm));
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
return addr;
|
||||
}
|
||||
|
||||
funchook_t* install_trampoline(void* fnAddr, void* trampoline, void** outOriginal) {
|
||||
funchook_t* fh = funchook_create();
|
||||
if (fh == nullptr) {
|
||||
DuskLog.warn("HookSystem: funchook_create failed for {:p}", fnAddr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void* fn = fnAddr;
|
||||
const int prep = funchook_prepare(fh, &fn, trampoline);
|
||||
const int inst = prep == 0 ? funchook_install(fh, 0) : -1;
|
||||
if (prep != 0 || inst != 0) {
|
||||
const char* message = funchook_error_message(fh);
|
||||
DuskLog.warn("HookSystem: funchook failed for {:p} (prepare={} install={}): {}", fnAddr,
|
||||
prep, inst, message != nullptr && message[0] != '\0' ? message : "no details");
|
||||
funchook_destroy(fh);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
*outOriginal = fn;
|
||||
return fh;
|
||||
}
|
||||
|
||||
ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, void** outOriginal) {
|
||||
if (fnAddr == nullptr || trampolineFn == nullptr || outOriginal == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
// Try to detect an invalid function pointer (possibly a vtable slot offset or Itanium mfp
|
||||
// value) and provide a helpful warning instead of faulting
|
||||
const auto raw = reinterpret_cast<uintptr_t>(fnAddr);
|
||||
if (raw < 0x10000
|
||||
#if defined(__aarch64__) || defined(_M_ARM64)
|
||||
|| (raw & 3) != 0 // code is 4-aligned
|
||||
#endif
|
||||
)
|
||||
{
|
||||
DuskLog.warn("HookSystem: {:p} from {} is not a code address (virtual member function "
|
||||
"pointer? hook via dusk::mods::Hook or resolve())",
|
||||
fnAddr, mod_id_from_context(context));
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
fnAddr = resolve_import_thunk(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;
|
||||
// hook_add_pre + hook_add_post on the same target share one g_orig per mod.
|
||||
const bool known = std::ranges::any_of(entry.candidates, [&](const HookCandidate& cand) {
|
||||
return cand.context == context && cand.origStore == outOriginal;
|
||||
});
|
||||
if (!known) {
|
||||
entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++});
|
||||
}
|
||||
*outOriginal = entry.original;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
// Inlining can't be intercepted by an entry patch: warn once per target when this
|
||||
// build inlined the function into callers.
|
||||
if (const char* name = nullptr; manifest::has_inline_sites(fnAddr, &name)) {
|
||||
DuskLog.warn("HookSystem: '{}' ({:p}) for {} was inlined into callers in this build; "
|
||||
"the hook only covers the calls that were not inlined",
|
||||
name != nullptr ? name : "?", fnAddr, mod_id_from_context(context));
|
||||
}
|
||||
|
||||
void* original = nullptr;
|
||||
funchook_t* fh = install_trampoline(fnAddr, trampolineFn, &original);
|
||||
if (fh == nullptr) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
auto& entry = s_installed[key];
|
||||
entry.handle = fh;
|
||||
entry.original = original;
|
||||
entry.active = context;
|
||||
entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++});
|
||||
*outOriginal = original;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_add_pre(
|
||||
ModContext* context, void* fnAddr, HookPreFn callback, const HookOptions* options) {
|
||||
if (fnAddr == nullptr || context == nullptr || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
auto& hooks = s_registry[reinterpret_cast<uintptr_t>(fnAddr)].pre;
|
||||
hooks.push_back({context, callback, normalize_options(options), s_nextOrder++});
|
||||
sort_hooks(hooks);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_add_post(
|
||||
ModContext* context, void* fnAddr, HookPostFn callback, const HookOptions* options) {
|
||||
if (fnAddr == nullptr || context == nullptr || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
fnAddr = resolve_import_thunk(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);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_replace(
|
||||
ModContext* context, void* fnAddr, HookReplaceFn callback, const HookOptions* options) {
|
||||
if (fnAddr == nullptr || context == nullptr || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const HookOptions normalized = normalize_options(options);
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
auto& slot = s_registry[reinterpret_cast<uintptr_t>(fnAddr)];
|
||||
if (slot.replace.replaceCallback == nullptr) {
|
||||
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
switch (normalized.replace_policy) {
|
||||
case HOOK_REPLACE_CONFLICT:
|
||||
DuskLog.error("HookSystem: '{}' conflicts with '{}', both replace the same function",
|
||||
mod_id_from_context(context), mod_id_from_context(slot.replace.context));
|
||||
return MOD_CONFLICT;
|
||||
case HOOK_REPLACE_PRIORITY:
|
||||
if (normalized.priority <= slot.replace.options.priority) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
|
||||
return MOD_OK;
|
||||
case HOOK_REPLACE_OVERRIDE:
|
||||
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
|
||||
return MOD_OK;
|
||||
}
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ModResult hook_dispatch_pre(
|
||||
ModContext*, void* fnAddr, void* args, void* retval, int* outSkipOriginal) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 0;
|
||||
}
|
||||
if (fnAddr == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
const auto it = s_registry.find(reinterpret_cast<uintptr_t>(fnAddr));
|
||||
if (it == s_registry.end()) {
|
||||
return MOD_OK;
|
||||
}
|
||||
auto& slot = it->second;
|
||||
for (auto& hook : slot.pre) {
|
||||
if (hook.callback != nullptr &&
|
||||
hook.callback(hook.context, args, retval, hook.options.userdata) == HOOK_SKIP_ORIGINAL)
|
||||
{
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 1;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
if (slot.replace.replaceCallback != nullptr) {
|
||||
slot.replace.replaceCallback(
|
||||
slot.replace.context, args, retval, slot.replace.options.userdata);
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 1;
|
||||
}
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval) {
|
||||
if (fnAddr == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
const auto it = s_registry.find(reinterpret_cast<uintptr_t>(fnAddr));
|
||||
if (it == s_registry.end()) {
|
||||
return MOD_OK;
|
||||
}
|
||||
for (auto& hook : it->second.post) {
|
||||
if (hook.postCallback != nullptr) {
|
||||
hook.postCallback(hook.context, args, retval, hook.options.userdata);
|
||||
}
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void hook_remove_mod(LoadedMod& mod) {
|
||||
ModContext* context = mod.context.get();
|
||||
|
||||
for (auto it = s_registry.begin(); it != s_registry.end();) {
|
||||
auto& slot = it->second;
|
||||
std::erase_if(slot.pre, [&](const PreHookFn& hook) { return hook.context == context; });
|
||||
std::erase_if(slot.post, [&](const VoidHookFn& hook) { return hook.context == context; });
|
||||
if (slot.replace.context == context) {
|
||||
slot.replace = {};
|
||||
}
|
||||
if (slot.pre.empty() && slot.post.empty() && slot.replace.replaceCallback == nullptr) {
|
||||
it = s_registry.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = s_installed.begin(); it != s_installed.end();) {
|
||||
auto& entry = it->second;
|
||||
// The departing mod's g_orig slots are about to be unmapped; drop its candidates before
|
||||
// any orig_store rewrites below.
|
||||
std::erase_if(entry.candidates,
|
||||
[context](const HookCandidate& cand) { return cand.context == context; });
|
||||
if (entry.active != context) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* target = reinterpret_cast<void*>(it->first);
|
||||
const int uninst = funchook_uninstall(entry.handle, 0);
|
||||
const int destr = funchook_destroy(entry.handle);
|
||||
if (uninst != 0 || destr != 0) {
|
||||
DuskLog.warn("HookSystem: funchook uninstall/destroy for {:p} returned {}/{}", target,
|
||||
uninst, destr);
|
||||
}
|
||||
entry.handle = nullptr;
|
||||
entry.active = nullptr;
|
||||
|
||||
if (entry.candidates.empty()) {
|
||||
it = s_installed.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hand the detour off to a surviving candidate (lowest registration order first; the
|
||||
// vector is append-ordered). A candidate whose install fails stays in the list: its
|
||||
// g_orig must still track the current original pointer.
|
||||
for (auto& cand : entry.candidates) {
|
||||
void* original = nullptr;
|
||||
funchook_t* fh = install_trampoline(target, cand.trampoline, &original);
|
||||
if (fh == nullptr) {
|
||||
continue;
|
||||
}
|
||||
entry.handle = fh;
|
||||
entry.original = original;
|
||||
entry.active = cand.context;
|
||||
DuskLog.info("HookSystem: reinstalled trampoline for {:p}: {} -> {} (tramp={:p})",
|
||||
target, mod_id_from_context(context), mod_id_from_context(cand.context),
|
||||
cand.trampoline);
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.active == nullptr) {
|
||||
DuskLog.warn("HookSystem: no reinstallable trampoline for {:p}; hooks there are "
|
||||
"disabled until a mod reinstalls one",
|
||||
target);
|
||||
for (auto& cand : entry.candidates) {
|
||||
*cand.origStore = target;
|
||||
}
|
||||
it = s_installed.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& cand : entry.candidates) {
|
||||
*cand.origStore = entry.original;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
#else // DUSK_CODE_MODS
|
||||
|
||||
ModResult hook_install(ModContext*, void*, void*, void**) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_add_pre(ModContext*, void*, HookPreFn, const HookOptions*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_add_post(ModContext*, void*, HookPostFn, const HookOptions*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_replace(ModContext*, void*, HookReplaceFn, const HookOptions*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_dispatch_pre(ModContext*, void*, void*, void*, int* outSkipOriginal) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 0;
|
||||
}
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_dispatch_post(ModContext*, void*, void*, void*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
void hook_remove_mod(LoadedMod&) {}
|
||||
|
||||
#endif // DUSK_CODE_MODS
|
||||
|
||||
// By-name resolution reads the symbol manifest, which is independent of the hook engine.
|
||||
ModResult hook_resolve(ModContext*, const char* symbol, void** outAddr, HookSymbolFlags* outFlags) {
|
||||
if (symbol == nullptr || outAddr == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
switch (manifest::resolve(symbol, outAddr, outFlags)) {
|
||||
case manifest::ResolveStatus::Ok:
|
||||
return MOD_OK;
|
||||
case manifest::ResolveStatus::Unavailable:
|
||||
return MOD_UNSUPPORTED;
|
||||
case manifest::ResolveStatus::NotFound:
|
||||
return MOD_UNAVAILABLE;
|
||||
case manifest::ResolveStatus::Ambiguous:
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
constexpr HookService s_hookService{
|
||||
.header = SERVICE_HEADER(HookService, HOOK_SERVICE_MAJOR, HOOK_SERVICE_MINOR),
|
||||
.install = hook_install,
|
||||
.add_pre = hook_add_pre,
|
||||
.add_post = hook_add_post,
|
||||
.replace = hook_replace,
|
||||
.dispatch_pre = hook_dispatch_pre,
|
||||
.dispatch_post = hook_dispatch_post,
|
||||
.resolve = hook_resolve,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_hookModule{
|
||||
.id = HOOK_SERVICE_ID,
|
||||
.majorVersion = HOOK_SERVICE_MAJOR,
|
||||
.minorVersion = HOOK_SERVICE_MINOR,
|
||||
.service = &s_hookService,
|
||||
.modDetached = hook_remove_mod,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -1,10 +1,12 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/loader/manifest.hpp"
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <version.h>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
@@ -117,8 +119,11 @@ void host_mod_detached(LoadedMod& mod) {
|
||||
}
|
||||
}
|
||||
|
||||
constexpr HostService s_hostService{
|
||||
constinit HostService s_hostService{
|
||||
.header = SERVICE_HEADER(HostService, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR),
|
||||
.version = DUSK_VERSION_STRING,
|
||||
.build_id = nullptr,
|
||||
.build_id_len = 0,
|
||||
.get_service = host_get_service,
|
||||
.publish_service = host_publish_service,
|
||||
.fail = host_fail,
|
||||
@@ -137,6 +142,12 @@ constinit const ServiceModule g_hostModule{
|
||||
.majorVersion = HOST_SERVICE_MAJOR,
|
||||
.minorVersion = HOST_SERVICE_MINOR,
|
||||
.service = &s_hostService,
|
||||
.initialize =
|
||||
[] {
|
||||
const auto& buildId = manifest::image_build_id();
|
||||
s_hostService.build_id = buildId.empty() ? nullptr : buildId.data();
|
||||
s_hostService.build_id_len = static_cast<uint32_t>(buildId.size());
|
||||
},
|
||||
.modDetached = host_mod_detached,
|
||||
};
|
||||
|
||||
|
||||
@@ -200,9 +200,11 @@ void ModLoader::init_services() {
|
||||
&svc::g_hostModule,
|
||||
&svc::g_logModule,
|
||||
&svc::g_resourceModule,
|
||||
&svc::g_hookModule,
|
||||
&svc::g_overlayModule,
|
||||
&svc::g_textureModule,
|
||||
&svc::g_configModule,
|
||||
&svc::g_gameModule,
|
||||
})
|
||||
{
|
||||
svc::register_module(*module);
|
||||
|
||||
@@ -65,8 +65,10 @@ void modules_shutdown();
|
||||
extern const ServiceModule g_hostModule;
|
||||
extern const ServiceModule g_logModule;
|
||||
extern const ServiceModule g_resourceModule;
|
||||
extern const ServiceModule g_hookModule;
|
||||
extern const ServiceModule g_overlayModule;
|
||||
extern const ServiceModule g_textureModule;
|
||||
extern const ServiceModule g_configModule;
|
||||
extern const ServiceModule g_gameModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
Reference in New Issue
Block a user