Compare commits

..

61 Commits

Author SHA1 Message Date
Luke Street a5de0c276a Merge remote-tracking branch 'origin/main' into mods
# Conflicts:
#	CMakeLists.txt
#	src/dusk/config.cpp
2026-06-25 22:29:50 -07:00
Luke Street b6f0e104bd Merge remote-tracking branch 'origin/main' into mods
# Conflicts:
#	CMakeLists.txt
#	include/dusk/gx_helper.h
2026-06-06 09:44:35 -06:00
PJB3005 66aca3b69d Fix string copy in LocateDllInBundle 2026-05-31 16:36:13 +02:00
PJB3005 bd09eea0f3 Disallow consecutive periods in mod IDs
Avoid config shenanigans
2026-05-31 16:35:28 +02:00
PJB3005 531313120f Merge branch 'main' into mods 2026-05-31 16:23:01 +02:00
PJB3005 fd26670b0e Remove unused include from mod_loader.hpp
This was for the code mods toggle before I moved the checking logic to CMake.
2026-05-29 09:56:47 +02:00
PJB3005 c83cc3b971 Horrible C++ template nightmares (fix clang-tidy warning about int conversions with ranges) 2026-05-29 05:47:12 +02:00
PJB3005 8c713d4535 Fix prelaunch animation timings
oops
2026-05-29 05:22:12 +02:00
PJB3005 183e7669c2 Add mods button to prelaunch UI
Fixes https://github.com/TwilitRealm/dusklight/issues/1904
2026-05-29 05:09:10 +02:00
PJB3005 f38514db79 Mods can be disabled by config
We now register a CVar for mod enablement for each mod.

Also made LoadedMod stored in unique_ptr because we keep pointers to them already, and I'm entirely uncomfortable with keeping pointers to the vector directly.
2026-05-29 04:59:42 +02:00
PJB3005 3792912ad1 Allow CVars to be registered late, improve usability for dynamically registered CVars.
Now we store the raw JSON value in memory for unregistered CVars.

Intended to be used for mod CVars, as we obviously can't statically define all of those.

CVar names are now stored as an std::string, so the lifetime is easy to manage when dynamically registered.

