Compare commits

...

78 Commits

Author SHA1 Message Date
Luke Street a5de0c276a Merge remote-tracking branch 'origin/main' into mods
# Conflicts:
#	CMakeLists.txt
#	src/dusk/config.cpp
2026-06-25 22:29:50 -07:00
Luke Street 277538bb81 New pipeline progress UI 2026-06-24 22:03:58 -07:00
Luke Street 5418b1831d Update aurora & flake.nix versions 2026-06-17 23:12:49 -06:00
Luke Street 2d4e69466b Refine menu_pointer click events
Only short clicks/taps count & they must
not move between targets
2026-06-17 22:48:44 -06:00
Luke Street 427dcfab82 Show active Graphics Backend in Settings; not configured 2026-06-17 18:09:00 -06:00
Luke Street f5642f3073 UI: Split active/visible concepts & fix nav forwarding 2026-06-16 15:10:09 -06:00
Luke Street cc9c15de54 Hawkeye support in touch controls 2026-06-16 13:51:17 -06:00
Luke Street 1fd8a2ca3c More fixes for clawshot touch controls 2026-06-16 13:37:04 -06:00
Luke Street 0c9c8795ce Update aurora 2026-06-16 13:12:37 -06:00
Luke Street 9eb9acfa11 Move frame limiter after aurora_end_frame 2026-06-16 12:56:02 -06:00
Luke Street facbf35343 Update aurora 2026-06-16 01:53:04 -06:00
Luke Street 7a34830dc7 Update aurora 2026-06-16 01:40:00 -06:00
Luke Street e4557efb23 Disable PkgConfig on Windows 2026-06-16 01:39:54 -06:00
Luke Street 42e12eb5ab Update aurora & fix RCSS warning 2026-06-16 00:14:19 -06:00
Luke Street 16cc37ca10 Android: Call Surface.setFrameRate & update it 2026-06-15 23:39:36 -06:00
Luke Street 44cb2c84ba Update aurora 2026-06-15 23:07:00 -06:00
Luke Street 8e9d4d624a Fix hookshot hanging w/ touch controls 2026-06-15 22:55:30 -06:00
Luke Street db87b91954 Update aurora & redraw hearts guage on HUD scale change 2026-06-15 22:46:50 -06:00
Luke Street b6f0e104bd Merge remote-tracking branch 'origin/main' into mods
# Conflicts:
#	CMakeLists.txt
#	include/dusk/gx_helper.h
2026-06-06 09:44:35 -06:00
PJB3005 66aca3b69d Fix string copy in LocateDllInBundle 2026-05-31 16:36:13 +02:00
PJB3005 bd09eea0f3 Disallow consecutive periods in mod IDs
Avoid config shenanigans
2026-05-31 16:35:28 +02:00
PJB3005 531313120f Merge branch 'main' into mods 2026-05-31 16:23:01 +02:00
PJB3005 fd26670b0e Remove unused include from mod_loader.hpp
This was for the code mods toggle before I moved the checking logic to CMake.
2026-05-29 09:56:47 +02:00
PJB3005 c83cc3b971 Horrible C++ template nightmares (fix clang-tidy warning about int conversions with ranges) 2026-05-29 05:47:12 +02:00
PJB3005 8c713d4535 Fix prelaunch animation timings
oops
2026-05-29 05:22:12 +02:00
PJB3005 183e7669c2 Add mods button to prelaunch UI
Fixes https://github.com/TwilitRealm/dusklight/issues/1904
2026-05-29 05:09:10 +02:00
PJB3005 f38514db79 Mods can be disabled by config
We now register a CVar for mod enablement for each mod.

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

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

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

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

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

