Compare commits

..

2 Commits

Author SHA1 Message Date
Luke Street bc651d2592 (TEMP) Update aurora 2026-06-16 11:50:04 -06:00
Luke Street 3798ce2810 (TEMP) Enable Tracy in CI builds 2026-06-16 09:54:49 -06:00
96 changed files with 292 additions and 3659 deletions
-2
View File
@@ -53,5 +53,3 @@ 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", "--mods", "${workspaceRoot}/mods"],
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console"],
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"symbolSearchPath": "${command:cmake.launchTargetPath}",
"console": "integratedTerminal",
"cwd":"${workspaceRoot}",
"cwd":"${workspaceRoot}"
}
]
}
+29 -141
View File
@@ -21,26 +21,26 @@ else ()
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
execute_process(WORKING_DIRECTORY ${CMAKE_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
execute_process(WORKING_DIRECTORY ${CMAKE_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}
execute_process(WORKING_DIRECTORY ${CMAKE_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
execute_process(WORKING_DIRECTORY ${CMAKE_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*"
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE)
@@ -49,11 +49,11 @@ if (GIT_FOUND)
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
execute_process(WORKING_DIRECTORY ${CMAKE_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
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
OUTPUT_VARIABLE DUSK_WC_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE)
else ()
@@ -211,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_CURRENT_SOURCE_DIR}")
get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}")
list(APPEND _jpeg_cmake_args -DCMAKE_TOOLCHAIN_FILE=${_jpeg_toolchain_file})
endif ()
set(_jpeg_passthrough_vars
@@ -292,42 +292,14 @@ 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 "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
)
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)
FetchContent_MakeAvailable(cxxopts json)
if (DUSK_ENABLE_SENTRY_NATIVE)
message(STATUS "dusklight: Fetching sentry-native")
@@ -375,7 +347,7 @@ else ()
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
endif ()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_BINARY_DIR}/version.h)
include(files.cmake)
@@ -400,20 +372,12 @@ set(GAME_INCLUDE_DIRS
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)
${CMAKE_BINARY_DIR})
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 funchook-static
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
Threads::Threads zstd::libzstd)
if (DUSK_ENABLE_SENTRY_NATIVE)
@@ -486,16 +450,6 @@ 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)
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)
include(GNUInstallDirs)
list(APPEND GAME_COMPILE_DEFS DUSK_ASSET_DIR="${CMAKE_INSTALL_FULL_DATADIR}/dusklight/")
@@ -523,6 +477,7 @@ set_source_files_properties(
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}
@@ -539,7 +494,6 @@ set_source_files_properties(
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
target_compile_definitions(${jsystem_lib} PRIVATE
${GAME_COMPILE_DEFS}
DUSK_BUILDING_GAME=1
$<$<CONFIG:Debug>:DEBUG=1>
$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>
)
@@ -555,61 +509,24 @@ if (CMAKE_CXX_LINK_GROUP_USING_RESCAN_SUPPORTED OR CMAKE_LINK_GROUP_USING_RESCAN
set(JSYSTEM_LINK_LIBRARIES "$<LINK_GROUP:RESCAN,${JSYSTEM_LIBRARIES}>")
endif ()
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_SOURCE_DIR}/miniz.c)
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES})
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(${DUSK_MAIN_TARGET} PRIVATE src/dusk/asan_options.c)
target_sources(dusklight PRIVATE src/dusk/asan_options.c)
endif ()
if (WIN32 AND TARGET imgui)
target_compile_definitions(imgui PRIVATE "IMGUI_API=__declspec(dllexport)")
target_sources(${DUSK_MAIN_TARGET} PRIVATE $<TARGET_OBJECTS:imgui>)
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(${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()
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusklight PRIVATE ${GAME_INCLUDE_DIRS})
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 (TARGET crashpad_handler)
add_dependencies(${DUSK_MAIN_TARGET} crashpad_handler)
add_custom_command(TARGET ${DUSK_MAIN_TARGET} POST_BUILD
add_dependencies(dusklight crashpad_handler)
add_custom_command(TARGET dusklight POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:crashpad_handler>"
"$<TARGET_FILE_DIR:dusklight>"
@@ -620,7 +537,7 @@ 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(${DUSK_MAIN_TARGET} PRIVATE "-Wl,-u,SDL_main")
target_link_options(dusklight PRIVATE "-Wl,-u,SDL_main")
endif ()
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
@@ -630,7 +547,7 @@ endif ()
if (NOT APPLE)
add_custom_command(TARGET dusklight POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/res"
"${CMAKE_SOURCE_DIR}/res"
"$<TARGET_FILE_DIR:dusklight>/res"
COMMENT "Copying resources"
)
@@ -658,13 +575,13 @@ 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/DuskModSDK.cmake)
if (APPLE)
if (IOS)
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios)
@@ -672,7 +589,6 @@ 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}/Dusk.entitlements)
endif ()
set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in)
file(GLOB_RECURSE DUSK_RESOURCE_FILES
@@ -703,8 +619,6 @@ 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"
)
endif ()
@@ -724,11 +638,7 @@ if (IOS)
endif ()
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
if(WIN32)
aurora_copy_runtime_dlls(dusklight dusklight_game)
else()
aurora_copy_runtime_dlls(dusklight)
endif()
aurora_copy_runtime_dlls(dusklight)
if (DUSK_SELECTED_OPT)
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
@@ -767,33 +677,15 @@ 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 (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)
if (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)
@@ -851,7 +743,3 @@ foreach (target IN LISTS BINARY_TARGETS)
endif ()
endforeach ()
endforeach ()
if (DUSK_ENABLE_CODE_MODS)
add_subdirectory(tools/mod_test)
endif ()
+9 -1
View File
@@ -544,7 +544,15 @@
"type": "FILEPATH",
"value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
},
"VCPKG_TARGET_TRIPLET": "x64-windows"
"VCPKG_TARGET_TRIPLET": "x64-windows",
"TRACY_ENABLE": {
"type": "BOOL",
"value": true
},
"TRACY_ON_DEMAND": {
"type": "BOOL",
"value": true
}
}
},
{
-49
View File
@@ -1,49 +0,0 @@
# 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()
-13
View File
@@ -1,13 +0,0 @@
# Patches capstone's CMakeLists.txt for compatibility with CMake >= 4.0:
# - Bumps cmake_minimum_required to 3.10 (CMake >= 4.0 dropped < 3.5 support; < 3.10 warns)
# - Removes cmake_policy(SET CMP0048 OLD) (rejected by CMake >= 3.27)
file(READ "${DIR}/CMakeLists.txt" _content)
string(REGEX REPLACE
"cmake_minimum_required[ \t]*\\([ \t]*VERSION[ \t]+[0-9]+\\.[0-9]+(\\.[0-9]+)?[ \t]*\\)"
"cmake_minimum_required(VERSION 3.10)"
_content "${_content}")
string(REGEX REPLACE
"cmake_policy[ \t]*\\([ \t]*SET[ \t]+CMP0048[ \t]+OLD[ \t]*\\)"
"# cmake_policy(SET CMP0048 OLD)"
_content "${_content}")
file(WRITE "${DIR}/CMakeLists.txt" "${_content}")
-11
View File
@@ -1,11 +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}")
-351
View File
@@ -1,351 +0,0 @@
# Dusk Mod API
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. [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/), 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)
```
**CMakeLists.txt:**
```cmake
cmake_minimum_required(VERSION 3.25)
project(my_mod CXX)
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusk" CACHE PATH "Path to dusk source root")
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
add_dusk_mod(my_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res # optional
)
```
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.
- Windows: `%APPDATA%\TwilitRealm\Dusk\mods`
- Linux: `~/.local/share/TwilitRealm/Dusk/mods`
- macOS: `~/Library/Application Support/TwilitRealm/Dusk/mods`
The `.dusk` archive is a standard zip containing `mod.json`, the compiled library, and an optional `res/` tree. `add_dusk_mod()` creates it automatically.
---
## mod.json
```json
{
"name": "My Mod",
"version": "1.0.0",
"author": "Your Name",
"description": "A short description shown in the mod manager."
}
```
All fields are optional but recommended. `name` falls back to the filename, `version` to `"?"`.
---
## Required Exports
```cpp
#include "dusk/mod_api.h"
DUSK_REQUIRE_API_VERSION // declares mod_api_version; loader rejects the mod if the engine is older
extern "C" {
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
}
```
`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.
---
## DuskModAPI Reference
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.
| 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 |
---
## Logging
```cpp
api->log_info("Player health: %d", hp);
api->log_warn("Something looks wrong");
api->log_error("Fatal: %s", msg);
```
Output appears in the dusk console as `[My Mod] ...`
The format string is `printf`-compatible.
---
## Loading Resources
```cpp
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);
}
```
- 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`
---
## ImGui Integration
**Tab content:** shown in the mod's panel in the Mods window, called every frame while visible:
```cpp
static void DrawPanel(void* userdata) {
ImGui::Text("Hello!");
}
api->register_tab_content(DrawPanel, nullptr);
```
Pass a pointer through `userdata` if your callback needs state:
```cpp
api->register_tab_content(DrawPanel, &g_state);
```
**Menu items:** added to the quick-access menu. Use `ImGui::MenuItem`, `ImGui::Separator`, etc.:
```cpp
static void DrawMenuEntry(void*) {
if (ImGui::MenuItem("Reset rotation")) { ... }
}
api->register_menu_item(DrawMenuEntry, nullptr);
```
---
## Hooking Game Functions
Call `dusk::init(api)` first.
```cpp
#include "dusk/hook.hpp"
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 `0` to let it proceed, non-zero to cancel it. Post-hooks still run either way.
```cpp
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::hookAddPre<&daAlink_c::posMove>(on_posMove_pre);
```
### Post-hooks
Run after the original (or replace-hook).
```cpp
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::hookAddPost<&daAlink_c::posMove>(on_posMove_post);
```
### Replace hooks
Completely substitutes the original. Only one replace-hook per function, a second install overwrites with a warning.
```cpp
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);
```
To call the original from inside a replace-hook:
```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 a `void*[N]` array. Index `0` is `this`, subsequent indices are parameters in declaration order.
```cpp
T value = dusk::arg <T>(args, n); // copy
T& ref = dusk::argRef<T>(args, n); // reference (read/write)
```
**Example:** halve incoming damage
```cpp
// 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::hookAddPre<&daEnemy_c::takeDamage>(on_takeDamage_pre);
```
For reference parameters (e.g. `const cXyz& pos`), use `argRef<cXyz>` to get a direct reference.
---
## Inter-Mod Communication
Mods can expose a public API to each other through a global service registry. The convention for names is `"mod_name/service_name"`.
**Mod A — publishing:**
```cpp
struct MyModAPI {
void (*do_thing)(int value);
};
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);
}
```
**Mod B — consuming:**
```cpp
#include "my_mod_api.h"
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"));
}
```
---
## Full Example
```cpp
#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"
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);
}
}
```
+1 -1
-11
View File
@@ -1507,8 +1507,6 @@ set(DUSK_FILES
src/dusk/ui/pane.hpp
src/dusk/ui/menu_bar.cpp
src/dusk/ui/menu_bar.hpp
src/dusk/ui/mods_window.cpp
src/dusk/ui/mods_window.hpp
src/dusk/ui/prelaunch.cpp
src/dusk/ui/prelaunch.hpp
src/dusk/ui/preset.cpp
@@ -1543,15 +1541,6 @@ set(DUSK_FILES
src/dusk/OSReport.cpp
src/dusk/OSThread.cpp
src/dusk/OSMutex.cpp
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
+14 -14
View File
@@ -16,37 +16,37 @@
];
forAllSystems = lib.genAttrs supportedSystems;
dawnVersion = "v20260618.032059";
nodVersion = "v2.0.0-alpha.10";
dawnVersion = "v20260423.175430";
nodVersion = "v2.0.0-alpha.8";
versionSuffix = "nix-" + (self.shortRev or self.dirtyShortRev or "dirty");
dawnInfo = {
"x86_64-linux" = {
triple = "linux-x86_64";
hash = "sha256-GFSd573b+VQx/VmFdNQgWDd0V9ayQlcw0Zuopke12ak=";
hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs=";
};
"aarch64-linux" = {
triple = "linux-aarch64";
hash = "sha256-ZaoP7BAjBMnfAv2/AMRi3FNH2ZtyqASCSFyU/oB2Mzg=";
hash = "sha256-34yyFpfqBZUwoFXQ41F0AwAU78FaNihOSY0oriwn6B0=";
};
"aarch64-darwin" = {
triple = "darwin-arm64";
hash = "sha256-HT+qtlLaSHyoXPrUcXgcTGa877X5YfzbxRD4bJb7i1Y=";
hash = "sha256-eQnzrBp6gjiBek1VYQ9A5W13ClYWrDDKjIqv/7eNTR4=";
};
"x86_64-darwin" = {
triple = "darwin-x86_64";
hash = "sha256-cUNaCbA7rlKSukDVKGaVEVw0Zt1+mSbaHbmUCMvMVWc=";
hash = "sha256-QGWiGdxiI9kci3NPXH6QFFirxn16851zB/w3jqhIBJ4=";
};
};
nodPrebuiltInfo = {
"x86_64-linux" = {
triple = "linux-x86_64";
hash = "sha256-FVQWECVA2gWdc+n5OQ/Tvwn8z0qdgjSd1WlFt5HKOec=";
hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE=";
};
"aarch64-darwin" = {
triple = "macos-arm64";
hash = "sha256-8ZEejxksVgShNKUVRCBYaLOp9x/qOC9pAeVrElQUGUk=";
hash = "sha256-UPy1ywCcv0K6VJOU3uUelJuUdBh3UNaPRlyP5LOBeDw=";
};
};
@@ -75,7 +75,7 @@
'';
dawn = pkgs.fetchzip {
url = "https://github.com/encounter/dawn/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
url = "https://github.com/encounter/dawn-build/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
hash = dawnInfo.${system}.hash;
stripRoot = false;
};
@@ -94,7 +94,7 @@
owner = "encounter";
repo = "nod";
rev = nodVersion;
hash = "sha256-r8qDlOVxv5iKiFjJQrcBuL9HVoOM3yEjRVnQIMqaICs=";
hash = "sha256-+zrtVzjo0+X/6uMcNUn1+FaSR+jOhrcQSDNBFjw0NDs=";
};
patches = [ ./fix-cmake-paths.patch ];
cargoDeps = pkgs.rustPlatform.importCargoLock {
@@ -141,12 +141,12 @@
XXHASH = pkgs.xxhash.src;
ZSTD = pkgs.zstd.src;
FMT = pkgs.fetchzip {
url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz";
hash = "sha256-ZmI1Dv0ZabPlxa02OpERI47jp7zFfjpeWCy1WyuPYZ0=";
url = "https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz";
hash = "sha256-sUbxlYi/Aupaox3JjWFqXIjcaQa0LFjclQAOleT+FRA=";
};
TRACY = pkgs.fetchzip {
url = "https://github.com/wolfpld/tracy/archive/6789e7d6f9a65ec98926b602097a33a9676d2606.tar.gz";
hash = "sha256-Xxyd7G/mnXEPpN+ehmwl0AkAhS3CwObpJNDgcqbdUJg=";
url = "https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz";
hash = "sha256-hbNGOsGeyGSvCJ2No8RkwOib1lX2on3vNZSzyVkZdXw=";
};
IMGUI = pkgs.fetchFromGitHub {
owner = "ocornut";
+1 -1
View File
@@ -34,7 +34,7 @@ public:
bool isResetting() { return mResettingFlag; }
static Z2AudioMgr* getInterface() { return mAudioMgrPtr; }
static DUSK_GAME_DATA Z2AudioMgr* mAudioMgrPtr;
static Z2AudioMgr* mAudioMgrPtr;
/* 0x0514 */ virtual bool startSound(JAISoundID soundID, JAISoundHandle* handle, const JGeometry::TVec3<f32>* posPtr);
/* 0x0518 */ bool mResettingFlag;
+1 -1
View File
@@ -4556,7 +4556,7 @@ public:
void handleWolfHowl();
void handleQuickTransform();
bool checkAimContext();
bool checkAimInputContext();
bool checkTouchAimCaptureContext();
void onIronBallChainInterpCallback();
+5 -5
View File
@@ -1049,11 +1049,11 @@ public:
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
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;
extern dComIfG_inf_c g_dComIfG_gameInfo;
extern GXColor g_blackColor;
extern GXColor g_clearColor;
extern GXColor g_whiteColor;
extern GXColor g_saftyWhiteColor;
int dComLbG_PhaseHandler(request_of_phase_process_class*, request_of_phase_process_fn*,
void*);
-1
View File
@@ -218,7 +218,6 @@ private:
bool mCursorInterpPrevAngular;
bool mCursorInterpCurrAngular;
bool mCursorInterpInit;
bool mPointerTouchPressHoveredCurrent;
#endif
};
+1 -4
View File
@@ -2,7 +2,6 @@
#define D_METER_D_METER2_INFO_H
#include "SSystem/SComponent/c_xyz.h"
#include "global.h"
class CPaneMgr;
class J2DTextBox;
@@ -302,7 +301,7 @@ public:
/* 0xF3 */ u8 unk_0xf3[5];
};
DUSK_GAME_EXTERN dMeter2Info_c g_meter2_info;
extern dMeter2Info_c g_meter2_info;
void dMeter2Info_setSword(u8 i_itemId, bool i_offItemBit);
void dMeter2Info_setCloth(u8 i_clothId, bool i_offItemBit);
@@ -850,8 +849,6 @@ 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);
}
+5 -4
View File
@@ -89,16 +89,17 @@ public:
*/
void Register(ConfigVarBase& configVar);
/**
* \brief Indicate that all registrations have happened and everything should lock in.
*/
void FinishRegistration();
/**
* \brief Load config from the standard user preferences location.
*/
void LoadFromUserPreferences();
void LoadFromFileName(const char* path);
void LoadArgOverride(std::string_view name, std::string_view value);
void Shutdown();
/**
* \brief Save the config to file.
*/
+5 -12
View File
@@ -69,7 +69,7 @@ protected:
/**
* The name of this CVar, used in the configuration file.
*/
std::string name;
const char* name;
/**
* Whether this CVar has been registered with the global managing logic.
@@ -87,10 +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);
ConfigVarBase(const char* name, const ConfigImplBase* impl);
virtual ~ConfigVarBase() = default;
/**
* Check that the CVar is registered, aborting if this is not the case.
@@ -101,8 +99,6 @@ protected:
}
public:
virtual ~ConfigVarBase();
/**
* Get the name of this CVar, used in the configuration file.
*/
@@ -125,7 +121,6 @@ public:
* This is necessary to make it legal to access.
*/
void markRegistered();
void unmarkRegistered();
/**
* Clear a speedrun-mode override if one is active on this CVar.
@@ -197,12 +192,10 @@ public:
* @param arg Arguments to forward to construct the default value.
*/
template <typename... Args>
ConfigVar(std::string name, Args&&... arg)
: ConfigVarBase(std::move(name), GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
ConfigVar(const char* name, Args&&... arg)
: ConfigVarBase(name, GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
value(), overrideValue() {}
ConfigVar(ConfigVar const&) = delete;
/**
* \brief Get the current value of the CVar.
*
+13 -4
View File
@@ -22,8 +22,9 @@
class GXTexObjRAII : public GXTexObj {
public:
GXTexObjRAII() : GXTexObj() {}
~GXTexObjRAII();
void reset();
~GXTexObjRAII() { GXDestroyTexObj(this); }
void reset() { GXDestroyTexObj(this); }
GXTexObjRAII(const GXTexObjRAII&) = delete;
GXTexObjRAII& operator=(const GXTexObjRAII&) = delete;
@@ -64,8 +65,16 @@ typedef GXTlutObj TGXTlutObj;
#endif
struct GXScopedDebugGroup {
explicit GXScopedDebugGroup(const char* text);
~GXScopedDebugGroup();
explicit GXScopedDebugGroup(const char* text) {
#if DUSK_GFX_DEBUG_GROUPS
GXPushDebugGroup(text);
#endif
}
~GXScopedDebugGroup() {
#if DUSK_GFX_DEBUG_GROUPS
GXPopDebugGroup();
#endif
}
};
#define GX_AND_TRACY_SCOPED(name) GXScopedDebugGroup scope(name); ZoneScopedN(name);
-122
View File
@@ -1,122 +0,0 @@
#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
@@ -1,18 +0,0 @@
#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
+2 -7
View File
@@ -6,9 +6,6 @@ class CPaneMgr;
namespace dusk::menu_pointer {
using TargetId = u16;
constexpr TargetId InvalidTarget = 0xffff;
enum class Context {
None,
FileSelect,
@@ -46,14 +43,12 @@ bool active() noexcept;
bool enabled() noexcept;
bool mouse_capture_active() noexcept;
const State& state() noexcept;
void set_hover_target(TargetId target) noexcept;
bool consume_click() noexcept;
bool peek_click() noexcept;
void set_dialog_choice(u8 choice, bool clicked) noexcept;
bool get_dialog_choice(u8& choice) noexcept;
bool consume_dialog_click(u8& choice) noexcept;
void defer_activation(Context context, TargetId target) noexcept;
bool consume_deferred_activation(Context context, TargetId target) noexcept;
void defer_activation(Context context, u8 target) noexcept;
bool consume_deferred_activation(Context context, u8 target) noexcept;
void clear_deferred_activation(Context context) noexcept;
u32 suppressed_pad_buttons(u32 port) noexcept;
void finish_pad_suppression_read(u32 port) noexcept;
-65
View File
@@ -1,65 +0,0 @@
#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);
}
-134
View File
@@ -1,134 +0,0 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include <ranges>
#include "dusk/mod_api.h"
#include "dusk/config_var.hpp"
namespace dusk::modding {
class ModBundle;
class NativeModule;
}
namespace dusk {
struct RmlTabContentCallback {
void (*build_fn)(void* panel, void* userdata);
void* userdata;
};
struct RmlTabUpdateCallback {
void (*update_fn)(void* userdata);
void* userdata;
};
struct ModMetadata {
std::string id;
std::string name;
std::string version;
std::string author;
std::string description;
bool hasCode;
};
struct NativeMod {
std::unique_ptr<modding::NativeModule> handle;
DuskModAPI api{};
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 {
/**
* Mod does not have native code included.
*/
None,
/**
* Native code mod loaded successfully.
*
* Note that this only indicates load status of the native library. If the native lib throws in
* its init function, it will still be disabled!
*/
Loaded,
/**
* This build was compiled without native mod support!
*/
BuildDisabled,
/**
* Mod does not have a native library suitable for this build's platform.
*/
ModMissingPlatform,
/**
* Mod is built for a different API version than this build of the game.
*/
ApiVersionMismatch,
/**
* Unknown error loading the native mod.
*/
Unknown,
};
struct LoadedMod {
ModMetadata metadata;
std::string mod_path;
std::string dir;
std::unique_ptr<ConfigVar<bool>> cvarIsEnabled;
bool active = false;
bool load_failed = false;
NativeModStatus native_status = NativeModStatus::None;
std::unique_ptr<NativeMod> native;
std::unique_ptr<modding::ModBundle> bundle;
std::vector<RmlTabContentCallback> tab_content;
std::vector<RmlTabUpdateCallback> tab_updates;
};
class ModLoader {
public:
static ModLoader& instance();
void setModsDir(std::filesystem::path dir) { m_modsDir = std::move(dir); }
void init();
void tick();
void shutdown();
[[nodiscard]] auto mods() const {
return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; });
}
[[nodiscard]] auto active_mods() const {
return mods() | std::views::filter([](const auto& m) { return m.active; });
}
private:
std::vector<std::unique_ptr<LoadedMod>> m_mods;
std::filesystem::path m_modsDir;
bool m_initialized = false;
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
-28
View File
@@ -1,28 +0,0 @@
#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;
}
+1
View File
@@ -275,6 +275,7 @@ struct UserSettings {
ConfigVar<DiscVerificationState> isoVerification;
ConfigVar<std::string> graphicsBackend;
ConfigVar<bool> skipPreLaunchUI;
ConfigVar<bool> showPipelineCompilation;
ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> checkForUpdates;
ConfigVar<int> cardFileType;
-13
View File
@@ -114,19 +114,6 @@ inline int __builtin_clz(unsigned int v) {
#endif
// 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)
#elif defined(TARGET_PC) && defined(_WIN32) && defined(DUSK_BUILDING_GAME)
# define DUSK_GAME_EXTERN extern __declspec(dllexport)
# define DUSK_GAME_DATA __declspec(dllexport)
#else
# define DUSK_GAME_EXTERN extern
# define DUSK_GAME_DATA
#endif
#define FAST_DIV(x, n) (x >> (n / 2))
#define SQUARE(x) ((x) * (x))
+1 -1
View File
@@ -33,7 +33,7 @@ public:
static void onBgmSet() { mBgmSet = true; }
static void offBgmSet() { mBgmSet = false; }
static DUSK_GAME_DATA u8 mInitFlag;
static u8 mInitFlag;
static u8 mResetFlag;
static u8 mBgmSet;
};
+1 -2
View File
@@ -4,7 +4,6 @@
#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 };
@@ -95,7 +94,7 @@ public:
static void stopMotorWaveHard(u32 pad) { return m_gamePad[pad]->stopMotorWaveHard(); }
static JUTGamePad* m_gamePad[4];
static DUSK_GAME_DATA interface_of_controller_pad m_cpadInfo[4];
static interface_of_controller_pad m_cpadInfo[4];
static interface_of_controller_pad m_debugCpadInfo[4];
};
+1 -1
View File
@@ -371,7 +371,6 @@ public:
static int m_height;
static f32 m_heightF;
static f32 m_widthF;
#endif
#if TARGET_PC
static f32 m_safeMinXF;
@@ -381,6 +380,7 @@ public:
static f32 m_safeWidthF;
static f32 m_safeHeightF;
#endif
#endif
};
#endif /* M_DO_M_DO_GRAPHIC_H */
@@ -4,7 +4,6 @@
#include <types.h>
#include <cmath>
#include <utility>
#include "global.h"
#ifdef __cplusplus
extern "C" {
@@ -142,9 +141,9 @@ struct TAsinAcosTable {
}
};
DUSK_GAME_EXTERN TSinCosTable<13, f32> sincosTable_;
DUSK_GAME_EXTERN TAtanTable<1024, f32> atanTable_;
DUSK_GAME_EXTERN TAsinAcosTable<1024, f32> asinAcosTable_;
extern TSinCosTable<13, f32> sincosTable_;
extern TAtanTable<1024, f32> atanTable_;
extern TAsinAcosTable<1024, f32> asinAcosTable_;
inline f32 acosDegree(f32 x) {
return asinAcosTable_.acosDegree(x);
+3 -3
View File
@@ -16,10 +16,10 @@ inline f64 getConst2() {
return 9.765625E-4;
}
DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
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>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
+2 -52
View File
@@ -21,7 +21,6 @@ body {
}
fps,
pipeline-progress,
toast {
position: absolute;
border: 1dp #92875B;
@@ -99,7 +98,7 @@ toast message row.muted {
opacity: 0.5;
}
progress {
toast progress {
height: 4dp;
position: absolute;
left: 0;
@@ -107,50 +106,10 @@ progress {
width: 100%;
}
progress fill {
toast progress fill {
background-color: rgba(194, 164, 45, 80%);
}
pipeline-progress {
left: 12dp;
bottom: 12dp;
display: flex;
flex-flow: column;
z-index: 100;
min-width: 260dp;
max-width: 90%;
padding: 10dp 16dp 12dp;
border-radius: 7dp;
overflow: hidden;
filter: opacity(0);
transition: filter 0.2s linear-in-out;
pointer-events: none;
}
pipeline-progress[open] {
filter: opacity(1);
}
pipeline-status {
display: flex;
align-items: center;
gap: 8dp;
font-size: 18dp;
font-weight: normal;
white-space: nowrap;
}
icon.pipeline-spinner {
width: 1.2em;
height: 1.2em;
line-height: 1.2em;
font-size: 1.2em;
color: #C2A42D;
text-align: center;
transform-origin: center;
animation: 1s linear infinite pipeline-spinner-spin;
}
toast.achievement {
border: 1dp #C2A42D;
}
@@ -351,15 +310,6 @@ logo img.outer {
}
}
@keyframes pipeline-spinner-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (max-height: 640dp) {
toast {
top: 20dp;
-4
View File
@@ -362,10 +362,6 @@ body.animate-in .intro-item {
transition: opacity transform 0.3s 0.6s cubic-in-out;
}
.delay-6 {
transition: opacity transform 0.3s 0.7s cubic-in-out;
}
/* Mobile layout */
@media (max-height: 640dp) {
.gradient {
-29
View File
@@ -395,11 +395,6 @@ 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;
@@ -514,30 +509,6 @@ 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;
-6
View File
@@ -142,12 +142,6 @@ 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
DUSK_GAME_DATA Z2AudioMgr* Z2AudioMgr::mAudioMgrPtr;
Z2AudioMgr* Z2AudioMgr::mAudioMgrPtr;
u8 gMuffleOutOfRangeMic = false;
Z2AudioMgr::Z2AudioMgr() : mSoundStarter(true) {
+1 -1
View File
@@ -176,7 +176,7 @@ bool daAlink_c::checkAimContext() {
}
}
bool daAlink_c::checkAimInputContext() {
bool daAlink_c::checkTouchAimCaptureContext() {
switch (mProcID) {
case PROC_HOOKSHOT_ROOF_WAIT:
case PROC_HOOKSHOT_WALL_WAIT:
+3 -3
View File
@@ -123,7 +123,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
}
#if TARGET_PC
if (dusk::getSettings().game.enableMouseAim && checkAimInputContext()) {
if (dusk::getSettings().game.enableMouseAim && checkAimContext()) {
sp8 = mBodyAngle.x;
} else
#endif
@@ -142,7 +142,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
#if TARGET_PC
if ((dusk::getSettings().game.enableGyroAim ||
dusk::getSettings().game.enableMouseAim) &&
checkAimInputContext())
checkAimContext())
{
f32 gyro_scale = 1.0f;
if (checkWolfEyeUp()) {
@@ -174,7 +174,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
}
}
if (dusk::getSettings().game.enableTouchControls && checkAimInputContext()) {
if (dusk::getSettings().game.enableTouchControls && checkAimContext()) {
f32 touchYawDp = 0.0f;
f32 touchPitchDp = 0.0f;
if (dusk::touch_camera::consume_delta(touchYawDp, touchPitchDp)) {
+1 -1
View File
@@ -7505,7 +7505,7 @@ static bool sTouchFreeCameraActive = false;
bool dCamera_c::isAimActive() {
auto* link = daAlink_getAlinkActorClass();
return link != nullptr && link->checkAimInputContext() &&
return link != nullptr && link->checkAimContext() &&
dComIfGp_checkCameraAttentionStatus(link->field_0x317c, 0x10);
}
+21 -32
View File
@@ -777,7 +777,6 @@ bool dFile_select_c::pointerDataSelect() {
if (!dusk::menu_pointer::hit_pane(mSelFilePanes[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerDataSelectTarget, i));
const bool clicked = dusk::menu_pointer::consume_click();
if (mSelectNum != i) {
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
@@ -806,7 +805,6 @@ bool dFile_select_c::pointerMenuSelect() {
if (!dusk::menu_pointer::hit_pane(m3mSelPane[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerMenuSelectTarget, i));
const bool clicked = dusk::menu_pointer::consume_click();
if (!mIsDataNew[mSelectNum] && mSelectMenuNum != i) {
mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0);
@@ -835,7 +833,6 @@ bool dFile_select_c::pointerCopyDataToSelect() {
if (!dusk::menu_pointer::hit_pane(mCpSelPane[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerCopySelectTarget, i));
const bool clicked = dusk::menu_pointer::consume_click();
if (field_0x026b != i) {
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
@@ -864,7 +861,6 @@ bool dFile_select_c::pointerYesNoSelect(bool errorSelect) {
if (!dusk::menu_pointer::hit_pane(mYnSelPane[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, i));
const bool clicked =
(!errorSelect || field_0x0268 == i) && dusk::menu_pointer::consume_click();
if (field_0x0268 != i) {
@@ -1107,13 +1103,12 @@ void dFile_select_c::dataSelectAnmSet() {
void dFile_select_c::dataSelectMoveAnime() {
#if TARGET_PC
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
if (mSelectNum != 0xFF && dusk::menu_pointer::hit_pane(mSelFilePanes[mSelectNum], 8.0f)) {
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerDataSelectTarget, mSelectNum));
if (dusk::menu_pointer::consume_click()) {
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerDataSelectTarget, mSelectNum));
}
if (mSelectNum != 0xFF && dusk::menu_pointer::hit_pane(mSelFilePanes[mSelectNum], 8.0f) &&
dusk::menu_pointer::consume_click())
{
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerDataSelectTarget, mSelectNum));
}
#endif
bool iVar7 = true;
@@ -1499,14 +1494,12 @@ void dFile_select_c::menuSelectMoveAnm() {
#if TARGET_PC
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
if (mSelectMenuNum != 0xFF &&
dusk::menu_pointer::hit_pane(m3mSelPane[mSelectMenuNum], 8.0f))
dusk::menu_pointer::hit_pane(m3mSelPane[mSelectMenuNum], 8.0f) &&
dusk::menu_pointer::consume_click())
{
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum));
if (dusk::menu_pointer::consume_click()) {
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum));
}
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum));
}
#endif
bool tmp1 = true;
@@ -2004,14 +1997,12 @@ void dFile_select_c::copyDataToSelectMoveAnm() {
#if TARGET_PC
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
if (field_0x026b != 0xFF &&
dusk::menu_pointer::hit_pane(mCpSelPane[field_0x026b], 8.0f))
dusk::menu_pointer::hit_pane(mCpSelPane[field_0x026b], 8.0f) &&
dusk::menu_pointer::consume_click())
{
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerCopySelectTarget, field_0x026b));
if (dusk::menu_pointer::consume_click()) {
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerCopySelectTarget, field_0x026b));
}
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerCopySelectTarget, field_0x026b));
}
#endif
bool iVar7 = true;
@@ -2531,14 +2522,12 @@ void dFile_select_c::yesNoCursorMoveAnm() {
#if TARGET_PC
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
if (field_0x0268 != 0xFF &&
dusk::menu_pointer::hit_pane(mYnSelPane[field_0x0268], 8.0f))
dusk::menu_pointer::hit_pane(mYnSelPane[field_0x0268], 8.0f) &&
dusk::menu_pointer::consume_click())
{
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, field_0x0268));
if (dusk::menu_pointer::consume_click()) {
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerYesNoSelectTarget, field_0x0268));
}
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerYesNoSelectTarget, field_0x0268));
}
#endif
bool isYnSelMove = yesnoSelectMoveAnm();
-1
View File
@@ -1960,7 +1960,6 @@ bool dMenu_Collect2D_c::pointerWait() {
if (getItemTag(x, y, true) == 0 || !dusk::menu_pointer::hit_pane(mpSelPm[x][y], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(static_cast<dusk::menu_pointer::TargetId>(x + y * 7));
if (mCursorX != x || mCursorY != y) {
mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0);
mCursorX = x;
-1
View File
@@ -320,7 +320,6 @@ bool dMenu_Insect_c::pointerWait() {
if (!isGetInsect(x, y) || !dusk::menu_pointer::hit_pane(mpINSParent[index], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(index);
if (field_0xf4 != x || field_0xf5 != y) {
field_0xf4 = x;
-1
View File
@@ -482,7 +482,6 @@ bool dMenu_Letter_c::pointerWait() {
if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(i);
if (mIndex != i) {
mIndex = i;
+5 -45
View File
@@ -83,12 +83,6 @@ enum SelectType {
SelectType8,
};
#if TARGET_PC
static dusk::menu_pointer::TargetId option_yes_no_target(u8 index) noexcept {
return static_cast<dusk::menu_pointer::TargetId>(0x100 + index);
}
#endif
dMenu_Option_c::dMenu_Option_c(JKRArchive* i_archive, STControl* i_stick) {
mUseFlag = 0;
mBarScale[0] = g_drawHIO.mOptionScreen.mBarScale[0];
@@ -1104,28 +1098,18 @@ void dMenu_Option_c::confirm_move_move() {
if (!dusk::menu_pointer::hit_pane(mpYesNoSelBase_c[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(option_yes_no_target(i));
const bool clicked = dusk::menu_pointer::consume_click();
if (field_0x3f9 != i) {
Z2GetAudioMgr()->seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0, 1.0f, 1.0f, -1.0f,
-1.0f, 0);
field_0x3fa = field_0x3f9;
field_0x3f9 = i;
if (clicked) {
yesNoSelectStart();
field_0x3ef = SelectType7;
dMeter2Info_set2DVibrationM();
mpWarning->_move();
setAnimation();
return;
}
yesnoSelectAnmSet();
field_0x3ef = SelectType6;
mpWarning->_move();
setAnimation();
return;
}
if (clicked) {
if (dusk::menu_pointer::consume_click()) {
yesNoSelectStart();
field_0x3ef = SelectType7;
dMeter2Info_set2DVibrationM();
@@ -1172,36 +1156,11 @@ void dMenu_Option_c::confirm_select_init() {
}
void dMenu_Option_c::confirm_select_move() {
#if TARGET_PC
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
if (field_0x3f9 != 0xff &&
dusk::menu_pointer::hit_pane(mpYesNoSelBase_c[field_0x3f9], 8.0f))
{
const dusk::menu_pointer::TargetId target = option_yes_no_target(field_0x3f9);
dusk::menu_pointer::set_hover_target(target);
if (dusk::menu_pointer::consume_click()) {
dusk::menu_pointer::defer_activation(dusk::menu_pointer::Context::Options, target);
}
}
#endif
u8 selectMoveAnm = yesnoSelectMoveAnm();
u8 wakuAlphaAnm = yesnoWakuAlpahAnm(field_0x3fa);
if (selectMoveAnm == 1 && wakuAlphaAnm == 1) {
yesnoCursorShow();
#if TARGET_PC
if (field_0x3f9 != 0xff &&
dusk::menu_pointer::consume_deferred_activation(
dusk::menu_pointer::Context::Options, option_yes_no_target(field_0x3f9)))
{
yesNoSelectStart();
field_0x3ef = SelectType7;
dMeter2Info_set2DVibrationM();
mpWarning->_move();
setAnimation();
return;
}
#endif
field_0x3ef = SelectType5;
}
mpWarning->_move();
@@ -2237,14 +2196,16 @@ bool dMenu_Option_c::isRumbleSupported() {
#if TARGET_PC
bool dMenu_Option_c::pointerConfirmSelect() {
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
if (!dusk::menu_pointer::state().clicked) {
return false;
}
for (u8 i = 0; i < SelectType3; ++i) {
if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
dusk::menu_pointer::set_hover_target(i);
return false;
}
}
dusk::menu_pointer::set_hover_target(0x200);
if (!dusk::menu_pointer::consume_click()) {
return false;
}
@@ -2265,7 +2226,6 @@ bool dMenu_Option_c::dpdMenuMove() {
if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(i);
if (getSelectType() != i) {
field_0x3ef = i;
setCursorPos(i);
+1 -23
View File
@@ -198,7 +198,6 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i
mCursorInterpPrevAngular = false;
mCursorInterpCurrAngular = false;
mCursorInterpInit = false;
mPointerTouchPressHoveredCurrent = false;
#endif
for (int i = 0; i < 4; i++) {
field_0x674[i] = 0;
@@ -1562,10 +1561,6 @@ bool dMenu_Ring_c::pointerMove() {
if (hoveredSlot < 0) {
return false;
}
if (pointer.pressed) {
mPointerTouchPressHoveredCurrent = pointer.touch && hoveredSlot == mCurrentSlot;
}
dusk::menu_pointer::set_hover_target(static_cast<dusk::menu_pointer::TargetId>(hoveredSlot));
if (mCurrentSlot != hoveredSlot) {
mDirectSelectCursorPos.x = mItemSlotPosX[mCurrentSlot];
@@ -1578,27 +1573,10 @@ bool dMenu_Ring_c::pointerMove() {
return true;
}
const bool clickOpensExplain = !pointer.touch || mPointerTouchPressHoveredCurrent;
if (clickOpensExplain && dusk::menu_pointer::consume_click()) {
const u8 item = dComIfGs_getItem(mItemSlots[mCurrentSlot], false);
if (!dMeter2Info_isTouchKeyCheck(0xe) && openExplain(item)) {
dMeter2Info_setItemExplainWindowStatus(1);
field_0x6c4 = mCurrentSlot;
setStatus(STATUS_EXPLAIN);
dMeter2Info_set2DVibration();
setDoStatus(0);
} else {
Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f,
-1.0f, 0);
}
mPointerTouchPressHoveredCurrent = false;
if (dusk::menu_pointer::consume_click()) {
return true;
}
if (pointer.released) {
mPointerTouchPressHoveredCurrent = false;
}
return false;
}
#endif
+10 -16
View File
@@ -1820,7 +1820,6 @@ bool dMenu_save_c::pointerSaveSelect() {
if (!dusk::menu_pointer::hit_pane(mpSelData[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerSaveSelectTarget, i));
const bool clicked = dusk::menu_pointer::consume_click();
if (mSelectedFile != i) {
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
@@ -1849,7 +1848,6 @@ bool dMenu_save_c::pointerYesNoSelect(bool errorSelect, u8 errParam, u8 soundPar
if (!dusk::menu_pointer::hit_pane(mpNoYes[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, i));
const bool clicked =
(!errorSelect || mYesNoCursor == i) && dusk::menu_pointer::consume_click();
if (mYesNoCursor != i) {
@@ -1954,14 +1952,12 @@ void dMenu_save_c::saveSelectMoveAnime() {
#if TARGET_PC
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save);
if (mSelectedFile != 0xFF &&
dusk::menu_pointer::hit_pane(mpSelData[mSelectedFile], 8.0f))
dusk::menu_pointer::hit_pane(mpSelData[mSelectedFile], 8.0f) &&
dusk::menu_pointer::consume_click())
{
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerSaveSelectTarget, mSelectedFile));
if (dusk::menu_pointer::consume_click()) {
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::Save,
pointer_target(s_pointerSaveSelectTarget, mSelectedFile));
}
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::Save,
pointer_target(s_pointerSaveSelectTarget, mSelectedFile));
}
#endif
bool bookWakuAnmComplete = true;
@@ -2134,14 +2130,12 @@ void dMenu_save_c::yesNoCursorMoveAnm() {
#if TARGET_PC
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save);
if (mYesNoCursor != 0xFF &&
dusk::menu_pointer::hit_pane(mpNoYes[mYesNoCursor], 8.0f))
dusk::menu_pointer::hit_pane(mpNoYes[mYesNoCursor], 8.0f) &&
dusk::menu_pointer::consume_click())
{
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor));
if (dusk::menu_pointer::consume_click()) {
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::Save,
pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor));
}
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::Save,
pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor));
}
#endif
bool selAnmComplete = yesnoSelectMoveAnm(0);
-1
View File
@@ -316,7 +316,6 @@ bool dMenu_Skill_c::pointerWait() {
if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) {
continue;
}
dusk::menu_pointer::set_hover_target(i);
if (mIndex != i) {
mIndex = i;
+1 -1
View File
@@ -594,7 +594,7 @@ BOOL dMeter2Info_c::isDirectUseItem(int param_0) {
return (mDirectUseItem & (u8)(1 << param_0)) ? TRUE : FALSE;
}
DUSK_GAME_DATA dMeter2Info_c g_meter2_info;
dMeter2Info_c g_meter2_info;
int dMeter2Info_c::setMeterString(s32 i_string) {
if (mMeterString != 0) {
+1 -2
View File
@@ -560,8 +560,7 @@ bool dMsgScrn3Select_c::pointerMove() {
mDPDPoint = choice;
field_0x110 = paneIndex;
dusk::menu_pointer::set_hover_target(choice);
dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::peek_click());
dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::state().clicked);
return true;
}
+17 -77
View File
@@ -17,7 +17,6 @@
#include <utility>
#include "dusk/action_bindings.h"
#include "dusk/logging.h"
#include "dusk/main.h"
using namespace dusk::config;
@@ -28,9 +27,8 @@ using json = nlohmann::json;
aurora::Module DuskConfigLog("dusk::config");
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;
static absl::flat_hash_map<std::string_view, ConfigVarBase*> RegisteredConfigVars;
static bool RegistrationDone = false;
static std::optional<dusk::ui::ControlAnchor> parse_control_anchor(std::string_view value) {
if (value == "none") {
@@ -150,23 +148,17 @@ static void ReplaceFile(const std::filesystem::path& source, const std::filesyst
}
}
ConfigVarBase::ConfigVarBase(std::string name, const ConfigImplBase* impl) : name(std::move(name)), registered(false), layer(ConfigVarLayer::Default), impl(impl) {
}
ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl)
: name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {}
const char* ConfigVarBase::getName() const noexcept {
return name.c_str();
return name;
}
const ConfigImplBase* ConfigVarBase::getImpl() const noexcept {
return impl;
}
ConfigVarBase::~ConfigVarBase() {
if (registered) {
DuskLog.fatal("CVar '{}' was destroyed while still registered!", name);
}
}
template <typename T>
static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
if constexpr (std::is_enum_v<T>) {
@@ -393,37 +385,17 @@ template class ConfigImpl<dusk::ui::ControlLayout>;
} // namespace dusk::config
void dusk::config::Register(ConfigVarBase& configVar) {
const std::string_view name = configVar.getName();
const auto& name = configVar.getName();
if (RegistrationDone) {
DuskConfigLog.fatal("Tried to register CVar {} after registrations closed!", name);
}
if (RegisteredConfigVars.contains(name)) {
DuskConfigLog.fatal("Tried to register CVar {} twice!", name);
}
RegisteredConfigVars[name] = &configVar;
configVar.markRegistered();
const auto unregPair = UnregisteredConfigVars.find(name);
if (unregPair != UnregisteredConfigVars.end()) {
const auto value = std::move(unregPair->second);
UnregisteredConfigVars.erase(name);
try {
configVar.getImpl()->loadFromJson(configVar, value);
} catch (std::exception& e) {
DuskConfigLog.error("Failed to load key '{}' from config value: {}", name, e.what());
}
}
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, value);
} catch (std::exception& e) {
DuskConfigLog.error("Failed to load key '{}' from override arg: {}", name, e.what());
}
}
}
void ConfigVarBase::markRegistered() {
@@ -433,11 +405,8 @@ void ConfigVarBase::markRegistered() {
registered = true;
}
void ConfigVarBase::unmarkRegistered() {
if (!registered)
abort();
registered = false;
void dusk::config::FinishRegistration() {
RegistrationDone = true;
}
void dusk::config::LoadFromUserPreferences() {
@@ -458,16 +427,11 @@ static void LoadFromPath(const char* path) {
return;
}
UnregisteredConfigVars.clear();
for (const auto& el : j.items()) {
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());
DuskConfigLog.error("Unknown key '{}' found in config!", key);
continue;
}
@@ -480,6 +444,10 @@ static void LoadFromPath(const char* path) {
}
void dusk::config::LoadFromFileName(const char* path) {
if (!RegistrationDone) {
DuskConfigLog.fatal("Registration not finished yet!");
}
DuskConfigLog.info("Loading config from '{}'", path);
try {
@@ -497,20 +465,6 @@ void dusk::config::LoadFromFileName(const char* path) {
}
}
void dusk::config::LoadArgOverride(std::string_view name, std::string_view value) {
const auto cVar = GetConfigVar(name);
if (!cVar) {
UnregisteredConfigVarOverrides.emplace(name, name);
return;
}
try {
cVar->getImpl()->loadFromArg(*cVar, value);
} catch (const std::exception& e) {
DuskLog.fatal("Unable to parse: '{}': {}", value, e.what());
}
}
void dusk::config::Save() {
const auto configJsonPath = GetConfigJsonPath();
if (configJsonPath.empty()) {
@@ -529,10 +483,6 @@ void dusk::config::Save() {
}
}
for (const auto& pair : UnregisteredConfigVars) {
j[pair.first] = pair.second;
}
try {
const auto tempConfigJsonPath = GetTempConfigJsonPath(configJsonPath);
io::FileStream::WriteAllText(tempConfigJsonPath, j.dump(4));
@@ -563,13 +513,3 @@ void dusk::config::EnumerateRegistered(std::function<void(ConfigVarBase&)> callb
callback(*pair.second);
}
}
void dusk::config::Shutdown() {
for (auto& pair : RegisteredConfigVars) {
pair.second->unmarkRegistered();
}
RegisteredConfigVars.clear();
UnregisteredConfigVars.clear();
UnregisteredConfigVarOverrides.clear();
}
-17
View File
@@ -1,17 +0,0 @@
#include "dusk/gx_helper.h"
GXTexObjRAII::~GXTexObjRAII() { GXDestroyTexObj(this); }
void GXTexObjRAII::reset() { GXDestroyTexObj(this); }
GXScopedDebugGroup::GXScopedDebugGroup(const char* text) {
#if DUSK_GFX_DEBUG_GROUPS
GXPushDebugGroup(text);
#else
(void)text;
#endif
}
GXScopedDebugGroup::~GXScopedDebugGroup() {
#if DUSK_GFX_DEBUG_GROUPS
GXPopDebugGroup();
#endif
}
-156
View File
@@ -1,156 +0,0 @@
#include "dusk/hook_system.hpp"
#include "dusk/logging.h"
#include <cstdint>
#include <cstring>
#include <funchook.h>
#include <string>
#include <unordered_map>
#include <vector>
namespace dusk {
namespace modding {
extern thread_local void* g_dusk_hook_current_mod;
}
struct PreHookFn {
void* mod;
int32_t (*fn)(void* args);
};
struct VoidHookFn {
void* mod;
const char* mod_name;
void (*fn)(void* args, void* retval);
};
struct HookSlot {
std::vector<PreHookFn> pre;
VoidHookFn replace = {};
std::vector<VoidHookFn> post;
};
static std::unordered_map<uintptr_t, HookSlot> s_registry;
static std::unordered_map<uintptr_t, void*> s_installed;
// Follow E9/FF25 chains to skip MSVC incremental-link and import stubs
static void* resolveImportThunk(void* addr) {
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
for (int i = 0; i < 8; ++i) {
const auto* p = static_cast<const uint8_t*>(addr);
if (p[0] == 0xFF && p[1] == 0x25) {
int32_t offset;
std::memcpy(&offset, p + 2, 4);
addr = const_cast<void*>(*reinterpret_cast<const void* const*>(p + 6 + offset));
break;
} else if (p[0] == 0xE9) {
int32_t offset;
std::memcpy(&offset, p + 1, 4);
addr = const_cast<uint8_t*>(p) + 5 + offset;
} else {
break;
}
}
#endif
return addr;
}
struct ModGuard {
void* prev;
explicit ModGuard(void* mod) : prev(modding::g_dusk_hook_current_mod) { modding::g_dusk_hook_current_mod = mod; }
~ModGuard() { modding::g_dusk_hook_current_mod = prev; }
};
void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store) {
fn_addr = resolveImportThunk(fn_addr);
auto key = reinterpret_cast<uintptr_t>(fn_addr);
auto it = s_installed.find(key);
if (it != s_installed.end()) {
*orig_store = it->second;
return;
}
funchook_t* fh = funchook_create();
void* fn = fn_addr;
int prep = funchook_prepare(fh, &fn, tramp_fn);
int inst = (prep == 0) ? funchook_install(fh, 0) : -1;
if (prep != 0 || inst != 0) {
DuskLog.warn(
"HookSystem: funchook failed for {:p} (prepare={} install={})", fn_addr, prep, inst);
funchook_destroy(fh);
return;
}
funchook_destroy(fh);
s_installed[key] = fn;
*orig_store = fn;
}
bool hookDispatchPre(void* fn_addr, void* args, void* retval) {
auto it = s_registry.find(reinterpret_cast<uintptr_t>(fn_addr));
if (it == s_registry.end()) {
return false;
}
auto& slot = it->second;
for (auto& h : slot.pre) {
ModGuard g(h.mod);
if (h.fn(args) != 0) {
return true;
}
}
if (slot.replace.fn) {
ModGuard g(slot.replace.mod);
slot.replace.fn(args, retval);
return true;
}
return false;
}
void hookDispatchPost(void* fn_addr, void* args, void* retval) {
auto it = s_registry.find(reinterpret_cast<uintptr_t>(fn_addr));
if (it == s_registry.end()) {
return;
}
for (auto& h : it->second.post) {
if (h.fn) {
ModGuard g(h.mod);
h.fn(args, retval);
}
}
}
void hookRegisterPre(void* fn_addr, void* mod, int32_t (*fn)(void* args)) {
s_registry[reinterpret_cast<uintptr_t>(fn_addr)].pre.push_back({mod, fn});
}
void hookRegisterPost(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval)) {
s_registry[reinterpret_cast<uintptr_t>(fn_addr)].post.push_back({mod, mod_name, fn});
}
bool hookSetReplace(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval)) {
auto& slot = s_registry[reinterpret_cast<uintptr_t>(fn_addr)];
if (slot.replace.fn) {
DuskLog.error("HookSystem: '{}' conflicts with '{}', both replace the same function",
mod_name, slot.replace.mod_name);
return false;
}
slot.replace = {mod, mod_name, fn};
return true;
}
void hookClearMod(void* mod) {
for (auto& [addr, slot] : s_registry) {
auto erase = [&](auto& v) {
v.erase(
std::remove_if(v.begin(), v.end(), [mod](const auto& h) { return h.mod == mod; }),
v.end());
};
erase(slot.pre);
erase(slot.post);
if (slot.replace.mod == mod) {
slot.replace = {};
}
}
}
} // namespace dusk
+28
View File
@@ -375,6 +375,7 @@ namespace dusk {
void ImGuiConsole::PostDraw() {
m_menuTools.afterDraw();
ShowPipelineProgress();
}
void ImGuiConsole::UpdateDragScroll() {
@@ -523,4 +524,31 @@ namespace dusk {
return false;
}
void ImGuiConsole::ShowPipelineProgress() {
const auto* stats = aurora_get_stats();
const u32 queuedPipelines = stats->queuedPipelines;
if (queuedPipelines == 0 || !getSettings().backend.showPipelineCompilation) {
return;
}
const u32 createdPipelines = stats->createdPipelines;
const u32 totalPipelines = queuedPipelines + createdPipelines;
const auto* viewport = ImGui::GetMainViewport();
const auto padding = viewport->WorkPos.y + 10.f;
const auto halfWidth = viewport->GetWorkCenter().x;
ImGui::SetNextWindowPos(ImVec2{halfWidth, padding}, ImGuiCond_Always, ImVec2{0.5f, 0.f});
ImGui::SetNextWindowSize(ImVec2{halfWidth, 0.f}, ImGuiCond_Always);
ImGui::SetNextWindowBgAlpha(0.65f);
ImGui::Begin("Pipelines", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing);
const auto percent = static_cast<float>(createdPipelines) / static_cast<float>(totalPipelines);
const auto progressStr = fmt::format("Processing pipelines: {} / {}", createdPipelines, totalPipelines);
const auto textSize = ImGui::CalcTextSize(progressStr.data(), progressStr.data() + progressStr.size());
ImGui::NewLine();
ImGui::SameLine(ImGui::GetWindowWidth() / 2.f - textSize.x + textSize.x / 2.f);
ImGuiStringViewText(progressStr);
ImGui::ProgressBar(percent);
ImGui::End();
}
}
+1
View File
@@ -35,6 +35,7 @@ private:
// Keep always last
ImGuiMenuTools m_menuTools;
void ShowPipelineProgress();
void UpdateDragScroll();
};
-15
View File
@@ -1,15 +0,0 @@
/**
* Thin Windows launcher EXE. The game lives in dusk.dll, this just forwards
* the Windows entry point to it. Keeping the game as a DLL lets mod .dll
* files link against dusk.lib and resolve all game symbols at load time.
*/
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
// see src/dusk/main.cpp
int dusk_WinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR cmd, int show);
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR cmd, int show) {
return dusk_WinMain(hInst, hPrev, cmd, show);
}
+2 -3
View File
@@ -1,5 +1,5 @@
#if _WIN32
#define WIN32_LEAN_AND_MEAN
#define WINDOWS_LEAN_AND_MEAN
#include <Windows.h>
#include <shellapi.h>
#endif
@@ -224,8 +224,7 @@ int main(int argc, char* argv[]) {
}
#if _WIN32
// Entry point called by the launcher executable.
int __declspec(dllexport) dusk_WinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
return RunWindowsGuiEntryPoint();
}
#endif
+14 -142
View File
@@ -1,34 +1,16 @@
#include "dusk/menu_pointer.h"
#include "m_Do/m_Do_graphic.h"
#include "d/d_pane_class.h"
#include "dusk/settings.h"
#include "m_Do/m_Do_graphic.h"
#include <aurora/rmlui.hpp>
#include <dolphin/pad.h>
#include <algorithm>
#include <chrono>
namespace dusk::menu_pointer {
namespace {
using Clock = std::chrono::steady_clock;
constexpr auto kTapMaxDuration = std::chrono::milliseconds(300);
constexpr f32 kTapMoveThresholdDp = 12.0f;
struct Gesture {
bool active = false;
bool movedTooFar = false;
bool crossedTarget = false;
bool pressTargetValid = false;
Context pressContext = Context::None;
TargetId pressTarget = InvalidTarget;
f32 startX = 0.0f;
f32 startY = 0.0f;
Clock::time_point startedAt{};
};
State s_state;
bool s_clickConsumed = false;
Context s_lastContext = Context::None;
@@ -45,14 +27,7 @@ s32 s_mouseButton = -1;
u32 s_suppressedPadHoldMask = 0;
u32 s_suppressedPadNextReadMask = 0;
Context s_deferredActivationContext = Context::None;
TargetId s_deferredActivationTarget = InvalidTarget;
Gesture s_gesture;
bool s_hoverTargetValid = false;
TargetId s_hoverTarget = InvalidTarget;
bool s_clickPending = false;
Context s_clickContext = Context::None;
TargetId s_clickTarget = InvalidTarget;
bool s_clickTargetValid = false;
u8 s_deferredActivationTarget = 0xFF;
s32 scancode_from_rml_button(s32 button) noexcept {
switch (button) {
@@ -129,37 +104,6 @@ void suppress_pad_for_mouse_button(s32 button, bool held) noexcept {
}
}
f32 tap_move_threshold() noexcept {
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return kTapMoveThresholdDp;
}
return kTapMoveThresholdDp * std::max(context->GetDensityIndependentPixelRatio(), 1.0f);
}
void update_gesture_movement(f32 x, f32 y) noexcept {
if (!s_gesture.active || s_gesture.movedTooFar) {
return;
}
const f32 dx = x - s_gesture.startX;
const f32 dy = y - s_gesture.startY;
const f32 threshold = tap_move_threshold();
if (dx * dx + dy * dy > threshold * threshold) {
s_gesture.movedTooFar = true;
}
}
void clear_click_state() noexcept {
s_clickConsumed = false;
s_clickPending = false;
s_clickContext = Context::None;
s_clickTarget = InvalidTarget;
s_clickTargetValid = false;
s_state.clicked = false;
}
void set_position_from_rml(f32 x, f32 y) noexcept {
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
@@ -177,7 +121,7 @@ void set_position_from_rml(f32 x, f32 y) noexcept {
void clear_input_state() noexcept {
s_state = {};
clear_click_state();
s_clickConsumed = false;
s_lastDialogChoice = 0xFF;
s_currentDialogChoice = 0xFF;
s_lastDialogChoiceValid = false;
@@ -190,10 +134,7 @@ void clear_input_state() noexcept {
s_suppressedPadHoldMask = 0;
s_suppressedPadNextReadMask = 0;
s_deferredActivationContext = Context::None;
s_deferredActivationTarget = InvalidTarget;
s_gesture = {};
s_hoverTargetValid = false;
s_hoverTarget = InvalidTarget;
s_deferredActivationTarget = 0xFF;
}
} // namespace
@@ -203,6 +144,8 @@ bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouse
return false;
}
s_clickConsumed = false;
if (!touch) {
if (phase == Phase::Press) {
if (!mouse_button_is_menu_confirm(mouseButton)) {
@@ -231,41 +174,21 @@ bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouse
}
if (phase != Phase::Cancel) {
update_gesture_movement(x, y);
set_position_from_rml(x, y);
}
s_state.touch = touch;
switch (phase) {
case Phase::Press:
clear_click_state();
s_gesture = {
.active = true,
.startX = x,
.startY = y,
.startedAt = Clock::now(),
};
s_state.down = true;
s_state.pressed = true;
break;
case Phase::Release: {
const bool shortEnough =
s_gesture.active && Clock::now() - s_gesture.startedAt <= kTapMaxDuration;
const bool stillEnough = s_gesture.active && !s_gesture.movedTooFar;
const bool targetClean = s_gesture.active && !s_gesture.crossedTarget;
s_clickContext = s_gesture.pressContext;
s_clickTarget = s_gesture.pressTarget;
s_clickTargetValid = s_gesture.pressTargetValid;
s_clickPending = shortEnough && stillEnough && targetClean;
case Phase::Release:
s_state.down = false;
s_state.released = true;
s_state.clicked = s_clickPending;
s_gesture = {};
s_state.clicked = true;
break;
}
case Phase::Cancel:
clear_click_state();
s_gesture = {};
s_state.down = false;
break;
case Phase::Move:
@@ -288,12 +211,6 @@ void begin_game_frame() noexcept {
}
void end_game_frame() noexcept {
if (s_gesture.active && s_gesture.pressTargetValid &&
s_currentContext == s_gesture.pressContext && !s_hoverTargetValid)
{
s_gesture.crossedTarget = true;
}
s_lastContext = s_currentContext;
s_lastDialogChoice = s_currentDialogChoice;
s_lastDialogChoiceValid = s_currentDialogChoiceValid;
@@ -305,12 +222,6 @@ void end_game_frame() noexcept {
s_state.valid = false;
}
s_clickConsumed = false;
s_clickPending = false;
s_clickContext = Context::None;
s_clickTarget = InvalidTarget;
s_clickTargetValid = false;
s_hoverTargetValid = false;
s_hoverTarget = InvalidTarget;
}
void begin_context(Context context) noexcept {
@@ -326,11 +237,7 @@ void begin_context(Context context) noexcept {
s_suppressedPadHoldMask = 0;
s_suppressedPadNextReadMask = 0;
s_deferredActivationContext = Context::None;
s_deferredActivationTarget = InvalidTarget;
s_gesture = {};
s_hoverTargetValid = false;
s_hoverTarget = InvalidTarget;
clear_click_state();
s_deferredActivationTarget = 0xFF;
}
s_currentContext = context;
@@ -352,50 +259,15 @@ const State& state() noexcept {
return s_state;
}
void set_hover_target(TargetId target) noexcept {
s_hoverTargetValid = true;
s_hoverTarget = target;
if (s_gesture.active && !s_gesture.pressTargetValid && s_state.down) {
s_gesture.pressContext = s_currentContext;
s_gesture.pressTarget = target;
s_gesture.pressTargetValid = true;
}
if (s_gesture.active && s_gesture.pressTargetValid &&
(s_currentContext != s_gesture.pressContext || target != s_gesture.pressTarget))
{
s_gesture.crossedTarget = true;
}
}
bool click_matches_hover_target() noexcept {
if (!s_clickPending || !s_hoverTargetValid) {
return false;
}
if (!s_clickTargetValid) {
return true;
}
return s_currentContext == s_clickContext && s_hoverTarget == s_clickTarget;
}
bool consume_click() noexcept {
if (s_clickConsumed || !click_matches_hover_target()) {
if (!s_state.clicked || s_clickConsumed) {
return false;
}
s_clickConsumed = true;
s_clickPending = false;
s_state.clicked = false;
return true;
}
bool peek_click() noexcept {
return !s_clickConsumed && click_matches_hover_target();
}
void set_dialog_choice(u8 choice, bool clicked) noexcept {
s_currentDialogChoice = choice;
s_currentDialogChoiceValid = true;
@@ -428,18 +300,18 @@ bool consume_dialog_click(u8& choice) noexcept {
return false;
}
void defer_activation(Context context, TargetId target) noexcept {
void defer_activation(Context context, u8 target) noexcept {
s_deferredActivationContext = context;
s_deferredActivationTarget = target;
}
bool consume_deferred_activation(Context context, TargetId target) noexcept {
bool consume_deferred_activation(Context context, u8 target) noexcept {
if (s_deferredActivationContext != context || s_deferredActivationTarget != target) {
return false;
}
s_deferredActivationContext = Context::None;
s_deferredActivationTarget = InvalidTarget;
s_deferredActivationTarget = 0xFF;
return true;
}
@@ -449,7 +321,7 @@ void clear_deferred_activation(Context context) noexcept {
}
s_deferredActivationContext = Context::None;
s_deferredActivationTarget = InvalidTarget;
s_deferredActivationTarget = 0xFF;
}
u32 suppressed_pad_buttons(u32 port) noexcept {
-61
View File
@@ -1,61 +0,0 @@
#include <utility>
#include "dusk/io.hpp"
#include "mod_loader.hpp"
namespace fs = std::filesystem;
namespace dusk::modding {
ModBundleDisk::ModBundleDisk(fs::path root) : root_path(std::move(root)) {}
std::vector<u8> ModBundleDisk::readFile(const std::string& fileName) {
return io::FileStream::ReadAllBytes(toRealPath(fileName));
}
std::vector<std::string> ModBundleDisk::getFileNames() {
std::vector<std::string> files;
std::error_code ec;
for (fs::recursive_directory_iterator it(root_path,
fs::directory_options::skip_permission_denied |
fs::directory_options::follow_directory_symlink,
ec);
it != fs::recursive_directory_iterator(); it.increment(ec))
{
if (ec) {
break;
}
if (!it->is_regular_file()) {
continue;
}
const auto& path = it->path();
const auto relPath = fs::relative(path, root_path);
auto string = io::fs_path_to_string(relPath);
if constexpr (fs::path::preferred_separator != '/') {
// Convert \ to / on Windows
for (auto& chr : string) {
if (chr == fs::path::preferred_separator) {
chr = '/';
}
}
}
files.emplace_back(std::move(string));
}
return files;
}
size_t ModBundleDisk::getFileSize(const std::string& fileName) {
return std::filesystem::file_size(toRealPath(fileName));
}
std::filesystem::path ModBundleDisk::toRealPath(const std::string& fileName) const {
const fs::path filePath = reinterpret_cast<const char8_t*>(fileName.c_str());
return root_path / filePath;
}
} // namespace dusk::modding
-65
View File
@@ -1,65 +0,0 @@
#include "fmt/format.h"
#include "mod_loader.hpp"
#include <span>
namespace dusk::modding {
ModBundleZip::ModBundleZip(std::vector<u8>&& data) : zip_data(std::move(data)) {
if (!mz_zip_reader_init_mem(&res_zip, zip_data.data(), zip_data.size(), 0)) {
const auto error = mz_zip_get_last_error(&res_zip);
throw std::runtime_error(
fmt::format("Opening zip failed: {}", mz_zip_get_error_string(error)));
}
}
ModBundleZip::~ModBundleZip() {
mz_zip_reader_end(&res_zip);
}
std::vector<u8> ModBundleZip::readFile(const std::string& fileName) {
size_t size;
const auto ptr = mz_zip_reader_extract_file_to_heap(&res_zip, fileName.c_str(), &size, 0);
if (!ptr) {
throw std::runtime_error(fmt::format("File does not exist: {}", fileName));
}
std::span data(static_cast<u8*>(ptr), size);
std::vector vec(data.begin(), data.end());
mz_free(ptr);
return vec;
}
std::vector<std::string> ModBundleZip::getFileNames() {
std::vector<std::string> results;
for (mz_uint i = 0, n = mz_zip_reader_get_num_files(&res_zip); i < n; ++i) {
mz_zip_archive_file_stat stat{};
if (!mz_zip_reader_file_stat(&res_zip, i, &stat)) {
continue;
}
if (mz_zip_reader_is_file_a_directory(&res_zip, i)) {
continue;
}
results.emplace_back(stat.m_filename);
}
return results;
}
size_t ModBundleZip::getFileSize(const std::string& fileName) {
const auto idx = mz_zip_reader_locate_file(&res_zip, fileName.c_str(), nullptr, 0);
if (idx < 0) {
throw std::runtime_error(fmt::format("Unable to locate file in zip: {}", fileName));
}
mz_zip_archive_file_stat stat{};
mz_zip_reader_file_stat(&res_zip, idx, &stat);
return stat.m_uncomp_size;
}
} // namespace dusk::modding
-445
View File
@@ -1,445 +0,0 @@
#include "dusk/mod_loader.hpp"
#include "dusk/hook_system.hpp"
#include "dusk/logging.h"
#include "mod_loader.hpp"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <string>
#include "aurora/dvd.h"
#include "dusk/config.hpp"
#include "dusk/io.hpp"
#include "miniz.h"
#include "native_module.hpp"
#include "nlohmann/json.hpp"
static aurora::Module Log("dusk::modLoader");
using namespace dusk::modding;
using namespace std::string_literals;
using namespace std::string_view_literals;
#if defined(_M_ARM64) || defined(__aarch64__)
static constexpr std::string_view k_archSuffix = "_arm64"sv;
#elif defined(_M_X64) || defined(__x86_64__)
static constexpr std::string_view k_archSuffix = "_x64"sv;
#elif defined(_M_IX86) || defined(__i386__)
static constexpr std::string_view k_archSuffix = "_x86"sv;
#else
static constexpr std::string_view k_archSuffix = ""sv;
#endif
static dusk::ModLoader g_modLoader;
// We cannot delete config vars registered by mods until the game shuts down fully.
// Therefore, orphan them during shutdown.
static std::vector<std::unique_ptr<dusk::ConfigVarBase>> OrphanedConfigVars;
namespace dusk {
ModLoader& ModLoader::instance() {
return g_modLoader;
}
static std::unique_ptr<ModBundle> loadBundle(const std::filesystem::path& modPath, bool fromDir) {
if (fromDir) {
return std::make_unique<ModBundleDisk>(modPath);
} else {
std::vector<u8> data = io::FileStream::ReadAllBytes(modPath);
return std::make_unique<ModBundleZip>(std::move(data));
}
}
struct DllLocateResult {
std::string primary;
std::string fallback;
};
static std::string_view getFileNameWithoutExtension(const std::string_view fileName) {
return fileName.substr(0, fileName.find_last_of('.'));
}
static DllLocateResult LocateDllInBundle(ModBundle& bundle) {
std::string dllEntry, dllFallback;
for (const auto& name : bundle.getFileNames()) {
if (!name.ends_with(".dll"sv) && !name.ends_with(".dylib"sv) && !name.ends_with(".so"sv)) {
continue;
}
if (!k_archSuffix.empty() && getFileNameWithoutExtension(name).ends_with(k_archSuffix)) {
dllEntry = name;
} else if (dllFallback.empty()) {
dllFallback = name;
}
}
return DllLocateResult{dllEntry, dllFallback};
}
class InvalidModDataException : public std::runtime_error {
public:
explicit InvalidModDataException(const std::string& msg) : runtime_error(msg) {}
explicit InvalidModDataException(const char* msg) : runtime_error(msg) {}
};
static void validateModId(std::string_view const str) {
if (str.empty()) {
throw InvalidModDataException("Missing ID value in mod metadata!");
}
bool lastWasPeriod = false;
for (auto const chr : str) {
if (chr == '.') {
if (lastWasPeriod) {
throw InvalidModDataException("Cannot have two consecutive periods in mod ID!");
}
lastWasPeriod = true;
continue;
}
lastWasPeriod = false;
if (chr == '_')
continue;
if (chr >= '0' && chr <= '9')
continue;
if (chr >= 'a' && chr <= 'z')
continue;
if (chr >= 'A' && chr <= 'Z')
continue;
throw InvalidModDataException(fmt::format("Invalid character '{}' in mod ID. Valid characters are period, underscore, and alphanumerics.", chr));
}
}
static ModMetadata loadMetadata(const std::filesystem::path& modPath, ModBundle& bundle) {
const auto metaJson = bundle.readFile("mod.json");
auto j = nlohmann::json::parse(metaJson);
std::string metaId = j.value("id", "");
std::string metaName = j.value("name", "");
std::string metaVersion = j.value("version", "");
std::string metaAuthor = j.value("author", "");
std::string metaDescription = j.value("description", "");
const bool hasCode = j.value("has_code", false);
validateModId(metaId);
if (metaName.empty()) {
metaName = io::fs_path_to_string(modPath.stem());
}
if (metaVersion.empty()) {
metaVersion = "?"s;
}
if (metaAuthor.empty()) {
metaAuthor = "unknown"s;
}
return ModMetadata{
std::move(metaId),
std::move(metaName),
std::move(metaVersion),
std::move(metaAuthor),
std::move(metaDescription),
hasCode,
};
}
template <std::ranges::input_range TIter>
bool checkDuplicateMod(
const ModMetadata& metadata, TIter mods) {
return std::ranges::any_of(mods,
[&](const LoadedMod& mod) { return mod.metadata.id == metadata.id; });
}
void ModLoader::tryLoadNativeMod(LoadedMod& mod) {
if (!EnableCodeMods) {
Log.error("Code mods are not available in this build");
mod.native_status = NativeModStatus::BuildDisabled;
return;
}
namespace fs = std::filesystem;
auto [dllEntry, dllFallback] = LocateDllInBundle(*mod.bundle);
if (dllEntry.empty()) {
dllEntry = dllFallback;
}
if (dllEntry.empty()) {
Log.error(
"no *{} found in {} — skipping", NativeModule::LibraryExtension, mod.metadata.id);
mod.native_status = NativeModStatus::ModMissingPlatform;
return;
}
const fs::path cacheDir = m_modsDir / ".cache" / mod.metadata.id;
std::error_code ec;
fs::create_directories(cacheDir, ec);
const fs::path dllCachePath = cacheDir / fs::path(dllEntry).filename();
std::vector<u8> dllData;
try {
dllData = mod.bundle->readFile(dllEntry);
} catch (const std::runtime_error& e) {
Log.error(
"failed to extract {} from {}", dllEntry, mod.metadata.id);
return;
}
{
std::ofstream out(dllCachePath, std::ios::binary | std::ios::out);
if (!out) {
Log.error("failed to write {}", io::fs_path_to_string(dllCachePath));
return;
}
out.write(
reinterpret_cast<const char*>(dllData.data()),
static_cast<std::streamsize>(dllData.size()));
}
auto nativeMod = std::make_unique<NativeMod>();
try {
nativeMod->handle = std::make_unique<NativeModule>(dllCachePath);
} catch (const std::runtime_error& e) {
Log.error("failed to open {}: {}", io::fs_path_to_string(dllCachePath), e.what());
return;
}
const auto mod_api_ver = nativeMod->handle->LookupSymbol<uint32_t*>("mod_api_version");
if (mod_api_ver && *mod_api_ver != DUSK_MOD_API_VERSION) {
Log.error("{} expects API v{} but engine is v{}, skipping",
io::fs_path_to_string(fs::path(dllEntry).filename()), *mod_api_ver, DUSK_MOD_API_VERSION);
mod.native_status = NativeModStatus::ApiVersionMismatch;
return;
}
nativeMod->fn_init = nativeMod->handle->LookupSymbol<NativeMod::FnInit>("mod_init");
nativeMod->fn_tick = nativeMod->handle->LookupSymbol<NativeMod::FnTick>("mod_tick");
nativeMod->fn_cleanup = nativeMod->handle->LookupSymbol<NativeMod::FnCleanup>("mod_cleanup");
if (!nativeMod->fn_init || !nativeMod->fn_tick) {
Log.error("{} missing mod_init or mod_tick — skipping",
io::fs_path_to_string(fs::path(dllEntry).filename()));
return;
}
mod.dir = io::fs_path_to_string(fs::absolute(cacheDir));
mod.native = std::move(nativeMod);
mod.native_status = NativeModStatus::Loaded;
}
static std::string escapeModIdForConfig(std::string_view const id) {
std::string buf;
// Simple escaping. All characters in mod IDs literal, except for '.' and '_'.
// '.' -> '_', '_' -> '__'
for (char const chr : id) {
if (chr == '.') {
buf.push_back('_');
} else if (chr == '_') {
buf.push_back('_');
buf.push_back('_');
} else {
buf.push_back(chr);
}
}
return buf;
}
static std::string modEnabledCVarName(std::string_view const id) {
return fmt::format("mod.{}.enabled", escapeModIdForConfig(id));
}
void ModLoader::tryLoadDusk(const std::filesystem::path& modPath, bool fromDir) {
namespace fs = std::filesystem;
std::unique_ptr<ModBundle> bundle;
try {
bundle = loadBundle(modPath, fromDir);
} catch (const std::runtime_error& e) {
Log.error("Failed to open {} bundle: {}", io::fs_path_to_string(modPath.filename()), e.what());
return;
}
ModMetadata metadata;
try
{
metadata = loadMetadata(modPath, *bundle);
}
catch (const std::runtime_error& e) {
Log.error(
"bad mod.json in {}: {}", io::fs_path_to_string(modPath.filename()), e.what());
return;
}
if (checkDuplicateMod(metadata, mods())) {
Log.error(
"mod with id '{}' already exists, not loading {}",
metadata.id,
io::fs_path_to_string(modPath.filename()));
return;
}
const auto& inserted = m_mods.emplace_back(std::make_unique<LoadedMod>());
auto& mod = *inserted;
mod.active = true;
mod.mod_path = io::fs_path_to_string(fs::absolute(modPath));
mod.metadata = std::move(metadata);
mod.bundle = std::move(bundle);
mod.cvarIsEnabled = std::make_unique<ConfigVar<bool>>(modEnabledCVarName(mod.metadata.id), true);
if (mod.metadata.hasCode) {
mod.native_status = NativeModStatus::Unknown;
tryLoadNativeMod(mod);
// Native mod lod failure DOES NOT block insertion into m_mods.
// We still want to be able to present the failed load in the UI!
if (mod.native_status != NativeModStatus::Loaded) {
Log.error("Native mod '{}' failed to load, disabling", metadata.id);
mod.active = false;
}
}
Log.info(
"found '{}' ('{}') v{} by {} ({})",
mod.metadata.name,
mod.metadata.id,
mod.metadata.version,
mod.metadata.author,
io::fs_path_to_string(modPath.filename()));
}
void ModLoader::init() {
if (m_initialized) {
return;
}
m_initialized = true;
namespace fs = std::filesystem;
if (!fs::is_directory(m_modsDir)) {
Log.info(
"mods directory '{}' not found — mod loading skipped", io::fs_path_to_string(m_modsDir));
return;
}
std::error_code ec;
std::vector<fs::directory_entry> entries;
for (auto& e : fs::directory_iterator(m_modsDir, ec)) {
if (e.is_directory() && std::filesystem::exists(e.path() / "mod.json")) {
entries.push_back(e);
} else if (e.is_regular_file() && e.path().extension() == ".dusk") {
entries.push_back(e);
}
}
std::sort(entries.begin(), entries.end(),
[](const fs::directory_entry& a, const fs::directory_entry& b) {
return a.path().filename() < b.path().filename();
});
m_mods.reserve(entries.size());
for (auto& entry : entries) {
tryLoadDusk(entry.path(), entry.is_directory());
}
if (m_mods.empty()) {
Log.info("no mods found");
return;
}
Log.info("initializing {} mod(s)...", m_mods.size());
for (auto& mod : mods()) {
Register(*mod.cvarIsEnabled);
if (!mod.cvarIsEnabled->getValue()) {
Log.info("Mod '{}' is disabled by config", mod.metadata.id);
mod.active = false;
}
}
for (auto& mod : active_mods()) {
if (mod.native) {
buildAPI(mod);
}
}
for (auto& mod : active_mods()) {
if (!mod.native) {
continue;
}
Log.debug("Initializing '{}'", mod.metadata.id);
ModGuard guard(&mod);
try {
mod.native->fn_init(&mod.native->api);
if (!mod.load_failed) {
Log.info("'{}' initialized", mod.metadata.id);
} else {
mod.active = false;
Log.error("'{}' failed to load due to hook conflicts", mod.metadata.id);
}
} catch (const std::exception& e) {
mod.active = false;
Log.error("exception in {}.mod_init(): {}", mod.metadata.id, e.what());
} catch (...) {
mod.active = false;
Log.error("unknown exception in {}.mod_init()", mod.metadata.id);
}
}
initOverlayFiles();
auto active = std::ranges::count_if(mods(), [](const LoadedMod& m) { return m.active; });
Log.info("{}/{} mod(s) active", active, m_mods.size());
}
void ModLoader::tick() {
for (auto& mod : active_mods()) {
if (!mod.native) {
continue;
}
ModGuard guard(&mod);
try {
mod.native->fn_tick(&mod.native->api);
} catch (const std::exception& e) {
Log.error("exception in {}.mod_tick(): {} — disabling", mod.metadata.id, e.what());
mod.active = false;
} catch (...) {
Log.error("unknown exception in {}.mod_tick() — disabling", mod.metadata.id);
mod.active = false;
}
}
}
void ModLoader::shutdown() {
for (auto& mod : mods()) {
hookClearMod(&mod);
if (mod.native && mod.native->fn_cleanup) {
ModGuard guard(&mod);
try {
mod.native->fn_cleanup(&mod.native->api);
} catch (...) {
}
}
OrphanedConfigVars.emplace_back(std::move(mod.cvarIsEnabled));
}
m_mods.clear();
g_services.clear();
Log.info("all mods unloaded");
}
} // namespace dusk
-73
View File
@@ -1,73 +0,0 @@
#pragma once
#include <filesystem>
#include "miniz.h"
#include "dusk/mod_loader.hpp"
namespace dusk::modding {
#if DUSK_CODE_MODS
constexpr bool EnableCodeMods = true;
#else
constexpr bool EnableCodeMods = false;
#endif
class ModBundle {
public:
virtual ~ModBundle() = default;
virtual std::vector<u8> readFile(const std::string& fileName) = 0;
virtual std::vector<std::string> getFileNames() = 0;
virtual size_t getFileSize(const std::string& fileName) = 0;
};
class ModBundleZip final : public ModBundle {
public:
explicit ModBundleZip(std::vector<u8>&& data);
~ModBundleZip() override;
std::vector<u8> readFile(const std::string& fileName) override;
std::vector<std::string> getFileNames() override;
size_t getFileSize(const std::string& fileName) override;
private:
std::vector<uint8_t> zip_data;
mz_zip_archive res_zip{};
bool res_zip_open = false;
};
class ModBundleDisk final : public ModBundle {
public:
explicit ModBundleDisk(std::filesystem::path root);
~ModBundleDisk() override = default;
std::vector<u8> readFile(const std::string& fileName) override;
std::vector<std::string> getFileNames() override;
size_t getFileSize(const std::string& fileName) override;
private:
[[nodiscard]] std::filesystem::path toRealPath(const std::string& fileName) const;
std::filesystem::path root_path;
};
extern thread_local LoadedMod* g_currentMod;
extern std::unordered_map<std::string, void*> g_services;
extern thread_local void* g_dusk_hook_current_mod;
struct ModGuard {
explicit ModGuard(dusk::LoadedMod* m) {
g_currentMod = m;
g_dusk_hook_current_mod = m;
}
~ModGuard() {
g_currentMod = nullptr;
g_dusk_hook_current_mod = nullptr;
}
};
inline const char* modName() {
return g_currentMod ? g_currentMod->metadata.id.c_str() : "mod";
}
} // namespace dusk::modding
-285
View File
@@ -1,285 +0,0 @@
#include <RmlUi/Core.h>
#include "dusk/hook_system.hpp"
#include "dusk/logging.h"
#include "dusk/mod_api.h"
#include "dusk/mod_loader.hpp"
#include "mod_loader.hpp"
using namespace dusk::modding;
namespace dusk::modding {
thread_local LoadedMod* g_currentMod = nullptr;
std::unordered_map<std::string, void*> g_services;
thread_local void* g_dusk_hook_current_mod = nullptr;
}
namespace {
void cb_log_info(const char* fmt, ...) {
va_list ap, ap2;
va_start(ap, fmt);
va_copy(ap2, ap);
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0');
va_end(ap2);
vsnprintf(s.data(), s.size() + 1, fmt, ap);
va_end(ap);
DuskLog.info("[{}] {}", modName(), s);
}
void cb_log_warn(const char* fmt, ...) {
va_list ap, ap2;
va_start(ap, fmt);
va_copy(ap2, ap);
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0');
va_end(ap2);
vsnprintf(s.data(), s.size() + 1, fmt, ap);
va_end(ap);
DuskLog.warn("[{}] {}", modName(), s);
}
void cb_log_error(const char* fmt, ...) {
va_list ap, ap2;
va_start(ap, fmt);
va_copy(ap2, ap);
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0');
va_end(ap2);
vsnprintf(s.data(), s.size() + 1, fmt, ap);
va_end(ap);
DuskLog.error("[{}] {}", modName(), s);
}
void* cb_load_resource(const char* relative_path, size_t* out_size) {
if (out_size) {
*out_size = 0;
}
if (!g_currentMod || !relative_path) {
DuskLog.error("load_resource: called outside mod context or with null path");
return nullptr;
}
std::string entry = std::string("res/") + relative_path;
std::vector<u8> data;
try {
data = g_currentMod->bundle->readFile(entry);
} catch (const std::runtime_error& e) {
DuskLog.error("[{}] load_resource: '{}' failed: {}", g_currentMod->metadata.id, entry, e.what());
return nullptr;
}
const auto retPtr = std::malloc(data.size());
std::memcpy(retPtr, data.data(), data.size());
if (out_size) {
*out_size = data.size();
}
return retPtr;
}
void cb_free_resource(void* data) {
std::free(data);
}
class ModClickListener : public Rml::EventListener {
public:
ModClickListener(void (*cb)(void*), void* ud) : m_cb(cb), m_ud(ud) {}
void ProcessEvent(Rml::Event&) override { m_cb(m_ud); }
void OnDetach(Rml::Element*) override { delete this; }
private:
void (*m_cb)(void*);
void* m_ud;
};
std::string escape_rml(const char* text) {
std::string out;
for (const char* p = text; *p; ++p) {
switch (*p) {
case '&': out += "&amp;"; break;
case '<': out += "&lt;"; break;
case '>': out += "&gt;"; break;
default: out += *p; break;
}
}
return out;
}
void cb_panel_add_section(DuskPanelHandle panel, const char* text) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane || !text) {
return;
}
auto el = pane->GetOwnerDocument()->CreateElement("div");
el->SetClass("section-heading", true);
el->SetInnerRML(escape_rml(text));
pane->AppendChild(std::move(el));
}
void cb_panel_add_button(DuskPanelHandle panel, const char* label,
void (*cb)(void*), void* userdata) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane || !label || !cb) {
return;
}
auto btn = pane->GetOwnerDocument()->CreateElement("button");
btn->SetInnerRML(escape_rml(label));
btn->AddEventListener(Rml::EventId::Click, new ModClickListener(cb, userdata));
pane->AppendChild(std::move(btn));
}
DuskElemHandle cb_panel_add_badge_row(DuskPanelHandle panel, const char* label, int ok) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane || !label) {
return nullptr;
}
auto* doc = pane->GetOwnerDocument();
auto row = doc->CreateElement("div");
row->SetClass("mod-info-row", true);
auto badge = doc->CreateElement("span");
badge->SetClass("achievement-badge", true);
badge->SetClass(ok ? "unlocked" : "locked", true);
badge->SetInnerRML(ok ? "PASS" : "WAIT");
Rml::Element* badgePtr = row->AppendChild(std::move(badge));
auto lbl = doc->CreateElement("span");
lbl->SetClass("mod-info-value", true);
lbl->SetInnerRML(escape_rml(label));
row->AppendChild(std::move(lbl));
pane->AppendChild(std::move(row));
return static_cast<DuskElemHandle>(badgePtr);
}
DuskElemHandle cb_panel_add_dyn_text(DuskPanelHandle panel, const char* text) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane) {
return nullptr;
}
auto el = pane->GetOwnerDocument()->CreateElement("div");
el->SetInnerRML(text ? escape_rml(text) : std::string{});
Rml::Element* ptr = pane->AppendChild(std::move(el));
return static_cast<DuskElemHandle>(ptr);
}
void cb_elem_set_badge(DuskElemHandle elem, int ok) {
auto* el = static_cast<Rml::Element*>(elem);
if (!el) {
return;
}
el->SetClass("unlocked", ok != 0);
el->SetClass("locked", ok == 0);
el->SetInnerRML(ok ? "PASS" : "WAIT");
}
void cb_elem_set_text(DuskElemHandle elem, const char* text) {
auto* el = static_cast<Rml::Element*>(elem);
if (!el || !text) {
return;
}
el->SetInnerRML(escape_rml(text));
}
DuskElemHandle cb_panel_add_progress(DuskPanelHandle panel, float value) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane) {
return nullptr;
}
auto el = pane->GetOwnerDocument()->CreateElement("progress");
el->SetClass("progress-health", true);
el->SetAttribute("value", value);
Rml::Element* ptr = pane->AppendChild(std::move(el));
return static_cast<DuskElemHandle>(ptr);
}
void cb_elem_set_progress(DuskElemHandle elem, float value) {
auto* el = static_cast<Rml::Element*>(elem);
if (!el) {
return;
}
el->SetAttribute("value", value);
}
void cb_register_tab_content(void (*build_fn)(void*, void*), void* userdata) {
if (g_currentMod && build_fn) {
g_currentMod->tab_content.push_back({build_fn, userdata});
}
}
void cb_register_tab_update(void (*update_fn)(void*), void* userdata) {
if (g_currentMod && update_fn) {
g_currentMod->tab_updates.push_back({update_fn, userdata});
}
}
void cb_service_publish(const char* name, void* ptr) {
if (!name) {
return;
}
if (g_services.count(name)) {
DuskLog.error(
"[{}] service_publish: '{}' already published by another mod", modName(), name);
}
g_services[name] = ptr;
}
void* cb_service_get(const char* name) {
if (!name) {
return nullptr;
}
auto it = g_services.find(name);
return it != g_services.end() ? it->second : nullptr;
}
void api_hook_pre(void* addr, int32_t (*fn)(void* args)) {
dusk::hookRegisterPre(addr, g_currentMod, fn);
}
void api_hook_post(void* addr, void (*fn)(void* args, void* retval)) {
dusk::hookRegisterPost(addr, g_currentMod, modName(), fn);
}
void api_hook_replace(void* addr, void (*fn)(void* args, void* retval)) {
if (!dusk::hookSetReplace(addr, g_currentMod, modName(), fn)) {
if (g_currentMod) {
g_currentMod->load_failed = true;
}
}
}
}
namespace dusk {
void ModLoader::buildAPI(LoadedMod& mod) {
auto& native = *mod.native;
native.api.api_version = DUSK_MOD_API_VERSION;
native.api.mod_dir = mod.dir.c_str();
native.api.log_info = cb_log_info;
native.api.log_warn = cb_log_warn;
native.api.log_error = cb_log_error;
native.api.load_resource = cb_load_resource;
native.api.free_resource = cb_free_resource;
native.api.register_tab_content = cb_register_tab_content;
native.api.register_tab_update = cb_register_tab_update;
native.api.panel_add_section = cb_panel_add_section;
native.api.panel_add_button = cb_panel_add_button;
native.api.panel_add_badge_row = cb_panel_add_badge_row;
native.api.panel_add_dyn_text = cb_panel_add_dyn_text;
native.api.elem_set_badge = cb_elem_set_badge;
native.api.elem_set_text = cb_elem_set_text;
native.api.panel_add_progress = cb_panel_add_progress;
native.api.elem_set_progress = cb_elem_set_progress;
native.api.hook_install = hookInstallByAddr;
native.api.hook_pre = api_hook_pre;
native.api.hook_post = api_hook_post;
native.api.hook_replace = api_hook_replace;
native.api.hook_dispatch_pre = hookDispatchPre;
native.api.hook_dispatch_post = hookDispatchPost;
native.api.service_publish = cb_service_publish;
native.api.service_get = cb_service_get;
}
}
-111
View File
@@ -1,111 +0,0 @@
#include "aurora/dvd.h"
#include "aurora/lib/logging.hpp"
#include "dusk/mod_loader.hpp"
#include "mod_loader.hpp"
#include <cstring>
using namespace std::string_literals;
namespace {
aurora::Module Log("dusk::modLoader::overlay");
struct OverlayFileData {
std::string bundlePath;
dusk::LoadedMod* mod; // TODO: is using a raw pointer a bad idea here?
};
std::vector<OverlayFileData> s_overlayFiles;
void findOverlayFiles(std::vector<AuroraOverlayFile>& files, dusk::LoadedMod& mod) {
for (const auto& file : mod.bundle->getFileNames()) {
if (!file.starts_with("overlay/")) {
continue;
}
auto overlayPath = file.substr("overlay/"s.size());
assert(!overlayPath.starts_with('/'));
overlayPath.insert(0, "/");
const auto size = mod.bundle->getFileSize(file);
const auto index = s_overlayFiles.size();
s_overlayFiles.emplace_back(file, &mod);
files.emplace_back(
strdup(overlayPath.c_str()),
reinterpret_cast<void*>(index),
size);
}
}
struct OpenOverlayFile {
std::vector<u8> data;
size_t pos;
};
void* cbOpen(void* userdata) {
const auto index = reinterpret_cast<size_t>(userdata);
const auto& fileData = s_overlayFiles[index];
auto fileContents = fileData.mod->bundle->readFile(fileData.bundlePath);
return new OpenOverlayFile(std::move(fileContents), 0);
}
void cbClose(void* handle) {
const auto openFile = static_cast<OpenOverlayFile*>(handle);
delete openFile;
}
int64_t cbRead(void* handle, uint8_t *buf, const size_t len) {
auto& openFile = *static_cast<OpenOverlayFile*>(handle);
const auto remainingSpace = openFile.data.size() - openFile.pos;
const auto toRead = std::min(remainingSpace, len);
std::memcpy(buf, openFile.data.data() + openFile.pos, toRead);
openFile.pos += toRead;
return static_cast<int64_t>(toRead);
}
int64_t cbSeek(void* handle, int64_t offset, int32_t whence) {
if (whence != 0) {
Log.fatal("Invalid seek mode from aurora: {}", whence);
}
auto& openFile = *static_cast<OpenOverlayFile*>(handle);
const auto posSigned = std::clamp(offset, static_cast<int64_t>(0), static_cast<int64_t>(openFile.data.size()));
openFile.pos = static_cast<size_t>(posSigned);
return posSigned;
}
constexpr AuroraOverlayCallbacks s_overlayCallbacks = {
.open = cbOpen,
.close = cbClose,
.read = cbRead,
.seek = cbSeek,
};
}
namespace dusk {
void ModLoader::initOverlayFiles() {
Log.debug("Initializing overlay files...");
aurora_dvd_overlay_callbacks(&s_overlayCallbacks);
std::vector<AuroraOverlayFile> files;
for (auto& mod : active_mods()) {
findOverlayFiles(files, mod);
}
Log.debug("Found {} overlay files.", files.size());
aurora_dvd_overlay_files(files.data(), files.size(), nullptr);
for (const auto& file : files) {
std::free(const_cast<char*>(file.fileName));
}
}
} // namespace dusk
-83
View File
@@ -1,83 +0,0 @@
#include "native_module.hpp"
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#endif
namespace {
#if defined(_WIN32)
void* pl_dlopen(const std::filesystem::path& p) {
return LoadLibraryW(p.wstring().c_str());
}
void* pl_dlsym(void* h, const char* name) {
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(h), name));
}
void pl_dlclose(void* h) {
FreeLibrary(static_cast<HMODULE>(h));
}
std::string pl_dlerror() {
char buf[256]{};
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr,
GetLastError(), 0, buf, sizeof(buf), nullptr);
std::string s = buf;
while (!s.empty() && (s.back() == '\r' || s.back() == '\n')) {
s.pop_back();
}
return s;
}
#else
#include <dlfcn.h>
static void* pl_dlopen(const std::filesystem::path& p) {
#if defined(__linux__)
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL | RTLD_DEEPBIND);
#else
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL);
#endif
}
static void* pl_dlsym(void* h, const char* name) {
return dlsym(h, name);
}
static void pl_dlclose(void* h) {
dlclose(h);
}
static std::string pl_dlerror() {
const char* e = dlerror();
return e ? e : "(unknown error)";
}
#endif
}
namespace dusk::modding {
NativeModule::NativeModule() noexcept : handle(nullptr) {
}
NativeModule::NativeModule(NativeModule&& other) noexcept {
handle = other.handle;
other.handle = nullptr;
}
NativeModule& NativeModule::operator=(NativeModule&& other) noexcept {
handle = other.handle;
other.handle = nullptr;
return *this;
}
NativeModule::NativeModule(const std::filesystem::path& path) {
handle = pl_dlopen(path);
if (!handle) {
throw std::runtime_error(pl_dlerror());
}
}
NativeModule::~NativeModule() {
if (handle) {
pl_dlclose(handle);
}
}
void* NativeModule::LookupSymbol(const char* name) const {
return pl_dlsym(handle, name);
}
} // namespace dusk::modding
-35
View File
@@ -1,35 +0,0 @@
#pragma once
#include <filesystem>
namespace dusk::modding {
class NativeModule final {
public:
NativeModule() noexcept;
NativeModule(const NativeModule& other) = delete;
NativeModule(NativeModule&& other) noexcept;
explicit NativeModule(const std::filesystem::path& path);
~NativeModule();
void* LookupSymbol(const char* name) const;
template<typename T>
T LookupSymbol(const char* name) const {
return reinterpret_cast<T>(LookupSymbol(name));
}
NativeModule& operator=(NativeModule&& other) noexcept;
#if defined(_WIN32)
static constexpr auto LibraryExtension = ".dll";
#elif defined(__APPLE__)
static constexpr auto LibraryExtension = ".dylib";
#else
static constexpr auto LibraryExtension = ".so";
#endif
private:
void* handle;
};
}
+2
View File
@@ -160,6 +160,7 @@ UserSettings g_userSettings = {
.isoVerification {"backend.isoVerification", DiscVerificationState::Unknown},
.graphicsBackend {"backend.graphicsBackend", "auto"},
.skipPreLaunchUI {"backend.skipPreLaunchUI", false},
.showPipelineCompilation {"backend.showPipelineCompilation", false},
.wasPresetChosen {"backend.wasPresetChosen", false},
.checkForUpdates {"backend.checkForUpdates", true},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)},
@@ -344,6 +345,7 @@ void registerSettings() {
Register(g_userSettings.backend.isoVerification);
Register(g_userSettings.backend.graphicsBackend);
Register(g_userSettings.backend.skipPreLaunchUI);
Register(g_userSettings.backend.showPipelineCompilation);
Register(g_userSettings.backend.wasPresetChosen);
Register(g_userSettings.backend.checkForUpdates);
Register(g_userSettings.backend.cardFileType);
+9 -14
View File
@@ -3,7 +3,9 @@
#include "aurora/rmlui.hpp"
#include "ui.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <imgui.h>
namespace dusk::ui {
namespace {
@@ -28,19 +30,19 @@ Document::Document(const Rml::String& source, bool passive)
return;
}
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::Menu && (!visible() || !active())) {
if (cmd != NavCommand::Menu && !visible()) {
event.StopImmediatePropagation();
}
},
true);
const auto blockUnlessActive = [this](Rml::Event& event) {
if (!visible() || !active()) {
const auto blockUnlessVisible = [this](Rml::Event& event) {
if (!visible()) {
event.StopImmediatePropagation();
}
};
listen(Rml::EventId::Mouseover, blockUnlessActive, true);
listen(Rml::EventId::Click, blockUnlessActive, true);
listen(Rml::EventId::Scroll, blockUnlessActive, true);
listen(Rml::EventId::Mouseover, blockUnlessVisible, true);
listen(Rml::EventId::Click, blockUnlessVisible, true);
listen(Rml::EventId::Scroll, blockUnlessVisible, true);
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
if (mPassive) {
@@ -122,16 +124,9 @@ bool Document::visible() const {
return *mDocument->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible;
}
bool Document::active() const {
return !mClosed && !mPendingClose;
}
bool Document::handle_nav_event(Rml::Event& event) {
if (!active()) {
return false;
}
const auto cmd = map_nav_event(event);
if (cmd == NavCommand::None || (cmd != NavCommand::Menu && !visible())) {
if (cmd == NavCommand::None) {
return false;
}
return handle_nav_command(event, cmd);
+3 -3
View File
@@ -18,7 +18,6 @@ public:
virtual void update();
virtual bool focus();
virtual bool visible() const;
virtual bool active() const;
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
bool capture = false);
@@ -42,11 +41,12 @@ public:
push_document(std::move(document));
hide(false);
}
void pop(bool show = true) {
void pop() {
hide(true);
focus_top_document(show);
show_top_document();
}
bool pending_close() const { return mPendingClose; }
bool closed() const { return mClosed; }
bool handle_nav_event(Rml::Event& event);
+2 -2
View File
@@ -16,7 +16,6 @@
#include "f_pc/f_pc_name.h"
#include "imgui.h"
#include "modal.hpp"
#include "mods_window.hpp"
#include "settings.hpp"
#include "ui.hpp"
#include "warp.hpp"
@@ -59,7 +58,8 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
}
mTabBar->add_tab("Achievements", [this] { push(std::make_unique<AchievementsWindow>()); });
mTabBar->add_tab("Mods", [this] { push(std::make_unique<ModsWindow>()); });
mTabBar->add_tab("Reset", [this] {
mTabBar->set_active_tab(-1);
const auto dismiss = [](Modal& modal) { modal.pop(); };
-125
View File
@@ -1,125 +0,0 @@
#include "mods_window.hpp"
#include "dusk/mod_loader.hpp"
#include "fmt/format.h"
#include "pane.hpp"
namespace dusk::ui {
namespace {
Rml::String build_mod_detail_rml(const dusk::LoadedMod& mod) {
const char* statusClass;
const char* statusText;
if (mod.load_failed) {
statusClass = "locked";
statusText = "Failed";
} else if (mod.active) {
statusClass = "unlocked";
statusText = "Active";
} else {
statusClass = "";
statusText = "Disabled";
}
return fmt::format(
R"(<div class="mod-info-row">)"
R"(<span class="mod-info-label">Version</span>)"
R"(<span class="mod-info-value">{}</span>)"
R"(</div>)"
R"(<div class="mod-info-row">)"
R"(<span class="mod-info-label">Author</span>)"
R"(<span class="mod-info-value">{}</span>)"
R"(</div>)"
R"(<div class="mod-info-row">)"
R"(<span class="mod-info-label">Status</span>)"
R"(<span class="achievement-badge {}">{}</span>)"
R"(</div>)"
R"(<div class="mod-info-row">)"
R"(<span class="mod-info-label">Path</span>)"
R"(<span class="mod-info-value mod-path">{}</span>)"
R"(</div>)",
mod.metadata.version,
mod.metadata.author,
statusClass, statusText,
mod.mod_path
);
}
} // namespace
ModsWindow::ModsWindow() {
const auto& mods = dusk::ModLoader::instance().mods();
if (mods.empty()) {
add_tab("Mods", [this](Rml::Element* content) {
auto& pane = add_child<Pane>(content, Pane::Type::Uncontrolled);
pane.add_text("No mods installed.");
pane.finalize();
});
return;
}
for (ModIndex i = 0; i < mods.size(); ++i) {
mSnapshot.push_back({mods[i].active, mods[i].load_failed});
add_tab(mods[i].metadata.name, [this, i](Rml::Element* content) {
mActiveModIndex = static_cast<int>(i);
const auto& curMods = dusk::ModLoader::instance().mods();
if (i >= curMods.size()) {
return;
}
const auto& mod = curMods[i];
auto& pane = add_child<Pane>(content, Pane::Type::Uncontrolled);
pane.add_section("Details");
pane.add_rml(build_mod_detail_rml(mod));
if (!mod.metadata.description.empty()) {
pane.add_section("Description");
pane.add_text(mod.metadata.description);
}
for (const auto& cb : mod.tab_content) {
cb.build_fn(static_cast<void*>(pane.root()), cb.userdata);
}
pane.finalize();
});
}
}
void ModsWindow::update() {
const auto& mods = dusk::ModLoader::instance().mods();
bool dirty = mods.size() != mSnapshot.size();
if (!dirty) {
for (ModIndex i = 0; i < mods.size(); ++i) {
if (mods[i].active != mSnapshot[i].active ||
mods[i].load_failed != mSnapshot[i].load_failed)
{
dirty = true;
break;
}
}
}
if (dirty) {
mSnapshot.clear();
for (const auto& mod : mods) {
mSnapshot.push_back({mod.active, mod.load_failed});
}
refresh_active_tab();
}
if (mActiveModIndex >= 0 && static_cast<size_t>(mActiveModIndex) < mods.size()) {
for (const auto& cb : mods[mActiveModIndex].tab_updates) {
cb.update_fn(cb.userdata);
}
}
Window::update();
}
} // namespace dusk::ui
-25
View File
@@ -1,25 +0,0 @@
#pragma once
#include "window.hpp"
#include <vector>
#include "dusk/mod_loader.hpp"
namespace dusk::ui {
class ModsWindow : public Window {
public:
ModsWindow();
void update() override;
private:
struct ModSnapshot {
bool active;
bool load_failed;
};
std::vector<ModSnapshot> mSnapshot;
ModIndex mActiveModIndex = 0;
};
} // namespace dusk::ui
+8 -66
View File
@@ -1,9 +1,9 @@
#include "overlay.hpp"
#include "aurora/lib/logging.hpp"
#include "controller_config.hpp"
#include "dusk/achievements.h"
#include "dusk/action_bindings.h"
#include "controller_config.hpp"
#include "dusk/livesplit.h"
#include "dusk/settings.h"
#include "dusk/speedrun.h"
@@ -33,13 +33,6 @@ const Rml::String kDocumentSource = R"RML(
</head>
<body>
<fps id="fps" />
<pipeline-progress id="pipeline-progress">
<pipeline-status>
<icon class="pipeline-spinner">&#xe9d0;</icon>
<span id="pipeline-progress-label" />
</pipeline-status>
<progress id="pipeline-progress-bar" />
</pipeline-progress>
<speedrun-timer id="speedrun-timer">
<speedrun-rta id="speedrun-rta" />
<speedrun-igt id="speedrun-igt" />
@@ -55,7 +48,6 @@ constexpr std::array<std::pair<const char*, const char*>, 3> kAutoSaveLayers{{
}};
constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500);
constexpr auto kPipelineProgressOpenDelay = std::chrono::milliseconds(250);
constexpr std::array<const char*, 4> kFpsCorners = {"tl", "tr", "bl", "br"};
@@ -168,8 +160,8 @@ Rml::Element* create_menu_notification(Rml::Element* parent) {
Rml::String padButton{};
SDL_Gamepad* gamepad = gamepad_for_port(PAD_CHAN0);
if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0) && gamepad != nullptr) {
padButton = native_button_name(
gamepad, getActionBindButton(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0));
padButton = native_button_name(gamepad,
getActionBindButton(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0));
} else {
padButton = back_button_name();
}
@@ -205,9 +197,6 @@ static std::string FormatTime(OSTime ticks) {
Overlay::Overlay() : Document(kDocumentSource, true) {
mFpsCounter = mDocument->GetElementById("fps");
mPipelineProgress = mDocument->GetElementById("pipeline-progress");
mPipelineProgressLabel = mDocument->GetElementById("pipeline-progress-label");
mPipelineProgressBar = mDocument->GetElementById("pipeline-progress-bar");
mSpeedrunTimer = mDocument->GetElementById("speedrun-timer");
mSpeedrunRta = mDocument->GetElementById("speedrun-rta");
mSpeedrunIgt = mDocument->GetElementById("speedrun-igt");
@@ -269,8 +258,6 @@ void Overlay::update() {
}
}
update_pipeline_progress();
#if !(defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST))
if (getSettings().game.speedrunMode && getSettings().game.liveSplitEnabled) {
dusk::speedrun::updateLiveSplit();
@@ -322,8 +309,7 @@ void Overlay::update() {
mSpeedrunRta->RemoveAttribute("open");
}
mSpeedrunIgt->SetInnerRML(
escape(fmt::format("IGT {}", FormatTime(m_speedrunInfo.m_igtTimer))));
mSpeedrunIgt->SetInnerRML(escape(fmt::format("IGT {}", FormatTime(m_speedrunInfo.m_igtTimer))));
} else {
mSpeedrunTimer->RemoveAttribute("open");
}
@@ -387,8 +373,10 @@ void Overlay::update() {
std::chrono::duration<float>(clock::now() - mCurrentToastStartTime).count();
const float ratio = duration > 0.0f ? std::clamp(elapsed / duration, 0.0f, 1.0f) : 1.0f;
const auto remaining = 1.f - ratio;
if (auto* progress = mCurrentToast->QuerySelector("progress")) {
progress->SetAttribute("value", remaining);
Rml::ElementList list;
mDocument->GetElementsByTagName(list, "progress");
for (auto* elem : list) {
elem->SetAttribute("value", remaining);
}
if (remaining == 0.f) {
if (mCurrentToast->IsPseudoClassSet("done") ||
@@ -407,52 +395,6 @@ void Overlay::update() {
}
}
void Overlay::update_pipeline_progress() {
if (mPipelineProgress == nullptr || mPipelineProgressLabel == nullptr ||
mPipelineProgressBar == nullptr)
{
return;
}
const auto* stats = aurora_get_stats();
const uint32_t queuedPipelines = stats != nullptr ? stats->queuedPipelines : 0;
if (queuedPipelines == 0) {
mPipelineProgress->RemoveAttribute("open");
mPipelineProgressActive = false;
mPipelineBatchCreatedBase = 0;
mLastQueuedPipelines = 0;
return;
}
const uint32_t createdPipelines = stats->createdPipelines;
if (!mPipelineProgressActive || createdPipelines < mPipelineBatchCreatedBase) {
mPipelineProgressActive = true;
mPipelineBatchCreatedBase = createdPipelines;
mPipelineProgressStartTime = clock::now();
mLastQueuedPipelines = 0;
}
const uint32_t builtPipelines = createdPipelines - mPipelineBatchCreatedBase;
const uint32_t totalPipelines = queuedPipelines + builtPipelines;
const float progress = totalPipelines > 0 ? static_cast<float>(builtPipelines) /
static_cast<float>(totalPipelines) :
0.0f;
if (queuedPipelines != mLastQueuedPipelines) {
mLastQueuedPipelines = queuedPipelines;
const auto noun = queuedPipelines == 1 ? "pipeline" : "pipelines";
mPipelineProgressLabel->SetInnerRML(
escape(fmt::format("Building {} {}", queuedPipelines, noun)));
}
mPipelineProgressBar->SetAttribute("value", progress);
if (clock::now() >= mPipelineProgressStartTime + kPipelineProgressOpenDelay) {
mPipelineProgress->SetAttribute("open", "");
} else {
mPipelineProgress->RemoveAttribute("open");
}
}
bool Overlay::handle_nav_command(Rml::Event& event, NavCommand cmd) {
Log.warn("Overlay received nav command: {}", magic_enum::enum_name(cmd));
return false;
-10
View File
@@ -16,13 +16,7 @@ public:
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
void update_pipeline_progress();
Rml::Element* mFpsCounter = nullptr;
Rml::Element* mPipelineProgress = nullptr;
Rml::Element* mPipelineProgressLabel = nullptr;
Rml::Element* mPipelineProgressBar = nullptr;
Rml::Element* mCurrentToast = nullptr;
Rml::Element* mControllerWarning = nullptr;
Rml::Element* mMenuNotification = nullptr;
@@ -31,11 +25,7 @@ private:
Rml::Element* mSpeedrunIgt = nullptr;
clock::time_point mCurrentToastStartTime;
clock::time_point mMenuNotificationStartTime;
clock::time_point mPipelineProgressStartTime;
Uint64 mFpsLastUpdate = 0;
uint32_t mPipelineBatchCreatedBase = 0;
uint32_t mLastQueuedPipelines = 0;
bool mPipelineProgressActive = false;
};
} // namespace dusk::ui
+4 -12
View File
@@ -28,7 +28,6 @@
#include <thread>
#include "m_Do/m_Do_MemCard.h"
#include "mods_window.hpp"
namespace dusk::ui {
namespace {
@@ -50,14 +49,14 @@ const Rml::String kDocumentSource = R"RML(
</hero>
<div id="menu-list" />
</menu>
<disc-info class="intro-item delay-5">
<disc-info class="intro-item delay-4">
<div id="disc-status">
<icon />
<span id="disc-status-label" />
</div>
<span id="disc-version" class="detail" />
</disc-info>
<version-info class="intro-item delay-6">
<version-info class="intro-item delay-5">
<div class="version">Version <span id="version-text"></span></div>
<div id="update-status" class="update">
<span id="update-message"></span>
@@ -716,7 +715,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
}
IsGameLaunched = true;
pop(false);
hide(true);
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
@@ -727,16 +726,9 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Mods"));
mMenuButtons.back()->on_pressed([this] {
mRestartSuppressed = false;
push(std::make_unique<ModsWindow>());
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit"));
mMenuButtons.back()->on_pressed([] { IsRunning = false; });
apply_intro_animation(mMenuButtons.back()->root(), "delay-4");
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
}
mDiscStatus = mDocument->GetElementById("disc-status");
+12 -7
View File
@@ -448,7 +448,7 @@ void add_speedrun_disabled_option(Pane& leftPane, Pane& rightPane, ConfigVar<boo
config_bool_select(leftPane, rightPane, var, {
.key = key,
.helpText = helpText,
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isDisabled = [] { return getSettings().game.speedrunMode; },
});
}
@@ -483,7 +483,7 @@ SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>&
std::string suffix = "") {
auto& button = leftPane.add_child<NumberButton>(NumberButton::Props{
.key = std::move(key),
.getValue = [&var] { return var.getValue(); },
.getValue = [&var] { return var; },
.setValue =
[&var, min, max, callback = std::move(onChange)](int value) {
const int clampedValue = std::clamp(value, min, max);
@@ -668,7 +668,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
leftPane.register_control(
leftPane.add_select_button({
.key = "Graphics Backend",
.getValue = [] { return Rml::String{backend_name(aurora_get_backend())}; },
.getValue = [] { return Rml::String{backend_name(configured_backend())}; },
.isModified =
[] {
return getSettings().backend.graphicsBackend.getValue() !=
@@ -1092,7 +1092,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
leftPane.add_section("Tools");
addOption("Turbo Key", getSettings().game.enableTurboKeybind,
"Hold Tab to increase game speed by up to 4x.",
[] { return getSettings().game.speedrunMode.getValue(); });
[] { return getSettings().game.speedrunMode; });
addOption("Reset Key (" + Rml::String{hotkeys::DO_RESET} + ")",
getSettings().game.enableResetKeybind,
"Press " + Rml::String{hotkeys::DO_RESET} + " to reset the game.");
@@ -1205,7 +1205,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
getSettings().game.damageMultiplier.setValue(value);
config::Save();
},
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isDisabled = [] { return getSettings().game.speedrunMode; },
.isModified =
[] {
return getSettings().game.damageMultiplier.getValue() !=
@@ -1349,7 +1349,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
[] {
return kMagicArmorModes[static_cast<u8>(getSettings().game.armorRupeeDrain.getValue())];
},
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isDisabled = [] { return getSettings().game.speedrunMode; },
.isModified =
[] {
return getSettings().game.armorRupeeDrain.getValue() !=
@@ -1485,6 +1485,11 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.helpText = "When starting Dusklight, skip the main menu and boot straight into the "
"game if a disc image is available.",
});
config_bool_select(leftPane, rightPane, getSettings().backend.showPipelineCompilation,
{
.key = "Show Pipeline Compilation",
.helpText = "Show an overlay when shaders are being compiled for your hardware.",
});
config_bool_select(leftPane, rightPane, getSettings().backend.checkForUpdates,
{
.key = "Check for Updates",
@@ -1521,7 +1526,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
}
}
},
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isDisabled = [] { return getSettings().game.speedrunMode; },
});
config_bool_select(leftPane, rightPane, getSettings().game.showInputViewer,
{
+14 -27
View File
@@ -156,8 +156,9 @@ bool player_attention_locked() noexcept {
return player != nullptr && (player->checkAttentionLock() || player->checkEnemyAttentionLock());
}
bool hawkeye_active() noexcept {
return dCamera_c::isAimActive() && dComIfGp_checkPlayerStatus0(0, 0x200000);
bool touch_aim_capture_active() noexcept {
auto* player = daAlink_getAlinkActorClass();
return player != nullptr && player->checkTouchAimCaptureContext() && dCamera_c::isAimActive();
}
bool item_wheel_active() noexcept {
@@ -178,7 +179,7 @@ enum class StickOutput {
};
StickOutput stick_output_mode() noexcept {
if (fishing_controls_active() || hawkeye_active()) {
if (fishing_controls_active()) {
return StickOutput::CStick;
}
return StickOutput::MainStick;
@@ -696,8 +697,8 @@ void TouchControls::sync_touch_state() noexcept {
}
sync_l_lock_state();
const bool aimActive = dCamera_c::isAimActive();
if (aimActive && !hawkeye_active() && mMoveTouch.active) {
const bool aimActive = touch_aim_capture_active();
if (aimActive && mMoveTouch.active) {
if (!mCameraTouch.active) {
mCameraTouch = mMoveTouch;
mCameraTouch.start = mMoveTouch.current;
@@ -1212,26 +1213,7 @@ void TouchControls::handle_touch_down(Rml::Event& event) noexcept {
}
const auto id = touch_event_id(event);
const auto dimensions = context->GetDimensions();
const float top = mSafeInsets.top + kAnalogZoneTopDp * touch_dp_scale();
const float bottom = static_cast<float>(dimensions.y) - mSafeInsets.bottom -
kAnalogZoneBottomDp * touch_dp_scale();
const auto width = static_cast<float>(dimensions.x);
const bool inAnalogZone = position.y >= top && position.y <= bottom;
const bool inLeftZone = position.x < width * kLeftZoneWidth;
if (dCamera_c::isAimActive()) {
if (hawkeye_active() && inAnalogZone && inLeftZone) {
if (!mMoveTouch.active) {
mMoveTouch = {
.id = id,
.start = position,
.current = position,
.active = true,
};
}
return;
}
if (touch_aim_capture_active()) {
if (!mCameraTouch.active) {
mCameraTouch = {
.id = id,
@@ -1243,11 +1225,16 @@ void TouchControls::handle_touch_down(Rml::Event& event) noexcept {
return;
}
if (!inAnalogZone) {
const auto dimensions = context->GetDimensions();
const float top = mSafeInsets.top + kAnalogZoneTopDp * touch_dp_scale();
const float bottom = static_cast<float>(dimensions.y) - mSafeInsets.bottom -
kAnalogZoneBottomDp * touch_dp_scale();
if (position.y < top || position.y > bottom) {
return;
}
if (!mMoveTouch.active && inLeftZone) {
const auto width = static_cast<float>(dimensions.x);
if (!mMoveTouch.active && position.x < width * kLeftZoneWidth) {
mMoveTouch = {
.id = id,
.start = position,
+5 -9
View File
@@ -195,13 +195,9 @@ Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive)
return ret;
}
void focus_top_document(bool show) noexcept {
void show_top_document() noexcept {
if (auto* doc = top_document()) {
if (show) {
doc->show();
} else {
doc->focus();
}
doc->show();
}
input::sync_input_block();
}
@@ -214,13 +210,13 @@ bool any_document_visible() noexcept {
bool is_prelaunch_open() noexcept {
return std::any_of(sDocumentStack.begin(), sDocumentStack.end(), [](const auto& doc) {
const auto* prelaunch = dynamic_cast<const Prelaunch*>(doc.get());
return prelaunch != nullptr && prelaunch->active();
return prelaunch != nullptr && !prelaunch->pending_close() && !prelaunch->closed();
});
}
Document* top_document() noexcept {
for (auto& doc : std::views::reverse(sDocumentStack)) {
if (doc->active()) {
if (!doc->closed() && !doc->pending_close()) {
return doc.get();
}
}
@@ -263,7 +259,7 @@ void update() noexcept {
context->GetFocusElement() == context->GetRootElement()))
{
for (auto& doc : std::views::reverse(sDocumentStack)) {
if (doc->active() && doc->focus()) {
if (!doc->closed() && !doc->pending_close() && doc->focus()) {
break;
}
}
+1 -1
View File
@@ -74,7 +74,7 @@ void update() noexcept;
Document& push_document(
std::unique_ptr<Document> doc, bool show = true, bool passive = false) noexcept;
void focus_top_document(bool show) noexcept;
void show_top_document() noexcept;
bool any_document_visible() noexcept;
bool is_prelaunch_open() noexcept;
Document* top_document() noexcept;
-2
View File
@@ -18,7 +18,6 @@
#include "dusk/frame_interpolation.h"
#include "dusk/livesplit.h"
#include "dusk/logging.h"
#include "dusk/mod_loader.hpp"
#include "f_op/f_op_camera_mng.h"
#include "f_op/f_op_draw_tag.h"
#include "f_op/f_op_overlap_mng.h"
@@ -833,7 +832,6 @@ void fapGm_Execute() {
#if TARGET_PC
duskExecute();
dusk::ModLoader::instance().tick();
#endif
#ifdef TARGET_PC
+1 -1
View File
@@ -19,7 +19,7 @@
#include <revolution/sc.h>
#endif
DUSK_GAME_DATA u8 mDoAud_zelAudio_c::mInitFlag;
u8 mDoAud_zelAudio_c::mInitFlag;
u8 mDoAud_zelAudio_c::mResetFlag;
+23 -27
View File
@@ -60,7 +60,6 @@
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/iso_validate.hpp"
#include "dusk/mod_loader.hpp"
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/ui/menu_bar.hpp"
@@ -335,21 +334,11 @@ void main01(void) {
mDoAud_Execute();
}
aurora_end_frame();
FrameMark;
#ifdef DUSK_DISCORD
dusk::discord::run_callbacks();
dusk::discord::update_presence();
#endif
static Limiter main_loop_limiter;
static double last_fps_setting = 0.0;
static Limiter::duration_t target_ns = 0;
if (dusk::getSettings().game.enableFrameInterpolation.getValue() == dusk::FrameInterpMode::Capped && !dusk::getTransientSettings().skipFrameRateLimit) {
ZoneScopedN("Frame limiter");
double current_fps = dusk::getSettings().video.maxFrameRate.getValue();
if (current_fps != last_fps_setting) {
last_fps_setting = current_fps;
@@ -361,10 +350,19 @@ void main01(void) {
} else {
main_loop_limiter.Reset();
}
aurora_end_frame();
FrameMark;
#ifdef DUSK_DISCORD
dusk::discord::run_callbacks();
dusk::discord::update_presence();
#endif
} while (dusk::IsRunning);
exit:;
dusk::ModLoader::instance().shutdown();
dusk::ui::shutdown();
}
@@ -432,7 +430,16 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) {
const auto name = std::string_view(cvarArg).substr(0, sep);
const auto value = std::string_view(cvarArg).substr(sep + 1);
dusk::config::LoadArgOverride(name, value);
const auto cVar = dusk::config::GetConfigVar(name);
if (!cVar) {
DuskLog.fatal("Unknown --cvar name: '{}'", name);
}
try {
cVar->getImpl()->loadFromArg(*cVar, value);
} catch (const std::exception& e) {
DuskLog.fatal("Unable to parse: '{}': {}", value, e.what());
}
}
}
@@ -503,6 +510,7 @@ int game_main(int argc, char* argv[]) {
mainCalled = true;
dusk::registerSettings();
dusk::config::FinishRegistration();
cxxopts::ParseResult parsed_arg_options;
@@ -514,7 +522,6 @@ int game_main(int argc, char* argv[]) {
("h,help", "Print usage")
("console", "Show the Windows console window for logs", cxxopts::value<bool>()->default_value("false")->implicit_value("true"))
("dvd", "Path to DVD image file", cxxopts::value<std::string>())
("mods", "Path to mods directory", cxxopts::value<std::string>())
("backend", "Graphics API backend to use (auto, d3d12, d3d11, metal, vulkan, null)", cxxopts::value<std::string>())
("cvar", "Override configuration variables without modifying config", cxxopts::value<std::vector<std::string>>());
@@ -785,19 +792,9 @@ int game_main(int argc, char* argv[]) {
// mDoMain::developmentMode = 1; // Force Dev Mode for Debugging
mDoDvdThd::SyncWidthSound = false;
// Setup mods
if (parsed_arg_options.contains("mods") &&
!parsed_arg_options["mods"].as<std::string>().empty())
{
dusk::ModLoader::instance().setModsDir(parsed_arg_options["mods"].as<std::string>());
} else {
dusk::ModLoader::instance().setModsDir(dusk::ConfigPath / "mods");
}
DuskLog.info("Initializing mods...");
dusk::ModLoader::instance().init();
OSReport("Starting main01 (Game Loop)...\n");
main01();
dusk::MoviePlayerShutdown();
@@ -817,7 +814,6 @@ int game_main(int argc, char* argv[]) {
#endif
dusk::ui::shutdown();
dusk::texture_replacements::shutdown();
dusk::config::Shutdown();
aurora_shutdown();
return 0;
-13
View File
@@ -1,13 +0,0 @@
cmake_minimum_required(VERSION 3.25)
project(cat_carry_mod CXX)
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into")
add_dusk_mod(cat_carry_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
)
-6
View File
@@ -1,6 +0,0 @@
{
"name": "Chloe the Cat",
"version": "1.0.0",
"author": "Maddie",
"description": "Carry your new kitty companion Chloe throughout your adventure, but don't let her die!"
}
View File
-250
View File
@@ -1,250 +0,0 @@
#include "d/actor/d_a_alink.h"
#include "d/actor/d_a_npc_ne.h"
#include "d/d_com_inf_game.h"
#include "d/d_meter2_info.h"
#include "d/d_msg_object.h"
#include "dusk/hook.hpp"
#include "dusk/mod_api.h"
#include "dusk/mod_utils.h"
#include "f_op/f_op_actor.h"
#include "f_op/f_op_actor_mng.h"
#include "f_op/f_op_overlap_mng.h"
#include "m_Do/m_Do_audio.h"
#include "m_Do/m_Do_controller_pad.h"
#include <cstdio>
#include <cstring>
static constexpr s16 ACTOR_NPC_NE = 269;
static constexpr u16 NOTIFY_MSG_ID = 0xFFFE;
static const char* DEATH_MSG_TEXT = "It seems Chloe has died...";
using GetStringEntry = dusk::HookEntry<&dMsgObject_c::getString>;
static void on_getString_post(void* args, void* retval) {
if (dusk::arg<u32>(args, 0) != NOTIFY_MSG_ID) {
return;
}
strcpy(dusk::arg<char*>(args, 5), DEATH_MSG_TEXT);
strcpy(dusk::arg<char*>(args, 7), DEATH_MSG_TEXT);
if (retval) {
*static_cast<bool*>(retval) = true;
}
}
static constexpr int CAT_MAX_HP = 1;
static fpc_ProcID s_cat_id = fpcM_ERROR_PROCESS_ID_e;
static int s_cat_hp = CAT_MAX_HP;
static bool s_cat_dead = false;
static bool s_summon_carry = false;
static bool s_has_spawn = false;
static cXyz s_spawn_pos = {};
static s8 s_spawn_room = -1;
static char s_spawn_stage[8] = {};
static DuskElemHandle s_el_hp = nullptr;
static DuskElemHandle s_el_hp_bar = nullptr;
static DuskElemHandle s_el_status = nullptr;
static fopAc_ac_c* getCat() {
if (s_cat_id == fpcM_ERROR_PROCESS_ID_e) {
return nullptr;
}
fopAc_ac_c* cat = fopAcM_SearchByID(s_cat_id);
if (!cat) {
s_cat_id = fpcM_ERROR_PROCESS_ID_e;
}
return cat;
}
static void killCat() {
fopAc_ac_c* cat = getCat();
if (cat) {
fopAcM_delete(cat);
s_cat_id = fpcM_ERROR_PROCESS_ID_e;
}
mDoAud_seStartMenu(Z2SE_CAT_CRY_ANNOY);
s_cat_dead = true;
dMeter2Info_setFloatingMessage(NOTIFY_MSG_ID, 150, false);
dusk::g_api->log_info("cat_mod: the cat has died");
}
static bool inSpawnStage() {
return strncmp(dComIfGp_getStartStageName(), s_spawn_stage, sizeof(s_spawn_stage)) == 0;
}
static void spawnCat(bool carry = false) {
if (s_cat_dead || dComIfGp_event_runCheck()) {
return;
}
daAlink_c* link = daAlink_getAlinkActorClass();
if (!link) {
return;
}
cXyz pos;
s8 roomNo;
csXyz angle = {};
if (s_has_spawn && inSpawnStage()) {
pos = s_spawn_pos;
roomNo = s_spawn_room;
} else {
f32 yaw = link->shape_angle.y;
pos = link->current.pos;
pos.x += cM_ssin(yaw) * 30.0f;
pos.z += cM_scos(yaw) * 30.0f;
roomNo = link->current.roomNo;
angle.y = (s16)(link->shape_angle.y + (s16)0x8000);
}
cXyz scale = {1.0f, 1.0f, 1.0f};
s_cat_id = fopAcM_createInPlayScene(
ACTOR_NPC_NE, -1, &pos, roomNo, &angle, &scale, -1);
if (s_cat_id != fpcM_ERROR_PROCESS_ID_e) {
dusk::g_api->log_info("cat_mod: cat spawned (hp %d/%d)", s_cat_hp, CAT_MAX_HP);
s_summon_carry = carry;
}
}
static void on_setDamagePoint_post(void* args, void* /*retval*/) {
if (s_cat_dead) {
return;
}
int dmg = dusk::arg<int>(args, 1);
if (dmg <= 0) {
return;
}
fopAc_ac_c* cat = getCat();
bool cat_free = cat != nullptr && !fopAcM_checkCarryNow(cat);
if (cat_free) {
return;
}
s_cat_hp -= dmg;
dusk::g_api->log_info("cat_mod: cat took %d damage (hp %d/%d)", dmg, s_cat_hp, CAT_MAX_HP);
if (s_cat_hp <= 0) {
s_cat_hp = 0;
killCat();
}
else {
mDoAud_seStartMenu(Z2SE_CAT_CRY_CARRY);
}
}
static void BuildPanel(DuskPanelHandle panel, void*) {
DuskModAPI* api = dusk::g_api;
api->panel_add_section(panel, "Cat");
s_el_status = api->panel_add_dyn_text(panel, s_cat_dead ? "Dead" : "Alive");
float fraction = static_cast<float>(s_cat_hp) / CAT_MAX_HP;
s_el_hp_bar = api->panel_add_progress(panel, fraction);
char buf[32];
snprintf(buf, sizeof(buf), "%d / %d HP", s_cat_hp, CAT_MAX_HP);
s_el_hp = api->panel_add_dyn_text(panel, buf);
}
static void UpdatePanel(void*) {
DuskModAPI* api = dusk::g_api;
api->elem_set_text(s_el_status, s_cat_dead ? "Dead" : "Alive");
float fraction = static_cast<float>(s_cat_hp) / CAT_MAX_HP;
api->elem_set_progress(s_el_hp_bar, fraction);
char buf[32];
snprintf(buf, sizeof(buf), "%d / %d HP", s_cat_hp, CAT_MAX_HP);
api->elem_set_text(s_el_hp, buf);
}
extern "C" {
void mod_init(DuskModAPI* api) {
dusk::init(api);
dusk::hookAddPost<&dMsgObject_c::getString>(on_getString_post);
dusk::hookAddPost<&daAlink_c::setDamagePoint>(on_setDamagePoint_post);
api->register_tab_content(BuildPanel, nullptr);
api->register_tab_update(UpdatePanel, nullptr);
api->log_info("cat_mod: ready");
}
void mod_tick(DuskModAPI* api) {
(void)api;
if (s_cat_dead) {
return;
}
fopAc_ac_c* cat = getCat();
// Load zone detected: dismiss cat into inventory before the area unloads.
if (cat && fopAcM_checkCarryNow(cat) && fopOvlpM_IsDoingReq()) {
fopAcM_delete(cat);
s_cat_id = fpcM_ERROR_PROCESS_ID_e;
s_has_spawn = false;
daAlink_c* link = daAlink_getAlinkActorClass();
if (link) {
link->procPreActionUnequipInit(0, nullptr);
}
return;
}
if (!cat) {
if (s_has_spawn && inSpawnStage() && !dComIfGp_event_runCheck()) {
spawnCat();
} else if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) {
consumeInput(PAD_1, PAD_TRIGGER_Z);
spawnCat(true);
}
return;
}
if (s_summon_carry) {
s_summon_carry = false;
daAlink_c* link = daAlink_getAlinkActorClass();
if (link) {
link->field_0x27f4 = cat;
link->procGrabReadyInit();
}
}
if (!fopAcM_checkCarryNow(cat)) {
memcpy(s_spawn_stage, dComIfGp_getStartStageName(), sizeof(s_spawn_stage));
s_spawn_room = cat->current.roomNo;
s_spawn_pos = cat->current.pos;
s_has_spawn = true;
}
npc_ne_class* ne = static_cast<npc_ne_class*>(cat);
ne->mBehavior = npc_ne_class::BHV_TAME;
ne->mNoFollow = 0;
ne->mTexture = 0;
ne->mBtkFrame = 0;
if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigZ(PAD_1) && fopAcM_checkCarryNow(cat)) {
consumeInput(PAD_1, PAD_TRIGGER_Z);
fopAcM_delete(cat);
s_cat_id = fpcM_ERROR_PROCESS_ID_e;
s_has_spawn = false;
daAlink_c* link = daAlink_getAlinkActorClass();
if (link) {
link->procPreActionUnequipInit(0, nullptr);
}
}
}
void mod_cleanup(DuskModAPI* api) {
(void)api;
s_cat_id = fpcM_ERROR_PROCESS_ID_e;
s_cat_hp = CAT_MAX_HP;
s_cat_dead = false;
s_summon_carry = false;
s_el_hp = nullptr;
s_el_hp_bar = nullptr;
s_el_status = nullptr;
}
}
-18
View File
@@ -1,18 +0,0 @@
cmake_minimum_required(VERSION 3.25)
project(my_mod CXX)
# Path to the dusk source root.
# Set this to your dusk submodule:
# set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusk")
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
# Output .dusk packages next to the build directory by default.
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into")
add_dusk_mod(template_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
)
-8
View File
@@ -1,8 +0,0 @@
{
"id": "example.template_mod",
"name": "Template Mod",
"version": "1.0.0",
"author": "Maddie",
"description": "An example Dusk mod",
"has_code": true
}
View File
-18
View File
@@ -1,18 +0,0 @@
#include "dusk/hook.hpp"
#include "dusk/mod_api.h"
extern "C" {
void mod_init(DuskModAPI* api) {
dusk::init(api);
}
void mod_tick(DuskModAPI* api) {
(void)api;
}
void mod_cleanup(DuskModAPI* api) {
(void)api;
}
}
-5
View File
@@ -1,5 +0,0 @@
add_dusk_mod(mod_test
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
)
-8
View File
@@ -1,8 +0,0 @@
{
"id": "dev.twilitrealm.test_mod",
"name": "API Test Mod",
"version": "1.0.0",
"author": "dusk",
"description": "Exercises every feature of the Dusk mod API.",
"has_code": true
}
-1
View File
@@ -1 +0,0 @@
Hello from the mod archive!
-178
View File
@@ -1,178 +0,0 @@
// Tests every feature of the Dusk mod API. Results shown in the mod tab.
#include "d/actor/d_a_alink.h"
#include "dusk/hook.hpp"
#include "dusk/mod_api.h"
#include "m_Do/m_Do_controller_pad.h"
#include <cstdio>
#include <cstring>
static int g_ticks = 0;
static bool g_pre_fired = false;
static bool g_post_fired = false;
static bool g_replace_fired = false;
static bool g_arg_write_ok = false;
static int g_pre_cancel_count = 0;
static int g_post_count = 0;
static bool g_resource_ok = false;
static char g_resource_text[256] = {};
static char g_mod_dir_snippet[64] = {};
static DuskElemHandle g_el_pre_badge = nullptr;
static DuskElemHandle g_el_post_badge = nullptr;
static DuskElemHandle g_el_replace_badge = nullptr;
static DuskElemHandle g_el_argwrite_badge = nullptr;
static DuskElemHandle g_el_cancel_count = nullptr;
static DuskElemHandle g_el_post_count = nullptr;
static DuskElemHandle g_el_link_angle = nullptr;
// Pre-hook on posMove. Hold L to test argRef write and cancellation.
static int32_t on_posMove_pre(void* args) {
g_pre_fired = true;
if (mDoCPd_c::getHoldL(PAD_1)) {
dusk::argRef<daAlink_c*>(args, 0)->shape_angle.y = 0;
g_arg_write_ok = true;
++g_pre_cancel_count;
return 1; // cancel
}
return 0;
}
// Post-hook on posMove. Fires even when the pre-hook cancelled.
static void on_posMove_post(void* args, void* retval) {
g_post_fired = true;
++g_post_count;
(void)args;
(void)retval;
}
// Replace-hook on execute. Calls through to the original so gameplay is unaffected.
using ExecuteEntry = dusk::HookEntry<&daAlink_c::execute>;
static void on_execute_replace(void* args, void* retval) {
g_replace_fired = true;
int result = ExecuteEntry::g_orig(dusk::arg<daAlink_c*>(args, 0));
if (retval) {
*static_cast<int*>(retval) = result;
}
}
static void on_reset(void*) {
g_pre_fired = false;
g_post_fired = false;
g_replace_fired = false;
g_arg_write_ok = false;
g_pre_cancel_count = 0;
g_post_count = 0;
}
static void BuildPanel(DuskPanelHandle panel, void*) {
DuskModAPI* api = dusk::g_api;
api->panel_add_section(panel, "Hooks");
g_el_pre_badge = api->panel_add_badge_row(panel, "pre-hook fired (posMove)", g_pre_fired);
g_el_post_badge = api->panel_add_badge_row(panel, "post-hook fired (posMove)", g_post_fired);
g_el_replace_badge =
api->panel_add_badge_row(panel, "replace-hook fired (execute)", g_replace_fired);
g_el_argwrite_badge =
api->panel_add_badge_row(panel, "argRef write + pre cancel (hold L)", g_arg_write_ok);
char countBuf[64];
snprintf(countBuf, sizeof(countBuf), "pre cancels: %d", g_pre_cancel_count);
g_el_cancel_count = api->panel_add_dyn_text(panel, countBuf);
snprintf(countBuf, sizeof(countBuf), "post calls: %d", g_post_count);
g_el_post_count = api->panel_add_dyn_text(panel, countBuf);
api->panel_add_section(panel, "Resources");
api->panel_add_badge_row(panel, "load_resource (hello.txt)", g_resource_ok);
if (g_resource_text[0] != '\0') {
api->panel_add_dyn_text(panel, g_resource_text);
}
api->panel_add_section(panel, "API Fields");
api->panel_add_badge_row(panel, "mod_dir non-empty", g_mod_dir_snippet[0] != '\0');
api->panel_add_dyn_text(panel, g_mod_dir_snippet);
api->panel_add_section(panel, "Actions");
api->panel_add_button(panel, "Reset Results", on_reset, nullptr);
g_el_link_angle = api->panel_add_dyn_text(panel, "");
}
static void UpdatePanel(void*) {
DuskModAPI* api = dusk::g_api;
api->elem_set_badge(g_el_pre_badge, g_pre_fired);
api->elem_set_badge(g_el_post_badge, g_post_fired);
api->elem_set_badge(g_el_replace_badge, g_replace_fired);
api->elem_set_badge(g_el_argwrite_badge, g_arg_write_ok);
char buf[64];
snprintf(buf, sizeof(buf), "pre cancels: %d", g_pre_cancel_count);
api->elem_set_text(g_el_cancel_count, buf);
snprintf(buf, sizeof(buf), "post calls: %d", g_post_count);
api->elem_set_text(g_el_post_count, buf);
daAlink_c* link = daAlink_getAlinkActorClass();
snprintf(buf, sizeof(buf), "Link y angle: %d", link ? (int)link->shape_angle.y : 0);
api->elem_set_text(g_el_link_angle, buf);
}
extern "C" {
void mod_init(DuskModAPI* api) {
dusk::init(api);
api->log_info("mod_test initializing");
api->log_warn("log_warn smoke test");
api->log_error("log_error smoke test");
snprintf(g_mod_dir_snippet, sizeof(g_mod_dir_snippet), "%.60s", api->mod_dir);
size_t size = 0;
void* data = api->load_resource("hello.txt", &size);
if (data) {
size_t copy = size < sizeof(g_resource_text) - 1 ? size : sizeof(g_resource_text) - 1;
memcpy(g_resource_text, data, copy);
g_resource_text[copy] = '\0';
while (copy > 0 && g_resource_text[copy - 1] == '\n') {
g_resource_text[--copy] = '\0';
}
api->free_resource(data);
g_resource_ok = true;
api->log_info("load_resource OK: \"%s\"", g_resource_text);
} else {
api->log_error("load_resource FAILED for hello.txt");
}
void* missing = api->load_resource("does_not_exist.bin", nullptr);
if (!missing) {
api->log_info("load_resource missing-file: correctly returned nullptr");
} else {
api->log_error("load_resource missing-file: unexpectedly returned data");
api->free_resource(missing);
}
dusk::hookAddPre<&daAlink_c::posMove>(on_posMove_pre);
dusk::hookAddPost<&daAlink_c::posMove>(on_posMove_post);
dusk::hookSetReplace<&daAlink_c::execute>(on_execute_replace);
api->register_tab_content(BuildPanel, nullptr);
api->register_tab_update(UpdatePanel, nullptr);
api->log_info("mod_test ready");
}
void mod_tick(DuskModAPI* api) {
++g_ticks;
(void)api;
}
void mod_cleanup(DuskModAPI* api) {
api->log_info("mod_test unloaded after %d ticks", g_ticks);
g_el_pre_badge = g_el_post_badge = g_el_replace_badge = nullptr;
g_el_argwrite_badge = g_el_cancel_count = g_el_post_count = nullptr;
g_el_link_angle = nullptr;
}
}