CVars cannot be moved/copied anymore. We had some code that was accidentally relying on this, and I fixed that.
2026-05-29 04:38:24 +02:00
PJB3005 d9795f4098 Validate that mod IDs have a restricted character set.
Avoid funny business.
2026-05-29 02:43:00 +02:00
PJB3005 87d56be232 Improve native mod load failure diagnostics 2026-05-29 02:29:50 +02:00
PJB3005 be2924b509 Replace DuskLog uses with local log for modloader 2026-05-29 01:59:12 +02:00
PJB3005 be3e6b80eb Move code mod API to separate C++ file 2026-05-29 01:55:26 +02:00
PJB3005 10c310f7b1 Merge branch 'main' into mods 2026-05-29 01:38:13 +02:00
PJB3005 5f0c44eb84 Don't force symbol export if code mods disabled
Allows us to re-enable PCH on RmlUI. We'll likely need to rethink how this works anyways IMO.
2026-05-29 01:25:03 +02:00
PJB3005 b88a5e4ac3 Allow code mods to be disabled on build, disable them by default for now. 2026-05-29 00:37:13 +02:00
PJB3005 af6ca3c80c Merge remote-tracking branch 'upstream/main' into mods 2026-05-28 00:41:11 +02:00
PJB3005 9973a28154 EntryNum assignment for new aurora API changes 2026-05-28 00:40:05 +02:00
PJB3005 358d218e8f Fix Windows Unicode paths in disk mods
oops
2026-05-28 00:17:25 +02:00
Ash 0692fa5423 wip: load other shared library formats (#1790) 2026-05-24 15:23:38 -06:00
PJB3005 40f49a8615 Merge branch 'main' into mods 2026-05-16 18:13:39 +02:00
PJB3005 fb9ffb444a Allow non-code mods to exist 2026-05-15 23:46:41 +02:00
PJB3005 9823ca7c4a Split native mod stuff out of LoadedMod 2026-05-15 23:37:23 +02:00
PJB3005 32069d936c Make native module handles a special type
We love RAII
2026-05-15 23:11:29 +02:00
PJB3005 3f018204b6 Add mod IDs to mod json
Each mod must have a unique ID
2026-05-15 22:40:46 +02:00
PJB3005 012b54b325 Use fs_path_to_string instead of .string()
Unicode fixes
2026-05-15 22:04:22 +02:00
PJB3005 42d412a06e Mod file overlay system
Mods can now replace DVD files with contents of their "overlay" folder

(I'll update the docs later when I do a full pass and make non-code mods
more of a first-class citizen)

Fixes https://github.com/TwilitRealm/dusklight/issues/1306
2026-05-15 21:04:48 +02:00
PJB3005 37e5b7409d Move mod init earlier
Probably necessary if we're gonna be replacing game files etc
2026-05-15 21:01:13 +02:00
PJB3005 cfc0fbc342 Allow mods to be loaded from extracted disk files
Also just some code cleanup
2026-05-14 20:42:20 +02:00
PJB3005 3e84c65657 Fix mod SDK being broken due to rebrand 2026-05-14 20:41:49 +02:00
PJB3005 08cbaff57b Merge branch 'main' into mods 2026-05-14 17:08:10 +02:00
PJB3005 d85718f802 Merge remote-tracking branch 'upstream/main' into mods 2026-05-13 17:26:22 +02:00
PJB3005 925bb069d9 Merge remote-tracking branch 'upstream/main' into mods 2026-05-13 17:25:29 +02:00
madeline b2871054a6 address review, rmlui, better api, catmod 2026-05-11 02:55:11 -07:00
madeline 5bead49902 fix pch conflict 2026-05-10 18:07:05 -07:00
madeline 4175d9c7f4 Merge branch 'main' of https://github.com/TakaRikka/dusk into mods 2026-05-10 16:50:36 -07:00
Luke Street 0d6b47ac73 Move mods menu after tools 2026-04-24 11:56:31 -06:00
Luke Street 1fb5d1ee2a Set FUNCHOOK_INSTALL=OFF 2026-04-24 10:29:05 -06:00
Luke Street c042de8a55 Restore DUSK_BUILDING_GAME=1 2026-04-24 10:12:52 -06:00
Luke Street e25a1f3ef6 Move mods to config dir & updates for macOS 2026-04-24 09:52:04 -06:00
Luke Street b7f9bc91b4 Merge remote-tracking branch 'origin/main' into mods
# Conflicts:
#	CMakeLists.txt
2026-04-24 01:37:37 -06:00
madeline 3281c64a55 more precise link debug info 2026-04-23 06:01:28 -07:00
madeline fb08cfcc6b handle hook conflicts 2026-04-23 05:17:15 -07:00
madeline 99fb2b89ce DUSK_REQUIRE_API_VERSION 2026-04-23 04:52:16 -07:00
madeline 53573eb795 inter mod communication 2026-04-23 04:49:21 -07:00
madeline 975ab1dc54 i fugged up the merge 2026-04-23 03:07:44 -07:00
madeline 9d10a48329 i fugged up the merge 2026-04-23 03:07:00 -07:00
madeline 52a067e412 Merge branch 'main' into mods 2026-04-23 02:20:13 -07:00
madeline 5dcbca392d fix turbo key getting stuck on 2026-04-20 09:51:33 -07:00
madeline ba906150d4 another attempt 2026-04-20 08:20:08 -07:00
madeline e5c7dfdedd maybe fix apple ci idk this shit is giga fucked 2026-04-20 08:04:05 -07:00
madeline 02a4e213e3 again 2026-04-20 07:57:21 -07:00
madeline 394627cd47 remove low level hook info 2026-04-20 07:56:09 -07:00
madeline e7081f770a modding.md, test mod, template mod, better imgui context 2026-04-20 07:52:35 -07:00
madeline 8356eff4ce fix apple ci 2026-04-20 05:24:46 -07:00
madeline 3597cb1bd6 incredibly scuffed funchook patching 2026-04-20 05:22:03 -07:00
madeline 507616015e fix CI cmake 2026-04-20 04:25:17 -07:00
madeline db20632130 Merge remote-tracking branch 'origin/main' into mods 2026-04-20 04:18:41 -07:00
madeline 22d906a248 mod loader 2026-04-20 04:17:42 -07:00
190 changed files with 3094 additions and 12578 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
---
Language: Cpp
Standard: c++20
Standard: C++03
AccessModifierOffset: -4
AlignAfterOpenBracket: DontAlign
AlignConsecutiveAssignments: false
+2
View File
@@ -53,3 +53,5 @@ compile_commands.json
pipeline_cache.bin
extract
*.dusk
+2 -2
View File
@@ -6,12 +6,12 @@
"type": "cppvsdbg",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console"],
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console", "--mods", "${workspaceRoot}/mods"],
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"symbolSearchPath": "${command:cmake.launchTargetPath}",
"console": "integratedTerminal",
"cwd":"${workspaceRoot}"
"cwd":"${workspaceRoot}",
}
]
}
+250 -145
View File
@@ -5,8 +5,96 @@ if (NOT CMAKE_BUILD_TYPE)
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
endif ()
include(cmake/DetectVersion.cmake)
detect_version()
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
if (DUSK_VERSION_OVERRIDE)
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
set(DUSK_VERSION_CODE "1")
set(DUSK_WC_REVISION "")
set(DUSK_WC_BRANCH "")
set(DUSK_WC_DATE "")
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
else ()
# obtain revision info from git
find_package(Git)
if (GIT_FOUND)
# make sure version information gets re-run when the current Git HEAD changes
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
OUTPUT_VARIABLE dusk_git_head_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
OUTPUT_VARIABLE dusk_git_head_symbolic
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
# defines DUSK_WC_REVISION
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
OUTPUT_VARIABLE DUSK_WC_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DESCRIBE
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE)
# remove the git hash, then collapse a clean "-0" suffix only
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
# defines DUSK_WC_BRANCH
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
OUTPUT_VARIABLE DUSK_WC_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DATE
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
OUTPUT_VARIABLE DUSK_WC_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE)
else ()
message(STATUS "Unable to find git, commit information will not be available")
endif ()
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
set(_ver_major ${CMAKE_MATCH_1})
set(_ver_minor ${CMAKE_MATCH_2})
set(_ver_patch ${CMAKE_MATCH_3})
set(DUSK_VERSION_TWEAK "0")
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
endif ()
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
if(DUSK_VERSION_TWEAK GREATER 999)
set(_tweak 999)
else()
set(_tweak ${DUSK_VERSION_TWEAK})
endif()
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
math(EXPR DUSK_VERSION_CODE
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
else ()
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
set(DUSK_VERSION_CODE "1")
endif ()
endif ()
# Add version information to CI environment variables
if(DEFINED ENV{GITHUB_ENV})
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
endif()
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
if (APPLE)
@@ -89,12 +177,12 @@ option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
# Edit & Continue
if (MSVC)
if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug"
AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue")
endif ()
if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue")
@@ -123,7 +211,7 @@ if (DUSK_MOVIE_SUPPORT)
-DWITH_JAVA=OFF
)
if (CMAKE_TOOLCHAIN_FILE)
get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}")
get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
list(APPEND _jpeg_cmake_args -DCMAKE_TOOLCHAIN_FILE=${_jpeg_toolchain_file})
endif ()
set(_jpeg_passthrough_vars
@@ -204,54 +292,42 @@ FetchContent_Declare(cxxopts
URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.3.1.tar.gz
URL_HASH SHA256=3bfc70542c521d4b55a46429d808178916a579b28d048bd8c727ee76c39e2072
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
message(STATUS "dusklight: Fetching nlohmann/json")
FetchContent_Declare(json
URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
message(STATUS "dusklight: Fetching miniz")
message(STATUS "dusk: Fetching miniz")
FetchContent_Declare(miniz
URL https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
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})
message(STATUS "dusk: Fetching funchook")
# cmake/patch_funchook.cmake patches funchook's cmake/capstone.cmake.in to inject a
# PATCH_COMMAND into capstone's inner ExternalProject. That PATCH_COMMAND runs
# cmake/fix_capstone_policy.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/fix_capstone_policy.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/patch_funchook.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)
FetchContent_MakeAvailable(cxxopts json miniz funchook)
if (DUSK_ENABLE_SENTRY_NATIVE)
message(STATUS "dusklight: Fetching sentry-native")
@@ -285,7 +361,21 @@ if(_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQU
add_compile_options(-fsigned-char)
endif()
configure_version_header()
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
set(PLATFORM_NAME win32)
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
if (IOS)
set(PLATFORM_NAME ios)
elseif (TVOS)
set(PLATFORM_NAME tvos)
else ()
set(PLATFORM_NAME macos)
endif ()
else ()
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
endif ()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
include(files.cmake)
@@ -301,15 +391,30 @@ set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
include(cmake/GameABIConfig.cmake)
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
set(GAME_INCLUDE_DIRS
include
src
assets/GZ2E01 # TODO: make this dynamic if needed?
libs/JSystem/include
libs
extern/aurora/include/dolphin
extern/aurora/include
extern
${CMAKE_CURRENT_BINARY_DIR}
${miniz_SOURCE_DIR})
# Interface target for mods and sub-projects to inherit game headers/defines
add_library(dusklight_game_headers INTERFACE)
target_include_directories(dusklight_game_headers INTERFACE ${GAME_INCLUDE_DIRS})
target_compile_definitions(dusklight_game_headers INTERFACE TARGET_PC=1)
target_link_libraries(dusklight_game_headers INTERFACE TracyClient)
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 ()
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt funchook-static
Threads::Threads zstd::libzstd)
if (DUSK_ENABLE_SENTRY_NATIVE)
list(APPEND GAME_LIBS sentry)
@@ -377,8 +482,18 @@ if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
endif ()
if(ANDROID)
list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1)
endif ()
set(DUSK_ENABLE_CODE_MODS_DEFAULT OFF)
option(DUSK_ENABLE_CODE_MODS "Enable code mods" ${DUSK_ENABLE_CODE_MODS_DEFAULT})
if (DUSK_ENABLE_CODE_MODS)
list(APPEND GAME_COMPILE_DEFS DUSK_CODE_MODS=1)
if (NOT ANDROID AND NOT IOS AND NOT TVOS)
list(APPEND GAME_COMPILE_DEFS DUSK_CODE_MODS=1)
else ()
message(FATAL_ERROR "Code mods not supported on the target platform!")
endif ()
endif ()
if (DUSK_PACKAGE_INSTALL)
@@ -405,10 +520,9 @@ set(GAME_DEBUG_FILES
set_source_files_properties(
${GAME_DEBUG_FILES}
PROPERTIES
COMPILE_DEFINITIONS "$<$<CONFIG:Debug>:DEBUG=1>"
COMPILE_DEFINITIONS "$<$<CONFIG:Debug>:DEBUG=1>;$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>"
)
# game_base is for all other game code files
set(GAME_BASE_FILES
${DOLZEL_FILES}
${Z2AUDIOLIB_FILES}
@@ -419,11 +533,17 @@ set(GAME_BASE_FILES
set_source_files_properties(
${GAME_BASE_FILES}
PROPERTIES
COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0"
COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0;$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>"
)
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_definitions(${jsystem_lib} PRIVATE ${GAME_COMPILE_DEFS} $<$<CONFIG:Debug>:DEBUG=1>)
target_compile_definitions(${jsystem_lib} PRIVATE
${GAME_COMPILE_DEFS}
DUSK_BUILDING_GAME=1
$<$<CONFIG:Debug>:DEBUG=1>
$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>
)
target_include_directories(${jsystem_lib} PRIVATE ${GAME_INCLUDE_DIRS})
target_link_libraries(${jsystem_lib} PRIVATE ${GAME_LIBS})
set_target_properties(${jsystem_lib} PROPERTIES FOLDER "JSystem")
endforeach()
@@ -439,66 +559,57 @@ set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_
if(ANDROID)
add_library(dusklight SHARED ${DUSK_FILES})
set_target_properties(dusklight PROPERTIES OUTPUT_NAME main)
set(DUSK_MAIN_TARGET dusklight)
elseif(WIN32)
add_library(dusklight_game SHARED ${DUSK_FILES})
set_target_properties(dusklight_game PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ${DUSK_ENABLE_CODE_MODS}
OUTPUT_NAME dusklight
PDB_NAME dusklight_game)
# rmlui_core uses its own PCH which creates a duplicate PCH marker symbol when linked
# Disabling rmlui's PCH removes the conflicting marker and lets the link succeed
if (MSVC AND TARGET rmlui_core AND DUSK_ENABLE_CODE_MODS)
set_target_properties(rmlui_core PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
endif ()
add_executable(dusklight WIN32 src/dusk/launcher_win32.cpp)
target_link_libraries(dusklight PRIVATE dusklight_game)
target_include_directories(dusklight PRIVATE include)
set(DUSK_MAIN_TARGET dusklight_game)
else ()
add_executable(dusklight ${DUSK_FILES})
set(DUSK_MAIN_TARGET dusklight)
endif ()
if (ENABLE_ASAN)
target_sources(dusklight PRIVATE src/dusk/asan_options.c)
target_sources(${DUSK_MAIN_TARGET} PRIVATE src/dusk/asan_options.c)
endif ()
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusklight PRIVATE ${miniz_SOURCE_DIR})
target_link_libraries(dusklight PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_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)
if (WIN32 AND TARGET imgui)
target_compile_definitions(imgui PRIVATE "IMGUI_API=__declspec(dllexport)")
target_sources(${DUSK_MAIN_TARGET} PRIVATE $<TARGET_OBJECTS:imgui>)
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 ()
target_compile_definitions(${DUSK_MAIN_TARGET} PRIVATE ${GAME_COMPILE_DEFS} DUSK_BUILDING_GAME=1)
target_include_directories(${DUSK_MAIN_TARGET} PRIVATE ${GAME_INCLUDE_DIRS})
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_precompile_headers(${DUSK_MAIN_TARGET} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_LIST_DIR}/include/dusk_pch.hpp>")
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(WIN32)
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE Psapi)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL Linux)
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE dl)
endif()
if(APPLE)
target_link_options(${DUSK_MAIN_TARGET} PRIVATE -Wl,-export_dynamic)
elseif(UNIX AND NOT ANDROID)
target_link_options(${DUSK_MAIN_TARGET} PRIVATE -rdynamic)
endif()
if (TARGET crashpad_handler)
add_dependencies(dusklight crashpad_handler)
add_custom_command(TARGET dusklight POST_BUILD
add_dependencies(${DUSK_MAIN_TARGET} crashpad_handler)
add_custom_command(TARGET ${DUSK_MAIN_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:crashpad_handler>"
"$<TARGET_FILE_DIR:dusklight>"
@@ -509,18 +620,17 @@ endif ()
if (ANDROID)
# SDLActivity loads SDL_main via dlsym on Android. Since aurora::main is a static
# archive, force an undefined reference so the linker keeps the SDL_main object.
target_link_options(dusklight PRIVATE "-Wl,-u,SDL_main")
target_link_options(${DUSK_MAIN_TARGET} PRIVATE "-Wl,-u,SDL_main")
endif ()
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1")
target_link_libraries(dusklight PRIVATE dl)
endif ()
if (NOT APPLE)
add_custom_command(TARGET dusklight POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/res"
"${CMAKE_CURRENT_SOURCE_DIR}/res"
"$<TARGET_FILE_DIR:dusklight>/res"
COMMENT "Copying resources"
)
@@ -548,19 +658,12 @@ if (WIN32)
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusklight.rc.in ${DUSK_WINDOWS_RC} @ONLY)
target_sources(dusklight PRIVATE ${DUSK_WINDOWS_ICON_ICO} ${DUSK_WINDOWS_RC})
set_target_properties(dusklight PROPERTIES WIN32_EXECUTABLE TRUE)
if (MSVC)
target_link_options(dusklight PRIVATE /MANIFEST:NO)
endif ()
endif ()
include(cmake/ModSDK.cmake)
if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_custom_target(dusklight_mods) # Aggregate target for all in-tree mods
add_subdirectory(tools/mod_template)
endif ()
include(cmake/DuskModSDK.cmake)
if (APPLE)
if (IOS)
@@ -569,7 +672,7 @@ if (APPLE)
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/tvos)
else ()
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos)
set(DUSK_ENTITLEMENTS ${DUSK_RESOURCE_DIR}/Dusklight.entitlements)
set(DUSK_ENTITLEMENTS ${DUSK_RESOURCE_DIR}/Dusk.entitlements)
endif ()
set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in)
file(GLOB_RECURSE DUSK_RESOURCE_FILES
@@ -589,7 +692,8 @@ if (APPLE)
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
endforeach ()
set(_apple_bundle_properties
set_target_properties(
dusklight PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
@@ -599,32 +703,9 @@ if (APPLE)
OUTPUT_NAME Dusklight
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${DUSK_ENTITLEMENTS}
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES"
)
if (DUSK_ENTITLEMENTS)
list(APPEND _apple_bundle_properties
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${DUSK_ENTITLEMENTS})
endif ()
if (NOT IOS AND NOT TVOS)
list(APPEND _apple_bundle_properties
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES")
endif ()
set_target_properties(dusklight PROPERTIES ${_apple_bundle_properties})
if (NOT IOS AND NOT TVOS AND NOT "${CMAKE_GENERATOR}" STREQUAL "Xcode")
set(_sign_nested_commands)
if (TARGET crashpad_handler)
list(APPEND _sign_nested_commands
COMMAND /usr/bin/codesign --force --sign -
"$<TARGET_FILE_DIR:dusklight>/crashpad_handler")
endif ()
add_custom_command(TARGET dusklight POST_BUILD
${_sign_nested_commands}
COMMAND /usr/bin/codesign --force --sign - --entitlements
"${DUSK_ENTITLEMENTS}" "$<TARGET_BUNDLE_DIR:dusklight>"
COMMENT "Signing Dusklight.app with entitlements"
VERBATIM
)
endif ()
endif ()
if (APPLE AND NOT IOS AND NOT TVOS)
@@ -643,7 +724,11 @@ if (IOS)
endif ()
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
aurora_copy_runtime_dlls(dusklight)
if(WIN32)
aurora_copy_runtime_dlls(dusklight dusklight_game)
else()
aurora_copy_runtime_dlls(dusklight)
endif()
if (DUSK_SELECTED_OPT)
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
@@ -682,15 +767,33 @@ function(get_target_prefix target result_var)
endif ()
endfunction()
list(APPEND BINARY_TARGETS dusklight)
if(WIN32)
list(APPEND BINARY_TARGETS dusklight_game)
endif()
set(EXTRA_TARGETS "")
if (TARGET crashpad_handler)
list(APPEND EXTRA_TARGETS crashpad_handler)
endif ()
if (DUSK_PACKAGE_INSTALL)
if (WIN32)
# Install the launcher and game DLL, but skip the DLL import library.
if (DUSK_PACKAGE_INSTALL)
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}
)
else ()
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS}
RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}
BUNDLE DESTINATION ${CMAKE_INSTALL_PREFIX}
)
endif ()
elseif (DUSK_PACKAGE_INSTALL)
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_BINDIR})
else()
else ()
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_PREFIX})
endif()
endif ()
aurora_install_runtime_dlls(dusklight ${CMAKE_INSTALL_PREFIX})
if (NOT APPLE)
if (DUSK_PACKAGE_INSTALL)
@@ -749,4 +852,6 @@ foreach (target IN LISTS BINARY_TARGETS)
endforeach ()
endforeach ()
install_bundled_mods()
if (DUSK_ENABLE_CODE_MODS)
add_subdirectory(tools/mod_test)
endif ()
+2 -18
View File
@@ -72,11 +72,7 @@
"type": "BOOL",
"value": false
},
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
}
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
@@ -163,10 +159,6 @@
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
@@ -270,11 +262,7 @@
"type": "BOOL",
"value": false
},
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
}
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
@@ -406,10 +394,6 @@
"type": "BOOL",
"value": false
},
"DUSK_ENABLE_CODE_MODS": {
"type": "BOOL",
"value": true
},
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
+18 -4
View File
@@ -20,17 +20,31 @@ It aims to be as accurate as possible to the original while also providing new o
> Dusklight does *not* provide any copyrighted assets. You must provide your own copy of the original game.
> [!IMPORTANT]
> At a minimum, Dusklight requires a GPU with support for D3D12, Vulkan 1.1+, or Metal. For older devices, best-effort support is provided for D3D11 and OpenGL ES (Android), but will not achieve full accuracy or performance. Your experience with specific hardware, operating systems, and drivers may vary.
> At a minimum, Dusklight requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelihood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them.
### 1. Dump your game
You must dump your own copy of the game. Please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to `.rvz` to save space.
You must dump your own copy of the game, please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to a `.rvz` to save space.
Currently, only the GameCube USA and EUR releases are supported. Support for other versions of the game is planned in the future.
### 2. Install Dusklight
### 2. Download [Dusklight](https://github.com/TwilitRealm/dusklight/releases)
Visit the [official installation guide](https://twilitrealm.dev/install/) for full instructions.
### 3. Setup the game
**Windows / macOS / Linux**
- Extract the .zip file
- Launch Dusklight
- Press **Select Disc Image** and provide the path to your supported game dump
- Press **Play**!
**iOS**
- Follow the [iOS setup guide](docs/ios-install-altstore.md)
**Android**
- Install the Dusklight APK
- Launch Dusklight
- Press **Select Disc Image** and provide the path to your supported game dump
- Press **Play**!
# Building
-121
View File
@@ -1,121 +0,0 @@
# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt)
include_guard(GLOBAL)
get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
macro(detect_version)
if (DUSK_VERSION_OVERRIDE)
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
set(DUSK_VERSION_CODE "1")
set(DUSK_WC_REVISION "")
set(DUSK_WC_BRANCH "")
set(DUSK_WC_DATE "")
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
else ()
# obtain revision info from git
find_package(Git)
if (GIT_FOUND)
# make sure version information gets re-run when the current Git HEAD changes
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
OUTPUT_VARIABLE dusk_git_head_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
OUTPUT_VARIABLE dusk_git_head_symbolic
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT}
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
# defines DUSK_WC_REVISION
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
OUTPUT_VARIABLE DUSK_WC_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DESCRIBE
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE)
# remove the git hash, then collapse a clean "-0" suffix only
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
# defines DUSK_WC_BRANCH
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
OUTPUT_VARIABLE DUSK_WC_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DATE
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
OUTPUT_VARIABLE DUSK_WC_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE)
else ()
message(STATUS "Unable to find git, commit information will not be available")
endif ()
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
set(_ver_major ${CMAKE_MATCH_1})
set(_ver_minor ${CMAKE_MATCH_2})
set(_ver_patch ${CMAKE_MATCH_3})
set(DUSK_VERSION_TWEAK "0")
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
endif ()
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
if (DUSK_VERSION_TWEAK GREATER 999)
set(_tweak 999)
else ()
set(_tweak ${DUSK_VERSION_TWEAK})
endif ()
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
math(EXPR DUSK_VERSION_CODE
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
else ()
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
set(DUSK_VERSION_CODE "1")
endif ()
endif ()
# Add version information to CI environment variables
if (DEFINED ENV{GITHUB_ENV})
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
endif ()
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
endmacro()
# Sets PLATFORM_NAME and configures version.h into the caller's binary dir.
macro(configure_version_header)
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
set(PLATFORM_NAME win32)
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
if (IOS)
set(PLATFORM_NAME ios)
elseif (TVOS)
set(PLATFORM_NAME tvos)
else ()
set(PLATFORM_NAME macos)
endif ()
else ()
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
endif ()
configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
endmacro()
+49
View File
@@ -0,0 +1,49 @@
# add_dusk_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>])
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into")
function(add_dusk_mod target_name)
cmake_parse_arguments(ARG "" "MOD_JSON;RES_DIR" "SOURCES" ${ARGN})
if(NOT ARG_MOD_JSON)
message(FATAL_ERROR "add_dusk_mod: MOD_JSON is required")
endif()
add_library(${target_name} SHARED ${ARG_SOURCES})
set_target_properties(${target_name} PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON)
target_compile_features(${target_name} PRIVATE cxx_std_20)
target_link_libraries(${target_name} PRIVATE dusklight_game_headers)
if(APPLE)
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
elseif(UNIX)
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
elseif(WIN32)
target_link_libraries(${target_name} PRIVATE dusklight_game)
if(MSVC)
target_link_options(${target_name} PRIVATE /INCREMENTAL:NO)
set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
endif()
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage")
set(_out "${DUSK_MODS_OUTPUT_DIR}/${target_name}.dusk")
file(MAKE_DIRECTORY "${_stage}") # must exist before POST_BUILD on Windows
set(_zip_args "$<TARGET_FILE_NAME:${target_name}>" mod.json)
set(_extra_cmds "")
if(ARG_RES_DIR)
list(APPEND _zip_args res)
set(_extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/${ARG_RES_DIR}" "${_stage}/res")
endif()
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${DUSK_MODS_OUTPUT_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${target_name}>" "${_stage}/$<TARGET_FILE_NAME:${target_name}>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/${ARG_MOD_JSON}" "${_stage}/mod.json"
${_extra_cmds}
COMMAND ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
WORKING_DIRECTORY "${_stage}"
COMMENT "Packaging ${target_name} -> ${_out}"
)
endfunction()
-27
View File
@@ -1,27 +0,0 @@
# The game ABI surface shared by the main build and the mod SDK (sdk/CMakeLists.txt)
include_guard(GLOBAL)
get_filename_component(_game_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
# PARTIAL_DEBUG makes debug and release share one struct/vtable ABI so a mod binary loads into either
set(_game_compile_defs TARGET_PC=1 WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1 PARTIAL_DEBUG=1)
if (ANDROID)
list(APPEND _game_compile_defs TARGET_ANDROID=1)
endif ()
set(_game_include_dirs
${_game_root}/include
${_game_root}/src
${_game_root}/assets/GZ2E01 # TODO: make this dynamic if needed?
${_game_root}/libs/JSystem/include
${_game_root}/libs
${_game_root}/extern/aurora/include/dolphin
${_game_root}/extern/aurora/include
${_game_root}/extern
${CMAKE_CURRENT_BINARY_DIR}
)
# Interface target for mods and sub-projects to inherit game headers/defines.
add_library(dusklight_game_headers INTERFACE)
target_include_directories(dusklight_game_headers INTERFACE ${_game_include_dirs})
target_compile_definitions(dusklight_game_headers INTERFACE ${_game_compile_defs})
-279
View File
@@ -1,279 +0,0 @@
# add_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>] [OVERLAY_DIR <overlay>]
# [TEXTURES_DIR <textures>] [OUTPUT_DIR <dir>] [BUNDLE])
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_BINARY_DIR}/mods" CACHE PATH "Directory to write mod packages into")
function(_mod_lib_name out_var)
set(_arch "${CMAKE_SYSTEM_PROCESSOR}")
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _count)
if (_count GREATER 1)
message(FATAL_ERROR "add_mod: universal binaries are not supported")
endif ()
set(_arch "${CMAKE_OSX_ARCHITECTURES}")
endif ()
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _platform)
string(TOLOWER "${_arch}" _arch)
if (_arch MATCHES "^(i[3-6]86|x86)$")
set(_arch "x86")
endif ()
if (WIN32)
set(_ext ".dll")
elseif (APPLE)
set(_ext ".dylib")
else ()
set(_ext ".so")
endif ()
set(${out_var} "${_platform}-${_arch}${_ext}" PARENT_SCOPE)
endfunction()
function(_mod_resolve_source_path out_var path)
if (IS_ABSOLUTE "${path}")
set(_path "${path}")
else ()
set(_path "${CMAKE_CURRENT_SOURCE_DIR}/${path}")
endif ()
set(${out_var} "${_path}" PARENT_SCOPE)
endfunction()
function(_mod_collect_assets out_var dir)
if (NOT IS_DIRECTORY "${dir}")
message(FATAL_ERROR "add_mod: asset directory does not exist: ${dir}")
endif ()
file(GLOB_RECURSE _files CONFIGURE_DEPENDS LIST_DIRECTORIES false "${dir}/*")
set(${out_var} ${_files} PARENT_SCOPE)
endfunction()
function(add_mod target_name)
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR" "SOURCES" ${ARGN})
if (NOT ARG_MOD_JSON)
message(FATAL_ERROR "add_mod: MOD_JSON is required")
endif ()
_mod_resolve_source_path(_mod_json "${ARG_MOD_JSON}")
if (NOT EXISTS "${_mod_json}")
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
endif ()
add_library(${target_name} SHARED ${ARG_SOURCES})
_mod_lib_name(_lib_name)
set_target_properties(${target_name} PROPERTIES
PREFIX ""
C_VISIBILITY_PRESET hidden
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
WINDOWS_EXPORT_ALL_SYMBOLS OFF)
target_compile_features(${target_name} PRIVATE cxx_std_20)
target_link_libraries(${target_name} PRIVATE dusklight_game_headers)
if (NOT TARGET dusklight)
# Apply global compile options for out-of-tree mod builds
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
target_compile_options(${target_name} PRIVATE
-Wno-multichar -Wno-trigraphs -Wno-deprecated-declarations)
elseif (APPLE)
target_compile_options(${target_name} PRIVATE
-Wno-declaration-after-statement -Wno-non-pod-varargs)
elseif (MSVC)
target_compile_options(${target_name} PRIVATE
"$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>"
"$<$<COMPILE_LANGUAGE:C,CXX>:/utf-8>")
endif ()
# Use signed char on ARM to match the original game (and x86)
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _mod_arch)
if (_mod_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
target_compile_options(${target_name} PRIVATE -fsigned-char)
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}")
endif ()
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage")
set(_out "${_output_dir}/${target_name}.dusk")
set(_zip_args "${_lib_name}" mod.json)
set(_package_deps "${_mod_json}")
set(_package_inputs "${_mod_json}")
set(_extra_cmds "")
if (ARG_RES_DIR)
_mod_resolve_source_path(_res_dir "${ARG_RES_DIR}")
_mod_collect_assets(_res_deps "${_res_dir}")
list(APPEND _package_deps ${_res_deps})
list(APPEND _package_inputs "${_res_dir}" ${_res_deps})
list(APPEND _zip_args res)
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
"${_res_dir}" "${_stage}/res")
endif ()
if (ARG_OVERLAY_DIR)
_mod_resolve_source_path(_overlay_dir "${ARG_OVERLAY_DIR}")
_mod_collect_assets(_overlay_deps "${_overlay_dir}")
list(APPEND _package_deps ${_overlay_deps})
list(APPEND _package_inputs "${_overlay_dir}" ${_overlay_deps})
list(APPEND _zip_args overlay)
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
"${_overlay_dir}" "${_stage}/overlay")
endif ()
if (ARG_TEXTURES_DIR)
_mod_resolve_source_path(_textures_dir "${ARG_TEXTURES_DIR}")
_mod_collect_assets(_textures_deps "${_textures_dir}")
list(APPEND _package_deps ${_textures_deps})
list(APPEND _package_inputs "${_textures_dir}" ${_textures_deps})
list(APPEND _zip_args textures)
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
"${_textures_dir}" "${_stage}/textures")
endif ()
set(_bundle_cmds "")
if (ARG_BUNDLE AND TARGET dusklight)
file(READ "${_mod_json}" _mod_json_text)
string(JSON _mod_id GET "${_mod_json_text}" id)
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_TARGETS "${target_name}")
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_IDS "${_mod_id}")
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_STAGES "${_stage}")
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_PACKAGES "${_out}")
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES "${_lib_name}")
set(_bundle_cmds
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundled_mods"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_out}" "${CMAKE_BINARY_DIR}/bundled_mods/${target_name}.dusk")
endif ()
set(_package_target "${target_name}_package")
set(_package_inputs_file "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_package_inputs.txt")
list(SORT _package_inputs)
set(_package_inputs_text "")
foreach (_package_input IN LISTS _package_inputs)
string(APPEND _package_inputs_text "${_package_input}\n")
endforeach ()
file(GENERATE OUTPUT "${_package_inputs_file}" CONTENT "${_package_inputs_text}")
add_custom_command(OUTPUT "${_out}"
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_stage}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${_output_dir}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${target_name}>" "${_stage}/${_lib_name}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_mod_json}" "${_stage}/mod.json"
${_extra_cmds}
COMMAND ${CMAKE_COMMAND} -E chdir "${_stage}" ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
${_bundle_cmds}
DEPENDS ${target_name} ${_package_deps} "${_package_inputs_file}"
COMMENT "Packaging ${target_name} -> ${_out}"
COMMAND_EXPAND_LISTS
VERBATIM
)
add_custom_target(${_package_target} ALL DEPENDS "${_out}")
if (TARGET dusklight_mods)
add_dependencies(dusklight_mods ${_package_target})
endif ()
endfunction()
# Install rules for BUNDLE mods.
# - Windows: the .dusk archives into <install>/mods (the loader extracts native libs to the
# user cache).
# - Linux: pre-extracted stage dirs into <install>/mods so native libs dlopen in place from
# read-only installs.
# - macOS: pre-extracted stage dirs into the installed app's Contents/Resources/mods, dylibs
# ad-hoc signed in place, then the whole bundle re-signed.
# - iOS/tvOS: assets into <app>/mods/<id> and the dylib into Frameworks/<id>.dylib.
# - Android: nothing here; gradle packs ${CMAKE_BINARY_DIR}/bundled_mods into APK assets.
function(install_bundled_mods)
get_property(_targets GLOBAL PROPERTY DUSK_BUNDLED_MOD_TARGETS)
if (NOT _targets OR ANDROID)
return ()
endif ()
get_property(_ids GLOBAL PROPERTY DUSK_BUNDLED_MOD_IDS)
get_property(_stages GLOBAL PROPERTY DUSK_BUNDLED_MOD_STAGES)
get_property(_lib_names GLOBAL PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES)
list(LENGTH _targets _count)
math(EXPR _last "${_count} - 1")
if (APPLE)
get_target_property(_app_name dusklight OUTPUT_NAME)
if (NOT _app_name)
set(_app_name dusklight)
endif ()
set(_bundle_dir "${CMAKE_INSTALL_PREFIX}/${_app_name}.app")
if (IOS OR TVOS)
foreach (_i RANGE ${_last})
list(GET _targets ${_i} _target)
list(GET _ids ${_i} _id)
list(GET _stages ${_i} _stage)
list(GET _lib_names ${_i} _lib_name)
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/mods/${_id}"
PATTERN "${_lib_name}" EXCLUDE)
install(PROGRAMS "$<TARGET_FILE:${_target}>"
DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.dylib")
endforeach ()
else ()
foreach (_i RANGE ${_last})
list(GET _ids ${_i} _id)
list(GET _stages ${_i} _stage)
list(GET _lib_names ${_i} _lib_name)
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/Contents/Resources/mods/${_id}")
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/Resources/mods/${_id}/${_lib_name}\" COMMAND_ERROR_IS_FATAL ANY)")
endforeach ()
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - --entitlements \"${DUSK_ENTITLEMENTS}\" \"${_bundle_dir}\" COMMAND_ERROR_IS_FATAL ANY)")
endif ()
return ()
endif ()
if (DUSK_PACKAGE_INSTALL)
set(_mods_dest "${CMAKE_INSTALL_DATAROOTDIR}/dusklight/mods")
else ()
set(_mods_dest "${CMAKE_INSTALL_PREFIX}/mods")
endif ()
if (WIN32)
foreach (_target IN LISTS _targets)
install(FILES "${CMAKE_BINARY_DIR}/bundled_mods/${_target}.dusk" DESTINATION "${_mods_dest}")
endforeach ()
else ()
foreach (_i RANGE ${_last})
list(GET _ids ${_i} _id)
list(GET _stages ${_i} _stage)
install(DIRECTORY "${_stage}/" DESTINATION "${_mods_dest}/${_id}")
endforeach ()
endif ()
endfunction()
-60
View File
@@ -1,60 +0,0 @@
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}")
-125
View File
@@ -1,125 +0,0 @@
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()
-79
View File
@@ -1,79 +0,0 @@
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(_config_subdir "")
if (CMAKE_CONFIGURATION_TYPES)
set(_config_subdir "$<CONFIG>/")
endif ()
set(_rsp_lines "$<TARGET_OBJECTS:${target}>")
foreach (_lib IN LISTS JSYSTEM_LIBRARIES)
list(APPEND _rsp_lines "$<TARGET_FILE:${_lib}>")
endforeach ()
list(JOIN _rsp_lines "\n" _rsp_content)
set(_rsp "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports_input.rsp")
file(GENERATE OUTPUT "${_rsp}" CONTENT "${_rsp_content}")
set(_sdk_args)
foreach (_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
aurora_os aurora_pad aurora_si aurora_vi)
if (TARGET ${_lib})
list(APPEND _sdk_args --sdk-lib "$<TARGET_FILE:${_lib}>")
endif ()
endforeach ()
# Generate curated exports list from the main binary
set(_def "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.def")
add_custom_command(TARGET ${target} PRE_LINK
# TODO: src/dusk/ is NOT excluded: inline code in game headers
# currently call into it (e.g. dusk::frame_interp::lookup_replacement).
COMMAND "${_symgen}" def
--rsp "${_rsp}"
--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}/${_config_subdir}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)
set(DUSK_GAME_IMPLIB "${_implib}" CACHE INTERNAL "Import library for Windows mod linking")
set(DUSK_GAME_DEF "${_def}" CACHE INTERNAL "Curated export .def for the game executable")
# Ship the import library as sdk/dusklight.lib in the install tree: mods may use it to
# compile against Dusklight without having to build the whole game. (See DUSK_GAME_IMPLIB)
install(FILES "${_implib}" DESTINATION sdk RENAME dusklight.lib)
endfunction()
+11
View File
@@ -0,0 +1,11 @@
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}")
+202 -567
View File
@@ -1,43 +1,36 @@
# Dusklight Mod API
# Dusk Mod API
Mods are `.dusk` bundles: zip archives that can contain code (in the form of native libraries), resources, DVD overlay
files, and texture replacements. Mods may be enabled, disabled and reloaded at runtime.
When code mods are loaded, they get dynamically linked by the operating system to the running game process. The mod
exports lifecycle functions that Dusklight calls into (`mod_initialize`, `mod_update`, `mod_shutdown`), and the mod
communicates with the host via **services**: plain C APIs, individually versioned. Dusklight exports several built-in
services, and mods may export services of their own, permitting framework mods and cross-mod integration.
Beyond services, mods have full access to the original game's code: include game headers, call directly into any public
function, read and write data fields, and hook the vast majority of game functions.
Mods are shared libraries packaged into a `.dusk` zip archive. The loader scans the `mods/` directory at startup, extracts each library, and calls your exports each frame.
## Table of Contents
1. [Getting Started](#getting-started)
2. [mod.json](#modjson)
3. [Anatomy of a Code Mod](#anatomy-of-a-code-mod)
4. [Services](#services)
5. [Built-in Services](#built-in-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)
3. [Required Exports](#required-exports)
4. [DuskModAPI Reference](#duskmodapi-reference)
5. [Logging](#logging)
6. [Loading Resources](#loading-resources)
7. [ImGui Integration](#imgui-integration)
8. [Hooking Game Functions](#hooking-game-functions)
- [Pre-hooks](#pre-hooks)
- [Post-hooks](#post-hooks)
- [Replace hooks](#replace-hooks)
- [Reading and writing arguments](#reading-and-writing-arguments)
9. [Inter-Mod Communication](#inter-mod-communication)
10. [Full Example](#full-example)
---
## Getting Started
Fork the [mod template](../tools/mod_template/), a self-contained CMake project that uses the Dusklight mod SDK.
Fork the [mod template](../tools/mod_template/), it is a self-contained CMake project that references dusk as a subdirectory.
```
my_mod/
├── CMakeLists.txt
├── mod.json
├── src/mod.cpp
── res/ (optional bundled resources)
├── overlay/ (optional game file overrides)
└── textures/ (optional texture replacements)
── res/ (optional bundled resources)
```
**CMakeLists.txt:**
@@ -46,29 +39,23 @@ my_mod/
cmake_minimum_required(VERSION 3.25)
project(my_mod CXX)
set(DUSKLIGHT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusklight" CACHE PATH "Path to dusklight source root")
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusk" CACHE PATH "Path to dusk source root")
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
add_mod(my_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res # optional
OVERLAY_DIR overlay # optional
TEXTURES_DIR textures # optional
add_dusk_mod(my_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res # optional
)
```
Building produces `my_mod.dusk` in `build/<preset>/mods/` (configurable via the `DUSK_MODS_OUTPUT_DIR` cache variable).
Dusklight searches a `mods/` directory next to the app in addition to the user directory, so a dev build launched from
`build/<preset>/` picks these up automatically: rebuild, relaunch (or click Reload), done.
After building, `my_mod.dusk` is placed in `mods/` next to the project root (`DUSK_MODS_OUTPUT_DIR` cache variable). Copy it to the game's `mods/` folder and launch.
For a regular game install, copy the `.dusk` into the user mods folder:
- Windows: `%APPDATA%\TwilitRealm\Dusk\mods`
- Linux: `~/.local/share/TwilitRealm/Dusk/mods`
- macOS: `~/Library/Application Support/TwilitRealm/Dusk/mods`
- Windows: `%APPDATA%\TwilitRealm\Dusklight\mods`
- Linux: `~/.local/share/TwilitRealm/Dusklight/mods`
- macOS: `~/Library/Application Support/TwilitRealm/Dusklight/mods`
Passing `--mods <dir>` on the command line replaces the user directory with one of your choosing.
The `.dusk` archive is a standard zip containing `mod.json`, the compiled library, and an optional `res/` tree. `add_dusk_mod()` creates it automatically.
---
@@ -76,641 +63,289 @@ Passing `--mods <dir>` on the command line replaces the user directory with one
```json
{
"id": "com.example.my_mod",
"name": "My Mod",
"version": "1.0.0",
"author": "Your Name",
"description": "A short description shown in the mod manager.",
"icon": "res/my_icon.png",
"banner": "res/my_banner.png"
"name": "My Mod",
"version": "1.0.0",
"author": "Your Name",
"description": "A short description shown in the mod manager."
}
```
`id` is required: a unique, stable identifier (reverse-DNS style; periods, underscores, and alphanumerics). Everything
else is optional but recommended.
`icon` and `banner` are bundle-relative paths to PNG images for the in-game mod manager: the square icon (e.g.
512x512), the banner (~3.5:1). If omitted, `res/icon.png` and `res/banner.png` are used automatically when present.
All fields are optional but recommended. `name` falls back to the filename, `version` to `"?"`.
---
## Anatomy of a Code Mod
## Required Exports
```cpp
#include "mods/service.hpp"
#include "mods/svc/log.h"
#include "dusk/mod_api.h"
DEFINE_MOD(); // once, in exactly one translation unit
IMPORT_SERVICE(LogService, svc_log); // resolved by the loader before mod_initialize
DUSK_REQUIRE_API_VERSION // declares mod_api_version; loader rejects the mod if the engine is older
extern "C" {
MOD_EXPORT ModResult mod_initialize(ModError* error) {
svc_log->info(mod_ctx, "hello from my_mod");
return MOD_OK;
}
void mod_init (DuskModAPI* api); // required, called once at startup
void mod_tick (DuskModAPI* api); // required, called every frame
void mod_cleanup(DuskModAPI* api); // optional, called on shutdown
MOD_EXPORT ModResult mod_update(ModError* error) { // called every frame
return MOD_OK;
}
MOD_EXPORT ModResult mod_shutdown(ModError* error) {
return MOD_OK;
}
}
```
All three lifecycle exports are required. `mod_ctx` is your mod's identity token, set by the loader before
`mod_initialize` runs. Pass it as the first argument to every service call.
`DUSK_REQUIRE_API_VERSION` is optional but recommended. When present, the loader will refuse to initialize the mod if its API version doesn't exactly match the engine's.
---
## Services
## DuskModAPI Reference
A service is a struct of C function pointers with a version header. You declare what you use at file scope, and the
loader resolves it before your mod initializes:
The `api` pointer is valid for the lifetime of the mod. When using `hook.hpp`, call `dusk::init(api)` once and `dusk::g_api` is set for you.
```cpp
IMPORT_SERVICE(LogService, svc_log); // required, any minor version
IMPORT_SERVICE_VERSION(LogService, svc_log, 2); // required, minor version >= 2
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
```
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major**
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
functions to the end of the struct without breaking existing callers and simply bumps the minor version. Mods that
want the newer functions may use `IMPORT_SERVICE_VERSION` to require that minor at **load time**, or `SERVICE_HAS` to
check at **runtime** whether a specific function is available.
The contract (see `include/mods/api.h` for the full version):
- **A required import is guaranteed valid.** If the service is missing or too old, the mod fails to load with a clear
error. No need to null check at call sites.
- **Anything at or below the minor version you imported can be called unconditionally.**
- Optional imports may be null; check once in `mod_initialize`.
- Fields newer than your imported minor must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a null
check.
| Field | Description |
|-------|-------------|
| `api_version` | ABI version, check against `DUSK_MOD_API_VERSION` if needed |
| `mod_dir` | Absolute path to the extracted mod cache directory |
| `log_info` / `log_warn` / `log_error` | `printf`-style logging, prefixed with the mod name |
| `load_resource` / `free_resource` | Load files from the `res/` tree in the `.dusk` archive |
| `register_tab_content` | Add a panel to the mod manager's per-mod tab |
| `register_menu_item` | Add an item to the quick-access menu |
| `hook_dispatch_pre` / `hook_dispatch_post` | Called by the trampoline, do not call directly |
| `service_publish` | Register a named pointer in the global service registry |
| `service_get` | Look up a named pointer registered by another mod |
---
## Built-in Services
### LogService (`mods/svc/log.h`)
## Logging
```cpp
IMPORT_SERVICE(LogService, svc_log);
svc_log->info(mod_ctx, "spawned the thing");
svc_log->warn(mod_ctx, "that looks wrong");
svc_log->error(mod_ctx, "very bad");
svc_log->write(mod_ctx, LOG_LEVEL_DEBUG, "verbose details");
api->log_info("Player health: %d", hp);
api->log_warn("Something looks wrong");
api->log_error("Fatal: %s", msg);
```
Messages appear in the console prefixed with your mod ID. Messages are plain UTF-8 strings and are copied before the
call returns; use `snprintf` or `fmt::format` for formatting.
Output appears in the dusk console as `[My Mod] ...`
### ResourceService (`mods/svc/resource.h`)
The format string is `printf`-compatible.
Loads files from the `res/` tree of your `.dusk` archive. Paths are relative to `res/` (pass `"config.txt"`, not
`"res/config.txt"`); absolute paths and `..` are rejected.
---
## Loading Resources
```cpp
IMPORT_SERVICE(ResourceService, svc_resource);
ResourceBuffer buf = RESOURCE_BUFFER_INIT;
if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
// buf.data / buf.size
svc_resource->free(mod_ctx, &buf);
size_t size = 0;
void* data = api->load_resource("config.txt", &size);
if (data) {
std::string text(static_cast<char*>(data), size);
api->free_resource(data);
}
```
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. Note that the bundle is read-only; for writable
storage, use the directory from `svc_host->mod_dir(mod_ctx)`.
- Path is relative to `res/`, pass `"config.txt"` not `"res/config.txt"`
- Always call `free_resource`, the buffer is owned by miniz
- For writable storage, write files under `api->mod_dir`
### HostService (`mods/svc/host.h`)
---
Mod metadata and runtime interaction with the loader:
## ImGui Integration
**Tab content:** shown in the mod's panel in the Mods window, called every frame while visible:
```cpp
IMPORT_SERVICE(HostService, svc_host);
const char* id = svc_host->mod_id(mod_ctx);
const char* dir = svc_host->mod_dir(mod_ctx); // writable per-mod directory
svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); // disables the mod
```
`get_service`/`publish_service` provide dynamic service lookup; see [Advanced](#advanced-exporting-services).
**Lifecycle watches.** If your mod provides a service that hands out per-caller state (registrations, callbacks,
handles), watch other mods' lifecycle and drop what you hold for a mod when it detaches.
```cpp
IMPORT_SERVICE_VERSION(HostService, svc_host, 1);
void on_mod_lifecycle(ModContext* ctx, ModContext* subject, const char* subject_id,
ModLifecycleEvent event, void* user_data) {
if (event == MOD_LIFECYCLE_DETACHED) {
drop_state_for(subject); // same ModContext* the subject passed into your service
}
static void DrawPanel(void* userdata) {
ImGui::Text("Hello!");
}
uint64_t watch = 0;
svc_host->watch_mod_lifecycle(mod_ctx, on_mod_lifecycle, nullptr, &watch);
api->register_tab_content(DrawPanel, nullptr);
```
`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`)
Installs hooks on game functions and resolves symbols by name. You'll rarely call it directly; use the typed helpers in
`mods/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
### OverlayService (`mods/svc/overlay.h`)
Registers DVD file overlays at runtime: the dynamic counterpart to the static `overlay/` directory (see
[Asset Overlays](#asset-overlays)). Overlay a disc path with a file from your bundle, or with a caller-owned buffer
(copied on registration):
Pass a pointer through `userdata` if your callback needs state:
```cpp
IMPORT_SERVICE(OverlayService, svc_overlay);
OverlayHandle handle = 0;
svc_overlay->add_file(mod_ctx, "/res/Msgus.arc", "res/replacement.arc", &handle);
svc_overlay->add_buffer(mod_ctx, "/generated.txt", data, size, nullptr);
svc_overlay->remove(mod_ctx, handle);
api->register_tab_content(DrawPanel, &g_state);
```
`disc_path` must be absolute (leading `/`) and is matched against the disc case-insensitively. Paths that don't exist
on the disc are added as new files. Changes are applied at the next frame boundary, and data the game already read
stays in memory until the file is re-read: sometimes a scene reload, and in the worst case, a full restart.
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
### TextureService (`mods/svc/texture.h`)
Registers texture replacements at runtime: the dynamic counterpart to the static `textures/` directory (see
[Asset Overlays](#asset-overlays)). Two forms: raw texel data with an explicit key, or an encoded `.dds`/`.png` from
your bundle whose filename encodes the key:
**Menu items:** added to the quick-access menu. Use `ImGui::MenuItem`, `ImGui::Separator`, etc.:
```cpp
IMPORT_SERVICE(TextureService, svc_texture);
// Encoded file; filename follows the replacement naming convention.
TextureReplacementHandle handle = 0;
svc_texture->register_file(mod_ctx, "res/tex1_32x32_$_6.png", &handle);
// Raw data: match by texel-data pointer or by content hash (TEXTURE_KEY_SOURCE).
TextureKey key = TEXTURE_KEY_INIT;
key.kind = TEXTURE_KEY_POINTER;
key.pointer = someTexObj.data;
TextureData data = TEXTURE_DATA_INIT;
data.data = pixels; data.size = pixelsSize;
data.width = 32; data.height = 32; data.gx_format = GX_TF_RGBA8_PC;
svc_texture->register_data(mod_ctx, &key, &data, nullptr);
svc_texture->unregister(mod_ctx, handle);
```
Filenames use the same Dolphin-style convention as the user's `texture_replacements` directory:
`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, where hashes may be `$` (wildcard). `_mipN` sidecar files next to
a registered file are picked up automatically. Files are decoded lazily on first use by the renderer; raw data is copied
at registration. Registrations follow your mod's lifecycle.
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
### ConfigService (`mods/svc/config.h`)
Persistent, mod-scoped configuration variables. Each var is stored in the user's `config.json` under
`mod.<escaped mod id>.<name>` (escaping: `.``_`, `_``__`, so `com.example.my_mod` becomes `com_example_my__mod`),
next to the host's own settings:
```cpp
IMPORT_SERVICE(ConfigService, svc_config);
ConfigVarDesc desc = CONFIG_VAR_DESC_INIT;
desc.name = "speedMultiplier"; // 1-64 chars from [A-Za-z0-9_-]; "enabled" is reserved
desc.type = CONFIG_VAR_FLOAT;
desc.default_float = 1.0;
ConfigVarHandle var = 0;
svc_config->register_var(mod_ctx, &desc, &var);
double speed = 1.0;
svc_config->get_float(mod_ctx, var, &speed);
svc_config->set_float(mod_ctx, var, 2.0);
// Optional: get notified when the value changes.
void on_speed_changed(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value,
const ConfigVarValue* previous, void* user_data) {
/* value->float_value is the new value, previous->float_value the old one */
static void DrawMenuEntry(void*) {
if (ImGui::MenuItem("Reset rotation")) { ... }
}
svc_config->subscribe(mod_ctx, var, on_speed_changed, nullptr, nullptr);
api->register_menu_item(DrawMenuEntry, nullptr);
```
Types: `CONFIG_VAR_BOOL` (`bool`), `CONFIG_VAR_INT` (`int64_t`), `CONFIG_VAR_FLOAT` (`double`), `CONFIG_VAR_STRING`
(UTF-8; `get_string` copies into a caller buffer, pass a `NULL` buffer with size 0 to query the length). Accessors are
typed and must match the registration.
Change callbacks fire on the game thread whenever the value changes at runtime (your own `set_*` calls included).
Writes that store the same value are silent. Values applied from `config.json` or `--cvar` at registration do
**not** fire callbacks; read the value after `register_var` for the starting state.
### UiService (`mods/svc/ui.h`)
Integrate seamlessly with Dusklight's UI system: add controls and buttons to your mod's detail pane in the Mods window,
create custom windows and modal dialogs, apply custom RCSS stylesheets (anywhere!), and add menu bar tabs.
**Mod panel:** Registers or replaces the panel rendered in your mod's detail pane; `build` runs every time the detail
content is rebuilt, and `update` runs every frame while that mod is selected. While your mod is selected, the detail
pane carries your mod's id as a `mod-id` attribute (like custom window roots), so scoped RCSS can target it (e.g.
`[mod-id="com.example.mod"]`).
```cpp
IMPORT_SERVICE(UiService, svc_ui);
UiElementHandle statusText = 0;
ModResult build(ModContext*, UiElementHandle panel, void*, ModError*) {
svc_ui->pane_add_section(mod_ctx, panel, "Status");
svc_ui->pane_add_text(mod_ctx, panel, "starting...", &statusText);
svc_ui->pane_add_progress(mod_ctx, panel, 0.5f, nullptr);
return MOD_OK;
}
ModResult update(ModContext*, void*, ModError*) {
svc_ui->elem_set_text(mod_ctx, statusText, "running");
return MOD_OK;
}
UiModsPanelDesc panel = UI_MODS_PANEL_DESC_INIT;
panel.build = build;
panel.update = update;
svc_ui->register_mods_panel(mod_ctx, &panel);
```
Element setters must match the element kind: `elem_set_text`/`elem_set_rml` on text rows, and `elem_set_progress` on
progress bars. `elem_set_class` sets or clears an RCSS class on any element handle, for styling via scoped or
per-window RCSS. A non-`MOD_OK` result from `build`/`update` fails your mod, as do exceptions thrown from any UI
callback.
**Controls:** `pane_add_control` adds an input row described by a `UiControlDesc`: `UI_CONTROL_BUTTON`,
`UI_CONTROL_TOGGLE`, `UI_CONTROL_NUMBER`, `UI_CONTROL_STRING`, or `UI_CONTROL_SELECT`. Values bind with callbacks or
directly to a config var.
```cpp
UiControlDesc control = UI_CONTROL_DESC_INIT;
control.kind = UI_CONTROL_TOGGLE;
control.label = "Enable rainbows";
control.help_rml = "Shown in the help pane while focused.";
control.binding = UI_BINDING_CONFIG_VAR;
control.config_var = myBoolVar; // from svc_config->register_var
svc_ui->pane_add_control(mod_ctx, leftPane, &control, nullptr);
```
`UI_BINDING_CONFIG_VAR` wires persistence, change notifications, and the modified indicator automatically. The var
type must match the control: `TOGGLE` = bool, `NUMBER` and `SELECT` = int, `STRING` = string. Float vars are not
bindable; use callbacks and convert. `help_rml` and `SELECT` option lists render in a help pane, so `SELECT` controls
are only available inside window tabs.
**Windows:** `window_push` pushes a tabbed two-pane window onto the document stack and shows it. Each tab's `build`
receives the window handle plus fresh left and right pane handles on every activation. The optional per-tab `update`
runs each frame while that tab is active. `on_closed` fires when the window is destroyed. `desc.rcss` optionally styles
that window's document only; custom windows carry the owning mod's id as a `mod-id` attribute on the window root, so
scoped RCSS can target your specific mod's windows (e.g. `window[mod-id="com.example.mod"]`).
```cpp
UiTabDesc tabs[1] = {UI_TAB_DESC_INIT};
tabs[0].title = "Options";
tabs[0].build = build_options_tab;
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
desc.tabs = tabs;
desc.tab_count = 1;
desc.on_closed = options_window_closed;
UiWindowHandle window = 0;
svc_ui->window_push(mod_ctx, &desc, &window);
```
**Dialogs:** `dialog_push` shows a modal dialog. `variant` picks the style, `icon` optionally overrides the variant's
default icon, and actions become buttons. After an action's `on_pressed` returns, the dialog closes unless the action
sets `keep_open`. A `keep_open` action can close it later (or immediately) with `dialog_close`. Cancel fires
`on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live
dialog.
**Menu bar tabs:** `register_menu_tab` adds a tab to the in-game menu bar. `on_selected` fires when the user activates
the tab: typically you'd push a window from it. The tab is removed by `unregister_menu_tab`, or automatically when the
mod is disabled.
**Custom styles:** `register_styles(scope, rcss, &handle)` applies an RCSS stylesheet to every document of a scope:
existing documents restyle immediately, and future ones pick it up when created. `register_styles_file(scope, path,
&handle)` reads the sheet from your bundle's `res/` directory. Scopes are `UI_SCOPE_PRELAUNCH`, `UI_SCOPE_WINDOW`,
`UI_SCOPE_MENU_BAR`, `UI_SCOPE_OVERLAY`, `UI_SCOPE_TOUCH_CONTROLS`, and `UI_SCOPE_GRAPHICS_TUNER`. Sheets apply after
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
unless changing host UI is intentional.
---
## Hooking Game Functions
Mods may hook the vast majority of game functions, including file-local static, private and virtual functions.
`mods/hook.hpp` provides typed helpers over the hook service:
Call `dusk::init(api)` first.
```cpp
#include "mods/hook.hpp"
#include "mods/svc/hook.h"
#include "dusk/hook.hpp"
IMPORT_SERVICE(HookService, svc_hook);
extern "C" void mod_init(DuskModAPI* api) {
dusk::init(api);
dusk::hookAddPre<&ClassName::Method>(callback);
}
```
The trampoline is installed once per address. Multiple mods can register pre/post callbacks for the same function independently.
### Pre-hooks
Run before the original. Return `HOOK_SKIP_ORIGINAL` to cancel it (post-hooks still run).
Run before the original. Return `0` to let it proceed, non-zero to cancel it. Post-hooks still run either way.
```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;
static int32_t on_posMove_pre(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0); // this
if (link->shape_angle.y > 10000)
return 1; // cancel
return 0;
}
dusk::mods::hook_add_pre<&daAlink_c::posMove>(svc_hook, on_pos_move_pre);
dusk::hookAddPre<&daAlink_c::posMove>(on_posMove_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.
Run after the original (or replace-hook).
```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;
}
static void on_posMove_post(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
dusk::g_api->log_info("New Y angle: %d", (int)link->shape_angle.y);
}
dusk::mods::hook_replace<&daAlink_c::execute>(svc_hook, on_execute_replace);
dusk::hookAddPost<&daAlink_c::posMove>(on_posMove_post);
```
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.
### Replace hooks
### 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.
Completely substitutes the original. Only one replace-hook per function, a second install overwrites with a warning.
```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
static void on_posMove_replace(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
link->shape_angle.y += 100;
}
dusk::hookSetReplace<&daAlink_c::posMove>(on_posMove_replace);
```
Class member functions must include `Class*` as the first argument.
To call the original from inside a replace-hook:
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.
```cpp
using Entry = dusk::HookEntry<&daAlink_c::posMove>;
static void on_posMove_replace(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
link->shape_angle.y = 0;
Entry::g_orig(link);
}
```
### 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.
`args` is a `void*[N]` array. Index `0` is `this`, subsequent indices are parameters 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
T value = dusk::arg <T>(args, n); // copy
T& ref = dusk::argRef<T>(args, n); // reference (read/write)
```
**Example:** halve incoming damage
```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;
// void daEnemy_c::takeDamage(int amount, daActor_c* source)
static int32_t on_takeDamage_pre(void* args) {
dusk::argRef<int>(args, 1) /= 2;
return 0;
}
dusk::mods::hook_add_pre<&fopAcM_createItem>(svc_hook, on_create_item_pre);
dusk::hookAddPre<&daEnemy_c::takeDamage>(on_takeDamage_pre);
```
For reference parameters (e.g. `const cXyz& pos`), `arg_ref<cXyz>` yields a direct reference.
For reference parameters (e.g. `const cXyz& pos`), use `argRef<cXyz>` to get a direct reference.
### Resolving symbols by name
`resolve` looks a symbol up in the **symbol manifest**: a name→address map generated alongside every game build and
keyed to that exact binary. It covers the whole image, including functions that aren't exported (file-local statics),
which makes them hookable:
```cpp
IMPORT_SERVICE(HookService, svc_hook);
void* addr = nullptr;
uint32_t flags = 0;
if (svc_hook->resolve(mod_ctx, "GXSetProjection", &addr, &flags) == MOD_OK) {
// addr is the function's real address in the running game; hook or call it.
}
```
Two spellings work on every platform:
- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. They carry no
signature, so overload sets (and file-local statics sharing a name) return `MOD_CONFLICT`.
- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled spelling in
dlsym convention (no Mach-O leading underscore). The escape hatch for overloads.
`MOD_UNSUPPORTED` means the manifest is missing or was built for a different game binary.
### Game code ABI contract
A primary consideration when letting mods link against the game is maintaining ABI stability across Dusklight
versions. If your mod calls or hooks game code directly (anything beyond the service APIs), import `GameService`
(`mods/svc/game.h`):
```cpp
IMPORT_SERVICE(GameService, svc_game);
```
Its major version is the game code ABI epoch: it's bumped when game struct or vtable layouts change incompatibly, and
the ordinary service version check then rejects your mod with a clear error instead of letting it corrupt memory in a
version it wasn't built for. Service-only and asset-only mods should *not* import it and will continue to work across
game ABI changes.
The more you can do through services, the better: a mod that avoids touching game code directly sidesteps future ABI
breaks entirely and plays nicer with other enabled mods.
---
## Asset Overlays
## Inter-Mod Communication
Files placed under `overlay/` in the `.dusk` archive override game files at the corresponding path, equivalent to
replacing files in the .iso. This requires no code: an archive with just `mod.json` and `overlay/` is a complete mod.
Mods can expose a public API to each other through a global service registry. The convention for names is `"mod_name/service_name"`.
Files placed under `textures/` register as texture replacements, and act just like the user's general
`texture_replacements/` directory: Dolphin-style naming, matched by texture hash
(`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, `$` as a hash wildcard). Subdirectories are scanned recursively;
only the filename needs to match.
Both mechanisms are tied to the mod's lifecycle: disabling the mod removes its overrides (files revert to the disc
contents on their next open; added files stop existing), and reloading serves the new bundle's content. However, game
data the engine already read stays as-is until it is loaded again, which may require a scene change or, in the worst
case, a full restart. Texture replacements usually take effect immediately.
If multiple sources replace the same file or texture, the last one wins: runtime registrations override static
`textures/` or `overlay/` files, and later-loaded mods override earlier ones. Cross-mod conflicts log warnings.
**All** mod-provided texture replacements override the user's `texture_replacements/`.
To configure overlays and texture replacements at runtime instead, see [OverlayService](#overlayservice-modssvcoverlayh)
and [TextureService](#textureservice-modssvctextureh).
---
## Runtime Lifecycle
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 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
loader doesn't manage for you (threads, files, game-side state you mutated).
- **Reload** additionally re-reads the `.dusk` from disk, picking up a rebuilt library and changed assets. This is the
fast iteration loop during development: rebuild, click Reload.
**Dependents restart too.** Disabling or reloading a mod that exports services shuts down the mods importing them
first (in reverse dependency order) and brings them back afterward. A mod whose *required* provider is disabled stays
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
Service calls report failure through `ModResult` return values (`MOD_OK`, `MOD_UNAVAILABLE`,
`MOD_INVALID_ARGUMENT`, ...). Lifecycle exports additionally receive a `ModError*`: fill it (e.g. with
`dusk::mods::set_error(error, code, "message")`) and return the code, and the loader disables the mod and shows the
message to the user.
**Mod A — publishing:**
```cpp
MOD_EXPORT ModResult mod_initialize(ModError* error) {
if (!load_my_data()) {
return dusk::mods::set_error(error, MOD_ERROR, "failed to load data");
}
return MOD_OK;
}
```
Throwing exceptions out of lifecycle functions also disables the mod (they are caught by the loader), but prefer
explicit results.
---
## Advanced: Exporting Services
Mods may export services of their own, permitting framework mods and cross-mod integration. Define the interface in a
header both mods share:
```cpp
// my_mod_api.h
#include "mods/api.h"
#define MY_MOD_SERVICE_ID "com.example.my_mod.api"
#define MY_MOD_SERVICE_MAJOR 1u
#define MY_MOD_SERVICE_MINOR 0u
typedef struct MyModService {
ServiceHeader header;
ModResult (*do_thing)(ModContext* ctx, int value);
} MyModService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct dusk::mods::ServiceTraits<MyModService> {
static constexpr const char* id = MY_MOD_SERVICE_ID;
static constexpr uint16_t major_version = MY_MOD_SERVICE_MAJOR;
struct MyModAPI {
void (*do_thing)(int value);
};
#endif
static void my_do_thing(int value) { ... }
static MyModAPI g_api = { my_do_thing };
extern "C" void mod_init(DuskModAPI* api) {
api->service_publish("my_mod/api", &g_api);
}
```
**Provider:**
**Mod B — consuming:**
```cpp
ModResult do_thing(ModContext* ctx, int value) { ... }
#include "my_mod_api.h"
constexpr MyModService g_service{
.header = SERVICE_HEADER(MyModService, MY_MOD_SERVICE_MAJOR, MY_MOD_SERVICE_MINOR),
.do_thing = do_thing,
};
EXPORT_SERVICE(g_service);
static MyModAPI* g_my_mod = nullptr;
extern "C" void mod_init(DuskModAPI* api) {
g_my_mod = static_cast<MyModAPI*>(api->service_get("my_mod/api"));
}
```
**Consumer:**
---
## Full Example
```cpp
IMPORT_SERVICE(MyModService, svc_my_mod);
// or IMPORT_OPTIONAL_SERVICE if the dependency is optional
#include "d/actor/d_a_alink.h"
#include "dusk/hook.hpp"
#include "dusk/mod_api.h"
#include "imgui.h"
#include "m_Do/m_Do_controller_pad.h"
svc_my_mod->do_thing(mod_ctx, 42);
static int g_ticks = 0;
static int32_t on_posMove_pre(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
if (mDoCPd_c::getHoldR(PAD_1)) {
link->shape_angle.y -= 2048;
}
return 0;
}
static void DrawPanel(void*) {
daAlink_c* link = daAlink_getAlinkActorClass();
ImGui::Text("Ticks: %d", g_ticks);
if (link) {
ImGui::Text("Y angle: %d", (int)link->shape_angle.y);
if (ImGui::Button("Reset rotation")) {
link->shape_angle.y = 0;
}
}
}
static void DrawMenuEntry(void*) {
daAlink_c* link = daAlink_getAlinkActorClass();
if (ImGui::MenuItem("Reset rotation", nullptr, false, link != nullptr)) {
link->shape_angle.y = 0;
}
}
extern "C" {
void mod_init(DuskModAPI* api) {
dusk::init(api);
dusk::hookAddPre<&daAlink_c::posMove>(on_posMove_pre);
api->register_tab_content(DrawPanel, nullptr);
api->register_menu_item(DrawMenuEntry, nullptr);
}
void mod_tick(DuskModAPI* api) {
++g_ticks;
}
void mod_cleanup(DuskModAPI* api) {
api->log_info("Unloaded after %d ticks.", g_ticks);
}
}
```
The loader registers all exports before resolving any imports, so declaration order between mods doesn't matter. Note
that the `ctx` a provider receives identifies the *calling* mod.
### Dependencies between mods
Service imports are also dependency declarations: the loader initializes mods in dependency order, so by the time your
`mod_initialize` runs, every mod you import services from (required *or* optional) has already finished its own
`mod_initialize`. This includes deferred services: a service the provider publishes during its initialization resolves
into your import slot just like a static export.
Consequences of that contract:
- If a provider fails to load, every mod that *requires* one of its services is disabled too, with an error naming the
provider. Optional imports of a failed provider simply resolve to `NULL`.
- Mods whose **required** imports form a cycle all fail to load. If the cycle runs through an **optional** import, the
loader breaks it there: the optional import still resolves, but its provider may not be initialized yet when you run.
- `svc_host->get_service(...)` is outside this system. It sees whatever is published at call time and gives no
initialization-order guarantee, which also makes it the escape hatch for intentionally cyclic designs.
Mods shut down in reverse initialization order, so services you import remain safe to call from `mod_shutdown`.
Rules for providers:
- Service IDs are global and use reverse-DNS names (e.g. `com.mydomain.mod.service`)
- Every function pointer covered by your declared minor version must be populated.
- Within a major version, only append fields; never reorder, remove, or repurpose them. Breaking changes require a major
bump (which is, in effect, a new service).
- Only one provider per `(id, major)` pair may be registered; duplicates are load errors.
For services whose construction can't happen at static-init time, declare the export with `EXPORT_DEFERRED_SERVICE(...)`
and publish the pointer later via `svc_host->publish_service(...)`. Consumers can fetch services dynamically with
`svc_host->get_service(...)`; prefer manifest imports whenever possible, since they give the loader dependency
information and fail fast with good errors.
+1 -1
+9 -32
View File
@@ -1496,12 +1496,6 @@ set(DUSK_FILES
src/dusk/ui/input.hpp
src/dusk/ui/icon_provider.cpp
src/dusk/ui/icon_provider.hpp
src/dusk/ui/logs_window.cpp
src/dusk/ui/logs_window.hpp
src/dusk/ui/mod_texture_provider.cpp
src/dusk/ui/mod_texture_provider.hpp
src/dusk/ui/mod_window.cpp
src/dusk/ui/mod_window.hpp
src/dusk/ui/modal.cpp
src/dusk/ui/modal.hpp
src/dusk/ui/nav_types.hpp
@@ -1549,32 +1543,15 @@ set(DUSK_FILES
src/dusk/OSReport.cpp
src/dusk/OSThread.cpp
src/dusk/OSMutex.cpp
src/dusk/mods/log_buffer.cpp
src/dusk/mods/log_buffer.hpp
src/dusk/mods/manifest.cpp
src/dusk/mods/manifest.hpp
src/dusk/mods/loader/bundle_disk.cpp
src/dusk/mods/loader/bundle_zip.cpp
src/dusk/mods/loader/context.cpp
src/dusk/mods/loader/depgraph.cpp
src/dusk/mods/loader/depgraph.hpp
src/dusk/mods/loader/loader.cpp
src/dusk/mods/loader/loader.hpp
src/dusk/mods/loader/native_module.cpp
src/dusk/mods/loader/native_module.hpp
src/dusk/mods/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
src/dusk/mods/svc/resource.cpp
src/dusk/mods/svc/texture.cpp
src/dusk/mods/svc/ui.cpp
src/dusk/mods/svc/ui.hpp
src/dusk/mods/svc/registry.cpp
src/dusk/mods/svc/registry.hpp
src/dusk/hook_system.cpp
src/dusk/modding/mod_loader.cpp
src/dusk/modding/mod_loader_api.cpp
src/dusk/modding/mod_loader_overlay.cpp
src/dusk/modding/native_module.cpp
src/dusk/modding/native_module.hpp
src/dusk/modding/bundle_disk.cpp
src/dusk/modding/bundle_zip.cpp
src/dusk/gx_helper.cpp
src/dusk/discord.cpp
src/dusk/discord.hpp
src/dusk/discord_presence.cpp
-6
View File
@@ -269,12 +269,6 @@
runHook postInstall
'';
postFixup = lib.optionalString (!isDarwin) ''
patchelf \
--add-needed "${pkgs.vulkan-loader}/lib/libvulkan.so" \
$out/bin/dusklight
'';
dontStrip = true;
meta = {
@@ -20,7 +20,7 @@ public:
/* 0x14 */ cM3dGAab mM3dGAab;
/* 0x30 */ cBgS_ShdwDraw_Callback mCallbackFun;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x34 */ int field_0x34;
#endif
};
+1 -3
View File
@@ -34,7 +34,7 @@ public:
bool isResetting() { return mResettingFlag; }
static Z2AudioMgr* getInterface() { return mAudioMgrPtr; }
static Z2AudioMgr* mAudioMgrPtr;
static DUSK_GAME_DATA Z2AudioMgr* mAudioMgrPtr;
/* 0x0514 */ virtual bool startSound(JAISoundID soundID, JAISoundHandle* handle, const JGeometry::TVec3<f32>* posPtr);
/* 0x0518 */ bool mResettingFlag;
@@ -48,8 +48,6 @@ public:
/* 0x1370 */ Z2FxLineMgr mFxLineMgr;
#if DEBUG
/* 0x13BC */ Z2DebugSys mDebugSys;
#elif PARTIAL_DEBUG
alignas(Z2DebugSys) u8 mDebugSys[sizeof(Z2DebugSys)];
#endif
}; // Size: 0x138C
+1 -1
View File
@@ -194,7 +194,7 @@ public:
JAISoundHandle* getMainBgmHandle() { return &mMainBgmHandle; }
JAISoundHandle* getSubBgmHandle() { return &mSubBgmHandle; }
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
f32 field_0x00_debug;
u8 field_0x04_debug;
#endif
+2 -2
View File
@@ -100,13 +100,13 @@ public:
bool isForceBattle() { return forceBattle_; }
JSUList<Z2CreatureEnemy>* getEnemyList() { return &field_0x0; }
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
JSUList<Z2SoundObjBase>* getAllList() { return &allList_; }
#endif
private:
/* 0x00 */ JSUList<Z2CreatureEnemy> field_0x0;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x0C */ JSUList<Z2SoundObjBase> allList_;
#endif
/* 0x0C */ Z2EnemyArea enemyArea_;
+1 -1
View File
@@ -7,7 +7,7 @@
struct Z2SoundStarter;
class Z2SoundObjBase : public Z2SoundHandles
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
, public JSULink<Z2SoundObjBase>
#endif
{
+2 -7
View File
@@ -88,14 +88,9 @@ public:
/* 0x396A */ u8 field_0x396A[0x399E - 0x396A];
/* 0x399E */ s16 field_0x399e;
/* 0x39A0 */ u8 field_0x39A0[0x39A4 - 0x39A0];
#if TARGET_PC
/* 0x39A4 */ cM_rnd_c mMantRng;
#endif
};
#if TARGET_PC
STATIC_ASSERT(sizeof(mant_class) == 0x39ac);
#else
STATIC_ASSERT(sizeof(mant_class) == 0x39a4);
#endif
#endif /* D_A_MANT_H */
+2 -2
View File
@@ -45,7 +45,7 @@ private:
class dAttParam_c : public JORReflexible {
public:
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x04 */ s8 mHIOChildNo;
#endif
@@ -66,7 +66,7 @@ public:
/* 0x35 */ u8 mAttnCursorDisappearFrames;
/* 0x38 */ f32 field_0x38;
/* 0x3C */ f32 field_0x3c;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x44 */ s32 mDebugDispPosX;
/* 0x48 */ s32 mDebugDispPosY;
#endif
+2 -2
View File
@@ -201,7 +201,7 @@ private:
/* 0x02C */ u32 m_flags;
/* 0x030 */ cXyz* pm_pos;
/* 0x034 */ cXyz* pm_old_pos;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x038 */ cXyz unk_0x38;
#endif
/* 0x038 */ cXyz* pm_speed;
@@ -229,7 +229,7 @@ private:
/* 0x0CC */ f32 field_0xcc;
/* 0x0D0 */ f32 m_wtr_chk_offset;
/* 0x0D4 */ cBgS_PolyInfo* pm_out_poly_info;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x0E4 */ cXyz unk_0xe4;
#endif
/* 0x0D8 */ f32 field_0xd8;
+1 -1
View File
@@ -79,7 +79,7 @@ public:
// /* 0x0000 */ cCcS mCCcS;
/* 0x284C */ dCcMassS_Mng mMass_Mng;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x2AD0 */ u8 m_is_mass_all_timer;
#endif
}; // Size = 0x2AC4
+7 -7
View File
@@ -17,7 +17,7 @@
#include "m_Do/m_Do_graphic.h"
#include <cstring>
#include "dusk/profiling.hpp"
#include "tracy/Tracy.hpp"
enum dComIfG_ButtonStatus {
/* 0x00 */ BUTTON_STATUS_NONE,
@@ -1037,7 +1037,7 @@ public:
/* 0x1DE09 */ u8 field_0x1de09;
/* 0x1DE0A */ u8 field_0x1de0a;
/* 0x1DE0B */ u8 mIsDebugMode;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x1DE0C */ OSStopwatch mStopwatch;
#endif
@@ -1049,11 +1049,11 @@ public:
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
extern dComIfG_inf_c g_dComIfG_gameInfo;
extern GXColor g_blackColor;
extern GXColor g_clearColor;
extern GXColor g_whiteColor;
extern GXColor g_saftyWhiteColor;
DUSK_GAME_EXTERN dComIfG_inf_c g_dComIfG_gameInfo;
DUSK_GAME_EXTERN GXColor g_blackColor;
DUSK_GAME_EXTERN GXColor g_clearColor;
DUSK_GAME_EXTERN GXColor g_whiteColor;
DUSK_GAME_EXTERN GXColor g_saftyWhiteColor;
int dComLbG_PhaseHandler(request_of_phase_process_class*, request_of_phase_process_fn*,
void*);
+1 -1
View File
@@ -41,7 +41,7 @@ public:
BASE_ROOM5,
BASE_DEMO,
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
BASE_DEBUG,
#endif
+1 -1
View File
@@ -259,7 +259,7 @@ public:
/* 0x09B8 */ DUNGEON_LIGHT dungeonlight[8];
/* 0x0C18 */ BOSS_LIGHT field_0x0c18[8];
/* 0x0D58 */ BOSS_LIGHT field_0x0d58[6];
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x0E48 */ NAVYCHAN navy;
/* 0x0E58 */ u8 field_0xe58[0xE68 - 0xE58]; // part of NAVYCHAN?
#endif
+4 -1
View File
@@ -2,6 +2,7 @@
#define D_METER_D_METER2_INFO_H
#include "SSystem/SComponent/c_xyz.h"
#include "global.h"
class CPaneMgr;
class J2DTextBox;
@@ -301,7 +302,7 @@ public:
/* 0xF3 */ u8 unk_0xf3[5];
};
extern dMeter2Info_c g_meter2_info;
DUSK_GAME_EXTERN dMeter2Info_c g_meter2_info;
void dMeter2Info_setSword(u8 i_itemId, bool i_offItemBit);
void dMeter2Info_setCloth(u8 i_clothId, bool i_offItemBit);
@@ -849,6 +850,8 @@ inline void dMeter2Info_setFloatingMessage(u16 i_msgID, s16 i_msgTimer, bool i_w
g_meter2_info.setFloatingMessage(i_msgID, i_msgTimer, i_wakuVisible);
}
// Show a custom text notification using the floating-message HUD display.
inline void dMeter2Info_setMiniGameCount(s8 i_count) {
g_meter2_info.setMiniGameCount(i_count);
}
-10
View File
@@ -360,12 +360,7 @@ inline void dMsgObject_demoMessageGroup() {
}
inline bool dMsgObject_isTalkNowCheck() {
#if TARGET_PC
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
return msgObject != NULL && msgObject->getStatus() != 1;
#else
return dMsgObject_getMsgObjectClass()->getStatus() == 1 ? false : true;
#endif
}
inline bool dMsgObject_isKillMessageFlag() {
@@ -502,12 +497,7 @@ inline void dMsgObject_onMsgSend() {
}
inline bool dMsgObject_isFukidashiCheck() {
#if TARGET_PC
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
return msgObject != NULL && msgObject->getScrnDrawPtr() != NULL;
#else
return dMsgObject_getMsgObjectClass()->getScrnDrawPtr() == NULL ? false : true;
#endif
}
inline void* dMsgObject_getTalkHeap() {
+2 -2
View File
@@ -521,13 +521,13 @@ private:
/* 0x019 */ u8 field_0x19;
/* 0x01A */ u8 field_0x1a;
/* 0x01B */ u8 field_0x1b;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x01C */ dPa_simpleEcallBack field_0x1c[48];
#else
/* 0x01C */ dPa_simpleEcallBack field_0x1c[25];
#endif
/* 0x210 */ level_c field_0x210;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
u8 mSceneCount;
#endif
};
+1 -1
View File
@@ -59,7 +59,7 @@ private:
/* 0x18 */ JKRHeap* heap;
/* 0x1C */ JKRSolidHeap* mDataHeap;
/* 0x20 */ void** mRes;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x24 */ int mSize;
#endif
}; // Size: 0x24
+1 -4
View File
@@ -1008,7 +1008,7 @@ public:
static const int ZONE_MAX = 0x20;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x000 */ u8 unk_0x0;
/* 0x001 */ char unk_0x1;
/* 0x000 */ u8 unk_0x2[0x48 - 0x2];
@@ -1029,9 +1029,6 @@ public:
/* 0xF30 */ s64 mSaveTotalTime;
#if DEBUG
/* 0xF80 */ flagFile_c mFlagFile;
#elif PARTIAL_DEBUG
// flagFile_c's ctor/virtuals are only defined under #if DEBUG (d_save.cpp)
alignas(flagFile_c) u8 mFlagFile[sizeof(flagFile_c)];
#endif
}; // Size: 0xF38
+4 -4
View File
@@ -538,7 +538,7 @@ public:
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const = 0;
/* vt[87] */ virtual void setDoor(stage_tgsc_class*) = 0;
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const = 0;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
virtual void setUnit(void*) = 0;
virtual void* getUnit() = 0;
#endif
@@ -796,7 +796,7 @@ public:
virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
virtual void setUnit(void* i_Unit) { mUnit = i_Unit; }
virtual void* getUnit() { return mUnit; }
#endif
@@ -845,7 +845,7 @@ public:
/* 0x54 */ stage_tgsc_class* mDrTg;
/* 0x58 */ stage_tgsc_class* mDoor;
/* 0x5C */ dStage_FloorInfo_c* mFloorInfo;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x60 */ void* mUnit;
#endif
/* 0x60 */ u16 mPlayerNum;
@@ -990,7 +990,7 @@ public:
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
/* vt[87] */ virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
virtual void setUnit(void* i_Unit) {
UNUSED(i_Unit);
OSReport("stage non unit list data !!\n");
-3
View File
@@ -97,9 +97,6 @@ public:
private:
#if DEBUG
/* 0x00 */ dVibTest_c mVibTest;
#elif PARTIAL_DEBUG
// dVibTest_c's ctor/virtuals are only defined under #if DEBUG (d_vibration.cpp)
alignas(dVibTest_c) u8 mVibTest[sizeof(dVibTest_c)];
#endif
class {
+7 -70
View File
@@ -1,11 +1,9 @@
#ifndef DUSK_CONFIG_HPP
#define DUSK_CONFIG_HPP
#include <concepts>
#include <functional>
#include <nlohmann/json.hpp>
#include <stdexcept>
#include "nlohmann/json.hpp"
#include "config_var.hpp"
namespace dusk::config {
@@ -42,7 +40,7 @@ public:
[[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0;
};
template <ConfigValue T>
template<ConfigValue T>
class ConfigImpl : public ConfigImplBase {
// Just downcasting the references...
void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final {
@@ -91,29 +89,20 @@ public:
*/
void Register(ConfigVarBase& configVar);
/**
* \brief Unregister a CVar, detaching it from the config system.
*
* If the CVar carries a user-set value (Value or Speedrun layer), it is stashed as an
* unregistered key: Save() keeps writing it, and a later Register() of the same name restores
* it through the normal back-fill path. The CVar may be destroyed after this returns.
*/
void unregister(ConfigVarBase& configVar);
/**
* \brief Load config from the standard user preferences location.
*/
void load_from_user_preferences();
void load_from_file_name(const char* path);
void LoadFromUserPreferences();
void LoadFromFileName(const char* path);
void load_arg_override(std::string_view name, std::string_view value);
void LoadArgOverride(std::string_view name, std::string_view value);
void shutdown();
void Shutdown();
/**
* \brief Save the config to file.
*/
void save();
void Save();
/**
* \brief Get a registered CVar by name.
@@ -134,58 +123,6 @@ void ClearAllActionBindings(int port);
*/
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
/**
* \brief Type-erased change callback. previousValue points at the value before the mutation
* (a `const T*` for a `ConfigVar<T>`) and is valid only for the duration of the call.
*/
using ChangeCallback = std::function<void(ConfigVarBase& cVar, const void* previousValue)>;
/**
* \brief Token identifying a change subscription. 0 is never a valid token.
*/
using Subscription = u64;
/**
* \brief Subscribe to changes of the named CVar (registered or not yet registered).
*
* Fired synchronously on the mutating thread (in practice the game thread) whenever the CVar's
* effective value changes at runtime: setValue, override/speedrun setters and clears. Values
* applied by config load or launch arguments do *not* notify: loads happen during startup
* before the subsystems callbacks push values into are initialized, and each subsystem reads
* its initial value itself at its own init. Callbacks may mutate other CVars; a nested
* mutation of the same CVar applies but does not re-notify.
*/
Subscription subscribe(std::string_view name, ChangeCallback callback);
/**
* \brief Typed convenience overload: the callback receives the current and previous values.
*/
template <ConfigValue T, typename Callback>
requires std::invocable<Callback, const T&, const T&> Subscription subscribe(
ConfigVar<T>& cVar, Callback&& callback) {
return subscribe(cVar.getName(),
[&cVar, cb = std::forward<Callback>(callback)](ConfigVarBase&, const void* previousValue) {
cb(cVar.getValue(), *static_cast<const T*>(previousValue));
});
}
void unsubscribe(Subscription token);
/**
* \brief Register a CVar and attach a change callback in one step.
*
* Useful for pushing settings into external systems (e.g. aurora) from one place instead of
* every UI setter. The callback fires only for runtime changes (see subscribe); not when
* loaded from config or launch arguments.
*/
template <ConfigValue T, typename Callback>
requires std::invocable<Callback, const T&, const T&> Subscription Register(
ConfigVar<T>& cVar, Callback&& onChange) {
auto subscription = subscribe(cVar, std::forward<Callback>(onChange));
Register(static_cast<ConfigVarBase&>(cVar));
return subscription;
}
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl() {
static ConfigImpl<T> config;
+3 -72
View File
@@ -2,12 +2,10 @@
#define DUSK_CONFIG_VAR_HPP
#include "dolphin/types.h"
#include <concepts>
#include <type_traits>
#include <cstdlib>
#include <limits>
#include <optional>
#include <string>
#include <type_traits>
/**
* The configuration system.
@@ -89,6 +87,8 @@ protected:
*/
const ConfigImplBase* impl;
// The configuration system stores a direct pointer to the ConfigVar instance.
// It is not legal to move or copy it.
ConfigVarBase(const ConfigVarBase&) = delete;
ConfigVarBase(std::string name, const ConfigImplBase* impl);
@@ -100,19 +100,6 @@ protected:
abort();
}
/**
* Whether any change subscriber (see config::subscribe) is attached to this CVar's name.
*/
[[nodiscard]] bool has_subscribers() const;
/**
* Notify change subscribers (see config::subscribe) that the effective value of this CVar
* changed. Called by mutators after the change has been applied; previousValue points at
* the old value (a `const T*` for a `ConfigVar<T>`), valid only for the duration of the
* call.
*/
void notify_changed(const void* previousValue);
public:
virtual ~ConfigVarBase();
@@ -173,7 +160,6 @@ template <typename T>
concept ConfigValue =
!std::is_const_v<T>
&& !std::is_volatile_v<T>
&& std::equality_comparable<T>
&& (std::is_same_v<T, bool>
|| ConfigValueInteger<T>
|| std::is_same_v<T, f32>
@@ -185,9 +171,6 @@ concept ConfigValue =
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl();
template <ConfigValue T>
class ConfigImpl;
template <typename T>
struct ConfigEnumRange {
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
@@ -258,7 +241,6 @@ public:
*/
void setValue(T newValue, bool replaceOverride = true) {
checkRegistered();
const auto previous = previous_for_notify();
value = std::move(newValue);
if (replaceOverride) {
@@ -267,7 +249,6 @@ public:
} else if (layer != ConfigVarLayer::Override) {
layer = ConfigVarLayer::Value;
}
notify_if_changed(previous);
}
operator const T&() {
@@ -284,10 +265,8 @@ public:
*/
void setOverrideValue(T newValue) {
checkRegistered();
const auto previous = previous_for_notify();
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Override;
notify_if_changed(previous);
}
/**
@@ -301,31 +280,25 @@ public:
void setSpeedrunValue(T newValue) {
checkRegistered();
if (layer != ConfigVarLayer::Override) {
const auto previous = previous_for_notify();
priorLayer = layer;
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Speedrun;
notify_if_changed(previous);
}
}
void clearOverride() {
checkRegistered();
if (layer == ConfigVarLayer::Override) {
const auto previous = previous_for_notify();
overrideValue = {};
layer = ConfigVarLayer::Value;
notify_if_changed(previous);
}
}
void clearSpeedrunOverride() override {
checkRegistered();
if (layer == ConfigVarLayer::Speedrun) {
const auto previous = previous_for_notify();
overrideValue = {};
layer = priorLayer;
notify_if_changed(previous);
}
}
@@ -339,48 +312,6 @@ public:
const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer;
return effectiveLayer == ConfigVarLayer::Default ? defaultValue : value;
}
private:
// The config loader applies values through the silent load_* methods below.
friend class ConfigImpl<T>;
/**
* Copy of the effective value before a mutation, taken only when someone is subscribed.
*/
[[nodiscard]] std::optional<T> previous_for_notify() const {
return has_subscribers() ? std::optional<T>{getValue()} : std::nullopt;
}
/**
* Notify subscribers if the effective value actually changed across a mutation.
*/
void notify_if_changed(const std::optional<T>& previous) {
if (previous.has_value() && !(getValue() == *previous)) {
notify_changed(&*previous);
}
}
/**
* setValue(newValue, false) without notifying change subscribers. Used when loading config:
* loads happen during startup before the subsystems change callbacks push values into are
* initialized, and each subsystem applies the loaded value itself at its own init.
*/
void load_value(T newValue) {
checkRegistered();
value = std::move(newValue);
if (layer != ConfigVarLayer::Override) {
layer = ConfigVarLayer::Value;
}
}
/**
* setOverrideValue without notifying change subscribers (see load_value).
*/
void load_override_value(T newValue) {
checkRegistered();
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Override;
}
};
using ActionBindConfigVar = ConfigVar<int>;
+5 -15
View File
@@ -5,8 +5,7 @@
#include <dolphin/gx/GXAurora.h>
#include <dolphin/gx/GXExtra.h>
#include "profiling.hpp"
#include "tracy/Tracy.hpp"
#if DUSK_GFX_DEBUG_GROUPS
#define GX_DEBUG_GROUP(name, ...) \
@@ -23,9 +22,8 @@
class GXTexObjRAII : public GXTexObj {
public:
GXTexObjRAII() : GXTexObj() {}
~GXTexObjRAII() { GXDestroyTexObj(this); }
void reset() { GXDestroyTexObj(this); }
~GXTexObjRAII();
void reset();
GXTexObjRAII(const GXTexObjRAII&) = delete;
GXTexObjRAII& operator=(const GXTexObjRAII&) = delete;
@@ -66,16 +64,8 @@ typedef GXTlutObj TGXTlutObj;
#endif
struct GXScopedDebugGroup {
explicit GXScopedDebugGroup(const char* text) {
#if DUSK_GFX_DEBUG_GROUPS
GXPushDebugGroup(text);
#endif
}
~GXScopedDebugGroup() {
#if DUSK_GFX_DEBUG_GROUPS
GXPopDebugGroup();
#endif
}
explicit GXScopedDebugGroup(const char* text);
~GXScopedDebugGroup();
};
#define GX_AND_TRACY_SCOPED(name) GXScopedDebugGroup scope(name); ZoneScopedN(name);
+122
View File
@@ -0,0 +1,122 @@
#pragma once
#include <cstring>
#include <memory>
#include <type_traits>
#include "dusk/mod_api.h"
namespace dusk {
inline DuskModAPI* g_api = nullptr;
inline void init(DuskModAPI* api) { g_api = api; }
template <class T>
T arg(void* args_raw, int n) noexcept {
void** a = static_cast<void**>(args_raw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(a[n]);
}
template <class T>
std::remove_reference_t<T>& argRef(void* args_raw, int n) noexcept {
void** a = static_cast<void**>(args_raw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(a[n]);
}
template <class F>
void* mfpAddr(F fn) noexcept {
void* p = nullptr;
static_assert(sizeof(fn) >= sizeof(void*), "unexpected MFP size");
std::memcpy(&p, &fn, sizeof(void*));
return p;
}
template <auto MFP, class R, class Self, class Orig, class... A>
struct HookEntryBase {
static inline Orig g_orig = nullptr;
static R trampoline(Self self, A... args) {
void* ptrs[] = {static_cast<void*>(std::addressof(self)), static_cast<void*>(std::addressof(args))...};
if constexpr (std::is_void_v<R>) {
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast<void*>(ptrs), nullptr);
if (!cancel) g_orig(self, args...);
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs), nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(self, args...);
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
return result;
}
}
};
template <auto FP, class R, class Orig, class... A>
struct HookEntryFreeBase {
static inline Orig g_orig = nullptr;
static R trampoline(A... args) {
if constexpr (sizeof...(A) == 0) {
if constexpr (std::is_void_v<R>) {
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), nullptr, nullptr);
if (!cancel) g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), nullptr, nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), nullptr, static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), 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 cancel = g_api->hook_dispatch_pre(mfpAddr(FP), static_cast<void*>(ptrs), nullptr);
if (!cancel) g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), static_cast<void*>(ptrs), nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
return result;
}
}
}
};
template <auto MFP>
struct HookEntry;
template <class C, class R, class... A, R (C::*MFP)(A...)>
struct HookEntry<MFP> : HookEntryBase<MFP, R, C*, R(*)(C*, A...), A...> {};
template <class C, class R, class... A, R (C::*MFP)(A...) const>
struct HookEntry<MFP> : HookEntryBase<MFP, R, const C*, R(*)(const C*, A...), A...> {};
template <class R, class... A, R (*FP)(A...)>
struct HookEntry<FP> : HookEntryFreeBase<FP, R, R(*)(A...), A...> {};
template <auto MFP>
void hookAddPre(int32_t (*fn)(void* args)) {
using E = HookEntry<MFP>;
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
reinterpret_cast<void**>(&E::g_orig));
g_api->hook_pre(mfpAddr(MFP), fn);
}
template <auto MFP>
void hookAddPost(void (*fn)(void* args, void* retval)) {
using E = HookEntry<MFP>;
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
reinterpret_cast<void**>(&E::g_orig));
g_api->hook_post(mfpAddr(MFP), fn);
}
template <auto MFP>
void hookSetReplace(void (*fn)(void* args, void* retval)) {
using E = HookEntry<MFP>;
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
reinterpret_cast<void**>(&E::g_orig));
g_api->hook_replace(mfpAddr(MFP), fn);
}
} // namespace dusk
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
namespace dusk {
void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store);
void hookRegisterPre (void* fn_addr, void* mod, int32_t (*fn)(void* args));
void hookRegisterPost(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval));
bool hookSetReplace (void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval));
bool hookDispatchPre (void* fn_addr, void* args, void* retval);
void hookDispatchPost(void* fn_addr, void* args, void* retval);
void hookClearMod(void* mod);
} // namespace dusk
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <cstddef>
#include <cstdint>
#if defined(_WIN32)
#define DUSK_MOD_EXPORT __declspec(dllexport)
#else
#define DUSK_MOD_EXPORT __attribute__((visibility("default")))
#endif
#define DUSK_MOD_API_VERSION 1
typedef void* DuskPanelHandle;
typedef void* DuskElemHandle;
// Place this once at file scope in your mod to declare the minimum API version required.
// The loader will refuse to initialize the mod if the engine's API version is older.
#define DUSK_REQUIRE_API_VERSION \
extern "C" DUSK_MOD_EXPORT uint32_t mod_api_version = DUSK_MOD_API_VERSION;
struct DuskModAPIv1 {
uint32_t api_version;
const char* mod_dir;
void (*log_info)(const char* fmt, ...);
void (*log_warn)(const char* fmt, ...);
void (*log_error)(const char* fmt, ...);
void* (*load_resource)(const char* relative_path, size_t* out_size);
void (*free_resource)(void* data);
void (*register_tab_content)(
void (*build_fn)(DuskPanelHandle panel, void* userdata), void* userdata);
void (*register_tab_update)(void (*update_fn)(void* userdata), void* userdata);
void (*panel_add_section)(DuskPanelHandle panel, const char* text);
void (*panel_add_button)(
DuskPanelHandle panel, const char* label, void (*cb)(void* userdata), void* userdata);
DuskElemHandle (*panel_add_badge_row)(DuskPanelHandle panel, const char* label, int ok);
DuskElemHandle (*panel_add_dyn_text)(DuskPanelHandle panel, const char* text);
DuskElemHandle (*panel_add_progress)(DuskPanelHandle panel, float value);
void (*elem_set_badge)(DuskElemHandle elem, int ok);
void (*elem_set_text)(DuskElemHandle elem, const char* text);
void (*elem_set_progress)(DuskElemHandle elem, float value);
void (*hook_install)(void* fn_addr, void* tramp_fn, void** orig_store);
void (*hook_pre)(void* fn_addr, int32_t (*fn)(void* args));
void (*hook_post)(void* fn_addr, void (*fn)(void* args, void* retval));
void (*hook_replace)(void* fn_addr, void (*fn)(void* args, void* retval));
bool (*hook_dispatch_pre)(void* fn_addr, void* args, void* retval);
void (*hook_dispatch_post)(void* fn_addr, void* args, void* retval);
void (*service_publish)(const char* name, void* ptr);
void* (*service_get)(const char* name);
};
using DuskModAPI = DuskModAPIv1;
extern "C" {
void mod_init(DuskModAPI* api);
void mod_tick(DuskModAPI* api);
}
+38 -149
View File
@@ -1,51 +1,28 @@
#pragma once
#include <filesystem>
#include <memory>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
#include <ranges>
#include "dusk/config.hpp"
#include "dusk/mod_api.h"
#include "dusk/config_var.hpp"
#include "mods/api.h"
namespace dusk::mods {
struct LoadedMod;
namespace dusk::modding {
class ModBundle;
} // namespace dusk::mods
struct ModContext {
dusk::mods::LoadedMod* mod = nullptr;
};
namespace dusk::mods::loader {
class NativeModule;
} // namespace dusk::mods::loader
}
namespace dusk::mods {
namespace dusk {
struct ModDependencyEdge {
LoadedMod* mod = nullptr;
bool required = false;
struct RmlTabContentCallback {
void (*build_fn)(void* panel, void* userdata);
void* userdata;
};
struct ModManifestInfo {
struct Import {
std::string id;
uint16_t major = 0;
bool required = false;
bool operator==(const Import&) const = default;
};
struct Export {
std::string id;
uint16_t major = 0;
bool operator==(const Export&) const = default;
};
std::vector<Import> imports;
std::vector<Export> exports;
bool operator==(const ModManifestInfo&) const = default;
struct RmlTabUpdateCallback {
void (*update_fn)(void* userdata);
void* userdata;
};
struct ModMetadata {
@@ -54,27 +31,20 @@ struct ModMetadata {
std::string version;
std::string author;
std::string description;
std::string iconPath;
std::string bannerPath;
};
struct ModSearchDir {
std::filesystem::path path;
// Directory bundles dlopen their native lib in place instead of extracting it to the cache.
// Required where extracted code cannot run (iOS), desirable for signed/read-only installs.
bool inPlaceNative = false;
// Native library location for platforms that restrict placement (e.g. iOS/tvOS Frameworks/)
std::filesystem::path nativeLibDir;
bool hasCode;
};
struct NativeMod {
std::unique_ptr<loader::NativeModule> handle;
const ModManifest* manifest = nullptr;
ModContext** contextSymbol = nullptr;
std::unique_ptr<modding::NativeModule> handle;
DuskModAPI api{};
ModInitializeFn fn_initialize = nullptr;
ModUpdateFn fn_update = nullptr;
ModShutdownFn fn_shutdown = nullptr;
using FnInit = void (*)(DuskModAPI*);
using FnTick = void (*)(DuskModAPI*);
using FnCleanup = void (*)(DuskModAPI*);
FnInit fn_init = nullptr;
FnTick fn_tick = nullptr;
FnCleanup fn_cleanup = nullptr;
};
enum class NativeModStatus : u8 {
@@ -97,20 +67,15 @@ enum class NativeModStatus : u8 {
BuildDisabled,
/**
* Mod ships native libraries, but none matches this build's platform and architecture.
* Mod does not have a native library suitable for this build's platform.
*/
ModMissingPlatform,
/**
* Mod is built for a different ABI version than this build of the game.
* Mod is built for a different API version than this build of the game.
*/
ApiVersionMismatch,
/**
* Mod is missing a required native API export.
*/
MissingExport,
/**
* Unknown error loading the native mod.
*/
@@ -119,65 +84,32 @@ enum class NativeModStatus : u8 {
struct LoadedMod {
ModMetadata metadata;
std::string modPath;
std::string mod_path;
std::string dir;
uint32_t searchDirIndex = 0;
// Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported.
bool inPlace = false;
std::unique_ptr<ConfigVar<bool> > cvarIsEnabled;
config::Subscription enabledSubscription = 0;
std::unique_ptr<ConfigVar<bool>> cvarIsEnabled;
bool active = false;
bool loadFailed = false;
std::string failureReason;
bool load_failed = false;
// mod_initialize succeeded; a mod_shutdown is owed on deactivation.
bool initialized = false;
// Static service exports are currently present in the registry.
bool servicesRegistered = false;
// Lifecycle state last applied by the loader; diffed against cvarIsEnabled to pick up
// runtime enable/disable requests.
bool enabledApplied = false;
// Deactivated because a provider it imports from was disabled, not by its own cvar.
bool suspendedByProvider = false;
// Bumped per native lib extraction so every dlopen sees a fresh path (and thus a fresh
// image with fresh statics; a previous dlclose may not fully unmap). Also bumped by
// asset-only reloads, so it doubles as a generation for anything caching per-mod content.
uint32_t cacheGeneration = 0;
// Currently extracted native library, empty if none.
std::string nativePath;
NativeModStatus nativeStatus = NativeModStatus::None;
NativeModStatus native_status = NativeModStatus::None;
std::unique_ptr<NativeMod> native;
std::unique_ptr<ModContext> context;
// Shared with overlay file registrations so in-flight DVD reads survive disable/reload.
std::shared_ptr<ModBundle> bundle;
std::unique_ptr<modding::ModBundle> bundle;
ModManifestInfo manifestInfo;
// Mods this mod imports services from, and mods importing services from this mod.
std::vector<ModDependencyEdge> dependencies;
std::vector<ModDependencyEdge> dependents;
std::vector<RmlTabContentCallback> tab_content;
std::vector<RmlTabUpdateCallback> tab_updates;
};
class ModLoader {
public:
static ModLoader& instance();
void set_search_dirs(std::vector<ModSearchDir> dirs) { m_searchDirs = std::move(dirs); }
void set_cache_dir(std::filesystem::path dir) { m_cacheDir = std::move(dir); }
void setModsDir(std::filesystem::path dir) { m_modsDir = std::move(dir); }
void init();
void tick();
void shutdown();
void request_enable(std::string_view id);
void request_disable(std::string_view id);
void request_reload(std::string_view id);
void notify_mod_failure(LoadedMod& mod, bool firstFailure);
[[nodiscard]] auto mods() const {
return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; });
}
@@ -187,59 +119,16 @@ public:
}
private:
enum class RequestKind : u8 { Enable, Disable, Reload };
struct Request {
std::string modId;
RequestKind kind;
};
// ModLoader::tick runs inside fapGm_Execute, so code from an unloading mod can still be
// live on the stack (its frame unwinds after the tick). dlclose is therefore deferred to
// the next tick, by which point every per-frame entry into the mod should have returned.
struct RetiredNative {
std::unique_ptr<NativeMod> native;
std::string path;
};
std::vector<std::unique_ptr<LoadedMod> > m_mods;
std::vector<ModSearchDir> m_searchDirs;
std::filesystem::path m_cacheDir;
std::vector<Request> m_pendingRequests;
std::vector<std::string> m_pendingFailures;
std::vector<RetiredNative> m_retiredNatives;
std::vector<std::unique_ptr<LoadedMod>> m_mods;
std::filesystem::path m_modsDir;
bool m_initialized = false;
bool m_startupComplete = false;
void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex);
void load_native(LoadedMod& mod, const std::string& dllEntry);
// Resolved <nativeLibDir>/<mod id><ext> if it exists on disk, empty otherwise.
[[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const;
void unload_native(LoadedMod& mod);
// Registers exports (if needed), resolves imports and runs mod_initialize.
// Returns whether the mod ended up active; failures go through fail_mod.
bool activate_mod(LoadedMod& mod);
// Runs mod_shutdown (if needed), detaches the mod from every service, and unloads the
// native lib. Must only run with no mod code on the stack (startup, shutdown, or top of tick).
void deactivate_mod(LoadedMod& mod);
void init_services();
bool register_static_service_exports(LoadedMod& mod);
bool resolve_service_imports(LoadedMod& mod);
[[nodiscard]] std::string describe_missing_import(
const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const;
LoadedMod* find_mod(std::string_view id) const;
void drain_retired_natives();
void apply_pending_requests();
void flush_toasts();
void on_enabled_changed(LoadedMod& mod);
// Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the
// bundle from disk, then reactivates whatever the current cvar/provider state allows.
void apply_lifecycle_change(LoadedMod& target, bool reload);
// `target` plus transitive active/suspended dependents, in m_mods (init) order.
std::vector<LoadedMod*> collect_lifecycle_set(LoadedMod& target);
bool reload_bundle(LoadedMod& mod);
bool ensure_native_loaded(LoadedMod& mod);
void tryLoadDusk(const std::filesystem::path& modPath, bool fromDir);
void tryLoadNativeMod(LoadedMod& mod);
void buildAPI(LoadedMod& mod);
void initOverlayFiles();
};
using ModIndex = std::ranges::range_difference_t<decltype(std::declval<ModLoader>().mods())>;
} // namespace dusk::mods
} // namespace dusk
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "f_op/f_op_actor_mng.h"
#include "f_pc/f_pc_layer.h"
#include "f_pc/f_pc_manager.h"
#include "f_pc/f_pc_node.h"
#include "m_Do/m_Do_controller_pad.h"
// Remove a button from this frame's trigger state so the game won't see it
// Call after detecting a combo in mod_tick to prevent double-processing
inline void consumeInput(u32 pad, u32 buttonMask) {
mDoCPd_c::getCpadInfo(pad).mPressedButtonFlags &= ~buttonMask;
}
// Spawn an actor in the play scene layer
// calling fopAcM_create directly outside game simulation context creates the actor in the wrong
// layer, corrupting its first-frame rendering setup
inline fpc_ProcID fopAcM_createInPlayScene(s16 proc_name, u32 params, const cXyz* pos, int room_no,
const csXyz* angle, const cXyz* scale, s8 argument) {
layer_class* savedLayer = fpcLy_CurrentLayer();
base_process_class* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e);
if (playScene != nullptr) {
fpcLy_SetCurrentLayer(&((process_node_class*)playScene)->layer);
}
fpc_ProcID result = fopAcM_create(proc_name, params, pos, room_no, angle, scale, argument);
fpcLy_SetCurrentLayer(savedLayer);
return result;
}
+4 -4
View File
@@ -4,9 +4,9 @@
namespace dusk::mouse {
void read();
void get_aim_deltas(float& out_yaw, float& out_pitch);
void get_camera_deltas(float& out_yaw, float& out_pitch);
void getAimDeltas(float& out_yaw, float& out_pitch);
void getCameraDeltas(float& out_yaw, float& out_pitch);
void handle_event(const SDL_Event& event) noexcept;
void on_focus_lost();
void on_focus_gained();
void onFocusLost();
void onFocusGained();
} // namespace dusk::mouse
-12
View File
@@ -1,12 +0,0 @@
#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
+1 -16
View File
@@ -7,8 +7,7 @@
namespace dusk {
using config::ConfigVar;
using config::ActionBindConfigVar;
using namespace config;
enum class BloomMode : int {
Off = 0,
@@ -47,12 +46,6 @@ enum class FrameInterpMode : u8 {
Unlimited = 2,
};
enum class TouchTargeting : u8 {
Hybrid = 0,
Hold = 1,
Switch = 2,
};
enum class MenuScaling : u8 {
GameCube = 0,
Wii = 1,
@@ -104,12 +97,6 @@ struct ConfigEnumRange<FrameInterpMode> {
static constexpr auto max = FrameInterpMode::Unlimited;
};
template <>
struct ConfigEnumRange<TouchTargeting> {
static constexpr auto min = TouchTargeting::Hybrid;
static constexpr auto max = TouchTargeting::Switch;
};
template <>
struct ConfigEnumRange<MenuScaling> {
static constexpr auto min = MenuScaling::GameCube;
@@ -229,7 +216,6 @@ struct UserSettings {
ConfigVar<bool> invertMouseY;
ConfigVar<bool> freeCamera;
ConfigVar<bool> enableTouchControls;
ConfigVar<TouchTargeting> touchTargeting;
ConfigVar<bool> enableMenuPointer;
ConfigVar<ui::ControlLayout> touchControlsLayout;
ConfigVar<bool> invertCameraXAxis;
@@ -266,7 +252,6 @@ struct UserSettings {
ConfigVar<bool> fastSpinner;
ConfigVar<MagicArmorMode> armorRupeeDrain;
ConfigVar<bool> invincibleEnemies;
ConfigVar<bool> easyQuickSpin;
// Technical
ConfigVar<bool> restoreWiiGlitches;
-5
View File
@@ -1,13 +1,8 @@
#ifndef DUSK_TEXTURE_REPLACEMENTS_HPP
#define DUSK_TEXTURE_REPLACEMENTS_HPP
#include <cstdint>
namespace dusk::texture_replacements {
// Mod replacements are prioritized *over* user replacements (<data folder>/texture_replacements/)
inline constexpr int32_t kUserTextureReplacementPriority = -1'000'000;
void reload();
void set_enabled(bool enabled);
void shutdown();
+1 -1
View File
@@ -36,7 +36,7 @@ typedef struct node_create_request {
/* 0x58 */ s16 name;
/* 0x5C */ void* data;
/* 0x60 */ s16 unk_0x60;
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x64 */ int unk_0x64;
/* 0x68 */ int unk_0x68;
#endif
+7 -7
View File
@@ -114,17 +114,17 @@ inline int __builtin_clz(unsigned int v) {
#endif
// Data symbols exported from the main exe need dllimport on the mod side.
// Data symbols in dusk.dll 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)
# 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)
# define DUSK_GAME_EXTERN extern __declspec(dllexport)
# define DUSK_GAME_DATA __declspec(dllexport)
#else
#define DUSK_GAME_EXTERN extern
#define DUSK_GAME_DATA
# define DUSK_GAME_EXTERN extern
# define DUSK_GAME_DATA
#endif
#define FAST_DIV(x, n) (x >> (n / 2))
+3 -3
View File
@@ -11,13 +11,13 @@ class mDoAud_zelAudio_c : public Z2AudioMgr {
public:
void reset();
mDoAud_zelAudio_c() {
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
setMode(2);
#endif
}
~mDoAud_zelAudio_c() {}
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
u8 getMode() { return field_0x13bd; }
void setMode(u8 mode) { field_0x13bd = mode; }
@@ -33,7 +33,7 @@ public:
static void onBgmSet() { mBgmSet = true; }
static void offBgmSet() { mBgmSet = false; }
static u8 mInitFlag;
static DUSK_GAME_DATA u8 mInitFlag;
static u8 mResetFlag;
static u8 mBgmSet;
};
+2 -1
View File
@@ -4,6 +4,7 @@
#include "JSystem/JUtility/JUTGamePad.h"
#include "SSystem/SComponent/c_API_controller_pad.h"
#include "dusk/settings.h"
#include "global.h"
// Controller Ports 1 - 4
enum { PAD_1, PAD_2, PAD_3, PAD_4 };
@@ -94,7 +95,7 @@ public:
static void stopMotorWaveHard(u32 pad) { return m_gamePad[pad]->stopMotorWaveHard(); }
static JUTGamePad* m_gamePad[4];
static interface_of_controller_pad m_cpadInfo[4];
static DUSK_GAME_DATA interface_of_controller_pad m_cpadInfo[4];
static interface_of_controller_pad m_debugCpadInfo[4];
};
+1 -1
View File
@@ -371,6 +371,7 @@ public:
static int m_height;
static f32 m_heightF;
static f32 m_widthF;
#endif
#if TARGET_PC
static f32 m_safeMinXF;
@@ -380,7 +381,6 @@ public:
static f32 m_safeWidthF;
static f32 m_safeHeightF;
#endif
#endif
};
#endif /* M_DO_M_DO_GRAPHIC_H */
-5
View File
@@ -35,11 +35,6 @@ public:
/* 0x4 */ s8 mNo;
/* 0x5 */ u8 mCount;
#else
#if PARTIAL_DEBUG
// Initialized here since the DEBUG ctor doesn't run.
/* 0x4 */ s8 mNo = -1;
/* 0x5 */ u8 mCount = 0;
#endif
virtual ~mDoHIO_entry_c() {}
#endif
};
-152
View File
@@ -1,152 +0,0 @@
#pragma once
#ifdef __cplusplus
#include <cstddef>
#include <cstdint>
extern "C" {
#else
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#endif
#if defined(_WIN32)
#define MOD_EXPORT __declspec(dllexport)
#else
#define MOD_EXPORT __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
#define MOD_EXTERN_C extern "C"
#else
#define MOD_EXTERN_C
#endif
#define MOD_ABI_VERSION 5u
#define MOD_ERROR_MESSAGE_SIZE 512u
typedef struct ModContext ModContext;
typedef enum ModResult {
MOD_OK = 0,
MOD_ERROR = 1,
MOD_UNAVAILABLE = 2,
MOD_UNSUPPORTED = 3,
MOD_CONFLICT = 4,
MOD_INVALID_ARGUMENT = 5,
} ModResult;
static_assert(sizeof(ModResult) == 4, "mod SDK enums must be int-sized; do not build mods with -fshort-enums");
typedef struct ModError {
uint32_t struct_size;
ModResult code;
char message[MOD_ERROR_MESSAGE_SIZE];
} ModError;
#define MOD_ERROR_INIT {sizeof(ModError), MOD_OK, {0}}
/*
* Opaque per-mod context, populated by the host before mod_initialize is called.
* Pass it as the first argument to every service call; it identifies the calling
* mod for attribution (logging, resource lookup, hook ownership, etc.).
*/
MOD_EXPORT extern ModContext* mod_ctx;
/*
* Service versioning contract:
*
* A service is a struct of function pointers, beginning with a ServiceHeader.
* Compatibility is tracked with a major/minor version pair:
*
* - A major version bump is a breaking change. Different majors are distinct
* services; the registry never matches an import against a different major.
* - A minor version bump may only append fields to the end of the struct.
* Existing fields must keep their offsets and semantics.
*
* Providers: exporting minor N means every function pointer introduced at or
* below N is populated (non-NULL). struct_size reflects the compiled struct.
*
* Importers: importing with min_minor_version N guarantees (enforced at load
* time) that the resolved service is at least minor N, so any field introduced
* at or below N may be used unconditionally, with no availability checks.
* Fields newer than the declared min_minor_version must be gated behind
* SERVICE_HAS plus a NULL check on the pointer itself.
*
* Load ordering: a manifest import of another mod's service (required or
* optional) guarantees that the provider's mod_initialize completed before the
* importer's runs, and deferred services published during the provider's
* initialization resolve into import slots just like static exports. If a
* provider fails to load, mods that required its services fail in turn. Mods
* whose required imports form a cycle all fail to load; a cycle involving an
* optional import is broken by dropping the ordering guarantee (not the
* resolution) of that optional import. Dynamic lookups via
* HostService::get_service carry no ordering guarantee: they see whatever has
* been published at call time.
*/
typedef struct ServiceHeader {
uint32_t struct_size;
uint16_t major_version;
uint16_t minor_version;
} ServiceHeader;
#define SERVICE_HEADER(service_type, major, minor) {sizeof(service_type), (major), (minor)}
#define SERVICE_HAS(service, service_type, field) \
((service) != NULL && \
(service)->header.struct_size >= \
(uint32_t)(offsetof(service_type, field) + sizeof(((service_type*)0)->field)))
typedef enum ServiceImportFlags {
SERVICE_IMPORT_REQUIRED = 0u,
SERVICE_IMPORT_OPTIONAL = 1u << 0u,
} ServiceImportFlags;
typedef enum ServiceExportFlags {
SERVICE_EXPORT_STATIC = 0u,
SERVICE_EXPORT_DEFERRED = 1u << 0u,
} ServiceExportFlags;
typedef struct ServiceImport {
uint32_t struct_size;
const char* service_id;
uint16_t major_version;
uint16_t min_minor_version;
uint32_t flags;
void* slot;
} ServiceImport;
typedef struct ServiceExport {
uint32_t struct_size;
const char* service_id;
uint16_t major_version;
uint16_t minor_version;
uint32_t flags;
const void* service;
} ServiceExport;
typedef struct ModManifest {
uint32_t struct_size;
uint32_t abi_version;
const ServiceImport* imports;
size_t import_count;
const ServiceExport* exports;
size_t export_count;
} ModManifest;
typedef const ModManifest* (*ModGetManifestFn)(void);
typedef ModResult (*ModInitializeFn)(ModError* out_error);
typedef ModResult (*ModUpdateFn)(ModError* out_error);
typedef ModResult (*ModShutdownFn)(ModError* out_error);
MOD_EXPORT const ModManifest* mod_get_manifest(void);
MOD_EXPORT ModResult mod_initialize(ModError* out_error);
MOD_EXPORT ModResult mod_update(ModError* out_error);
MOD_EXPORT ModResult mod_shutdown(ModError* out_error);
#ifdef __cplusplus
}
#endif
-464
View File
@@ -1,464 +0,0 @@
#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
-138
View File
@@ -1,138 +0,0 @@
#pragma once
#include "mods/api.h"
#include <cstdint>
#include <cstdio>
#include <type_traits>
#include <vector>
namespace dusk::mods {
template <class Service>
struct ServiceTraits;
namespace detail {
inline std::vector<ServiceImport>& imports() {
static std::vector<ServiceImport> entries;
return entries;
}
inline std::vector<ServiceExport>& exports() {
static std::vector<ServiceExport> entries;
return entries;
}
inline int register_import(ServiceImport entry) {
imports().push_back(entry);
return 0;
}
inline int register_export(ServiceExport entry) {
exports().push_back(entry);
return 0;
}
inline const ModManifest* manifest() {
static ModManifest manifest{
sizeof(ModManifest),
MOD_ABI_VERSION,
nullptr,
0,
nullptr,
0,
};
auto& importEntries = imports();
auto& exportEntries = exports();
manifest.imports = importEntries.data();
manifest.import_count = importEntries.size();
manifest.exports = exportEntries.data();
manifest.export_count = exportEntries.size();
return &manifest;
}
} // namespace detail
inline ModResult set_error(ModError* outError, ModResult code, const char* message) {
if (outError != nullptr && outError->struct_size >= sizeof(ModError)) {
outError->code = code;
outError->message[0] = '\0';
if (message != nullptr) {
std::snprintf(outError->message, sizeof(outError->message), "%s", message);
}
}
return code;
}
} // namespace dusk::mods
#define DEFINE_MOD() \
extern "C" { \
MOD_EXPORT ModContext* mod_ctx = nullptr; \
MOD_EXPORT const ModManifest* mod_get_manifest(void) { \
return ::dusk::mods::detail::manifest(); \
} \
}
// Declares `static const service_type* variable`, filled in by the host before mod_initialize.
// Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports
// must be checked against nullptr before use.
#define IMPORT_SERVICE_EX( \
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
static const service_type* variable = nullptr; \
[[maybe_unused]] static const int mod_import_registration_##variable = \
::dusk::mods::detail::register_import(ServiceImport{ \
sizeof(ServiceImport), \
(service_id_value), \
static_cast<uint16_t>(major_value), \
static_cast<uint16_t>(min_minor_value), \
static_cast<uint32_t>(flags_value), \
&(variable), \
})
#define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
SERVICE_IMPORT_REQUIRED)
#define IMPORT_SERVICE(service_type, variable) IMPORT_SERVICE_VERSION(service_type, variable, 0)
#define IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, min_minor_value) \
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
SERVICE_IMPORT_OPTIONAL)
#define IMPORT_OPTIONAL_SERVICE(service_type, variable) \
IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, 0)
#define EXPORT_SERVICE_AS(instance, service_id_value) \
namespace { \
const int mod_export_registration_##instance = \
::dusk::mods::detail::register_export(ServiceExport{ \
sizeof(ServiceExport), \
(service_id_value), \
(instance).header.major_version, \
(instance).header.minor_version, \
SERVICE_EXPORT_STATIC, \
&(instance), \
}); \
}
#define EXPORT_SERVICE(instance) \
EXPORT_SERVICE_AS( \
instance, ::dusk::mods::ServiceTraits<std::remove_cv_t<decltype(instance)> >::id)
#define EXPORT_DEFERRED_SERVICE(token, service_id_value, major_value, minor_value) \
namespace { \
const int mod_deferred_export_registration_##token = \
::dusk::mods::detail::register_export(ServiceExport{ \
sizeof(ServiceExport), \
(service_id_value), \
static_cast<uint16_t>(major_value), \
static_cast<uint16_t>(minor_value), \
SERVICE_EXPORT_DEFERRED, \
nullptr, \
}); \
}
-107
View File
@@ -1,107 +0,0 @@
#pragma once
#include "mods/api.h"
#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config"
#define CONFIG_SERVICE_MAJOR 1u
#define CONFIG_SERVICE_MINOR 0u
/* Handle for a config var registered by the calling mod. 0 is never a valid handle. */
typedef uint64_t ConfigVarHandle;
/* Handle for a change subscription. 0 is never a valid handle. */
typedef uint64_t ConfigSubscriptionHandle;
typedef enum ConfigVarType {
CONFIG_VAR_BOOL = 0, /* bool */
CONFIG_VAR_INT = 1, /* int64_t */
CONFIG_VAR_FLOAT = 2, /* double */
CONFIG_VAR_STRING = 3, /* UTF-8 */
} ConfigVarType;
typedef struct ConfigVarDesc {
uint32_t struct_size;
/* Name fragment: 1-64 characters from [A-Za-z0-9_-]. The full config key is
* "mod.<escaped mod id>.<name>", persisted in config.json alongside host settings.
* "enabled" is reserved by the loader. */
const char* name;
ConfigVarType type;
/* Default value; only the field matching `type` is read. */
bool default_bool;
int64_t default_int;
double default_float;
const char* default_string; /* NULL means "" */
} ConfigVarDesc;
#define CONFIG_VAR_DESC_INIT {sizeof(ConfigVarDesc), NULL, CONFIG_VAR_BOOL, false, 0, 0.0, NULL}
/* Snapshot of a var's value; only the field matching `type` is meaningful. */
typedef struct ConfigVarValue {
uint32_t struct_size;
ConfigVarType type;
bool bool_value;
int64_t int_value;
double float_value;
const char* string_value; /* NUL-terminated; NULL for non-string vars */
size_t string_length; /* excludes the NUL */
} ConfigVarValue;
/*
* Fired on the game thread whenever the var's effective value changes at runtime: the calling mod's
* own set_* calls and any other runtime writer. Writes that leave the value unchanged do not fire,
* and neither do values applied from config.json or --cvar during registration. `value` holds the
* new (current) value and `previous` the one it replaced; both snapshots are valid only for the
* duration of the call (copy string_value if you need to keep it). Setting the same var from inside
* its own callback applies the write but is not re-notified.
*/
typedef void (*ConfigChangedFn)(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value,
const ConfigVarValue* previous, void* user_data);
/*
* Scoped configuration variables.
*
* Registrations are owned by the calling mod and removed automatically (subscriptions included)
* when it is disabled, reloaded, or fails. Values are saved to config.json. Writes are debounced,
* not flushed per set.
*/
typedef struct ConfigService {
ServiceHeader header;
/* Register a config var. If a value for the full key was saved earlier (or set via --cvar),
* it takes effect immediately; otherwise the var starts at the default. Registering a name
* that is already live is MOD_CONFLICT. */
ModResult (*register_var)(
ModContext* ctx, const ConfigVarDesc* desc, ConfigVarHandle* out_handle);
/* Unregister a var previously registered by the calling mod. Its persisted value is kept. */
ModResult (*unregister_var)(ModContext* ctx, ConfigVarHandle var);
/* Typed accessors; the type must match the registration (MOD_INVALID_ARGUMENT otherwise). */
ModResult (*get_bool)(ModContext* ctx, ConfigVarHandle var, bool* out_value);
ModResult (*set_bool)(ModContext* ctx, ConfigVarHandle var, bool value);
ModResult (*get_int)(ModContext* ctx, ConfigVarHandle var, int64_t* out_value);
ModResult (*set_int)(ModContext* ctx, ConfigVarHandle var, int64_t value);
ModResult (*get_float)(ModContext* ctx, ConfigVarHandle var, double* out_value);
ModResult (*set_float)(ModContext* ctx, ConfigVarHandle var, double value);
/* Copies the NUL-terminated value into buffer. out_length (optional) receives the full
* length excluding the NUL regardless of buffer size; call with buffer == NULL and
* buffer_size == 0 to query the length. A non-NULL buffer that is too small fails with
* MOD_INVALID_ARGUMENT and writes nothing. */
ModResult (*get_string)(
ModContext* ctx, ConfigVarHandle var, char* buffer, size_t buffer_size, size_t* out_length);
ModResult (*set_string)(ModContext* ctx, ConfigVarHandle var, const char* value);
/* Subscribe to changes of a var registered by the calling mod. out_handle may be NULL if
* the subscription is never removed manually (cleanup on mod teardown is automatic). */
ModResult (*subscribe)(ModContext* ctx, ConfigVarHandle var, ConfigChangedFn callback,
void* user_data, ConfigSubscriptionHandle* out_handle);
ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle);
} ConfigService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct dusk::mods::ServiceTraits<ConfigService> {
static constexpr const char* id = CONFIG_SERVICE_ID;
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
};
#endif
-30
View File
@@ -1,30 +0,0 @@
#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
-125
View File
@@ -1,125 +0,0 @@
#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;
/*
* 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;
HookReplacePolicy replace_policy;
void* userdata; /* passed back to the callback */
} HookOptions;
#define HOOK_OPTIONS_INIT {sizeof(HookOptions), 0, 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
-104
View File
@@ -1,104 +0,0 @@
#pragma once
#include "mods/api.h"
/*
* The host service: the calling mod's identity and its runtime interface to the loader.
* Always available; every other service can be reached from it.
*/
#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host"
#define HOST_SERVICE_MAJOR 2u
#define HOST_SERVICE_MINOR 0u
/*
* Ignore unknown values: later service minors may add events.
*/
typedef enum ModLifecycleEvent {
/*
* The subject mod is gone: its mod_shutdown has run (when it initialized at all) and
* every service has already dropped the state it held for it. The subject's library is
* still mapped, so pointers into it are valid to compare against, but they must not be
* called or dereferenced after the callback returns. Drop everything keyed to the
* mod: callbacks it registered, its ModContext*, state indexed by it.
*/
MOD_LIFECYCLE_DETACHED = 0,
} ModLifecycleEvent;
/*
* ctx is the watching mod's own context; subject identifies the mod the event is about.
* subject_id is valid only for the duration of the call.
*/
typedef void (*ModLifecycleFn)(ModContext* ctx, ModContext* subject, const char* subject_id,
ModLifecycleEvent event, void* user_data);
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).
* MOD_UNAVAILABLE if no matching service is published; *out_service is null on failure.
*/
ModResult (*get_service)(ModContext* ctx, const char* service_id, uint16_t major_version,
uint16_t min_minor_version, const void** out_service);
/*
* Publish a service the calling mod declared as a deferred export in its manifest.
* Must happen during mod_initialize so importers can resolve it; `service` must stay
* valid until the mod shuts down.
*/
ModResult (*publish_service)(
ModContext* ctx, const char* service_id, uint16_t major_version, const void* service);
/*
* Report an unrecoverable failure. The calling mod's services stop resolving immediately
* and the loader fully disables it at the next safe point; `message` is shown to the user.
* Safe to call from any mod callback.
*/
void (*fail)(ModContext* ctx, ModResult code, const char* message);
/*
* The calling mod's manifest metadata. Returned strings remain valid while the mod is
* loaded.
*/
const char* (*mod_id)(ModContext* ctx);
const char* (*mod_name)(ModContext* ctx);
const char* (*mod_version)(ModContext* ctx);
/*
* A writable scratch directory reserved for the calling mod. Contents survive disable
* and reload within a session, but the directory is wiped at game startup.
*/
const char* (*mod_dir)(ModContext* ctx);
/*
* Observe other mods' lifecycle events. Any mod whose service hands out per-caller state
* (registrations, callbacks, handles) should watch for MOD_LIFECYCLE_DETACHED and drop what it
* holds for the subject.
*
* Callbacks fire on the game thread at a lifecycle safe point (never mid-frame), for
* every mod but the watcher itself (use mod_shutdown for self-cleanup).
*/
ModResult (*watch_mod_lifecycle)(
ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle);
ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle);
} HostService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct dusk::mods::ServiceTraits<HostService> {
static constexpr const char* id = HOST_SERVICE_ID;
static constexpr uint16_t major_version = HOST_SERVICE_MAJOR;
};
#endif
-47
View File
@@ -1,47 +0,0 @@
#pragma once
#include "mods/api.h"
/*
* Logging into the game's console and log files. Messages are attributed to the calling mod
* (prefixed with its ID).
*/
#define LOG_SERVICE_ID "dev.twilitrealm.dusklight.log"
#define LOG_SERVICE_MAJOR 1u
#define LOG_SERVICE_MINOR 0u
typedef enum LogLevel {
LOG_LEVEL_TRACE = 0,
LOG_LEVEL_DEBUG = 1,
LOG_LEVEL_INFO = 2,
LOG_LEVEL_WARN = 3,
LOG_LEVEL_ERROR = 4,
} LogLevel;
typedef struct LogService {
ServiceHeader header;
/*
* Write a log message at the given level.
* `message` is a plain UTF-8 string and is copied before returning.
*/
void (*write)(ModContext* ctx, LogLevel level, const char* message);
/* Per-level shorthands for write. */
void (*trace)(ModContext* ctx, const char* message);
void (*debug)(ModContext* ctx, const char* message);
void (*info)(ModContext* ctx, const char* message);
void (*warn)(ModContext* ctx, const char* message);
void (*error)(ModContext* ctx, const char* message);
} LogService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct dusk::mods::ServiceTraits<LogService> {
static constexpr const char* id = LOG_SERVICE_ID;
static constexpr uint16_t major_version = LOG_SERVICE_MAJOR;
};
#endif
-57
View File
@@ -1,57 +0,0 @@
#pragma once
#include "mods/api.h"
#define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay"
#define OVERLAY_SERVICE_MAJOR 1u
#define OVERLAY_SERVICE_MINOR 0u
/* Handle for a runtime overlay registration. 0 is never a valid handle. */
typedef uint64_t OverlayHandle;
/*
* Runtime DVD file overlays.
*
* Registrations are owned by the calling mod and removed automatically when it is disabled,
* reloaded, or fails. Changes are applied at the next frame boundary; data the game has already
* read stays in memory until it re-reads the file (sometimes on scene reload, sometimes on
* restart).
*
* disc_path names the file to overlay: absolute with a leading '/', matched against the disc
* case-insensitively (e.g. "/res/Stage/R04_00.arc"). Paths that do not exist on the disc are added
* as new files.
*
* If multiple sources overlay the same path, the last one wins: a mod's runtime registrations beat
* its static overlay/ files, and later-loaded mods beat earlier ones.
*/
typedef struct OverlayService {
ServiceHeader header;
/*
* Overlay disc_path with a file from the calling mod's bundle (bundle-relative path, e.g.
* "res/replacement.arc"). The file's contents are read lazily on each open, so the bundle
* file must not change size while registered.
*/
ModResult (*add_file)(
ModContext* ctx, const char* disc_path, const char* bundle_path, OverlayHandle* out_handle);
/*
* Overlay disc_path with a caller-owned buffer. The data is copied; the caller may free it
* as soon as this returns.
*/
ModResult (*add_buffer)(ModContext* ctx, const char* disc_path, const void* data, size_t size,
OverlayHandle* out_handle);
/* Remove a runtime overlay previously added by the calling mod. */
ModResult (*remove)(ModContext* ctx, OverlayHandle handle);
} OverlayService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct dusk::mods::ServiceTraits<OverlayService> {
static constexpr const char* id = OVERLAY_SERVICE_ID;
static constexpr uint16_t major_version = OVERLAY_SERVICE_MAJOR;
};
#endif
-52
View File
@@ -1,52 +0,0 @@
#pragma once
#include "mods/api.h"
/*
* Read-only access to the res/ tree of the calling mod's own bundle. Reload serves the new
* bundle's contents. For writable storage, use HostService::mod_dir.
*/
#define RESOURCE_SERVICE_ID "dev.twilitrealm.dusklight.resource"
#define RESOURCE_SERVICE_MAJOR 1u
#define RESOURCE_SERVICE_MINOR 0u
/*
* A loaded resource, allocated by the service. Return every successful load with free;
* buffers still live when the mod is disabled or reloaded are reclaimed with a warning.
*/
typedef struct ResourceBuffer {
uint32_t struct_size;
void* data;
size_t size;
} ResourceBuffer;
#define RESOURCE_BUFFER_INIT {sizeof(ResourceBuffer), NULL, 0u}
typedef struct ResourceService {
ServiceHeader header;
/*
* Load a file into a fresh allocation. `relative_path` is resolved against the bundle's
* res/ directory. Absolute paths and ".." are rejected. MOD_UNAVAILABLE if the file does not
* exist. An empty file loads as data == NULL with size 0. Previous contents of `out_buffer` are
* overwritten, not freed.
*/
ModResult (*load)(ModContext* ctx, const char* relative_path, ResourceBuffer* out_buffer);
/*
* Release a loaded buffer and reset it to the empty state. Safe to call on an empty or
* already-freed buffer.
*/
void (*free)(ModContext* ctx, ResourceBuffer* buffer);
} ResourceService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct dusk::mods::ServiceTraits<ResourceService> {
static constexpr const char* id = RESOURCE_SERVICE_ID;
static constexpr uint16_t major_version = RESOURCE_SERVICE_MAJOR;
};
#endif
-88
View File
@@ -1,88 +0,0 @@
#pragma once
#include "mods/api.h"
#define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture"
#define TEXTURE_SERVICE_MAJOR 1u
#define TEXTURE_SERVICE_MINOR 0u
/* Handle for a runtime texture replacement registration. 0 is never a valid handle. */
typedef uint64_t TextureReplacementHandle;
typedef enum TextureKeyKind {
/* Match a texture by the address of its in-memory GX texel data. */
TEXTURE_KEY_POINTER = 0,
/* Match by content: XXH64 of the base mip level (and of the referenced TLUT range for
* palette formats), as encoded in replacement filenames / texture dumps. */
TEXTURE_KEY_SOURCE = 1,
} TextureKeyKind;
/* Wildcard values for TEXTURE_KEY_SOURCE hashes ("$" in the filename convention). */
#define TEXTURE_HASH_WILDCARD UINT64_C(0xFFFFFFFFFFFFFFFF)
#define TEXTURE_TLUT_WILDCARD UINT64_C(0xFFFFFFFFFFFFFFFE)
typedef struct TextureKey {
uint32_t struct_size;
TextureKeyKind kind;
const void* pointer; /* TEXTURE_KEY_POINTER only */
uint64_t texture_hash; /* TEXTURE_KEY_SOURCE */
uint64_t tlut_hash; /* TEXTURE_KEY_SOURCE, palette formats only */
uint32_t width;
uint32_t height;
uint32_t gx_format;
bool has_tlut;
} TextureKey;
#define TEXTURE_KEY_INIT {sizeof(TextureKey), TEXTURE_KEY_POINTER, NULL, 0u, 0u, 0u, 0u, 0u, false}
typedef struct TextureData {
uint32_t struct_size;
const void* data; /* texel data laid out in gx_format; copied by the service */
size_t size;
uint32_t width;
uint32_t height;
uint32_t mip_count;
uint32_t gx_format; /* any GX texture format supported by Aurora's converter */
} TextureData;
#define TEXTURE_DATA_INIT {sizeof(TextureData), NULL, 0u, 0u, 0u, 1u, 0u}
/*
* Runtime texture replacements.
*
* Registrations are owned by the calling mod and removed automatically when it is disabled,
* reloaded, or fails. When multiple sources replace the same texture, the highest priority wins:
* later-loaded mods beat earlier ones, and any mod beats the user's texture_replacements config
* directory. Files shipped in a mod's textures/ directory register automatically with the same
* ownership and priority; this service is for replacements decided at runtime.
*/
typedef struct TextureService {
ServiceHeader header;
/* Register a replacement from raw texel data. The data is copied; the caller may free it as
* soon as this returns. */
ModResult (*register_data)(ModContext* ctx, const TextureKey* key, const TextureData* data,
TextureReplacementHandle* out_handle);
/*
* Register a replacement from an encoded .dds/.png inside the calling mod's bundle. The
* filename encodes the key (same convention as the texture_replacements directory, e.g.
* "tex1_{w}x{h}_{hash}_{fmt}.dds"); "_mipN" sidecars next to it are picked up automatically.
* The file is decoded lazily on first use by the renderer.
*/
ModResult (*register_file)(ModContext* ctx, const char* bundle_path,
TextureReplacementHandle* out_handle);
/* Remove a replacement previously registered by the calling mod. */
ModResult (*unregister)(ModContext* ctx, TextureReplacementHandle handle);
} TextureService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct dusk::mods::ServiceTraits<TextureService> {
static constexpr const char* id = TEXTURE_SERVICE_ID;
static constexpr uint16_t major_version = TEXTURE_SERVICE_MAJOR;
};
#endif
-284
View File
@@ -1,284 +0,0 @@
#pragma once
#include "mods/api.h"
#include "mods/svc/config.h"
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
#define UI_SERVICE_MAJOR 1u
#define UI_SERVICE_MINOR 0u
/*
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, scoped
* RCSS stylesheets and menu bar tabs.
*
* All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI
* callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with
* MOD_INVALID_ARGUMENT. Element handles die with the content that owns them: a panel or tab rebuild
* destroys the previous build's elements, so re-acquire handles inside the build callback rather
* than caching them. Strings are UTF-8 and, in both directions, only valid for the duration of the
* call.
*/
/* 0 is never a valid handle. */
typedef uint64_t UiWindowHandle;
typedef uint64_t UiDialogHandle;
typedef uint64_t UiElementHandle;
typedef uint64_t UiStyleHandle;
typedef uint64_t UiMenuTabHandle;
typedef enum UiStyleScope {
UI_SCOPE_PRELAUNCH = 0, /* the pre-launch menu */
UI_SCOPE_WINDOW = 1, /* every tabbed/small window, host and mod alike */
UI_SCOPE_MENU_BAR = 2, /* the in-game menu bar */
UI_SCOPE_OVERLAY = 3, /* the passive overlay (toasts, FPS counter, timers) */
UI_SCOPE_TOUCH_CONTROLS = 4, /* touch controls and their editor */
UI_SCOPE_GRAPHICS_TUNER = 5, /* the graphics tuner overlay window */
} UiStyleScope;
typedef enum UiDialogVariant {
UI_DIALOG_NORMAL = 0,
UI_DIALOG_WARNING = 1, /* warning icon by default */
UI_DIALOG_DANGER = 2, /* red styling, error icon by default */
} UiDialogVariant;
typedef enum UiControlKind {
UI_CONTROL_BUTTON = 0, /* action button (on_pressed) */
UI_CONTROL_TOGGLE = 1, /* boolean on/off */
UI_CONTROL_NUMBER = 2, /* integer stepper with min/max/step */
UI_CONTROL_STRING = 3, /* text input */
UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */
} UiControlKind;
typedef enum UiControlBinding {
/* Values flow through the `get`/`set` callbacks. Getters are polled every frame while the
control is visible and must be cheap. */
UI_BINDING_CALLBACKS = 0,
/* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod)
* directly: persistence, change notifications and the modified indicator (value != default) are
* wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and
* SELECT = int, STRING = string. Float vars are not bindable; use callbacks. */
UI_BINDING_CONFIG_VAR = 1,
} UiControlBinding;
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
* reads string_value. string_value passed to a setter is only valid during the call; a getter
* should point it at storage owned by the mod (e.g. a static buffer) that stays valid until the
* next call into the mod the host copies it right after the getter returns. */
typedef struct UiControlValue {
uint32_t struct_size;
bool bool_value;
int64_t int_value;
const char* string_value;
} UiControlValue;
#define UI_CONTROL_VALUE_INIT {sizeof(UiControlValue), false, 0, NULL}
typedef void (*UiControlGetFn)(ModContext* ctx, void* user_data, UiControlValue* out_value);
typedef void (*UiControlSetFn)(ModContext* ctx, void* user_data, const UiControlValue* value);
/* Polled every frame while the control is visible. */
typedef bool (*UiPredicateFn)(ModContext* ctx, void* user_data);
typedef void (*UiPressedFn)(ModContext* ctx, void* user_data);
typedef struct UiControlDesc {
uint32_t struct_size;
UiControlKind kind;
/* Row label (plain text). Required. */
const char* label;
/* Optional RML shown as contextual help when the control is focused or hovered. Only rendered
* where a help pane exists (mod window tabs). */
const char* help_rml;
UiControlBinding binding; /* ignored for BUTTON */
ConfigVarHandle config_var; /* UI_BINDING_CONFIG_VAR */
UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
UiPressedFn on_pressed; /* BUTTON only. Required for BUTTON. */
UiPredicateFn is_disabled; /* optional */
/* Optional override for the modified indicator. CONFIG_VAR controls derive it from value !=
* default when this is NULL. */
UiPredicateFn is_modified;
/* Passed to every callback above. */
void* user_data;
/* NUMBER: inclusive clamp range and step. min == max means the defaults (0 .. INT32_MAX); step
* < 1 means 1. */
int64_t min;
int64_t max;
int64_t step;
const char* prefix; /* NUMBER: optional text before the value */
const char* suffix; /* NUMBER: optional text after the value */
/* SELECT: option labels (plain text). Required for SELECT. SELECT controls
* present their options in the help pane, so they are only available where
* one exists (mod window tabs); MOD_UNSUPPORTED elsewhere. */
const char* const* options;
size_t option_count;
int32_t max_length; /* STRING: maximum input length; < 1 means unlimited */
} UiControlDesc;
#define UI_CONTROL_DESC_INIT \
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0}
/* Build the panel contents. `panel` accepts the pane_add_* functions; it and
* every element created in it are destroyed (handles invalidated) whenever the
* panel is rebuilt, e.g. on tab switches. A non-MOD_OK result fails the mod. */
typedef ModResult (*UiPanelBuildFn)(
ModContext* ctx, UiElementHandle panel, void* user_data, ModError* out_error);
/* Called every frame while the panel is the visible tab. */
typedef ModResult (*UiPanelUpdateFn)(ModContext* ctx, void* user_data, ModError* out_error);
/* A panel rendered inside the calling mod's tab of the host Mods window. */
typedef struct UiModsPanelDesc {
uint32_t struct_size;
UiPanelBuildFn build; /* required */
UiPanelUpdateFn update;
void* user_data;
} UiModsPanelDesc;
#define UI_MODS_PANEL_DESC_INIT {sizeof(UiModsPanelDesc), NULL, NULL, NULL}
/* Build one tab of a mod window. `left_pane` is the interactive column,
* `right_pane` shows contextual help (controls' help_rml and SELECT options
* render there). Rebuilt on every tab activation. */
typedef ModResult (*UiTabBuildFn)(ModContext* ctx, UiWindowHandle window, UiElementHandle left_pane,
UiElementHandle right_pane, void* user_data, ModError* out_error);
typedef struct UiTabDesc {
uint32_t struct_size;
const char* title; /* required */
UiTabBuildFn build; /* required */
UiPanelUpdateFn update;
void* user_data;
} UiTabDesc;
#define UI_TAB_DESC_INIT {sizeof(UiTabDesc), NULL, NULL, NULL, NULL}
typedef void (*UiWindowClosedFn)(ModContext* ctx, UiWindowHandle window, void* user_data);
typedef struct UiWindowDesc {
uint32_t struct_size;
const UiTabDesc* tabs; /* at least one */
size_t tab_count;
/* Optional RCSS applied to this window's document only (on top of any
* UI_SCOPE_WINDOW sheets, which apply automatically). */
const char* rcss;
/* Fired when the window is destroyed by user close or window_close. Not
* fired during owning-mod teardown/shutdown. The handle is already invalid. */
UiWindowClosedFn on_closed;
void* user_data;
} UiWindowDesc;
#define UI_WINDOW_DESC_INIT {sizeof(UiWindowDesc), NULL, 0u, NULL, NULL, NULL}
typedef void (*UiDialogActionFn)(ModContext* ctx, UiDialogHandle dialog, void* user_data);
/* Note: array element without struct_size; a future change requires appending
* a v2 desc struct rather than growing this one. */
typedef struct UiDialogAction {
const char* label; /* required */
UiDialogActionFn on_pressed;
void* user_data;
bool keep_open; /* false = the dialog closes after on_pressed returns */
} UiDialogAction;
typedef struct UiDialogDesc {
uint32_t struct_size;
const char* title; /* plain text; required */
const char* body_rml; /* RML body; required */
UiDialogVariant variant;
/* Optional icon name overriding the variant default: "warning", "error",
* "question-mark", "verifying", "celebration". */
const char* icon;
const UiDialogAction* actions; /* at least one */
size_t action_count;
/* Fired on cancel (B/Escape) before the dialog closes; the dialog always
* closes on dismiss. */
UiDialogActionFn on_dismiss;
void* user_data; /* passed to on_dismiss */
} UiDialogDesc;
#define UI_DIALOG_DESC_INIT \
{sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL}
/* A tab added to the in-game menu bar. */
typedef struct UiMenuTabDesc {
uint32_t struct_size;
const char* label; /* plain text; required */
UiPressedFn on_selected; /* fired when the user activates the tab; required */
void* user_data;
} UiMenuTabDesc;
#define UI_MENU_TAB_DESC_INIT {sizeof(UiMenuTabDesc), NULL, NULL, NULL}
typedef struct UiService {
ServiceHeader header;
/* Register or replace the panel shown in the calling mod's Mods-window tab. */
ModResult (*register_mods_panel)(ModContext* ctx, const UiModsPanelDesc* desc);
/* Content builders. `pane` is a panel or tab pane handle; out_elem (where
* present, optional) receives a handle for later elem_set_* updates. */
ModResult (*pane_add_section)(ModContext* ctx, UiElementHandle pane, const char* title);
ModResult (*pane_add_text)(
ModContext* ctx, UiElementHandle pane, const char* text, UiElementHandle* out_elem);
ModResult (*pane_add_rml)(
ModContext* ctx, UiElementHandle pane, const char* rml, UiElementHandle* out_elem);
ModResult (*pane_add_progress)(
ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem);
ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc,
UiElementHandle* out_elem);
/* Element updates. The handle kind must match the setter (text/rml on text
* rows, progress on progress bars). */
ModResult (*elem_set_text)(ModContext* ctx, UiElementHandle elem, const char* text);
ModResult (*elem_set_rml)(ModContext* ctx, UiElementHandle elem, const char* rml);
ModResult (*elem_set_progress)(ModContext* ctx, UiElementHandle elem, float value);
/* Set or clear an RCSS class on any element handle (rows, progress bars,
* controls, panes), for styling via scoped or window RCSS. */
ModResult (*elem_set_class)(
ModContext* ctx, UiElementHandle elem, const char* name, bool active);
/* Push a tabbed two-pane window onto the document stack and show it. */
ModResult (*window_push)(ModContext* ctx, const UiWindowDesc* desc, UiWindowHandle* out_window);
ModResult (*window_close)(ModContext* ctx, UiWindowHandle window);
/* Push a modal dialog onto the document stack and show it. */
ModResult (*dialog_push)(ModContext* ctx, const UiDialogDesc* desc, UiDialogHandle* out_dialog);
ModResult (*dialog_close)(ModContext* ctx, UiDialogHandle dialog);
ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml);
/* Replace the dialog icon ("" removes it; names as in UiDialogDesc.icon). */
ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon);
/* Append one action button (same callback rules as at push). */
ModResult (*dialog_add_action)(
ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action);
/* Whether any focus-stack document is currently visible (visible documents block gamepad
* input). */
ModResult (*is_any_document_visible)(ModContext* ctx, bool* out_visible);
/* Register an RCSS sheet applied to every document of `scope`, now and in the future, until
* unregistered or the calling mod is torn down. Sheets apply in registration order after the
* host styles. Fails with MOD_INVALID_ARGUMENT if the RCSS does not parse. */
ModResult (*register_styles)(
ModContext* ctx, UiStyleScope scope, const char* rcss, UiStyleHandle* out_style);
/* Like register_styles, but reads the RCSS from the mod bundle's res/ directory (same path
* rules as ResourceService::load). MOD_UNAVAILABLE if the file does not exist. */
ModResult (*register_styles_file)(
ModContext* ctx, UiStyleScope scope, const char* path, UiStyleHandle* out_style);
ModResult (*unregister_styles)(ModContext* ctx, UiStyleHandle style);
/* Add a tab to the in-game menu bar, on_selected runs with the menu open; pushing a window from
* it stacks the window over the menu like the host tabs do. The tab is removed when
* unregistered or the mod is torn down. */
ModResult (*register_menu_tab)(
ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab);
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
} UiService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct dusk::mods::ServiceTraits<UiService> {
static constexpr const char* id = UI_SERVICE_ID;
static constexpr uint16_t major_version = UI_SERVICE_MAJOR;
};
#endif
@@ -12,23 +12,12 @@ struct JORNodeEvent;
class JORMContext;
class JORServer;
// NOTE (stable game ABI): these classes stay non-polymorphic outside DEBUG
// on purpose. Making them polymorphic under PARTIAL_DEBUG would give every one of the ~250
// derived HIO classes a vptr and turn their plain `void genMessage(JORMContext*);`
// declarations into implicit virtual overrides whose definitions are #if DEBUG-gated; every
// instantiated one then fails to link (missing vtable). Closure types shared with DEBUG TUs
// either declare their own unconditional virtuals (vptr in all TUs anyway) or add a
// PARTIAL_DEBUG-only virtual dtor for vptr parity (see dAttParam_c).
class JOREventListener {
public:
#if DEBUG
JOREventListener() {}
#if TARGET_PC
virtual void listenPropertyEvent(const JORPropertyEvent*) {}
#else
virtual void listenPropertyEvent(const JORPropertyEvent*) = 0;
#endif
#endif
};
class JORReflexible : public JOREventListener {
@@ -41,11 +30,7 @@ public:
virtual void listenPropertyEvent(const JORPropertyEvent*);
virtual void listen(u32, const JOREvent*);
virtual void genObjectInfo(const JORGenEvent*);
#if TARGET_PC
virtual void genMessage(JORMContext*) {}
#else
virtual void genMessage(JORMContext*) = 0;
#endif
virtual void listenNodeEvent(const JORNodeEvent*);
#endif
};
@@ -4,6 +4,7 @@
#include <types.h>
#include <cmath>
#include <utility>
#include "global.h"
#ifdef __cplusplus
extern "C" {
@@ -141,9 +142,9 @@ struct TAsinAcosTable {
}
};
extern TSinCosTable<13, f32> sincosTable_;
extern TAtanTable<1024, f32> atanTable_;
extern TAsinAcosTable<1024, f32> asinAcosTable_;
DUSK_GAME_EXTERN TSinCosTable<13, f32> sincosTable_;
DUSK_GAME_EXTERN TAtanTable<1024, f32> atanTable_;
DUSK_GAME_EXTERN TAsinAcosTable<1024, f32> asinAcosTable_;
inline f32 acosDegree(f32 x) {
return asinAcosTable_.acosDegree(x);
@@ -314,7 +314,7 @@ public:
>
{
TIterator_data_(const TFunctionValue_list_parameter& rParent, const f32* value) {
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
pOwn_ = &rParent;
#endif
pf_ = value;
@@ -372,7 +372,7 @@ public:
return (r1.pf_ - r2.pf_) / suData_size;
}
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x00 */ const TFunctionValue_list_parameter* pOwn_;
#endif
/* 0x00 */ const f32* pf_;
@@ -425,7 +425,7 @@ public:
>
{
TIterator_data_(const TFunctionValue_hermite& rParent, const f32* value) {
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
pOwn_ = &rParent;
#endif
pf_ = value;
@@ -491,7 +491,7 @@ public:
return (r1.pf_ - r2.pf_) / r1.uSize_;
}
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
/* 0x00 */ const TFunctionValue_hermite* pOwn_;
/* 0x04 */ const f32* pf_;
/* 0x08 */ u32 uSize_;
+3 -3
View File
@@ -16,10 +16,10 @@ inline f64 getConst2() {
return 9.765625E-4;
}
TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
} // namespace JMath
@@ -22,7 +22,7 @@
It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience.
</p>
</description>
<provides>
<binary>dusklight</binary>
<id>dev.twilitrealm.dusk.desktop</id>
-97
View File
@@ -1,97 +0,0 @@
window.logs content {
flex-flow: column;
}
window.logs .log-toolbar {
display: flex;
flex-flow: row;
flex: 0 0 64dp;
height: 64dp;
align-items: center;
gap: 8dp;
padding-right: 72dp;
background-color: rgba(217, 217, 217, 10%);
border-bottom: 2dp #92875B;
font-family: "Fira Sans Condensed";
font-weight: bold;
font-size: 18dp;
}
window.logs > close {
top: 8dp;
right: 8dp;
}
window.logs .log-title {
align-self: stretch;
flex: 0 0 auto;
padding: 0 24dp;
line-height: 64dp;
text-transform: uppercase;
border-bottom: 4dp #C2A42D;
font-effect: glow(0dp 4dp 0dp 4dp black);
}
window.logs .log-title-mod {
flex: 0 1 auto;
min-width: 0;
white-space: nowrap;
overflow: hidden;
font-family: "Fira Sans";
font-weight: normal;
font-size: 15dp;
color: rgba(224, 219, 200, 55%);
}
.log-toolbar-spacer {
flex: 1 1 0;
}
.log-toolbar button {
flex: 0 0 auto;
font-family: "Fira Sans";
font-weight: normal;
font-size: 15dp;
padding: 5dp 12dp;
}
window.logs content pane.log-view {
flex: 1 1 0;
padding: 12dp 16dp;
padding-bottom: 0dp;
gap: 0dp;
}
.log-lines {
display: block;
}
.log-line {
display: block;
font-family: "Noto Mono";
font-size: 13dp;
line-height: 1.5;
word-break: break-word;
white-space: pre-wrap;
}
.log-line .log-time {
color: rgba(224, 219, 200, 45%);
}
.log-line .log-mod {
color: rgba(194, 164, 45, 80%);
}
.log-line.lvl-trace,
.log-line.lvl-debug {
opacity: 0.55;
}
.log-line.lvl-warn .log-msg {
color: #ffa826;
}
.log-line.lvl-error .log-msg {
color: #cc4444;
}
-218
View File
@@ -1,218 +0,0 @@
window.mods content pane.mod-list {
flex: 0 0 360dp;
padding: 16dp;
padding-bottom: 0dp;
gap: 4dp;
}
@media (max-height: 640dp) {
window.mods content pane.mod-list {
flex: 1 1 0;
}
}
window.mods content pane.mod-detail {
gap: 12dp;
}
.mod-info-row {
display: flex;
align-items: center;
gap: 12dp;
padding: 4dp 0;
}
.mod-info-label {
font-family: "Fira Sans Condensed";
font-weight: bold;
opacity: 0.55;
flex: 0 0 auto;
}
.mod-info-value {
flex: 1 1 0;
}
.mod-path {
font-size: 14dp;
word-break: break-all;
opacity: 0.7;
}
mod-entry {
display: flex;
flex-flow: row;
gap: 12dp;
padding: 10dp;
border-radius: 10dp;
decorator: vertical-gradient(#c2a42d00 #c2a42d00);
transition: decorator 0.1s linear-in-out;
cursor: pointer;
focus: auto;
}
mod-entry.current {
box-shadow: rgba(146, 135, 91, 40%) 0 0 0 1dp;
}
mod-entry:hover,
mod-entry:focus-visible {
decorator: vertical-gradient(#c2a42d00 #c2a42d26);
}
mod-entry:selected {
decorator: vertical-gradient(#c2a42d10 #c2a42d40);
}
mod-entry .mod-icon {
flex: 0 0 auto;
width: 56dp;
height: 56dp;
border-radius: 8dp;
}
mod-entry icon.mod-icon {
font-size: 36dp;
background-color: rgba(17, 16, 10, 20%);
color: rgba(224, 219, 200, 45%);
decorator: text("&#xe87b;" center center);
}
mod-entry .mod-entry-info {
display: flex;
flex-flow: column;
flex: 1 1 0;
min-width: 0;
gap: 2dp;
}
mod-entry .mod-entry-name {
display: flex;
flex-flow: row;
align-items: baseline;
gap: 6dp;
}
mod-entry .mod-entry-name-text {
flex: 0 1 auto;
min-width: 0;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
}
mod-entry .mod-entry-version {
flex: 0 0 auto;
font-size: 13dp;
color: rgba(224, 219, 200, 50%);
}
mod-entry .mod-entry-status.active {
color: #44cc55;
}
mod-entry .mod-entry-status.failed {
color: #cc4444;
}
mod-entry .mod-entry-desc {
font-size: 14dp;
line-height: 1.3;
color: rgba(224, 219, 200, 65%);
max-height: 2.6em;
overflow: hidden;
}
mod-entry .mod-entry-sub {
font-size: 13dp;
color: rgba(224, 219, 200, 50%);
}
mod-entry.inactive .mod-icon {
filter: grayscale(1);
}
mod-entry.inactive .mod-entry-info {
opacity: 0.5;
}
mod-header {
position: relative;
flex: 0 0 auto;
}
mod-header.has-banner {
height: 180dp;
margin: -24dp -24dp 0dp -24dp;
}
mod-header .mod-actions {
position: absolute;
top: 24dp;
left: 24dp;
display: flex;
flex-flow: row;
gap: 8dp;
}
mod-header .mod-actions button {
font-size: 16dp;
padding: 6dp 14dp;
background-color: rgba(21, 22, 16, 80%);
box-shadow: rgba(146, 135, 91, 60%) 0 0 0 1dp;
}
mod-header.no-banner {
display: flex;
flex-flow: row;
align-items: center;
}
mod-header.no-banner .mod-actions {
position: static;
}
window.mods .mod-title {
display: block;
font-size: 28dp;
font-weight: bold;
}
window.mods .mod-title .mod-title-version {
font-weight: normal;
font-size: 16dp;
color: rgba(224, 219, 200, 55%);
}
window.mods .mod-author {
display: block;
font-size: 15dp;
color: rgba(224, 219, 200, 55%);
}
window.mods .mod-restart-note {
font-size: 15dp;
color: #ffa826;
opacity: 0.85;
}
window.mods .mod-description {
line-height: 1.5;
}
.status-badge {
font-size: 14dp;
opacity: 0.7;
}
.status-badge.active,
.mod-info-label.active {
color: #44cc55;
opacity: 1;
}
.status-badge.failed,
.mod-info-label.failed {
color: #cc4444;
opacity: 1;
}
-8
View File
@@ -159,14 +159,6 @@ toast.achievement heading {
color: #C2A42D;
}
toast.warning {
border: 1dp #C2A42D;
}
toast.warning heading {
color: #C2A42D;
}
toast.controller-warning {
top: auto;
right: auto;
+3 -12
View File
@@ -50,8 +50,7 @@ tab-bar[closable] tab-end-spacer {
pointer-events: none;
}
tab-bar[closable] close,
window > close {
tab-bar[closable] close {
display: block;
position: fixed;
top: 8dp;
@@ -71,20 +70,12 @@ window > close {
}
tab-bar[closable] close:hover,
tab-bar[closable] close:focus-visible,
window > close:hover,
window > close:focus-visible {
tab-bar[closable] close:focus-visible {
color: #fff;
background-color: rgba(194, 164, 45, 24%);
}
window > close {
top: 16dp;
right: 16dp;
}
tab-bar[closable] close:active,
window > close:active {
tab-bar[closable] close:active {
color: #fff;
background-color: rgba(194, 164, 45, 40%);
}
+29
View File
@@ -395,6 +395,11 @@ progress.progress-ongoing fill {
border-radius: 3dp;
}
progress.progress-health fill {
background-color: #cc3322;
border-radius: 3dp;
}
button.achievement-clear {
flex: 0 0 auto;
align-self: center;
@@ -509,6 +514,30 @@ progress.verification-progress-bar {
color: rgba(224, 219, 200, 65%);
}
.mod-info-row {
display: flex;
align-items: center;
gap: 12dp;
padding: 4dp 0;
}
.mod-info-label {
font-family: "Fira Sans Condensed";
font-weight: bold;
opacity: 0.55;
flex: 0 0 80dp;
}
.mod-info-value {
flex: 1 1 0;
}
.mod-path {
font-size: 14dp;
word-break: break-all;
opacity: 0.7;
}
.modal-actions {
display: flex;
flex-direction: row;
-43
View File
@@ -1,43 +0,0 @@
# Dusklight Mod SDK entry point
#
# Provides game/service headers, compile definitions and version.h without
# configuring the full game tree.
#
# 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)
if (TARGET dusklight_game_headers)
message(FATAL_ERROR "Mod SDK already configured")
endif ()
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/WindowsTargetProcessor.cmake")
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
endif ()
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# Version detection & version.h
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/DetectVersion.cmake")
detect_version()
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")
-139
View File
@@ -1,139 +0,0 @@
// Mod SDK runtime (Windows): applies lld's MinGW-style runtime pseudo-relocations on
// the plain MSVC CRT. This is what lets mods reference game data (`extern` globals) with no
// __declspec(dllimport) annotations: `lld-link -lldmingw` auto-imports the data through IAT
// slots and records fixups, and this module applies them at load time.
//
// The fixup pass runs as an early CRT initializer, and the IAT slots it reads were already
// bound by the OS loader, so even mod static initializers observe patched references.
#include <windows.h>
#include <cstdint>
#include <cstdio>
#include <cstring>
extern "C" char __RUNTIME_PSEUDO_RELOC_LIST__;
extern "C" char __RUNTIME_PSEUDO_RELOC_LIST_END__;
extern "C" IMAGE_DOS_HEADER __ImageBase;
namespace {
struct HdrV2 {
uint32_t magic1;
uint32_t magic2;
uint32_t version;
};
struct ItemV2 {
uint32_t sym; // RVA of the __imp_ slot (OS-bound IAT entry)
uint32_t target; // RVA of the reference to patch
uint32_t flags; // low 8 bits: bit width of the reference
};
bool g_relocsFailed = false;
void report(const char* fmt, ...) {
char buf[512];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
fprintf(stderr, "%s\n", buf);
OutputDebugStringA(buf);
}
bool compute_fixup(const ItemV2& item, char* base, intptr_t* out) {
char* impSlot = base + item.sym;
const intptr_t real = *reinterpret_cast<intptr_t*>(impSlot);
char* target = base + item.target;
const int bits = static_cast<int>(item.flags & 0xff);
intptr_t reldata;
switch (bits) {
case 8:
reldata = *reinterpret_cast<int8_t*>(target);
break;
case 16:
reldata = *reinterpret_cast<int16_t*>(target);
break;
case 32:
reldata = *reinterpret_cast<int32_t*>(target);
break;
case 64:
reldata = *reinterpret_cast<int64_t*>(target);
break;
default:
report("unsupported %d-bit pseudo-relocation at RVA 0x%x", bits, item.target);
return false;
}
reldata -= reinterpret_cast<intptr_t>(impSlot);
reldata += real;
if (bits < 64) {
const intptr_t maxUnsigned = (intptr_t{1} << bits) - 1;
const intptr_t minSigned = -(intptr_t{1} << (bits - 1));
if (reldata > maxUnsigned || reldata < minSigned) {
report("%d-bit data fixup at RVA 0x%x is out of range (delta %+lld)", bits, item.target,
static_cast<long long>(reldata));
return false;
}
}
*out = reldata;
return true;
}
} // namespace
// lld refuses to emit runtime pseudo-relocs unless a function with exactly this (mingw CRT)
// name exists in the image. It is also our actual entry point.
extern "C" void _pei386_runtime_relocator() {
char* base = reinterpret_cast<char*>(&__ImageBase);
char* start = &__RUNTIME_PSEUDO_RELOC_LIST__;
char* end = &__RUNTIME_PSEUDO_RELOC_LIST_END__;
if (end - start < static_cast<ptrdiff_t>(sizeof(HdrV2))) {
return; // no data auto-imports in this mod
}
const HdrV2* hdr = reinterpret_cast<const HdrV2*>(start);
if (hdr->magic1 != 0 || hdr->magic2 != 0 || hdr->version != 1) {
report("unexpected pseudo-relocation list format");
g_relocsFailed = true;
return;
}
const ItemV2* items = reinterpret_cast<const ItemV2*>(hdr + 1);
const ItemV2* itemsEnd = reinterpret_cast<const ItemV2*>(end);
// Validate everything before writing anything, so a bad fixup can't leave the image
// half-patched.
intptr_t scratch;
for (const ItemV2* it = items; it < itemsEnd; ++it) {
if (!compute_fixup(*it, base, &scratch)) {
g_relocsFailed = true;
return;
}
}
for (const ItemV2* it = items; it < itemsEnd; ++it) {
intptr_t reldata;
compute_fixup(*it, base, &reldata);
char* target = base + it->target;
const size_t len = static_cast<size_t>(it->flags & 0xff) / 8;
DWORD old = 0;
if (!VirtualProtect(target, len, PAGE_EXECUTE_READWRITE, &old)) {
report("VirtualProtect failed at RVA 0x%x", it->target);
g_relocsFailed = true;
return;
}
std::memcpy(target, &reldata, len);
VirtualProtect(target, len, old, &old);
}
}
using PVFV = void (*)();
#pragma section(".CRT$XCB", read)
extern "C" __declspec(allocate(".CRT$XCB")) PVFV dusk_mod_pseudo_reloc_init =
_pei386_runtime_relocator;
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH && g_relocsFailed) {
return FALSE;
}
return TRUE;
}
+6
View File
@@ -142,6 +142,12 @@ DynamicModuleControl::DynamicModuleControl(char const* name) {
}
#endif
#if TARGET_PC
// dump() is declared but its definition is inside #if !TARGET_PC above; stub it out.
void DynamicModuleControlBase::dump() {}
void DynamicModuleControlBase::dump(char*) {}
#endif
u32 DynamicModuleControl::sAllocBytes;
JKRArchive* DynamicModuleControl::sArchive;
+1 -1
View File
@@ -19,7 +19,7 @@
#include "Z2AudioCS/Z2AudioCS.h"
#endif
Z2AudioMgr* Z2AudioMgr::mAudioMgrPtr;
DUSK_GAME_DATA Z2AudioMgr* Z2AudioMgr::mAudioMgrPtr;
u8 gMuffleOutOfRangeMic = false;
Z2AudioMgr::Z2AudioMgr() : mSoundStarter(true) {
+1 -1
View File
@@ -22,7 +22,7 @@
#include "os_report.h"
Z2SoundObjBase::Z2SoundObjBase()
#if PARTIAL_DEBUG || DEBUG
#if DEBUG
: JSULink<Z2SoundObjBase>(this)
#endif
{
-6
View File
@@ -165,12 +165,6 @@ BOOL daAlink_c::checkCutTypeNoBlur() const {
}
bool daAlink_c::checkCutTurnInput() const {
#if TARGET_PC
if (mDoCPd_c::getHoldR(PAD_1) && dusk::getSettings().game.easyQuickSpin) {
return true;
}
#endif
return 0xF800 < abs(field_0x3180);
}
+12 -5
View File
@@ -122,11 +122,18 @@ BOOL daAlink_c::setBodyAngleToCamera() {
var_f31 /= dComIfGp_getCameraZoomScale(field_0x317c);
}
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
#if TARGET_PC
if (dusk::getSettings().game.enableMouseAim && checkAimInputContext()) {
sp8 = mBodyAngle.x;
} else
#endif
{
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
sp8 = mBodyAngle.x;
}
}
} else {
sp8 = mBodyAngle.x;
@@ -149,7 +156,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
f32 final_yaw = 0.f;
f32 final_pitch = 0.f;
if (dusk::getSettings().game.enableMouseAim) {
dusk::mouse::get_aim_deltas(final_yaw, final_pitch);
dusk::mouse::getAimDeltas(final_yaw, final_pitch);
}
if (dusk::getSettings().game.enableGyroAim) {
f32 gyro_yaw = 0.f;
+3 -22
View File
@@ -11,7 +11,6 @@
#include "d/d_com_inf_game.h"
#if TARGET_PC
#include <aurora/texture.hpp>
#include "dusk/dvd_asset.hpp"
#include "dusk/frame_interpolation.h"
@@ -41,8 +40,6 @@ static f32* l_texCoord_get() { alignas(32) static f32 buf[338]; static bool _
//#define l_pos (l_pos_get())
#define l_normal (l_normal_get())
#define l_texCoord (l_texCoord_get())
static bool l_Egnd_mantTEX_hasReplacement = false;
#else
#include "assets/l_Egnd_mantTEX.h"
@@ -226,7 +223,6 @@ void daMant_packet_c::draw() {
GXInitTexObjCI(
&undersideTexObj, l_Egnd_mantTEX_U, 0x80, 0x80, GX_TF_C8, GX_CLAMP, GX_CLAMP, 0, 0);
GXInitTexObjLOD(&undersideTexObj, GX_LINEAR, GX_LINEAR, 0.0, 0.0, 0.0, 0, 0, GX_ANISO_1);
l_Egnd_mantTEX_hasReplacement = aurora::texture::has_replacement(&mainTexObj, &tlutObj);
textureObjsInitialized = true;
}
#else
@@ -640,11 +636,7 @@ static int daMant_Execute(mant_class* i_this) {
iVar8 = 0;
if (i_this->field_0x3967 != 0) {
#if TARGET_PC
mant_cut_type = l_Egnd_mantTEX_hasReplacement ? 1 : i_this->field_0x3967;
#else
mant_cut_type = i_this->field_0x3967;
#endif
if (i_this->field_0x3968 < 15) {
i_this->field_0x3968++;
@@ -656,18 +648,9 @@ static int daMant_Execute(mant_class* i_this) {
iVar8 = 20;
}
#if TARGET_PC
if (l_Egnd_mantTEX_hasReplacement) {
unaff_r29 = i_this->mMantRng.getF(65536.0f);
var_f31 = i_this->mMantRng.getFX(32.0f);
var_f30 = i_this->mMantRng.getFX(32.0f);
} else
#endif
{
unaff_r29 = cM_rndF(65536.0f);
var_f31 = cM_rndFX(32.0f);
var_f30 = cM_rndFX(32.0f);
}
unaff_r29 = cM_rndF(65536.0f);
var_f31 = cM_rndFX(32.0f);
var_f30 = cM_rndFX(32.0f);
}
i_this->field_0x3967 = 0;
@@ -777,8 +760,6 @@ static int daMant_Create(fopAc_ac_c* i_this) {
if(textureObjsInitialized) {
GXInitTlutObjData(&tlutObj, l_Egnd_mantPAL); // make sure the cached textures are updated
}
m_this->mMantRng.init(66, 16983, 855);
#endif
lbl_277_bss_0 = 0;
+1 -5
View File
@@ -7602,10 +7602,6 @@ bool dCamera_c::executeDebugFlyCam() {
sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f};
}
if (dusk::getSettings().game.enableMirrorMode) {
stickX *= -1.0f;
}
f32 verticalDisp = 0.0f;
if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
verticalDisp += trigR;
@@ -7716,7 +7712,7 @@ bool dCamera_c::freeCamera() {
f32 yaw_rad = 0.0f;
f32 pitch_rad = 0.0f;
dusk::mouse::get_camera_deltas(yaw_rad, pitch_rad);
dusk::mouse::getCameraDeltas(yaw_rad, pitch_rad);
if (dusk::getSettings().game.enableMouseCamera && (yaw_rad != 0.0f || pitch_rad != 0.0f) &&
!dComIfGp_checkCameraAttentionStatus(dComIfGp_getPlayerCameraID(0), 0x8))
{
+1 -6
View File
@@ -1640,7 +1640,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX;
mCenterZ -= mPackZ;
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -field_0x64 : ) field_0x64;
mCenterX += field_0x64;
mCenterZ += mPackPlusZ;
}
@@ -1649,8 +1649,6 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
{
#if DEBUG
field_0x64 = 33830.0f;
#elif TARGET_PC
field_0x64 = dusk::getSettings().game.enableMirrorMode ? 33830.0f : 0.0f;
#else
field_0x64 = 0.0f;
#endif
@@ -1659,9 +1657,6 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
f32 temp = (field_0x58 * (f32)(field_0x74 + 4)) * 0.5f;
#if DEBUG
mRightEdgePlus = -(((dMpath_c::getMinZ() - (-127103.67f)) - temp) / field_0x58);
#elif TARGET_PC
mRightEdgePlus = dusk::getSettings().game.enableMirrorMode ? -(((dMpath_c::getMinX() - (-127103.67f)) - temp) / field_0x58) : 0.0f;
#else
mRightEdgePlus = 0.0f;
#endif
+1 -24
View File
@@ -914,20 +914,6 @@ void dMenu_DmapBg_c::dMapBgWide() {
void dMenu_DmapBg_c::draw() {
#if TARGET_PC
dMapBgWide();
static bool prevMirror = false; // default state of panes is not mirrored
if(prevMirror != dusk::getSettings().game.enableMirrorMode) {
if(dusk::getSettings().game.enableMirrorMode) {
static_cast<J2DPicture*>(mFloorScreen->search(MULTI_CHAR('rink')))->setMirror(J2DMirror_X);
static_cast<J2DPicture*>(mBaseScreen->search(MULTI_CHAR('map000')))->setMirror(J2DMirror_X);
}
else {
static_cast<J2DPicture*>(mFloorScreen->search(MULTI_CHAR('rink')))->setMirror(MIRROR0);
static_cast<J2DPicture*>(mBaseScreen->search(MULTI_CHAR('map000')))->setMirror(MIRROR0);
}
prevMirror = dusk::getSettings().game.enableMirrorMode;
}
#endif
u32 scissor_left;
@@ -974,15 +960,6 @@ void dMenu_DmapBg_c::draw() {
mpBackTexture->setAlpha(dVar17 * (field_0xdbc * field_0xd9c));
f32 local_28c = mpBackTexture->getBounds().i.x;
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
CPaneMgr mgr;
Vec local_94 = mgr.getGlobalVtxCenter(mMapPane, true, 0);
local_28c = (local_94.x * 2.0f) - (local_28c + 0.5f * mpBackTexture->getWidth()) - 0.5f * mpBackTexture->getWidth();
}
#endif
mpBackTexture->setBlackWhite(color_black, color_white);
mpBackTexture->draw(local_28c, field_0xd94 + mpBackTexture->getBounds().i.y, mpBackTexture->getWidth(),
mpBackTexture->getHeight(),
@@ -2004,7 +1981,7 @@ void dMenu_Dmap_c::mapControl() {
f32 temp_f28 = (var_f29 / 100.0f) * var_f31;
f32 sp18 = temp_f28 * cM_ssin(stick_angle);
f32 sp14 = temp_f28 * cM_scos(stick_angle);
mMapCtrl->setPlusZoomCenterX(IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -sp18 :) sp18);
mMapCtrl->setPlusZoomCenterX(sp18);
mMapCtrl->setPlusZoomCenterZ(sp14);
}
+20 -1
View File
@@ -359,7 +359,14 @@ f32 dMenu_StageMapCtrl_c::getPixelStageSizeZ() const {
f32 dMenu_StageMapCtrl_c::getPixelCenterX() const {
f32 var_f31 = dMpath_c::getCenterX();
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
return (1.0f / field_0xbc) * (field_0x9c + var_f31);
}
else return (1.0f / field_0xbc) * (field_0x9c - var_f31);
#else
return (1.0f / field_0xbc) * (field_0x9c - var_f31);
#endif
}
f32 dMenu_StageMapCtrl_c::getPixelCenterZ() const {
@@ -418,7 +425,18 @@ inline static f32 rightModeCnvPos(f32 param_0) {
void dMenu_StageMapCtrl_c::cnvPosTo2Dpos(f32 param_0, f32 param_1, f32* param_2,
f32* param_3) const {
if (param_2 != NULL) {
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
*param_2 =
(0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (field_0x9c + param_0));
} else {
*param_2 =
(0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (param_0 - field_0x9c));
}
#else
*param_2 = (0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (param_0 - field_0x9c));
#endif
}
if (param_3 != NULL) {
@@ -915,7 +933,8 @@ void dMenu_StageMapCtrl_c::move() {
void dMenu_DmapMapCtrl_c::draw() {
if (field_0xef != 0) {
setPos(field_0xeb, field_0xec, field_0x9c,
setPos(field_0xeb, field_0xec,
IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -field_0x9c :) field_0x9c,
field_0xa0, field_0xbc, true, field_0xd8);
}
}
+15 -10
View File
@@ -931,8 +931,17 @@ void dMenu_Fmap_c::region_map_proc() {
mpDraw2DBack->regionMapMove(mpStick);
int stage_no, room_no;
#if TARGET_PC
f32 arrow_pos_x = mpDraw2DBack->getArrowPos2DX();
if (dusk::getSettings().game.enableMirrorMode) {
arrow_pos_x = mpDraw2DBack->getMirrorPosX(arrow_pos_x, 0.0f);
}
f32 pos_x = arrow_pos_x - mDoGph_gInf_c::getMinXF() - mDoGph_gInf_c::getWidthF() * 0.5f;
#else
f32 pos_x = mpDraw2DBack->getArrowPos2DX() - mDoGph_gInf_c::getMinXF()
- mDoGph_gInf_c::getWidthF() * 0.5f;
#endif
f32 pos_y = mpDraw2DBack->getArrowPos2DY() - mDoGph_gInf_c::getHeightF() * 0.5f;
mpMenuFmapMap->getPointStagePathInnerNo(getNowFmapRegionData(), pos_x, pos_y,
@@ -2477,6 +2486,12 @@ void dMenu_Fmap_c::portalWarpMapMove(STControl* i_stick) {
f32 arrow_y = mpDraw2DBack->getArrowPos2DY();
u8 uVar6 = 0xff;
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
arrow_x = mpDraw2DBack->getMirrorPosX(arrow_x, 0.0f);
}
#endif
for (int i = 0; i < portal_dat->mCount; i++) {
if (portals[i].mRegionNo == mpDraw2DBack->getRegionCursor() + 1
@@ -2546,11 +2561,6 @@ void dMenu_Fmap_c::drawIcon(f32 param_0, bool param_1) {
if (mProcess == PROC_PORTAL_DEMO1) {
is_portal_demo1 = 1;
}
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
angle = 0x10000 - angle;
}
#endif
mpDraw2DBack->setIcon2DPos(0x11, stage_name, pos.x, pos.z, cM_sht2d(angle),
is_portal_demo1, param_1);
@@ -2639,11 +2649,6 @@ void dMenu_Fmap_c::drawPlayEnterIcon() {
angle = dComIfGs_getPlayerFieldLastStayAngleY();
SAFE_STRCPY(stage_name, dComIfGs_getPlayerFieldLastStayName());
}
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
angle = 0x10000 - angle;
}
#endif
mpDraw2DBack->setIcon2DPos(0x15, stage_name, pos.x, pos.z, cM_sht2d(angle), 0, false);
}
}
+19 -73
View File
@@ -426,15 +426,7 @@ void dMenu_Fmap2DBack_c::draw() {
}
mpPointParent->setAlphaRate(mArrowAlpha * mSpotTextureFadeAlpha);
f32 drawX = mArrowPos2DX + mTransX;
#ifdef TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
drawX = getMirrorPosX(drawX, 0.0f);
}
#endif
mpPointParent->translate(drawX, mArrowPos2DY + mTransZ);
mpPointParent->translate(mArrowPos2DX + mTransX, mArrowPos2DY + mTransZ);
mpPointScreen->draw(0.0f, 0.0f, grafPort);
}
@@ -753,7 +745,7 @@ void dMenu_Fmap2DBack_c::zoomMapCalc(f32 i_zoom) {
f32 tmp2 = (dVar12 + (i_zoom * (centerX - dVar12)));
f32 tmp2_ = (dVar11 + (i_zoom * (centerY - dVar11)));
field_0xf0c[mRegionCursor] =
((tmp2 + (tmp3 * mZoom)) - mRegionMapSizeX[mRegionCursor] * mZoom * 0.5f) -
mRegionMinMapX[mRegionCursor];
@@ -1026,11 +1018,6 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
f32 delta_y = speed * cM_ssin(angle);
f32 delta_x = speed * cM_scos(angle);
#ifdef TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
delta_y = -delta_y;
}
#endif
control_xpos = control_xpos + delta_y;
control_ypos = control_ypos + delta_x;
}
@@ -1059,6 +1046,11 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
calcAllMapPos2D((mArrowPos3DX + control_xpos) - mStageTransX,
(mArrowPos3DZ + control_ypos) - mStageTransZ, &sp14, &sp10);
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
sp14 = getMirrorPosX(sp14, 0.0f);
}
#endif
mSelectRegion = 0xff;
for (int i = 7; i >= 0; i--) {
@@ -1473,12 +1465,6 @@ void dMenu_Fmap2DBack_c::worldGridDraw() {
f32 dVar8 = -mStageTransZ;
calcAllMapPos2D(dVar9, dVar8, &local_74, &local_78);
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
local_74 = getMirrorPosX(local_74, 0.0f);
}
#endif
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
JUtility::TColor(255, 255, 255, 255), 6);
@@ -1487,11 +1473,6 @@ void dMenu_Fmap2DBack_c::worldGridDraw() {
while (true) {
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
if (local_74 >= getMapScissorAreaLX()) {
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
local_74 = getMirrorPosX(local_74, 0.0f);
}
#endif
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
JUtility::TColor(255, 255, 255, 255), 6);
@@ -1553,12 +1534,6 @@ void dMenu_Fmap2DBack_c::regionGridDraw() {
f32 dVar8 = mRegionOriginZ[mRegionCursor] - mStageTransZ;
calcAllMapPos2D(dVar9, dVar8, &local_74, &local_78);
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
local_74 = getMirrorPosX(local_74, 0.0f);
}
#endif
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
JUtility::TColor(180, 0, 0, 255), 6);
@@ -1567,12 +1542,6 @@ void dMenu_Fmap2DBack_c::regionGridDraw() {
while (true) {
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
if (local_74 >= getMapScissorAreaLX()) {
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
local_74 = getMirrorPosX(local_74, 0.0f);
}
#endif
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
JUtility::TColor(180, 0, 0, 255), 6);
@@ -1635,12 +1604,6 @@ void dMenu_Fmap2DBack_c::worldOriginDraw() {
f32 local_44, local_48;
calcAllMapPos2D(-mStageTransX, -mStageTransZ, &local_44, &local_48);
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
local_44 = getMirrorPosX(local_44, 0.0f);
}
#endif
J2DDrawLine(mDoGph_gInf_c::getMinXF(), local_48 - local_44 + mDoGph_gInf_c::getMinXF(),
mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF(),
local_48 - local_44 + (mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF()),
@@ -1675,13 +1638,6 @@ void dMenu_Fmap2DBack_c::scrollAreaDraw() {
calcAllMapPos2D(x_min - mStageTransX, z_min - mStageTransZ, &local_4c, &local_50);
calcAllMapPos2D(x_max - mStageTransX, z_max - mStageTransZ, &local_54, &local_58);
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
local_4c = getMirrorPosX(local_4c, 0.0f);
local_54 = getMirrorPosX(local_54, 0.0f);
}
#endif
J2DDrawLine(local_4c, local_50, local_4c, local_58,
JUtility::TColor(255, 255, 255, 255), 6);
J2DDrawLine(local_54, local_50, local_54, local_58,
@@ -1702,11 +1658,6 @@ void dMenu_Fmap2DBack_c::regionOriginDraw() {
f32 center_x, center_y;
calcAllMapPos2D(mRegionOriginX[i] - mStageTransX, mRegionOriginZ[i] - mStageTransZ,
&center_x, &center_y);
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
center_x = getMirrorPosX(center_x - 3.0f, 3.0f);
}
#endif
J2DFillBox(center_x - 3.0f, center_y - 3.0f, 6.0f, 6.0f, JUtility::TColor(255, 0, 0, 255));
}
}
@@ -1724,11 +1675,6 @@ void dMenu_Fmap2DBack_c::stageOriginDraw() {
f32 v1 = mRegionOriginX[mRegionCursor] + stage_data[i].mOffsetX - mStageTransX;
f32 v2 = mRegionOriginZ[mRegionCursor] + stage_data[i].mOffsetZ - mStageTransZ;
calcAllMapPos2D(v1, v2, &center_x, &center_y);
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
center_x = getMirrorPosX(center_x - 3.0f, 3.0f);
}
#endif
J2DFillBox(center_x - 3.0f, center_y - 3.0f, 6.0f, 6.0f,
JUtility::TColor(0, 0, 255, 255));
}
@@ -1975,7 +1921,7 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) {
f32 speed = base_speed / 100.0f * local_78;
f32 speed_y = speed * cM_ssin(angle);
f32 speed_x = speed * cM_scos(angle);
control_xpos += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_y :) speed_y;
control_xpos += speed_y;
control_ypos += speed_x;
}
}
@@ -2000,6 +1946,11 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) {
calcAllMapPos2D(mArrowPos3DX + control_xpos - mStageTransX,
mArrowPos3DZ + control_ypos - mStageTransZ, &pos_x, &pos_y);
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
pos_x = getMirrorPosX(pos_x, 0.0f);
}
#endif
mSelectRegion = 0xff;
int region = mRegionCursor;
@@ -2031,6 +1982,11 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param
if (stick_value >= slow_bound && param_2 && field_0x1238 != 2) {
bVar6 = true;
s16 angle = i_stick->getAngleStick();
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
angle = -angle;
}
#endif
f32 local_68 = mTexMaxX - mTexMinX;
f32 spot_zoom = getSpotMapZoomRate();
f32 region_zoom = getRegionMapZoomRate(mRegionCursor);
@@ -2045,7 +2001,7 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param
f32 speed = base_speed / 100.0f * local_78;
f32 speed_x = speed * cM_ssin(angle);
f32 speed_z = speed * cM_scos(angle);
mStageTransX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_x :) speed_x;
mStageTransX += speed_x;
mStageTransZ += speed_z;
} else if (!param_2) {
return;
@@ -2139,11 +2095,6 @@ void dMenu_Fmap2DBack_c::drawDebugStageArea() {
if (stage_no >= 0) {
f32 v = i + mDoGph_gInf_c::getMinXF();
f32 v2 = j;
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
v = getMirrorPosX(v - 3.0f, 3.0f);
}
#endif
J2DFillBox(v - 3.0f, v2 - 3.0f, 6.0f, 6.0f, colors[stage_no % 6]);
}
}
@@ -2179,11 +2130,6 @@ void dMenu_Fmap2DBack_c::drawDebugRegionArea() {
mRegionMapSizeX[region] * mZoom, mRegionMapSizeY[region] * mZoom,
mpAreaTex[region]->getTexture(0)->getTexInfo());
if (u) {
#if TARGET_PC
if(dusk::getSettings().game.enableMirrorMode) {
pos_x = getMirrorPosX(pos_x - 3.0f, 3.0f);
}
#endif
J2DFillBox(pos_x - 3.0f, pos_y - 3.0f, 6.0f, 6.0f, colors[region]);
break;
}
+15 -7
View File
@@ -394,8 +394,21 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
icon_size_y *= _c7c;
}
#if TARGET_PC
f32 rotation = mIconInfo[info_idx].rotation;
if (dusk::getSettings().game.enableMirrorMode &&
(mIconInfo[info_idx].icon_no == ICON_LINK_e ||
mIconInfo[info_idx].icon_no == ICON_LINK_ENTER_e))
{
rotation = -rotation;
}
mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z,
rotation);
#else
mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z,
mIconInfo[info_idx].rotation);
#endif
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
mPictures[mIconInfo[info_idx].icon_no]->setAlpha((180.0f * _c80) / 255.0f);
@@ -410,15 +423,10 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
}
f32 pos_x = i_posX + (icon_pos_x - (icon_size_x / 2));
bool r4 = false;
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
pos_x = getMirrorCenterPosX(i_posX + (icon_pos_x - (icon_size_x / 2)), icon_size_x / 2);
}
if(mIconInfo[info_idx].icon_no == ICON_GOLD_WOLF_e) {
r4 = true;
}
#endif
mPictures[mIconInfo[info_idx].icon_no]->draw(pos_x, (i_posY + (icon_pos_y - icon_size_y / 2)),
@@ -427,7 +435,7 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
mLightDropPic->draw((pos_x + (icon_size_x / 2)) - (var_f29 / 2),
((icon_size_y / 2) + (i_posY + (icon_pos_y - icon_size_y / 2))) - (var_f28 / 2),
var_f29, var_f28, r4, false, false);
var_f29, var_f28, false, false, false);
}
}
}
@@ -861,4 +869,4 @@ void dMenuMapCommon_c::getFmapPoeCount(const int regionNo, int& nowCount, int& t
}
}
}
#endif
#endif
-11
View File
@@ -437,12 +437,7 @@ void dMeter2_c::checkStatus() {
field_0x128 = daPy_py_c::checkNowWolf();
#if TARGET_PC
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
if (!dComIfGp_2dShowCheck() || (msgObject != NULL && msgObject->isPlaceMessage())) {
#else
if (!dComIfGp_2dShowCheck() || dMsgObject_getMsgObjectClass()->isPlaceMessage()) {
#endif
mStatus |= 0x4000;
} else if (dComIfGp_checkPlayerStatus1(0, 1) && dComIfGp_getAStatus() == 0x12) {
mStatus |= 0x200000;
@@ -2875,14 +2870,8 @@ void dMeter2_c::alphaAnimeButton() {
u8 var_31;
var_31 = 0;
#if TARGET_PC
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
if ((mStatus & 0x4000) ||
((mStatus & 0x100) && (msgObject != NULL && msgObject->isAutoMessageFlag())) ||
#else
if ((mStatus & 0x4000) ||
((mStatus & 0x100) && dMsgObject_getMsgObjectClass()->isAutoMessageFlag()) ||
#endif
((mStatus & 0x40000000) && !(mStatus & 0x100)) || (mStatus & 0x80000000) || (mStatus & 8) ||
(mStatus & 0x10) || (mStatus & 0x20) || (mStatus & 0x04000000) || (mStatus & 0x10000000))
{
+1 -1
View File
@@ -594,7 +594,7 @@ BOOL dMeter2Info_c::isDirectUseItem(int param_0) {
return (mDirectUseItem & (u8)(1 << param_0)) ? TRUE : FALSE;
}
dMeter2Info_c g_meter2_info;
DUSK_GAME_DATA dMeter2Info_c g_meter2_info;
int dMeter2Info_c::setMeterString(s32 i_string) {
if (mMeterString != 0) {
+11
View File
@@ -428,6 +428,17 @@ static void dummyStrings() {
dMsgObject_HIO_c g_MsgObject_HIO_c;
int dMsgObject_c::_execute() {
// TODO: enabling wii message overrides fixes direction text, but gives wrong item control text
/*#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
// enable wii message index override
g_MsgObject_HIO_c.mMessageDisplay = 1;
} else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) {
g_MsgObject_HIO_c.mMessageDisplay = 0;
}
#endif*/
field_0x4c7 = 0;
if (mpTalkHeap != NULL) {
field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap);
+153 -220
View File
@@ -7,7 +7,6 @@
#include "dusk/io.hpp"
#include "dusk/settings.h"
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <limits>
@@ -16,90 +15,79 @@
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>
#include "dusk/action_bindings.h"
#include "dusk/logging.h"
#include "dusk/main.h"
namespace dusk::config {
namespace {
using namespace dusk::config;
constexpr auto ConfigFileName = "config.json";
using json = nlohmann::json;
aurora::Module DuskConfigLog("dusk::config");
absl::flat_hash_map<std::string, ConfigVarBase*> RegisteredConfigVars;
absl::flat_hash_map<std::string, nlohmann::json> UnregisteredConfigVars;
absl::flat_hash_map<std::string, std::string> UnregisteredConfigVarOverrides;
static absl::flat_hash_map<std::string, ConfigVarBase*> RegisteredConfigVars;
static absl::flat_hash_map<std::string, nlohmann::json> UnregisteredConfigVars;
static absl::flat_hash_map<std::string, std::string> UnregisteredConfigVarOverrides;
struct ChangeSubscription {
Subscription token;
ChangeCallback callback;
};
absl::flat_hash_map<std::string, std::vector<ChangeSubscription> > s_changeSubscriptions;
absl::flat_hash_map<Subscription, std::string> s_changeTokenNames;
Subscription s_nextChangeToken = 1;
// Names currently being notified; guards against a callback re-notifying its own CVar.
std::vector<std::string> s_activeChangeNotifications;
std::optional<ui::ControlAnchor> parse_control_anchor(std::string_view value) {
static std::optional<dusk::ui::ControlAnchor> parse_control_anchor(std::string_view value) {
if (value == "none") {
return ui::ControlAnchor::None;
return dusk::ui::ControlAnchor::None;
}
if (value == "top") {
return ui::ControlAnchor::Top;
return dusk::ui::ControlAnchor::Top;
}
if (value == "left") {
return ui::ControlAnchor::Left;
return dusk::ui::ControlAnchor::Left;
}
if (value == "bottom") {
return ui::ControlAnchor::Bottom;
return dusk::ui::ControlAnchor::Bottom;
}
if (value == "right") {
return ui::ControlAnchor::Right;
return dusk::ui::ControlAnchor::Right;
}
if (value == "topLeft") {
return ui::ControlAnchor::TopLeft;
return dusk::ui::ControlAnchor::TopLeft;
}
if (value == "topRight") {
return ui::ControlAnchor::TopRight;
return dusk::ui::ControlAnchor::TopRight;
}
if (value == "bottomLeft") {
return ui::ControlAnchor::BottomLeft;
return dusk::ui::ControlAnchor::BottomLeft;
}
if (value == "bottomRight") {
return ui::ControlAnchor::BottomRight;
return dusk::ui::ControlAnchor::BottomRight;
}
return std::nullopt;
}
const char* control_anchor_value(ui::ControlAnchor anchor) {
static const char* control_anchor_value(dusk::ui::ControlAnchor anchor) {
switch (anchor) {
case ui::ControlAnchor::None:
case dusk::ui::ControlAnchor::None:
return "none";
case ui::ControlAnchor::Top:
case dusk::ui::ControlAnchor::Top:
return "top";
case ui::ControlAnchor::Left:
case dusk::ui::ControlAnchor::Left:
return "left";
case ui::ControlAnchor::Bottom:
case dusk::ui::ControlAnchor::Bottom:
return "bottom";
case ui::ControlAnchor::Right:
case dusk::ui::ControlAnchor::Right:
return "right";
case ui::ControlAnchor::TopLeft:
case dusk::ui::ControlAnchor::TopLeft:
return "topLeft";
case ui::ControlAnchor::TopRight:
case dusk::ui::ControlAnchor::TopRight:
return "topRight";
case ui::ControlAnchor::BottomLeft:
case dusk::ui::ControlAnchor::BottomLeft:
return "bottomLeft";
case ui::ControlAnchor::BottomRight:
case dusk::ui::ControlAnchor::BottomRight:
return "bottomRight";
}
return "none";
}
std::optional<float> json_finite_float(const json& object, const char* key) {
static std::optional<float> json_finite_float(const json& object, const char* key) {
const auto iter = object.find(key);
if (iter == object.end() || !iter->is_number()) {
return std::nullopt;
@@ -113,7 +101,7 @@ std::optional<float> json_finite_float(const json& object, const char* key) {
return value;
}
std::optional<ui::ControlProps> parse_control_props(const json& value) {
static std::optional<dusk::ui::ControlProps> parse_control_props(const json& value) {
if (!value.is_object()) {
return std::nullopt;
}
@@ -132,7 +120,7 @@ std::optional<ui::ControlProps> parse_control_props(const json& value) {
if (!anchor || *w <= 0.0f || *h <= 0.0f || *scale <= 0.0f) {
return std::nullopt;
}
return ui::ControlProps{
return dusk::ui::ControlProps{
.x = *x,
.y = *y,
.w = *w,
@@ -142,17 +130,17 @@ std::optional<ui::ControlProps> parse_control_props(const json& value) {
};
}
std::filesystem::path GetConfigJsonPath() {
return ConfigPath / ConfigFileName;
static std::filesystem::path GetConfigJsonPath() {
return dusk::ConfigPath / ConfigFileName;
}
std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) {
static std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) {
auto tempPath = configJsonPath;
tempPath.replace_filename(fmt::format(".{}.tmp", configJsonPath.filename().string()));
return tempPath;
}
void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) {
static void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) {
std::error_code ec;
std::filesystem::rename(source, target, ec);
if (ec) {
@@ -162,84 +150,9 @@ void ReplaceFile(const std::filesystem::path& source, const std::filesystem::pat
}
}
template <typename T>
T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
if constexpr (std::is_enum_v<T>) {
using Underlying = std::underlying_type_t<T>;
const Underlying raw = static_cast<Underlying>(value);
const Underlying min = static_cast<Underlying>(ConfigEnumRange<T>::min);
const Underlying max = static_cast<Underlying>(ConfigEnumRange<T>::max);
if (raw < min || raw > max) {
return cVar.getDefaultValue();
}
}
return value;
ConfigVarBase::ConfigVarBase(std::string name, const ConfigImplBase* impl) : name(std::move(name)), registered(false), layer(ConfigVarLayer::Default), impl(impl) {
}
template <ConfigValue T>
requires std::is_integral_v<T>&& std::is_signed_v<T> T parse_arg_value(
const ConfigVar<T>&, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stoll(str);
if (result >= std::numeric_limits<T>::min() && result <= std::numeric_limits<T>::max()) {
return static_cast<T>(result);
}
throw std::out_of_range("Value is too large");
}
template <ConfigValue T>
requires std::is_integral_v<T>&& std::is_unsigned_v<T> T parse_arg_value(
const ConfigVar<T>&, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stoull(str);
if (result <= std::numeric_limits<T>::max()) {
return static_cast<T>(result);
}
throw std::out_of_range("Value is too large");
}
f32 parse_arg_value(const ConfigVar<f32>&, const std::string_view stringValue) {
const std::string str(stringValue);
return std::stof(str);
}
f64 parse_arg_value(const ConfigVar<f64>&, const std::string_view stringValue) {
const std::string str(stringValue);
return std::stod(str);
}
std::string parse_arg_value(const ConfigVar<std::string>&, const std::string_view stringValue) {
return std::string(stringValue);
}
template <ConfigValue T>
requires std::is_enum_v<T> T parse_arg_value(
const ConfigVar<T>& cVar, const std::string_view stringValue) {
using Underlying = std::underlying_type_t<T>;
const std::string str(stringValue);
if constexpr (std::is_signed_v<Underlying>) {
const auto result = std::stoll(str);
if (result >= std::numeric_limits<Underlying>::min() &&
result <= std::numeric_limits<Underlying>::max())
{
return sanitizeEnumValue(cVar, static_cast<T>(result));
}
throw std::out_of_range("Value is too large");
} else {
const auto result = std::stoull(str);
if (result <= std::numeric_limits<Underlying>::max()) {
return sanitizeEnumValue(cVar, static_cast<T>(result));
}
throw std::out_of_range("Value is too large");
}
}
} // namespace
ConfigVarBase::ConfigVarBase(std::string name, const ConfigImplBase* impl)
: name(std::move(name)), registered(false), layer(ConfigVarLayer::Default), impl(impl) {}
const char* ConfigVarBase::getName() const noexcept {
return name.c_str();
}
@@ -254,6 +167,21 @@ ConfigVarBase::~ConfigVarBase() {
}
}
template <typename T>
static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
if constexpr (std::is_enum_v<T>) {
using Underlying = std::underlying_type_t<T>;
const Underlying raw = static_cast<Underlying>(value);
const Underlying min = static_cast<Underlying>(ConfigEnumRange<T>::min);
const Underlying max = static_cast<Underlying>(ConfigEnumRange<T>::max);
if (raw < min || raw > max) {
return cVar.getDefaultValue();
}
}
return value;
}
template <ConfigValue T>
void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
if constexpr (std::is_enum_v<T>) {
@@ -267,12 +195,12 @@ void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
const Underlying raw = b ? static_cast<Underlying>(1) : static_cast<Underlying>(0);
cVar.load_value(sanitizeEnumValue(cVar, static_cast<T>(raw)));
cVar.setValue(sanitizeEnumValue(cVar, static_cast<T>(raw)), false);
return;
}
}
cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get<T>()));
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<T>()), false);
}
template <ConfigValue T>
@@ -280,9 +208,74 @@ nlohmann::json ConfigImpl<T>::dumpToJson(const ConfigVar<T>& cVar) {
return cVar.getValueForSave();
}
template <ConfigValue T>
requires std::is_integral_v<T>&& std::is_signed_v<T> static void loadFromArgImpl(
ConfigVar<T>& cVar, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stoll(str);
if (result >= std::numeric_limits<T>::min() && result <= std::numeric_limits<T>::max()) {
cVar.setOverrideValue(result);
} else {
throw std::out_of_range("Value is too large");
}
}
template <ConfigValue T>
requires std::is_integral_v<T>&& std::is_unsigned_v<T> static void loadFromArgImpl(
ConfigVar<T>& cVar, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stoull(str);
if (result <= std::numeric_limits<T>::max()) {
cVar.setOverrideValue(result);
} else {
throw std::out_of_range("Value is too large");
}
}
static void loadFromArgImpl(ConfigVar<f32>& cVar, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stof(str);
cVar.setOverrideValue(result);
}
static void loadFromArgImpl(ConfigVar<f64>& cVar, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stod(str);
cVar.setOverrideValue(result);
}
static void loadFromArgImpl(ConfigVar<std::string>& cVar, const std::string_view stringValue) {
cVar.setOverrideValue(std::string(stringValue));
}
template <ConfigValue T>
requires std::is_enum_v<T> static void loadFromArgImpl(
ConfigVar<T>& cVar, const std::string_view stringValue) {
using Underlying = std::underlying_type_t<T>;
const std::string str(stringValue);
if constexpr (std::is_signed_v<Underlying>) {
const auto result = std::stoll(str);
if (result >= std::numeric_limits<Underlying>::min() &&
result <= std::numeric_limits<Underlying>::max())
{
cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast<T>(result)));
} else {
throw std::out_of_range("Value is too large");
}
} else {
const auto result = std::stoull(str);
if (result <= std::numeric_limits<Underlying>::max()) {
cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast<T>(result)));
} else {
throw std::out_of_range("Value is too large");
}
}
}
template <ConfigValue T>
void ConfigImpl<T>::loadFromArg(ConfigVar<T>& cVar, const std::string_view stringValue) {
cVar.load_override_value(parse_arg_value(cVar, stringValue));
loadFromArgImpl(cVar, stringValue);
}
template <>
@@ -290,16 +283,18 @@ void ConfigImpl<bool>::loadFromArg(ConfigVar<bool>& cVar, const std::string_view
if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" ||
stringValue == "True")
{
cVar.load_override_value(true);
cVar.setOverrideValue(true);
} else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" ||
stringValue == "False")
{
cVar.load_override_value(false);
cVar.setOverrideValue(false);
} else {
throw InvalidConfigError("Value cannot be parsed as boolean");
}
}
// My IDE is convinced this namespace is necessary. It shouldn't be AFAICT?
namespace dusk::config {
template class ConfigImpl<bool>;
template class ConfigImpl<s8>;
template class ConfigImpl<u8>;
@@ -312,10 +307,10 @@ template class ConfigImpl<u64>;
template class ConfigImpl<f32>;
template class ConfigImpl<f64>;
template class ConfigImpl<std::string>;
template class ConfigImpl<BloomMode>;
template class ConfigImpl<DepthOfFieldMode>;
template class ConfigImpl<DiscVerificationState>;
template class ConfigImpl<GameLanguage>;
template class ConfigImpl<dusk::BloomMode>;
template class ConfigImpl<dusk::DepthOfFieldMode>;
template class ConfigImpl<dusk::DiscVerificationState>;
template class ConfigImpl<dusk::GameLanguage>;
template <>
void ConfigImpl<FrameInterpMode>::loadFromJson(
@@ -325,11 +320,11 @@ void ConfigImpl<FrameInterpMode>::loadFromJson(
const FrameInterpMode mode = b ? FrameInterpMode::Unlimited : FrameInterpMode::Off;
cVar.load_value(sanitizeEnumValue(cVar, mode));
cVar.setValue(sanitizeEnumValue(cVar, mode), false);
return;
}
cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get<FrameInterpMode>()));
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<FrameInterpMode>()), false);
}
template <>
@@ -360,7 +355,7 @@ void ConfigImpl<ui::ControlLayout>::loadFromJson(
}
}
cVar.load_value(std::move(layout));
cVar.setValue(std::move(layout), false);
}
template <>
@@ -390,14 +385,14 @@ nlohmann::json ConfigImpl<ui::ControlLayout>::dumpToJson(const ConfigVar<ui::Con
};
}
template class ConfigImpl<FrameInterpMode>;
template class ConfigImpl<TouchTargeting>;
template class ConfigImpl<MenuScaling>;
template class ConfigImpl<Resampler>;
template class ConfigImpl<MagicArmorMode>;
template class ConfigImpl<ui::ControlLayout>;
template class ConfigImpl<dusk::FrameInterpMode>;
template class ConfigImpl<dusk::MenuScaling>;
template class ConfigImpl<dusk::Resampler>;
template class ConfigImpl<dusk::MagicArmorMode>;
template class ConfigImpl<dusk::ui::ControlLayout>;
} // namespace dusk::config
void Register(ConfigVarBase& configVar) {
void dusk::config::Register(ConfigVarBase& configVar) {
const std::string_view name = configVar.getName();
if (RegisteredConfigVars.contains(name)) {
DuskConfigLog.fatal("Tried to register CVar {} twice!", name);
@@ -420,31 +415,17 @@ void Register(ConfigVarBase& configVar) {
const auto overridePair = UnregisteredConfigVarOverrides.find(name);
if (overridePair != UnregisteredConfigVarOverrides.end()) {
const auto value = std::move(overridePair->second);
UnregisteredConfigVars.erase(name);
try {
configVar.getImpl()->loadFromArg(configVar, overridePair->second);
configVar.getImpl()->loadFromArg(configVar, value);
} catch (std::exception& e) {
DuskConfigLog.error("Failed to load key '{}' from override arg: {}", name, e.what());
}
}
}
void unregister(ConfigVarBase& configVar) {
const std::string_view name = configVar.getName();
const auto it = RegisteredConfigVars.find(name);
if (it == RegisteredConfigVars.end() || it->second != &configVar) {
DuskConfigLog.fatal("Tried to unregister CVar '{}' that is not registered!", name);
}
const auto layer = configVar.getLayer();
if (layer == ConfigVarLayer::Value || layer == ConfigVarLayer::Speedrun) {
UnregisteredConfigVars.insert_or_assign(
std::string{name}, configVar.getImpl()->dumpToJson(configVar));
}
RegisteredConfigVars.erase(it);
configVar.unmarkRegistered();
}
void ConfigVarBase::markRegistered() {
if (registered)
abort();
@@ -459,17 +440,17 @@ void ConfigVarBase::unmarkRegistered() {
registered = false;
}
void load_from_user_preferences() {
void dusk::config::LoadFromUserPreferences() {
const auto configJsonPath = GetConfigJsonPath();
if (configJsonPath.empty()) {
return;
}
const auto configPathString = io::fs_path_to_string(configJsonPath);
load_from_file_name(configPathString.c_str());
LoadFromFileName(configPathString.c_str());
}
static void LoadFromPath(const char* path) {
auto data = io::FileStream::ReadAllBytes(path);
auto data = dusk::io::FileStream::ReadAllBytes(path);
json j = json::parse(data);
if (!j.is_object()) {
@@ -483,6 +464,9 @@ static void LoadFromPath(const char* path) {
const auto& key = el.key();
auto configVar = RegisteredConfigVars.find(key);
if (configVar == RegisteredConfigVars.end()) {
DuskConfigLog.debug(
"Unknown key '{}' found in config! If this gets registered later, that's acceptable!",
key);
UnregisteredConfigVars.emplace(key, el.value());
continue;
}
@@ -495,7 +479,7 @@ static void LoadFromPath(const char* path) {
}
}
void load_from_file_name(const char* path) {
void dusk::config::LoadFromFileName(const char* path) {
DuskConfigLog.info("Loading config from '{}'", path);
try {
@@ -513,10 +497,10 @@ void load_from_file_name(const char* path) {
}
}
void load_arg_override(std::string_view name, std::string_view value) {
void dusk::config::LoadArgOverride(std::string_view name, std::string_view value) {
const auto cVar = GetConfigVar(name);
if (!cVar) {
UnregisteredConfigVarOverrides.emplace(name, value);
UnregisteredConfigVarOverrides.emplace(name, name);
return;
}
@@ -527,7 +511,7 @@ void load_arg_override(std::string_view name, std::string_view value) {
}
}
void save() {
void dusk::config::Save() {
const auto configJsonPath = GetConfigJsonPath();
if (configJsonPath.empty()) {
return;
@@ -558,14 +542,14 @@ void save() {
}
}
void ClearAllActionBindings(int port) {
void dusk::config::ClearAllActionBindings(int port) {
for (auto& actionBinding : getActionBinds() | std::views::values) {
actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID);
}
save();
Save();
}
ConfigVarBase* GetConfigVar(std::string_view name) {
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
const auto configVar = RegisteredConfigVars.find(name);
if (configVar != RegisteredConfigVars.end()) {
return configVar->second;
@@ -574,59 +558,13 @@ ConfigVarBase* GetConfigVar(std::string_view name) {
return nullptr;
}
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
void dusk::config::EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
for (auto& pair : RegisteredConfigVars) {
callback(*pair.second);
}
}
Subscription subscribe(std::string_view name, ChangeCallback callback) {
const auto token = s_nextChangeToken++;
s_changeSubscriptions[std::string{name}].push_back({token, std::move(callback)});
s_changeTokenNames.emplace(token, std::string{name});
return token;
}
void unsubscribe(Subscription token) {
const auto nameIt = s_changeTokenNames.find(token);
if (nameIt == s_changeTokenNames.end()) {
DuskConfigLog.fatal("Tried to unsubscribe unknown change token {}!", token);
}
const auto subsIt = s_changeSubscriptions.find(nameIt->second);
auto& subscriptions = subsIt->second;
std::erase_if(
subscriptions, [token](const ChangeSubscription& sub) { return sub.token == token; });
if (subscriptions.empty()) {
s_changeSubscriptions.erase(subsIt);
}
s_changeTokenNames.erase(nameIt);
}
bool ConfigVarBase::has_subscribers() const {
return s_changeSubscriptions.contains(name);
}
void ConfigVarBase::notify_changed(const void* previousValue) {
const auto subsIt = s_changeSubscriptions.find(name);
if (subsIt == s_changeSubscriptions.end()) {
return;
}
if (std::ranges::find(s_activeChangeNotifications, name) != s_activeChangeNotifications.end()) {
DuskConfigLog.error("Recursive change notification for CVar '{}' suppressed", name);
return;
}
s_activeChangeNotifications.push_back(name);
// Copied so callbacks can subscribe/unsubscribe safely.
const auto subscriptions = subsIt->second;
for (const auto& sub : subscriptions) {
sub.callback(*this, previousValue);
}
s_activeChangeNotifications.pop_back();
}
void shutdown() {
void dusk::config::Shutdown() {
for (auto& pair : RegisteredConfigVars) {
pair.second->unmarkRegistered();
}
@@ -634,9 +572,4 @@ void shutdown() {
RegisteredConfigVars.clear();
UnregisteredConfigVars.clear();
UnregisteredConfigVarOverrides.clear();
s_changeSubscriptions.clear();
s_changeTokenNames.clear();
s_activeChangeNotifications.clear();
}
} // namespace dusk::config
+8 -8
View File
@@ -121,6 +121,14 @@ std::filesystem::path active_pref_path() {
return get_pref_path();
}
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
const auto* basePath = SDL_GetBasePath();
if (!basePath) {
return path;
}
return path_from_utf8(basePath) / path;
}
std::filesystem::path default_data_path(const std::filesystem::path& prefPath) {
#ifdef __APPLE__
#if TARGET_OS_IOS && !TARGET_OS_TV
@@ -880,14 +888,6 @@ void ensure_data_directory(const std::filesystem::path& dataPath) {
} // namespace
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
const auto* basePath = SDL_GetBasePath();
if (!basePath) {
return path;
}
return path_from_utf8(basePath) / path;
}
bool open_data_path() {
#if DUSK_CAN_OPEN_DATA_FOLDER
std::error_code ec;
-1
View File
@@ -29,7 +29,6 @@ struct Paths {
};
Paths initialize_data();
std::filesystem::path base_path_relative(const std::filesystem::path& path);
std::filesystem::path configured_data_path();
std::filesystem::path cache_path();
bool open_data_path();

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