Fixes https://github.com/TwilitRealm/dusklight/issues/1306
2026-05-15 21:04:48 +02:00
PJB3005 37e5b7409d Move mod init earlier
Probably necessary if we're gonna be replacing game files etc
2026-05-15 21:01:13 +02:00
PJB3005 cfc0fbc342 Allow mods to be loaded from extracted disk files
Also just some code cleanup
2026-05-14 20:42:20 +02:00
PJB3005 3e84c65657 Fix mod SDK being broken due to rebrand 2026-05-14 20:41:49 +02:00
PJB3005 08cbaff57b Merge branch 'main' into mods 2026-05-14 17:08:10 +02:00
PJB3005 d85718f802 Merge remote-tracking branch 'upstream/main' into mods 2026-05-13 17:26:22 +02:00
PJB3005 925bb069d9 Merge remote-tracking branch 'upstream/main' into mods 2026-05-13 17:25:29 +02:00
madeline b2871054a6 address review, rmlui, better api, catmod 2026-05-11 02:55:11 -07:00
madeline 5bead49902 fix pch conflict 2026-05-10 18:07:05 -07:00
madeline 4175d9c7f4 Merge branch 'main' of https://github.com/TakaRikka/dusk into mods 2026-05-10 16:50:36 -07:00
Luke Street 0d6b47ac73 Move mods menu after tools 2026-04-24 11:56:31 -06:00
Luke Street 1fb5d1ee2a Set FUNCHOOK_INSTALL=OFF 2026-04-24 10:29:05 -06:00
Luke Street c042de8a55 Restore DUSK_BUILDING_GAME=1 2026-04-24 10:12:52 -06:00
Luke Street e25a1f3ef6 Move mods to config dir & updates for macOS 2026-04-24 09:52:04 -06:00
Luke Street b7f9bc91b4 Merge remote-tracking branch 'origin/main' into mods
# Conflicts:
#	CMakeLists.txt
2026-04-24 01:37:37 -06:00
madeline 3281c64a55 more precise link debug info 2026-04-23 06:01:28 -07:00
madeline fb08cfcc6b handle hook conflicts 2026-04-23 05:17:15 -07:00
madeline 99fb2b89ce DUSK_REQUIRE_API_VERSION 2026-04-23 04:52:16 -07:00
madeline 53573eb795 inter mod communication 2026-04-23 04:49:21 -07:00
madeline 975ab1dc54 i fugged up the merge 2026-04-23 03:07:44 -07:00
madeline 9d10a48329 i fugged up the merge 2026-04-23 03:07:00 -07:00
madeline 52a067e412 Merge branch 'main' into mods 2026-04-23 02:20:13 -07:00
madeline 5dcbca392d fix turbo key getting stuck on 2026-04-20 09:51:33 -07:00
madeline ba906150d4 another attempt 2026-04-20 08:20:08 -07:00
madeline e5c7dfdedd maybe fix apple ci idk this shit is giga fucked 2026-04-20 08:04:05 -07:00
madeline 02a4e213e3 again 2026-04-20 07:57:21 -07:00
madeline 394627cd47 remove low level hook info 2026-04-20 07:56:09 -07:00
madeline e7081f770a modding.md, test mod, template mod, better imgui context 2026-04-20 07:52:35 -07:00
madeline 8356eff4ce fix apple ci 2026-04-20 05:24:46 -07:00
madeline 3597cb1bd6 incredibly scuffed funchook patching 2026-04-20 05:22:03 -07:00
madeline 507616015e fix CI cmake 2026-04-20 04:25:17 -07:00
madeline db20632130 Merge remote-tracking branch 'origin/main' into mods 2026-04-20 04:18:41 -07:00
madeline 22d906a248 mod loader 2026-04-20 04:17:42 -07:00
101 changed files with 3860 additions and 284 deletions
+2
View File
@@ -53,3 +53,5 @@ compile_commands.json
pipeline_cache.bin
extract
*.dusk
+2 -2
View File
@@ -6,12 +6,12 @@
"type": "cppvsdbg",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console"],
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console", "--mods", "${workspaceRoot}/mods"],
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"symbolSearchPath": "${command:cmake.launchTargetPath}",
"console": "integratedTerminal",
"cwd":"${workspaceRoot}"
"cwd":"${workspaceRoot}",
}
]
}
+141 -29
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_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
OUTPUT_VARIABLE dusk_git_head_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
OUTPUT_VARIABLE dusk_git_head_symbolic
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
# defines DUSK_WC_REVISION
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
OUTPUT_VARIABLE DUSK_WC_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DESCRIBE
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE)
@@ -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_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
OUTPUT_VARIABLE DUSK_WC_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DATE
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
OUTPUT_VARIABLE DUSK_WC_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE)
else ()
@@ -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_SOURCE_DIR}")
get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
list(APPEND _jpeg_cmake_args -DCMAKE_TOOLCHAIN_FILE=${_jpeg_toolchain_file})
endif ()
set(_jpeg_passthrough_vars
@@ -292,14 +292,42 @@ FetchContent_Declare(cxxopts
URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.3.1.tar.gz
URL_HASH SHA256=3bfc70542c521d4b55a46429d808178916a579b28d048bd8c727ee76c39e2072
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
message(STATUS "dusklight: Fetching nlohmann/json")
FetchContent_Declare(json
URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(cxxopts json)
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)
if (DUSK_ENABLE_SENTRY_NATIVE)
message(STATUS "dusklight: Fetching sentry-native")
@@ -347,7 +375,7 @@ else ()
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
endif ()
configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_BINARY_DIR}/version.h)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
include(files.cmake)
@@ -372,12 +400,20 @@ set(GAME_INCLUDE_DIRS
libs/JSystem/include
libs
extern/aurora/include/dolphin
extern/aurora/include
extern
${CMAKE_BINARY_DIR})
${CMAKE_CURRENT_BINARY_DIR}
${miniz_SOURCE_DIR})
# Interface target for mods and sub-projects to inherit game headers/defines
add_library(dusklight_game_headers INTERFACE)
target_include_directories(dusklight_game_headers INTERFACE ${GAME_INCLUDE_DIRS})
target_compile_definitions(dusklight_game_headers INTERFACE TARGET_PC=1)
target_link_libraries(dusklight_game_headers INTERFACE TracyClient)
find_package(Threads REQUIRED)
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt funchook-static
Threads::Threads zstd::libzstd)
if (DUSK_ENABLE_SENTRY_NATIVE)
@@ -450,6 +486,16 @@ 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/")
@@ -477,7 +523,6 @@ 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}
@@ -494,6 +539,7 @@ 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>
)
@@ -509,24 +555,61 @@ 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})
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_SOURCE_DIR}/miniz.c)
if(ANDROID)
add_library(dusklight SHARED ${DUSK_FILES})
set_target_properties(dusklight PROPERTIES OUTPUT_NAME main)
set(DUSK_MAIN_TARGET dusklight)
elseif(WIN32)
add_library(dusklight_game SHARED ${DUSK_FILES})
set_target_properties(dusklight_game PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ${DUSK_ENABLE_CODE_MODS}
OUTPUT_NAME dusklight
PDB_NAME dusklight_game)
# rmlui_core uses its own PCH which creates a duplicate PCH marker symbol when linked
# Disabling rmlui's PCH removes the conflicting marker and lets the link succeed
if (MSVC AND TARGET rmlui_core AND DUSK_ENABLE_CODE_MODS)
set_target_properties(rmlui_core PROPERTIES DISABLE_PRECOMPILE_HEADERS ON)
endif ()
add_executable(dusklight WIN32 src/dusk/launcher_win32.cpp)
target_link_libraries(dusklight PRIVATE dusklight_game)
target_include_directories(dusklight PRIVATE include)
set(DUSK_MAIN_TARGET dusklight_game)
else ()
add_executable(dusklight ${DUSK_FILES})
set(DUSK_MAIN_TARGET dusklight)
endif ()
if (ENABLE_ASAN)
target_sources(dusklight PRIVATE src/dusk/asan_options.c)
target_sources(${DUSK_MAIN_TARGET} PRIVATE src/dusk/asan_options.c)
endif ()
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusklight PRIVATE ${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 (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()
if (TARGET crashpad_handler)
add_dependencies(dusklight crashpad_handler)
add_custom_command(TARGET dusklight POST_BUILD
add_dependencies(${DUSK_MAIN_TARGET} crashpad_handler)
add_custom_command(TARGET ${DUSK_MAIN_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:crashpad_handler>"
"$<TARGET_FILE_DIR:dusklight>"
@@ -537,7 +620,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(dusklight PRIVATE "-Wl,-u,SDL_main")
target_link_options(${DUSK_MAIN_TARGET} PRIVATE "-Wl,-u,SDL_main")
endif ()
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
@@ -547,7 +630,7 @@ endif ()
if (NOT APPLE)
add_custom_command(TARGET dusklight POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/res"
"${CMAKE_CURRENT_SOURCE_DIR}/res"
"$<TARGET_FILE_DIR:dusklight>/res"
COMMENT "Copying resources"
)
@@ -575,13 +658,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)
@@ -589,6 +672,7 @@ if (APPLE)
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/tvos)
else ()
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos)
set(DUSK_ENTITLEMENTS ${DUSK_RESOURCE_DIR}/Dusk.entitlements)
endif ()
set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in)
file(GLOB_RECURSE DUSK_RESOURCE_FILES
@@ -619,6 +703,8 @@ 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 ()
@@ -638,7 +724,11 @@ if (IOS)
endif ()
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
aurora_copy_runtime_dlls(dusklight)
if(WIN32)
aurora_copy_runtime_dlls(dusklight dusklight_game)
else()
aurora_copy_runtime_dlls(dusklight)
endif()
if (DUSK_SELECTED_OPT)
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
@@ -677,15 +767,33 @@ function(get_target_prefix target result_var)
endif ()
endfunction()
list(APPEND BINARY_TARGETS dusklight)
if(WIN32)
list(APPEND BINARY_TARGETS dusklight_game)
endif()
set(EXTRA_TARGETS "")
if (TARGET crashpad_handler)
list(APPEND EXTRA_TARGETS crashpad_handler)
endif ()
if (DUSK_PACKAGE_INSTALL)
if (WIN32)
# Install the launcher and game DLL, but skip the DLL import library.
if (DUSK_PACKAGE_INSTALL)
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_BINDIR}
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR}
)
else ()
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS}
RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}
LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}
BUNDLE DESTINATION ${CMAKE_INSTALL_PREFIX}
)
endif ()
elseif (DUSK_PACKAGE_INSTALL)
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_BINDIR})
else()
else ()
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_PREFIX})
endif()
endif ()
aurora_install_runtime_dlls(dusklight ${CMAKE_INSTALL_PREFIX})
if (NOT APPLE)
if (DUSK_PACKAGE_INSTALL)
@@ -743,3 +851,7 @@ foreach (target IN LISTS BINARY_TARGETS)
endif ()
endforeach ()
endforeach ()
if (DUSK_ENABLE_CODE_MODS)
add_subdirectory(tools/mod_test)
endif ()
+5 -1
View File
@@ -158,7 +158,11 @@
"cacheVariables": {
"CMAKE_C_COMPILER": "cl",
"CMAKE_CXX_COMPILER": "cl",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
"type": "BOOL",
"value": true
}
},
"vendor": {
"microsoft.com/VisualStudioSettings/CMake/1.0": {
+49
View File
@@ -0,0 +1,49 @@
# add_dusk_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>])
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into")
function(add_dusk_mod target_name)
cmake_parse_arguments(ARG "" "MOD_JSON;RES_DIR" "SOURCES" ${ARGN})
if(NOT ARG_MOD_JSON)
message(FATAL_ERROR "add_dusk_mod: MOD_JSON is required")
endif()
add_library(${target_name} SHARED ${ARG_SOURCES})
set_target_properties(${target_name} PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON)
target_compile_features(${target_name} PRIVATE cxx_std_20)
target_link_libraries(${target_name} PRIVATE dusklight_game_headers)
if(APPLE)
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
elseif(UNIX)
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
elseif(WIN32)
target_link_libraries(${target_name} PRIVATE dusklight_game)
if(MSVC)
target_link_options(${target_name} PRIVATE /INCREMENTAL:NO)
set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
endif()
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage")
set(_out "${DUSK_MODS_OUTPUT_DIR}/${target_name}.dusk")
file(MAKE_DIRECTORY "${_stage}") # must exist before POST_BUILD on Windows
set(_zip_args "$<TARGET_FILE_NAME:${target_name}>" mod.json)
set(_extra_cmds "")
if(ARG_RES_DIR)
list(APPEND _zip_args res)
set(_extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_CURRENT_SOURCE_DIR}/${ARG_RES_DIR}" "${_stage}/res")
endif()
add_custom_command(TARGET ${target_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${DUSK_MODS_OUTPUT_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${target_name}>" "${_stage}/$<TARGET_FILE_NAME:${target_name}>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/${ARG_MOD_JSON}" "${_stage}/mod.json"
${_extra_cmds}
COMMAND ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
WORKING_DIRECTORY "${_stage}"
COMMENT "Packaging ${target_name} -> ${_out}"
)
endfunction()
+13
View File
@@ -0,0 +1,13 @@
# Patches capstone's CMakeLists.txt for compatibility with CMake >= 4.0:
# - Bumps cmake_minimum_required to 3.10 (CMake >= 4.0 dropped < 3.5 support; < 3.10 warns)
# - Removes cmake_policy(SET CMP0048 OLD) (rejected by CMake >= 3.27)
file(READ "${DIR}/CMakeLists.txt" _content)
string(REGEX REPLACE
"cmake_minimum_required[ \t]*\\([ \t]*VERSION[ \t]+[0-9]+\\.[0-9]+(\\.[0-9]+)?[ \t]*\\)"
"cmake_minimum_required(VERSION 3.10)"
_content "${_content}")
string(REGEX REPLACE
"cmake_policy[ \t]*\\([ \t]*SET[ \t]+CMP0048[ \t]+OLD[ \t]*\\)"
"# cmake_policy(SET CMP0048 OLD)"
_content "${_content}")
file(WRITE "${DIR}/CMakeLists.txt" "${_content}")
+11
View File
@@ -0,0 +1,11 @@
file(READ "${SOURCE_DIR}/cmake/capstone.cmake.in" _content)
# Insert PATCH_COMMAND before CONFIGURE_COMMAND in the ExternalProject_Add.
# Bracket args prevent cmake from substituting ${...} while writing this file.
string(REPLACE
" CONFIGURE_COMMAND \"\""
[=[ PATCH_COMMAND "${CMAKE_COMMAND}" -DDIR=${CMAKE_CURRENT_BINARY_DIR}/capstone-src -P "${CAPSTONE_FIX_SCRIPT}"
CONFIGURE_COMMAND ""]=]
_content "${_content}")
file(WRITE "${SOURCE_DIR}/cmake/capstone.cmake.in" "${_content}")
+351
View File
@@ -0,0 +1,351 @@
# 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
+13
View File
@@ -1418,6 +1418,8 @@ set(DUSK_FILES
include/dusk/scope_guard.hpp
src/dusk/dvd_asset.cpp
src/d/actor/d_a_alink_dusk.cpp
src/dusk/android_frame_rate.hpp
src/dusk/android_frame_rate.cpp
src/dusk/asserts.cpp
src/dusk/batch.cpp
src/dusk/batch.hpp
@@ -1505,6 +1507,8 @@ 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
@@ -1539,6 +1543,15 @@ 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 = "v20260423.175430";
nodVersion = "v2.0.0-alpha.8";
dawnVersion = "v20260618.032059";
nodVersion = "v2.0.0-alpha.10";
versionSuffix = "nix-" + (self.shortRev or self.dirtyShortRev or "dirty");
dawnInfo = {
"x86_64-linux" = {
triple = "linux-x86_64";
hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs=";
hash = "sha256-GFSd573b+VQx/VmFdNQgWDd0V9ayQlcw0Zuopke12ak=";
};
"aarch64-linux" = {
triple = "linux-aarch64";
hash = "sha256-34yyFpfqBZUwoFXQ41F0AwAU78FaNihOSY0oriwn6B0=";
hash = "sha256-ZaoP7BAjBMnfAv2/AMRi3FNH2ZtyqASCSFyU/oB2Mzg=";
};
"aarch64-darwin" = {
triple = "darwin-arm64";
hash = "sha256-eQnzrBp6gjiBek1VYQ9A5W13ClYWrDDKjIqv/7eNTR4=";
hash = "sha256-HT+qtlLaSHyoXPrUcXgcTGa877X5YfzbxRD4bJb7i1Y=";
};
"x86_64-darwin" = {
triple = "darwin-x86_64";
hash = "sha256-QGWiGdxiI9kci3NPXH6QFFirxn16851zB/w3jqhIBJ4=";
hash = "sha256-cUNaCbA7rlKSukDVKGaVEVw0Zt1+mSbaHbmUCMvMVWc=";
};
};
nodPrebuiltInfo = {
"x86_64-linux" = {
triple = "linux-x86_64";
hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE=";
hash = "sha256-FVQWECVA2gWdc+n5OQ/Tvwn8z0qdgjSd1WlFt5HKOec=";
};
"aarch64-darwin" = {
triple = "macos-arm64";
hash = "sha256-UPy1ywCcv0K6VJOU3uUelJuUdBh3UNaPRlyP5LOBeDw=";
hash = "sha256-8ZEejxksVgShNKUVRCBYaLOp9x/qOC9pAeVrElQUGUk=";
};
};
@@ -75,7 +75,7 @@
'';
dawn = pkgs.fetchzip {
url = "https://github.com/encounter/dawn-build/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
url = "https://github.com/encounter/dawn/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-+zrtVzjo0+X/6uMcNUn1+FaSR+jOhrcQSDNBFjw0NDs=";
hash = "sha256-r8qDlOVxv5iKiFjJQrcBuL9HVoOM3yEjRVnQIMqaICs=";
};
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/11.1.4.tar.gz";
hash = "sha256-sUbxlYi/Aupaox3JjWFqXIjcaQa0LFjclQAOleT+FRA=";
url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz";
hash = "sha256-ZmI1Dv0ZabPlxa02OpERI47jp7zFfjpeWCy1WyuPYZ0=";
};
TRACY = pkgs.fetchzip {
url = "https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz";
hash = "sha256-hbNGOsGeyGSvCJ2No8RkwOib1lX2on3vNZSzyVkZdXw=";
url = "https://github.com/wolfpld/tracy/archive/6789e7d6f9a65ec98926b602097a33a9676d2606.tar.gz";
hash = "sha256-Xxyd7G/mnXEPpN+ehmwl0AkAhS3CwObpJNDgcqbdUJg=";
};
IMGUI = pkgs.fetchFromGitHub {
owner = "ocornut";
+1 -1
View File
@@ -34,7 +34,7 @@ public:
bool isResetting() { return mResettingFlag; }
static Z2AudioMgr* getInterface() { return mAudioMgrPtr; }
static Z2AudioMgr* mAudioMgrPtr;
static DUSK_GAME_DATA Z2AudioMgr* mAudioMgrPtr;
/* 0x0514 */ virtual bool startSound(JAISoundID soundID, JAISoundHandle* handle, const JGeometry::TVec3<f32>* posPtr);
/* 0x0518 */ bool mResettingFlag;
+1
View File
@@ -4556,6 +4556,7 @@ public:
void handleWolfHowl();
void handleQuickTransform();
bool checkAimContext();
bool checkAimInputContext();
void onIronBallChainInterpCallback();
+5 -5
View File
@@ -1049,11 +1049,11 @@ public:
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
extern dComIfG_inf_c g_dComIfG_gameInfo;
extern GXColor g_blackColor;
extern GXColor g_clearColor;
extern GXColor g_whiteColor;
extern GXColor g_saftyWhiteColor;
DUSK_GAME_EXTERN dComIfG_inf_c g_dComIfG_gameInfo;
DUSK_GAME_EXTERN GXColor g_blackColor;
DUSK_GAME_EXTERN GXColor g_clearColor;
DUSK_GAME_EXTERN GXColor g_whiteColor;
DUSK_GAME_EXTERN GXColor g_saftyWhiteColor;
int dComLbG_PhaseHandler(request_of_phase_process_class*, request_of_phase_process_fn*,
void*);
+1
View File
@@ -218,6 +218,7 @@ private:
bool mCursorInterpPrevAngular;
bool mCursorInterpCurrAngular;
bool mCursorInterpInit;
bool mPointerTouchPressHoveredCurrent;
#endif
};
+4 -1
View File
@@ -2,6 +2,7 @@
#define D_METER_D_METER2_INFO_H
#include "SSystem/SComponent/c_xyz.h"
#include "global.h"
class CPaneMgr;
class J2DTextBox;
@@ -301,7 +302,7 @@ public:
/* 0xF3 */ u8 unk_0xf3[5];
};
extern dMeter2Info_c g_meter2_info;
DUSK_GAME_EXTERN dMeter2Info_c g_meter2_info;
void dMeter2Info_setSword(u8 i_itemId, bool i_offItemBit);
void dMeter2Info_setCloth(u8 i_clothId, bool i_offItemBit);
@@ -849,6 +850,8 @@ inline void dMeter2Info_setFloatingMessage(u16 i_msgID, s16 i_msgTimer, bool i_w
g_meter2_info.setFloatingMessage(i_msgID, i_msgTimer, i_wakuVisible);
}
// Show a custom text notification using the floating-message HUD display.
inline void dMeter2Info_setMiniGameCount(s8 i_count) {
g_meter2_info.setMiniGameCount(i_count);
}
+4 -5
View File
@@ -89,17 +89,16 @@ 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.
*/
+12 -5
View File
@@ -69,7 +69,7 @@ protected:
/**
* The name of this CVar, used in the configuration file.
*/
const char* name;
std::string name;
/**
* Whether this CVar has been registered with the global managing logic.
@@ -87,8 +87,10 @@ protected:
*/
const ConfigImplBase* impl;
ConfigVarBase(const char* name, const ConfigImplBase* impl);
virtual ~ConfigVarBase() = default;
// 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);
/**
* Check that the CVar is registered, aborting if this is not the case.
@@ -99,6 +101,8 @@ protected:
}
public:
virtual ~ConfigVarBase();
/**
* Get the name of this CVar, used in the configuration file.
*/
@@ -121,6 +125,7 @@ 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.
@@ -192,10 +197,12 @@ public:
* @param arg Arguments to forward to construct the default value.
*/
template <typename... Args>
ConfigVar(const char* name, Args&&... arg)
: ConfigVarBase(name, GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
ConfigVar(std::string name, Args&&... arg)
: ConfigVarBase(std::move(name), GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
value(), overrideValue() {}
ConfigVar(ConfigVar const&) = delete;
/**
* \brief Get the current value of the CVar.
*
+4 -13
View File
@@ -22,9 +22,8 @@
class GXTexObjRAII : public GXTexObj {
public:
GXTexObjRAII() : GXTexObj() {}
~GXTexObjRAII() { GXDestroyTexObj(this); }
void reset() { GXDestroyTexObj(this); }
~GXTexObjRAII();
void reset();
GXTexObjRAII(const GXTexObjRAII&) = delete;
GXTexObjRAII& operator=(const GXTexObjRAII&) = delete;
@@ -65,16 +64,8 @@ typedef GXTlutObj TGXTlutObj;
#endif
struct GXScopedDebugGroup {
explicit GXScopedDebugGroup(const char* text) {
#if DUSK_GFX_DEBUG_GROUPS
GXPushDebugGroup(text);
#endif
}
~GXScopedDebugGroup() {
#if DUSK_GFX_DEBUG_GROUPS
GXPopDebugGroup();
#endif
}
explicit GXScopedDebugGroup(const char* text);
~GXScopedDebugGroup();
};
#define GX_AND_TRACY_SCOPED(name) GXScopedDebugGroup scope(name); ZoneScopedN(name);
+122
View File
@@ -0,0 +1,122 @@
#pragma once
#include <cstring>
#include <memory>
#include <type_traits>
#include "dusk/mod_api.h"
namespace dusk {
inline DuskModAPI* g_api = nullptr;
inline void init(DuskModAPI* api) { g_api = api; }
template <class T>
T arg(void* args_raw, int n) noexcept {
void** a = static_cast<void**>(args_raw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(a[n]);
}
template <class T>
std::remove_reference_t<T>& argRef(void* args_raw, int n) noexcept {
void** a = static_cast<void**>(args_raw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(a[n]);
}
template <class F>
void* mfpAddr(F fn) noexcept {
void* p = nullptr;
static_assert(sizeof(fn) >= sizeof(void*), "unexpected MFP size");
std::memcpy(&p, &fn, sizeof(void*));
return p;
}
template <auto MFP, class R, class Self, class Orig, class... A>
struct HookEntryBase {
static inline Orig g_orig = nullptr;
static R trampoline(Self self, A... args) {
void* ptrs[] = {static_cast<void*>(std::addressof(self)), static_cast<void*>(std::addressof(args))...};
if constexpr (std::is_void_v<R>) {
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast<void*>(ptrs), nullptr);
if (!cancel) g_orig(self, args...);
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs), nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(self, args...);
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
return result;
}
}
};
template <auto FP, class R, class Orig, class... A>
struct HookEntryFreeBase {
static inline Orig g_orig = nullptr;
static R trampoline(A... args) {
if constexpr (sizeof...(A) == 0) {
if constexpr (std::is_void_v<R>) {
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), nullptr, nullptr);
if (!cancel) g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), nullptr, nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), nullptr, static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), nullptr, static_cast<void*>(std::addressof(result)));
return result;
}
} else {
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
if constexpr (std::is_void_v<R>) {
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), static_cast<void*>(ptrs), nullptr);
if (!cancel) g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), static_cast<void*>(ptrs), nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
return result;
}
}
}
};
template <auto MFP>
struct HookEntry;
template <class C, class R, class... A, R (C::*MFP)(A...)>
struct HookEntry<MFP> : HookEntryBase<MFP, R, C*, R(*)(C*, A...), A...> {};
template <class C, class R, class... A, R (C::*MFP)(A...) const>
struct HookEntry<MFP> : HookEntryBase<MFP, R, const C*, R(*)(const C*, A...), A...> {};
template <class R, class... A, R (*FP)(A...)>
struct HookEntry<FP> : HookEntryFreeBase<FP, R, R(*)(A...), A...> {};
template <auto MFP>
void hookAddPre(int32_t (*fn)(void* args)) {
using E = HookEntry<MFP>;
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
reinterpret_cast<void**>(&E::g_orig));
g_api->hook_pre(mfpAddr(MFP), fn);
}
template <auto MFP>
void hookAddPost(void (*fn)(void* args, void* retval)) {
using E = HookEntry<MFP>;
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
reinterpret_cast<void**>(&E::g_orig));
g_api->hook_post(mfpAddr(MFP), fn);
}
template <auto MFP>
void hookSetReplace(void (*fn)(void* args, void* retval)) {
using E = HookEntry<MFP>;
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
reinterpret_cast<void**>(&E::g_orig));
g_api->hook_replace(mfpAddr(MFP), fn);
}
} // namespace dusk
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
namespace dusk {
void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store);
void hookRegisterPre (void* fn_addr, void* mod, int32_t (*fn)(void* args));
void hookRegisterPost(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval));
bool hookSetReplace (void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval));
bool hookDispatchPre (void* fn_addr, void* args, void* retval);
void hookDispatchPost(void* fn_addr, void* args, void* retval);
void hookClearMod(void* mod);
} // namespace dusk
+7 -2
View File
@@ -6,6 +6,9 @@ class CPaneMgr;
namespace dusk::menu_pointer {
using TargetId = u16;
constexpr TargetId InvalidTarget = 0xffff;
enum class Context {
None,
FileSelect,
@@ -43,12 +46,14 @@ 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, u8 target) noexcept;
bool consume_deferred_activation(Context context, u8 target) noexcept;
void defer_activation(Context context, TargetId target) noexcept;
bool consume_deferred_activation(Context context, TargetId 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
@@ -0,0 +1,65 @@
#pragma once
#include <cstddef>
#include <cstdint>
#if defined(_WIN32)
#define DUSK_MOD_EXPORT __declspec(dllexport)
#else
#define DUSK_MOD_EXPORT __attribute__((visibility("default")))
#endif
#define DUSK_MOD_API_VERSION 1
typedef void* DuskPanelHandle;
typedef void* DuskElemHandle;
// Place this once at file scope in your mod to declare the minimum API version required.
// The loader will refuse to initialize the mod if the engine's API version is older.
#define DUSK_REQUIRE_API_VERSION \
extern "C" DUSK_MOD_EXPORT uint32_t mod_api_version = DUSK_MOD_API_VERSION;
struct DuskModAPIv1 {
uint32_t api_version;
const char* mod_dir;
void (*log_info)(const char* fmt, ...);
void (*log_warn)(const char* fmt, ...);
void (*log_error)(const char* fmt, ...);
void* (*load_resource)(const char* relative_path, size_t* out_size);
void (*free_resource)(void* data);
void (*register_tab_content)(
void (*build_fn)(DuskPanelHandle panel, void* userdata), void* userdata);
void (*register_tab_update)(void (*update_fn)(void* userdata), void* userdata);
void (*panel_add_section)(DuskPanelHandle panel, const char* text);
void (*panel_add_button)(
DuskPanelHandle panel, const char* label, void (*cb)(void* userdata), void* userdata);
DuskElemHandle (*panel_add_badge_row)(DuskPanelHandle panel, const char* label, int ok);
DuskElemHandle (*panel_add_dyn_text)(DuskPanelHandle panel, const char* text);
DuskElemHandle (*panel_add_progress)(DuskPanelHandle panel, float value);
void (*elem_set_badge)(DuskElemHandle elem, int ok);
void (*elem_set_text)(DuskElemHandle elem, const char* text);
void (*elem_set_progress)(DuskElemHandle elem, float value);
void (*hook_install)(void* fn_addr, void* tramp_fn, void** orig_store);
void (*hook_pre)(void* fn_addr, int32_t (*fn)(void* args));
void (*hook_post)(void* fn_addr, void (*fn)(void* args, void* retval));
void (*hook_replace)(void* fn_addr, void (*fn)(void* args, void* retval));
bool (*hook_dispatch_pre)(void* fn_addr, void* args, void* retval);
void (*hook_dispatch_post)(void* fn_addr, void* args, void* retval);
void (*service_publish)(const char* name, void* ptr);
void* (*service_get)(const char* name);
};
using DuskModAPI = DuskModAPIv1;
extern "C" {
void mod_init(DuskModAPI* api);
void mod_tick(DuskModAPI* api);
}
+134
View File
@@ -0,0 +1,134 @@
#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
@@ -0,0 +1,28 @@
#pragma once
#include "f_op/f_op_actor_mng.h"
#include "f_pc/f_pc_layer.h"
#include "f_pc/f_pc_manager.h"
#include "f_pc/f_pc_node.h"
#include "m_Do/m_Do_controller_pad.h"
// Remove a button from this frame's trigger state so the game won't see it
// Call after detecting a combo in mod_tick to prevent double-processing
inline void consumeInput(u32 pad, u32 buttonMask) {
mDoCPd_c::getCpadInfo(pad).mPressedButtonFlags &= ~buttonMask;
}
// Spawn an actor in the play scene layer
// calling fopAcM_create directly outside game simulation context creates the actor in the wrong
// layer, corrupting its first-frame rendering setup
inline fpc_ProcID fopAcM_createInPlayScene(s16 proc_name, u32 params, const cXyz* pos, int room_no,
const csXyz* angle, const cXyz* scale, s8 argument) {
layer_class* savedLayer = fpcLy_CurrentLayer();
base_process_class* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e);
if (playScene != nullptr) {
fpcLy_SetCurrentLayer(&((process_node_class*)playScene)->layer);
}
fpc_ProcID result = fopAcM_create(proc_name, params, pos, room_no, angle, scale, argument);
fpcLy_SetCurrentLayer(savedLayer);
return result;
}
-1
View File
@@ -275,7 +275,6 @@ 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,6 +114,19 @@ 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 u8 mInitFlag;
static DUSK_GAME_DATA u8 mInitFlag;
static u8 mResetFlag;
static u8 mBgmSet;
};
+2 -1
View File
@@ -4,6 +4,7 @@
#include "JSystem/JUtility/JUTGamePad.h"
#include "SSystem/SComponent/c_API_controller_pad.h"
#include "dusk/settings.h"
#include "global.h"
// Controller Ports 1 - 4
enum { PAD_1, PAD_2, PAD_3, PAD_4 };
@@ -94,7 +95,7 @@ public:
static void stopMotorWaveHard(u32 pad) { return m_gamePad[pad]->stopMotorWaveHard(); }
static JUTGamePad* m_gamePad[4];
static interface_of_controller_pad m_cpadInfo[4];
static DUSK_GAME_DATA interface_of_controller_pad m_cpadInfo[4];
static interface_of_controller_pad m_debugCpadInfo[4];
};
+1 -1
View File
@@ -371,6 +371,7 @@ public:
static int m_height;
static f32 m_heightF;
static f32 m_widthF;
#endif
#if TARGET_PC
static f32 m_safeMinXF;
@@ -380,7 +381,6 @@ public:
static f32 m_safeWidthF;
static f32 m_safeHeightF;
#endif
#endif
};
#endif /* M_DO_M_DO_GRAPHIC_H */
@@ -4,6 +4,7 @@
#include <types.h>
#include <cmath>
#include <utility>
#include "global.h"
#ifdef __cplusplus
extern "C" {
@@ -141,9 +142,9 @@ struct TAsinAcosTable {
}
};
extern TSinCosTable<13, f32> sincosTable_;
extern TAtanTable<1024, f32> atanTable_;
extern TAsinAcosTable<1024, f32> asinAcosTable_;
DUSK_GAME_EXTERN TSinCosTable<13, f32> sincosTable_;
DUSK_GAME_EXTERN TAtanTable<1024, f32> atanTable_;
DUSK_GAME_EXTERN TAsinAcosTable<1024, f32> asinAcosTable_;
inline f32 acosDegree(f32 x) {
return asinAcosTable_.acosDegree(x);
+3 -3
View File
@@ -16,10 +16,10 @@ inline f64 getConst2() {
return 9.765625E-4;
}
TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
} // namespace JMath
@@ -4,6 +4,7 @@ import android.app.ActionBar;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
@@ -14,12 +15,16 @@ import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import org.libsdl.app.SDLActivity;
import org.libsdl.app.SDLSurface;
import java.io.File;
import java.util.ArrayList;
@@ -27,6 +32,7 @@ import java.util.List;
public class DuskActivity extends SDLActivity {
private static final String TAG = "DuskActivity";
private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f;
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456;
private static final String EXTERNAL_STORAGE_AUTHORITY =
@@ -88,6 +94,11 @@ public class DuskActivity extends SDLActivity {
hideSystemBars();
}
@Override
protected SDLSurface createSDLSurface(Context context) {
return new DuskSurface(context);
}
@Override
protected void onResume() {
super.onResume();
@@ -139,6 +150,77 @@ public class DuskActivity extends SDLActivity {
};
}
public void setPreferredSurfaceFrameRate(float frameRate) {
runOnUiThread(() -> {
if (mSurface instanceof DuskSurface) {
((DuskSurface)mSurface).setPreferredFrameRate(frameRate);
}
});
}
private static final class DuskSurface extends SDLSurface {
private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE;
DuskSurface(Context context) {
super(context);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.surfaceChanged(holder, format, width, height);
setTargetFrameRate(holder);
}
void setPreferredFrameRate(float frameRate) {
preferredFrameRate = frameRate;
setTargetFrameRate(getHolder());
}
private void setTargetFrameRate(SurfaceHolder holder) {
if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return;
}
Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface();
if (surface == null || !surface.isValid()) {
return;
}
float targetFrameRate = getMaxSupportedFrameRate();
if (preferredFrameRate > 0.0f) {
targetFrameRate = preferredFrameRate;
}
if (targetFrameRate <= 0.0f) {
return;
}
try {
surface.setFrameRate(
targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps");
} catch (RuntimeException e) {
Log.w(TAG, "Failed to request surface frame rate", e);
}
}
private float getMaxSupportedFrameRate() {
if (mDisplay == null) {
return 0.0f;
}
float maxFrameRate = mDisplay.getRefreshRate();
Display.Mode[] modes = mDisplay.getSupportedModes();
if (modes == null) {
return maxFrameRate;
}
for (Display.Mode mode : modes) {
maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate());
}
return maxFrameRate;
}
}
@Override
protected String[] getArguments() {
Intent intent = getIntent();
@@ -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
@@ -0,0 +1,8 @@
<?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>
+52 -2
View File
@@ -21,6 +21,7 @@ body {
}
fps,
pipeline-progress,
toast {
position: absolute;
border: 1dp #92875B;
@@ -98,7 +99,7 @@ toast message row.muted {
opacity: 0.5;
}
toast progress {
progress {
height: 4dp;
position: absolute;
left: 0;
@@ -106,10 +107,50 @@ toast progress {
width: 100%;
}
toast progress fill {
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;
}
@@ -310,6 +351,15 @@ 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,6 +362,10 @@ 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 {
-1
View File
@@ -139,7 +139,6 @@ action-bar {
position: absolute;
display: flex;
align-items: center;
justify-content: stretch;
border: 1dp rgba(255, 255, 255, 22%);
border-radius: 23dp;
background-color: rgba(22, 24, 28, 48%);
+29
View File
@@ -395,6 +395,11 @@ progress.progress-ongoing fill {
border-radius: 3dp;
}
progress.progress-health fill {
background-color: #cc3322;
border-radius: 3dp;
}
button.achievement-clear {
flex: 0 0 auto;
align-self: center;
@@ -509,6 +514,30 @@ progress.verification-progress-bar {
color: rgba(224, 219, 200, 65%);
}
.mod-info-row {
display: flex;
align-items: center;
gap: 12dp;
padding: 4dp 0;
}
.mod-info-label {
font-family: "Fira Sans Condensed";
font-weight: bold;
opacity: 0.55;
flex: 0 0 80dp;
}
.mod-info-value {
flex: 1 1 0;
}
.mod-path {
font-size: 14dp;
word-break: break-all;
opacity: 0.7;
}
.modal-actions {
display: flex;
flex-direction: row;
+6
View File
@@ -142,6 +142,12 @@ DynamicModuleControl::DynamicModuleControl(char const* name) {
}
#endif
#if TARGET_PC
// dump() is declared but its definition is inside #if !TARGET_PC above; stub it out.
void DynamicModuleControlBase::dump() {}
void DynamicModuleControlBase::dump(char*) {}
#endif
u32 DynamicModuleControl::sAllocBytes;
JKRArchive* DynamicModuleControl::sArchive;
+1 -1
View File
@@ -19,7 +19,7 @@
#include "Z2AudioCS/Z2AudioCS.h"
#endif
Z2AudioMgr* Z2AudioMgr::mAudioMgrPtr;
DUSK_GAME_DATA Z2AudioMgr* Z2AudioMgr::mAudioMgrPtr;
u8 gMuffleOutOfRangeMic = false;
Z2AudioMgr::Z2AudioMgr() : mSoundStarter(true) {
+10
View File
@@ -175,3 +175,13 @@ bool daAlink_c::checkAimContext() {
return false;
}
}
bool daAlink_c::checkAimInputContext() {
switch (mProcID) {
case PROC_HOOKSHOT_ROOF_WAIT:
case PROC_HOOKSHOT_WALL_WAIT:
return false;
default:
return checkAimContext();
}
}
+3 -3
View File
@@ -123,7 +123,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
}
#if TARGET_PC
if (dusk::getSettings().game.enableMouseAim && checkAimContext()) {
if (dusk::getSettings().game.enableMouseAim && checkAimInputContext()) {
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) &&
checkAimContext())
checkAimInputContext())
{
f32 gyro_scale = 1.0f;
if (checkWolfEyeUp()) {
@@ -174,7 +174,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
}
}
if (dusk::getSettings().game.enableTouchControls && checkAimContext()) {
if (dusk::getSettings().game.enableTouchControls && checkAimInputContext()) {
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->checkAimContext() &&
return link != nullptr && link->checkAimInputContext() &&
dComIfGp_checkCameraAttentionStatus(link->field_0x317c, 0x10);
}
+32 -21
View File
@@ -777,6 +777,7 @@ 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);
@@ -805,6 +806,7 @@ 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);
@@ -833,6 +835,7 @@ 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);
@@ -861,6 +864,7 @@ 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) {
@@ -1103,12 +1107,13 @@ 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::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::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));
}
}
#endif
bool iVar7 = true;
@@ -1494,12 +1499,14 @@ 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::consume_click())
dusk::menu_pointer::hit_pane(m3mSelPane[mSelectMenuNum], 8.0f))
{
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum));
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));
}
}
#endif
bool tmp1 = true;
@@ -1997,12 +2004,14 @@ 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::consume_click())
dusk::menu_pointer::hit_pane(mCpSelPane[field_0x026b], 8.0f))
{
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerCopySelectTarget, field_0x026b));
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));
}
}
#endif
bool iVar7 = true;
@@ -2522,12 +2531,14 @@ 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::consume_click())
dusk::menu_pointer::hit_pane(mYnSelPane[field_0x0268], 8.0f))
{
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::FileSelect,
pointer_target(s_pointerYesNoSelectTarget, field_0x0268));
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));
}
}
#endif
bool isYnSelMove = yesnoSelectMoveAnm();
+1
View File
@@ -1960,6 +1960,7 @@ 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,6 +320,7 @@ 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,6 +482,7 @@ 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;
+45 -5
View File
@@ -83,6 +83,12 @@ 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];
@@ -1098,18 +1104,28 @@ 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 (dusk::menu_pointer::consume_click()) {
if (clicked) {
yesNoSelectStart();
field_0x3ef = SelectType7;
dMeter2Info_set2DVibrationM();
@@ -1156,11 +1172,36 @@ 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();
@@ -2196,16 +2237,14 @@ 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;
}
@@ -2226,6 +2265,7 @@ 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);
+23 -1
View File
@@ -198,6 +198,7 @@ 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;
@@ -1561,6 +1562,10 @@ 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];
@@ -1573,10 +1578,27 @@ bool dMenu_Ring_c::pointerMove() {
return true;
}
if (dusk::menu_pointer::consume_click()) {
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;
return true;
}
if (pointer.released) {
mPointerTouchPressHoveredCurrent = false;
}
return false;
}
#endif
+16 -10
View File
@@ -1820,6 +1820,7 @@ 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);
@@ -1848,6 +1849,7 @@ 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) {
@@ -1952,12 +1954,14 @@ 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::consume_click())
dusk::menu_pointer::hit_pane(mpSelData[mSelectedFile], 8.0f))
{
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::Save,
pointer_target(s_pointerSaveSelectTarget, mSelectedFile));
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));
}
}
#endif
bool bookWakuAnmComplete = true;
@@ -2130,12 +2134,14 @@ 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::consume_click())
dusk::menu_pointer::hit_pane(mpNoYes[mYesNoCursor], 8.0f))
{
dusk::menu_pointer::defer_activation(
dusk::menu_pointer::Context::Save,
pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor));
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));
}
}
#endif
bool selAnmComplete = yesnoSelectMoveAnm(0);
+1
View File
@@ -316,6 +316,7 @@ 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;
+9 -2
View File
@@ -663,8 +663,15 @@ void dMeter2_c::moveLife() {
draw_life = true;
}
if (mLifeGaugeScale != g_drawHIO.mLifeParentScale) {
mLifeGaugeScale = g_drawHIO.mLifeParentScale;
#if TARGET_PC
const f32 lifeGaugeScale =
g_drawHIO.mLifeParentScale *
std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
#else
const f32 lifeGaugeScale = g_drawHIO.mLifeParentScale;
#endif
if (mLifeGaugeScale != lifeGaugeScale) {
mLifeGaugeScale = lifeGaugeScale;
draw_life = true;
}
+1 -1
View File
@@ -594,7 +594,7 @@ BOOL dMeter2Info_c::isDirectUseItem(int param_0) {
return (mDirectUseItem & (u8)(1 << param_0)) ? TRUE : FALSE;
}
dMeter2Info_c g_meter2_info;
DUSK_GAME_DATA dMeter2Info_c g_meter2_info;
int dMeter2Info_c::setMeterString(s32 i_string) {
if (mMeterString != 0) {
+2 -1
View File
@@ -560,7 +560,8 @@ bool dMsgScrn3Select_c::pointerMove() {
mDPDPoint = choice;
field_0x110 = paneIndex;
dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::state().clicked);
dusk::menu_pointer::set_hover_target(choice);
dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::peek_click());
return true;
}
+74
View File
@@ -0,0 +1,74 @@
#include "dusk/android_frame_rate.hpp"
#if defined(TARGET_ANDROID) || defined(__ANDROID__) || defined(ANDROID)
#include "dusk/settings.h"
#include <SDL3/SDL_system.h>
#include <jni.h>
namespace dusk::android {
namespace {
float preferred_surface_frame_rate() {
switch (getSettings().game.enableFrameInterpolation.getValue()) {
case FrameInterpMode::Off:
return 30.0f;
case FrameInterpMode::Unlimited:
default:
return 0.0f;
case FrameInterpMode::Capped:
return static_cast<float>(getSettings().video.maxFrameRate.getValue());
}
}
bool clear_pending_exception(JNIEnv* env) {
if (env == nullptr || !env->ExceptionCheck()) {
return false;
}
env->ExceptionClear();
return true;
}
} // namespace
void update_surface_frame_rate() {
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
if (env == nullptr) {
return;
}
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (activity == nullptr || clear_pending_exception(env)) {
if (activity != nullptr) {
env->DeleteLocalRef(activity);
}
return;
}
jclass activityClass = env->GetObjectClass(activity);
if (activityClass == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return;
}
jmethodID setPreferredFrameRate =
env->GetMethodID(activityClass, "setPreferredSurfaceFrameRate", "(F)V");
env->DeleteLocalRef(activityClass);
if (setPreferredFrameRate == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return;
}
jvalue args[1]{};
args[0].f = preferred_surface_frame_rate();
env->CallVoidMethodA(activity, setPreferredFrameRate, args);
env->DeleteLocalRef(activity);
clear_pending_exception(env);
}
} // namespace dusk::android
#else
namespace dusk::android {
void update_surface_frame_rate() {}
} // namespace dusk::android
#endif
+7
View File
@@ -0,0 +1,7 @@
#pragma once
namespace dusk::android {
void update_surface_frame_rate();
} // namespace dusk::android
+77 -17
View File
@@ -17,6 +17,7 @@
#include <utility>
#include "dusk/action_bindings.h"
#include "dusk/logging.h"
#include "dusk/main.h"
using namespace dusk::config;
@@ -27,8 +28,9 @@ using json = nlohmann::json;
aurora::Module DuskConfigLog("dusk::config");
static absl::flat_hash_map<std::string_view, ConfigVarBase*> RegisteredConfigVars;
static bool RegistrationDone = false;
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 std::optional<dusk::ui::ControlAnchor> parse_control_anchor(std::string_view value) {
if (value == "none") {
@@ -148,17 +150,23 @@ static void ReplaceFile(const std::filesystem::path& source, const std::filesyst
}
}
ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl)
: name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {}
ConfigVarBase::ConfigVarBase(std::string name, const ConfigImplBase* impl) : name(std::move(name)), registered(false), layer(ConfigVarLayer::Default), impl(impl) {
}
const char* ConfigVarBase::getName() const noexcept {
return name;
return name.c_str();
}
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>) {
@@ -385,17 +393,37 @@ template class ConfigImpl<dusk::ui::ControlLayout>;
} // namespace dusk::config
void dusk::config::Register(ConfigVarBase& configVar) {
const auto& name = configVar.getName();
if (RegistrationDone) {
DuskConfigLog.fatal("Tried to register CVar {} after registrations closed!", name);
}
const std::string_view name = configVar.getName();
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() {
@@ -405,8 +433,11 @@ void ConfigVarBase::markRegistered() {
registered = true;
}
void dusk::config::FinishRegistration() {
RegistrationDone = true;
void ConfigVarBase::unmarkRegistered() {
if (!registered)
abort();
registered = false;
}
void dusk::config::LoadFromUserPreferences() {
@@ -427,11 +458,16 @@ 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.error("Unknown key '{}' found in config!", key);
DuskConfigLog.debug(
"Unknown key '{}' found in config! If this gets registered later, that's acceptable!",
key);
UnregisteredConfigVars.emplace(key, el.value());
continue;
}
@@ -444,10 +480,6 @@ 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 {
@@ -465,6 +497,20 @@ 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()) {
@@ -483,6 +529,10 @@ 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));
@@ -513,3 +563,13 @@ 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
@@ -0,0 +1,17 @@
#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
@@ -0,0 +1,156 @@
#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,7 +375,6 @@ namespace dusk {
void ImGuiConsole::PostDraw() {
m_menuTools.afterDraw();
ShowPipelineProgress();
}
void ImGuiConsole::UpdateDragScroll() {
@@ -524,31 +523,4 @@ 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,7 +35,6 @@ private:
// Keep always last
ImGuiMenuTools m_menuTools;
void ShowPipelineProgress();
void UpdateDragScroll();
};
+15
View File
@@ -0,0 +1,15 @@
/**
* 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);
}
+3 -2
View File
@@ -1,5 +1,5 @@
#if _WIN32
#define WINDOWS_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <shellapi.h>
#endif
@@ -224,7 +224,8 @@ int main(int argc, char* argv[]) {
}
#if _WIN32
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
// Entry point called by the launcher executable.
int __declspec(dllexport) dusk_WinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
return RunWindowsGuiEntryPoint();
}
#endif
+142 -14
View File
@@ -1,16 +1,34 @@
#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;
@@ -27,7 +45,14 @@ s32 s_mouseButton = -1;
u32 s_suppressedPadHoldMask = 0;
u32 s_suppressedPadNextReadMask = 0;
Context s_deferredActivationContext = Context::None;
u8 s_deferredActivationTarget = 0xFF;
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;
s32 scancode_from_rml_button(s32 button) noexcept {
switch (button) {
@@ -104,6 +129,37 @@ 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) {
@@ -121,7 +177,7 @@ void set_position_from_rml(f32 x, f32 y) noexcept {
void clear_input_state() noexcept {
s_state = {};
s_clickConsumed = false;
clear_click_state();
s_lastDialogChoice = 0xFF;
s_currentDialogChoice = 0xFF;
s_lastDialogChoiceValid = false;
@@ -134,7 +190,10 @@ void clear_input_state() noexcept {
s_suppressedPadHoldMask = 0;
s_suppressedPadNextReadMask = 0;
s_deferredActivationContext = Context::None;
s_deferredActivationTarget = 0xFF;
s_deferredActivationTarget = InvalidTarget;
s_gesture = {};
s_hoverTargetValid = false;
s_hoverTarget = InvalidTarget;
}
} // namespace
@@ -144,8 +203,6 @@ 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)) {
@@ -174,21 +231,41 @@ 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:
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;
s_state.down = false;
s_state.released = true;
s_state.clicked = true;
s_state.clicked = s_clickPending;
s_gesture = {};
break;
}
case Phase::Cancel:
clear_click_state();
s_gesture = {};
s_state.down = false;
break;
case Phase::Move:
@@ -211,6 +288,12 @@ 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;
@@ -222,6 +305,12 @@ 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 {
@@ -237,7 +326,11 @@ void begin_context(Context context) noexcept {
s_suppressedPadHoldMask = 0;
s_suppressedPadNextReadMask = 0;
s_deferredActivationContext = Context::None;
s_deferredActivationTarget = 0xFF;
s_deferredActivationTarget = InvalidTarget;
s_gesture = {};
s_hoverTargetValid = false;
s_hoverTarget = InvalidTarget;
clear_click_state();
}
s_currentContext = context;
@@ -259,15 +352,50 @@ 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_state.clicked || s_clickConsumed) {
if (s_clickConsumed || !click_matches_hover_target()) {
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;
@@ -300,18 +428,18 @@ bool consume_dialog_click(u8& choice) noexcept {
return false;
}
void defer_activation(Context context, u8 target) noexcept {
void defer_activation(Context context, TargetId target) noexcept {
s_deferredActivationContext = context;
s_deferredActivationTarget = target;
}
bool consume_deferred_activation(Context context, u8 target) noexcept {
bool consume_deferred_activation(Context context, TargetId target) noexcept {
if (s_deferredActivationContext != context || s_deferredActivationTarget != target) {
return false;
}
s_deferredActivationContext = Context::None;
s_deferredActivationTarget = 0xFF;
s_deferredActivationTarget = InvalidTarget;
return true;
}
@@ -321,7 +449,7 @@ void clear_deferred_activation(Context context) noexcept {
}
s_deferredActivationContext = Context::None;
s_deferredActivationTarget = 0xFF;
s_deferredActivationTarget = InvalidTarget;
}
u32 suppressed_pad_buttons(u32 port) noexcept {
+61
View File
@@ -0,0 +1,61 @@
#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
@@ -0,0 +1,65 @@
#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
@@ -0,0 +1,445 @@
#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
@@ -0,0 +1,73 @@
#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
@@ -0,0 +1,285 @@
#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
@@ -0,0 +1,111 @@
#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
@@ -0,0 +1,83 @@
#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
@@ -0,0 +1,35 @@
#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,7 +160,6 @@ 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)},
@@ -345,7 +344,6 @@ 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);
+14 -9
View File
@@ -3,9 +3,7 @@
#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 {
@@ -30,19 +28,19 @@ Document::Document(const Rml::String& source, bool passive)
return;
}
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::Menu && !visible()) {
if (cmd != NavCommand::Menu && (!visible() || !active())) {
event.StopImmediatePropagation();
}
},
true);
const auto blockUnlessVisible = [this](Rml::Event& event) {
if (!visible()) {
const auto blockUnlessActive = [this](Rml::Event& event) {
if (!visible() || !active()) {
event.StopImmediatePropagation();
}
};
listen(Rml::EventId::Mouseover, blockUnlessVisible, true);
listen(Rml::EventId::Click, blockUnlessVisible, true);
listen(Rml::EventId::Scroll, blockUnlessVisible, true);
listen(Rml::EventId::Mouseover, blockUnlessActive, true);
listen(Rml::EventId::Click, blockUnlessActive, true);
listen(Rml::EventId::Scroll, blockUnlessActive, true);
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
if (mPassive) {
@@ -124,9 +122,16 @@ 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) {
if (cmd == NavCommand::None || (cmd != NavCommand::Menu && !visible())) {
return false;
}
return handle_nav_command(event, cmd);
+3 -3
View File
@@ -18,6 +18,7 @@ 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);
@@ -41,12 +42,11 @@ public:
push_document(std::move(document));
hide(false);
}
void pop() {
void pop(bool show = true) {
hide(true);
show_top_document();
focus_top_document(show);
}
bool pending_close() const { return mPendingClose; }
bool closed() const { return mClosed; }
bool handle_nav_event(Rml::Event& event);
+2 -2
View File
@@ -16,6 +16,7 @@
#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"
@@ -58,8 +59,7 @@ 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
@@ -0,0 +1,125 @@
#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
@@ -0,0 +1,25 @@
#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
+66 -8
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,6 +33,13 @@ 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" />
@@ -48,6 +55,7 @@ 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"};
@@ -160,8 +168,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();
}
@@ -197,6 +205,9 @@ 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");
@@ -258,6 +269,8 @@ 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();
@@ -309,7 +322,8 @@ 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");
}
@@ -373,10 +387,8 @@ 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;
Rml::ElementList list;
mDocument->GetElementsByTagName(list, "progress");
for (auto* elem : list) {
elem->SetAttribute("value", remaining);
if (auto* progress = mCurrentToast->QuerySelector("progress")) {
progress->SetAttribute("value", remaining);
}
if (remaining == 0.f) {
if (mCurrentToast->IsPseudoClassSet("done") ||
@@ -395,6 +407,52 @@ 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,7 +16,13 @@ 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;
@@ -25,7 +31,11 @@ protected:
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
+12 -4
View File
@@ -28,6 +28,7 @@
#include <thread>
#include "m_Do/m_Do_MemCard.h"
#include "mods_window.hpp"
namespace dusk::ui {
namespace {
@@ -49,14 +50,14 @@ const Rml::String kDocumentSource = R"RML(
</hero>
<div id="menu-list" />
</menu>
<disc-info class="intro-item delay-4">
<disc-info class="intro-item delay-5">
<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-5">
<version-info class="intro-item delay-6">
<div class="version">Version <span id="version-text"></span></div>
<div id="update-status" class="update">
<span id="update-message"></span>
@@ -715,7 +716,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
}
IsGameLaunched = true;
hide(true);
pop(false);
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
@@ -726,9 +727,16 @@ 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-3");
apply_intro_animation(mMenuButtons.back()->root(), "delay-4");
}
mDiscStatus = mDocument->GetElementById("disc-status");
+19 -16
View File
@@ -6,6 +6,7 @@
#include "dusk/app_info.hpp"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/android_frame_rate.hpp"
#include "dusk/config.hpp"
#include "dusk/hotkeys.h"
#include "dusk/data.hpp"
@@ -447,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; },
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
});
}
@@ -478,14 +479,19 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar<f
SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>& var,
Rml::String key, Rml::String helpText, int min, int max, int step = 5,
std::function<bool()> isDisabled = {}, std::string suffix = "") {
std::function<bool()> isDisabled = {}, std::function<void(int)> onChange = {},
std::string suffix = "") {
auto& button = leftPane.add_child<NumberButton>(NumberButton::Props{
.key = std::move(key),
.getValue = [&var] { return var; },
.getValue = [&var] { return var.getValue(); },
.setValue =
[&var, min, max](int value) {
var.setValue(std::clamp(value, min, max));
[&var, min, max, callback = std::move(onChange)](int value) {
const int clampedValue = std::clamp(value, min, max);
var.setValue(clampedValue);
config::Save();
if (callback) {
callback(clampedValue);
}
},
.isDisabled = std::move(isDisabled),
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
@@ -662,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(configured_backend())}; },
.getValue = [] { return Rml::String{backend_name(aurora_get_backend())}; },
.isModified =
[] {
return getSettings().backend.graphicsBackend.getValue() !=
@@ -929,6 +935,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().game.enableFrameInterpolation.setValue(static_cast<FrameInterpMode>(i));
android::update_surface_frame_rate();
config::Save();
});
}
@@ -936,7 +943,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
});
config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate,
"Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1,
[] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; });
[] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; },
[](int) { android::update_surface_frame_rate(); });
config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground,
{
.key = "Enable Mini-Map Shadows",
@@ -1084,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; });
[] { return getSettings().game.speedrunMode.getValue(); });
addOption("Reset Key (" + Rml::String{hotkeys::DO_RESET} + ")",
getSettings().game.enableResetKeybind,
"Press " + Rml::String{hotkeys::DO_RESET} + " to reset the game.");
@@ -1197,7 +1205,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
getSettings().game.damageMultiplier.setValue(value);
config::Save();
},
.isDisabled = [] { return getSettings().game.speedrunMode; },
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isModified =
[] {
return getSettings().game.damageMultiplier.getValue() !=
@@ -1341,7 +1349,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
[] {
return kMagicArmorModes[static_cast<u8>(getSettings().game.armorRupeeDrain.getValue())];
},
.isDisabled = [] { return getSettings().game.speedrunMode; },
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
.isModified =
[] {
return getSettings().game.armorRupeeDrain.getValue() !=
@@ -1477,11 +1485,6 @@ 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",
@@ -1518,7 +1521,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
}
}
},
.isDisabled = [] { return getSettings().game.speedrunMode; },
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
});
config_bool_select(leftPane, rightPane, getSettings().game.showInputViewer,
{
+27 -9
View File
@@ -156,6 +156,10 @@ 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 item_wheel_active() noexcept {
return dMeter2Info_getWindowStatus() == 2;
}
@@ -174,7 +178,7 @@ enum class StickOutput {
};
StickOutput stick_output_mode() noexcept {
if (fishing_controls_active()) {
if (fishing_controls_active() || hawkeye_active()) {
return StickOutput::CStick;
}
return StickOutput::MainStick;
@@ -693,7 +697,7 @@ void TouchControls::sync_touch_state() noexcept {
sync_l_lock_state();
const bool aimActive = dCamera_c::isAimActive();
if (aimActive && mMoveTouch.active) {
if (aimActive && !hawkeye_active() && mMoveTouch.active) {
if (!mCameraTouch.active) {
mCameraTouch = mMoveTouch;
mCameraTouch.start = mMoveTouch.current;
@@ -1208,7 +1212,26 @@ 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 (!mCameraTouch.active) {
mCameraTouch = {
.id = id,
@@ -1220,16 +1243,11 @@ void TouchControls::handle_touch_down(Rml::Event& event) noexcept {
return;
}
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) {
if (!inAnalogZone) {
return;
}
const auto width = static_cast<float>(dimensions.x);
if (!mMoveTouch.active && position.x < width * kLeftZoneWidth) {
if (!mMoveTouch.active && inLeftZone) {
mMoveTouch = {
.id = id,
.start = position,
+9 -5
View File
@@ -195,9 +195,13 @@ Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive)
return ret;
}
void show_top_document() noexcept {
void focus_top_document(bool show) noexcept {
if (auto* doc = top_document()) {
doc->show();
if (show) {
doc->show();
} else {
doc->focus();
}
}
input::sync_input_block();
}
@@ -210,13 +214,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->pending_close() && !prelaunch->closed();
return prelaunch != nullptr && prelaunch->active();
});
}
Document* top_document() noexcept {
for (auto& doc : std::views::reverse(sDocumentStack)) {
if (!doc->closed() && !doc->pending_close()) {
if (doc->active()) {
return doc.get();
}
}
@@ -259,7 +263,7 @@ void update() noexcept {
context->GetFocusElement() == context->GetRootElement()))
{
for (auto& doc : std::views::reverse(sDocumentStack)) {
if (!doc->closed() && !doc->pending_close() && doc->focus()) {
if (doc->active() && 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 show_top_document() noexcept;
void focus_top_document(bool show) noexcept;
bool any_document_visible() noexcept;
bool is_prelaunch_open() noexcept;
Document* top_document() noexcept;
+2
View File
@@ -18,6 +18,7 @@
#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"
@@ -832,6 +833,7 @@ 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
u8 mDoAud_zelAudio_c::mInitFlag;
DUSK_GAME_DATA u8 mDoAud_zelAudio_c::mInitFlag;
u8 mDoAud_zelAudio_c::mResetFlag;
+29 -23
View File
@@ -47,6 +47,7 @@
#include <system_error>
#include <thread>
#include "SSystem/SComponent/c_API.h"
#include "dusk/android_frame_rate.hpp"
#include "dusk/app_info.hpp"
#include "dusk/crash_handler.h"
#include "dusk/crash_reporting.h"
@@ -59,6 +60,7 @@
#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"
@@ -333,11 +335,21 @@ 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;
@@ -349,19 +361,10 @@ 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();
}
@@ -429,16 +432,7 @@ 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);
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());
}
dusk::config::LoadArgOverride(name, value);
}
}
@@ -509,7 +503,6 @@ int game_main(int argc, char* argv[]) {
mainCalled = true;
dusk::registerSettings();
dusk::config::FinishRegistration();
cxxopts::ParseResult parsed_arg_options;
@@ -521,6 +514,7 @@ 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>>());
@@ -555,6 +549,7 @@ int game_main(int argc, char* argv[]) {
dusk::resetForSpeedrunMode();
}
ApplyCVarOverrides(parsed_arg_options["cvar"]);
dusk::android::update_surface_frame_rate();
dusk::crash_reporting::initialize();
dusk::crash_handler::install();
// TODO: How to handle this?
@@ -790,9 +785,19 @@ 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();
@@ -812,6 +817,7 @@ 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
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,6 @@
{
"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
@@ -0,0 +1,250 @@
#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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,8 @@
{
"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
@@ -0,0 +1,18 @@
#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
@@ -0,0 +1,5 @@
add_dusk_mod(mod_test
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
)
+8
View File
@@ -0,0 +1,8 @@
{
"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
@@ -0,0 +1 @@
Hello from the mod archive!

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