mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-10 12:54:50 -04:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5de0c276a | |||
| b6f0e104bd | |||
| 66aca3b69d | |||
| bd09eea0f3 | |||
| 531313120f | |||
| fd26670b0e | |||
| c83cc3b971 | |||
| 8c713d4535 | |||
| 183e7669c2 | |||
| f38514db79 | |||
| 3792912ad1 | |||
| d9795f4098 | |||
| 87d56be232 | |||
| be2924b509 | |||
| be3e6b80eb | |||
| 10c310f7b1 | |||
| 5f0c44eb84 | |||
| b88a5e4ac3 | |||
| af6ca3c80c | |||
| 9973a28154 | |||
| 358d218e8f | |||
| 0692fa5423 | |||
| 40f49a8615 | |||
| fb9ffb444a | |||
| 9823ca7c4a | |||
| 32069d936c | |||
| 3f018204b6 | |||
| 012b54b325 | |||
| 42d412a06e | |||
| 37e5b7409d | |||
| cfc0fbc342 | |||
| 3e84c65657 | |||
| 08cbaff57b | |||
| d85718f802 | |||
| 925bb069d9 | |||
| b2871054a6 | |||
| 5bead49902 | |||
| 4175d9c7f4 | |||
| 0d6b47ac73 | |||
| 1fb5d1ee2a | |||
| c042de8a55 | |||
| e25a1f3ef6 | |||
| b7f9bc91b4 | |||
| 3281c64a55 | |||
| fb08cfcc6b | |||
| 99fb2b89ce | |||
| 53573eb795 | |||
| 975ab1dc54 | |||
| 9d10a48329 | |||
| 52a067e412 | |||
| 5dcbca392d | |||
| ba906150d4 | |||
| e5c7dfdedd | |||
| 02a4e213e3 | |||
| 394627cd47 | |||
| e7081f770a | |||
| 8356eff4ce | |||
| 3597cb1bd6 | |||
| 507616015e | |||
| db20632130 | |||
| 22d906a248 |
@@ -53,3 +53,5 @@ compile_commands.json
|
||||
pipeline_cache.bin
|
||||
|
||||
extract
|
||||
|
||||
*.dusk
|
||||
|
||||
Vendored
+2
-2
@@ -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}",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+145
-34
@@ -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)
|
||||
|
||||
@@ -363,8 +391,7 @@ set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
|
||||
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
|
||||
source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
|
||||
|
||||
# PARTIAL_DEBUG makes debug and release share one struct/vtable ABI so a mod binary loads into either
|
||||
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1 PARTIAL_DEBUG=1)
|
||||
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
|
||||
|
||||
set(GAME_INCLUDE_DIRS
|
||||
include
|
||||
@@ -373,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)
|
||||
@@ -451,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/")
|
||||
@@ -475,10 +520,9 @@ set(GAME_DEBUG_FILES
|
||||
set_source_files_properties(
|
||||
${GAME_DEBUG_FILES}
|
||||
PROPERTIES
|
||||
COMPILE_DEFINITIONS "$<$<CONFIG:Debug>:DEBUG=1>;PARTIAL_DEBUG=1"
|
||||
COMPILE_DEFINITIONS "$<$<CONFIG:Debug>:DEBUG=1>;$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>"
|
||||
)
|
||||
|
||||
# game_base is for all other game code files
|
||||
set(GAME_BASE_FILES
|
||||
${DOLZEL_FILES}
|
||||
${Z2AUDIOLIB_FILES}
|
||||
@@ -489,14 +533,15 @@ set(GAME_BASE_FILES
|
||||
set_source_files_properties(
|
||||
${GAME_BASE_FILES}
|
||||
PROPERTIES
|
||||
COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0;PARTIAL_DEBUG=1"
|
||||
COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0;$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>"
|
||||
)
|
||||
|
||||
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||
target_compile_definitions(${jsystem_lib} PRIVATE
|
||||
${GAME_COMPILE_DEFS}
|
||||
DUSK_BUILDING_GAME=1
|
||||
$<$<CONFIG:Debug>:DEBUG=1>
|
||||
PARTIAL_DEBUG=1
|
||||
$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>
|
||||
)
|
||||
target_include_directories(${jsystem_lib} PRIVATE ${GAME_INCLUDE_DIRS})
|
||||
target_link_libraries(${jsystem_lib} PRIVATE ${GAME_LIBS})
|
||||
@@ -510,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>"
|
||||
@@ -538,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)
|
||||
@@ -548,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"
|
||||
)
|
||||
@@ -576,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)
|
||||
@@ -590,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
|
||||
@@ -620,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 ()
|
||||
|
||||
@@ -639,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")
|
||||
@@ -678,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)
|
||||
@@ -744,3 +851,7 @@ foreach (target IN LISTS BINARY_TARGETS)
|
||||
endif ()
|
||||
endforeach ()
|
||||
endforeach ()
|
||||
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
add_subdirectory(tools/mod_test)
|
||||
endif ()
|
||||
|
||||
@@ -20,17 +20,31 @@ It aims to be as accurate as possible to the original while also providing new o
|
||||
> Dusklight does *not* provide any copyrighted assets. You must provide your own copy of the original game.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> At a minimum, Dusklight requires a GPU with support for D3D12, Vulkan 1.1+, or Metal. For older devices, best-effort support is provided for D3D11 and OpenGL ES (Android), but will not achieve full accuracy or performance. Your experience with specific hardware, operating systems, and drivers may vary.
|
||||
> At a minimum, Dusklight requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelihood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them.
|
||||
|
||||
### 1. Dump your game
|
||||
|
||||
You must dump your own copy of the game. Please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to `.rvz` to save space.
|
||||
You must dump your own copy of the game, please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to a `.rvz` to save space.
|
||||
|
||||
Currently, only the GameCube USA and EUR releases are supported. Support for other versions of the game is planned in the future.
|
||||
|
||||
### 2. Install Dusklight
|
||||
### 2. Download [Dusklight](https://github.com/TwilitRealm/dusklight/releases)
|
||||
|
||||
Visit the [official installation guide](https://twilitrealm.dev/install/) for full instructions.
|
||||
### 3. Setup the game
|
||||
**Windows / macOS / Linux**
|
||||
- Extract the .zip file
|
||||
- Launch Dusklight
|
||||
- Press **Select Disc Image** and provide the path to your supported game dump
|
||||
- Press **Play**!
|
||||
|
||||
**iOS**
|
||||
- Follow the [iOS setup guide](docs/ios-install-altstore.md)
|
||||
|
||||
**Android**
|
||||
- Install the Dusklight APK
|
||||
- Launch Dusklight
|
||||
- Press **Select Disc Image** and provide the path to your supported game dump
|
||||
- Press **Play**!
|
||||
|
||||
# Building
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1,13 @@
|
||||
# Patches capstone's CMakeLists.txt for compatibility with CMake >= 4.0:
|
||||
# - Bumps cmake_minimum_required to 3.10 (CMake >= 4.0 dropped < 3.5 support; < 3.10 warns)
|
||||
# - Removes cmake_policy(SET CMP0048 OLD) (rejected by CMake >= 3.27)
|
||||
file(READ "${DIR}/CMakeLists.txt" _content)
|
||||
string(REGEX REPLACE
|
||||
"cmake_minimum_required[ \t]*\\([ \t]*VERSION[ \t]+[0-9]+\\.[0-9]+(\\.[0-9]+)?[ \t]*\\)"
|
||||
"cmake_minimum_required(VERSION 3.10)"
|
||||
_content "${_content}")
|
||||
string(REGEX REPLACE
|
||||
"cmake_policy[ \t]*\\([ \t]*SET[ \t]+CMP0048[ \t]+OLD[ \t]*\\)"
|
||||
"# cmake_policy(SET CMP0048 OLD)"
|
||||
_content "${_content}")
|
||||
file(WRITE "${DIR}/CMakeLists.txt" "${_content}")
|
||||
@@ -0,0 +1,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
@@ -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);
|
||||
}
|
||||
}
|
||||
```
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: e6c3a4409c...e145b9ec20
+11
@@ -1507,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
|
||||
@@ -1541,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
|
||||
|
||||
@@ -269,12 +269,6 @@
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
postFixup = lib.optionalString (!isDarwin) ''
|
||||
patchelf \
|
||||
--add-needed "${pkgs.vulkan-loader}/lib/libvulkan.so" \
|
||||
$out/bin/dusklight
|
||||
'';
|
||||
|
||||
dontStrip = true;
|
||||
|
||||
meta = {
|
||||
|
||||
@@ -20,7 +20,7 @@ public:
|
||||
|
||||
/* 0x14 */ cM3dGAab mM3dGAab;
|
||||
/* 0x30 */ cBgS_ShdwDraw_Callback mCallbackFun;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x34 */ int field_0x34;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
bool isResetting() { return mResettingFlag; }
|
||||
|
||||
static Z2AudioMgr* getInterface() { return mAudioMgrPtr; }
|
||||
static Z2AudioMgr* mAudioMgrPtr;
|
||||
static DUSK_GAME_DATA Z2AudioMgr* mAudioMgrPtr;
|
||||
|
||||
/* 0x0514 */ virtual bool startSound(JAISoundID soundID, JAISoundHandle* handle, const JGeometry::TVec3<f32>* posPtr);
|
||||
/* 0x0518 */ bool mResettingFlag;
|
||||
@@ -48,8 +48,6 @@ public:
|
||||
/* 0x1370 */ Z2FxLineMgr mFxLineMgr;
|
||||
#if DEBUG
|
||||
/* 0x13BC */ Z2DebugSys mDebugSys;
|
||||
#elif PARTIAL_DEBUG
|
||||
alignas(Z2DebugSys) u8 mDebugSys[sizeof(Z2DebugSys)];
|
||||
#endif
|
||||
}; // Size: 0x138C
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ public:
|
||||
JAISoundHandle* getMainBgmHandle() { return &mMainBgmHandle; }
|
||||
JAISoundHandle* getSubBgmHandle() { return &mSubBgmHandle; }
|
||||
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
f32 field_0x00_debug;
|
||||
u8 field_0x04_debug;
|
||||
#endif
|
||||
|
||||
@@ -100,13 +100,13 @@ public:
|
||||
bool isForceBattle() { return forceBattle_; }
|
||||
JSUList<Z2CreatureEnemy>* getEnemyList() { return &field_0x0; }
|
||||
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
JSUList<Z2SoundObjBase>* getAllList() { return &allList_; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
/* 0x00 */ JSUList<Z2CreatureEnemy> field_0x0;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x0C */ JSUList<Z2SoundObjBase> allList_;
|
||||
#endif
|
||||
/* 0x0C */ Z2EnemyArea enemyArea_;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
struct Z2SoundStarter;
|
||||
|
||||
class Z2SoundObjBase : public Z2SoundHandles
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
, public JSULink<Z2SoundObjBase>
|
||||
#endif
|
||||
{
|
||||
|
||||
@@ -88,14 +88,9 @@ public:
|
||||
/* 0x396A */ u8 field_0x396A[0x399E - 0x396A];
|
||||
/* 0x399E */ s16 field_0x399e;
|
||||
/* 0x39A0 */ u8 field_0x39A0[0x39A4 - 0x39A0];
|
||||
#if TARGET_PC
|
||||
/* 0x39A4 */ cM_rnd_c mMantRng;
|
||||
#endif
|
||||
|
||||
};
|
||||
#if TARGET_PC
|
||||
STATIC_ASSERT(sizeof(mant_class) == 0x39ac);
|
||||
#else
|
||||
|
||||
STATIC_ASSERT(sizeof(mant_class) == 0x39a4);
|
||||
#endif
|
||||
|
||||
#endif /* D_A_MANT_H */
|
||||
|
||||
@@ -45,7 +45,7 @@ private:
|
||||
|
||||
class dAttParam_c : public JORReflexible {
|
||||
public:
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x04 */ s8 mHIOChildNo;
|
||||
#endif
|
||||
|
||||
@@ -66,7 +66,7 @@ public:
|
||||
/* 0x35 */ u8 mAttnCursorDisappearFrames;
|
||||
/* 0x38 */ f32 field_0x38;
|
||||
/* 0x3C */ f32 field_0x3c;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x44 */ s32 mDebugDispPosX;
|
||||
/* 0x48 */ s32 mDebugDispPosY;
|
||||
#endif
|
||||
|
||||
@@ -201,7 +201,7 @@ private:
|
||||
/* 0x02C */ u32 m_flags;
|
||||
/* 0x030 */ cXyz* pm_pos;
|
||||
/* 0x034 */ cXyz* pm_old_pos;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x038 */ cXyz unk_0x38;
|
||||
#endif
|
||||
/* 0x038 */ cXyz* pm_speed;
|
||||
@@ -229,7 +229,7 @@ private:
|
||||
/* 0x0CC */ f32 field_0xcc;
|
||||
/* 0x0D0 */ f32 m_wtr_chk_offset;
|
||||
/* 0x0D4 */ cBgS_PolyInfo* pm_out_poly_info;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x0E4 */ cXyz unk_0xe4;
|
||||
#endif
|
||||
/* 0x0D8 */ f32 field_0xd8;
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ public:
|
||||
|
||||
// /* 0x0000 */ cCcS mCCcS;
|
||||
/* 0x284C */ dCcMassS_Mng mMass_Mng;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x2AD0 */ u8 m_is_mass_all_timer;
|
||||
#endif
|
||||
}; // Size = 0x2AC4
|
||||
|
||||
@@ -1037,7 +1037,7 @@ public:
|
||||
/* 0x1DE09 */ u8 field_0x1de09;
|
||||
/* 0x1DE0A */ u8 field_0x1de0a;
|
||||
/* 0x1DE0B */ u8 mIsDebugMode;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x1DE0C */ OSStopwatch mStopwatch;
|
||||
#endif
|
||||
|
||||
@@ -1049,11 +1049,11 @@ public:
|
||||
|
||||
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
|
||||
|
||||
extern dComIfG_inf_c g_dComIfG_gameInfo;
|
||||
extern GXColor g_blackColor;
|
||||
extern GXColor g_clearColor;
|
||||
extern GXColor g_whiteColor;
|
||||
extern GXColor g_saftyWhiteColor;
|
||||
DUSK_GAME_EXTERN dComIfG_inf_c g_dComIfG_gameInfo;
|
||||
DUSK_GAME_EXTERN GXColor g_blackColor;
|
||||
DUSK_GAME_EXTERN GXColor g_clearColor;
|
||||
DUSK_GAME_EXTERN GXColor g_whiteColor;
|
||||
DUSK_GAME_EXTERN GXColor g_saftyWhiteColor;
|
||||
|
||||
int dComLbG_PhaseHandler(request_of_phase_process_class*, request_of_phase_process_fn*,
|
||||
void*);
|
||||
|
||||
@@ -41,7 +41,7 @@ public:
|
||||
BASE_ROOM5,
|
||||
BASE_DEMO,
|
||||
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
BASE_DEBUG,
|
||||
#endif
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ public:
|
||||
/* 0x09B8 */ DUNGEON_LIGHT dungeonlight[8];
|
||||
/* 0x0C18 */ BOSS_LIGHT field_0x0c18[8];
|
||||
/* 0x0D58 */ BOSS_LIGHT field_0x0d58[6];
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x0E48 */ NAVYCHAN navy;
|
||||
/* 0x0E58 */ u8 field_0xe58[0xE68 - 0xE58]; // part of NAVYCHAN?
|
||||
#endif
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -360,12 +360,7 @@ inline void dMsgObject_demoMessageGroup() {
|
||||
}
|
||||
|
||||
inline bool dMsgObject_isTalkNowCheck() {
|
||||
#if TARGET_PC
|
||||
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
|
||||
return msgObject != NULL && msgObject->getStatus() != 1;
|
||||
#else
|
||||
return dMsgObject_getMsgObjectClass()->getStatus() == 1 ? false : true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool dMsgObject_isKillMessageFlag() {
|
||||
@@ -502,12 +497,7 @@ inline void dMsgObject_onMsgSend() {
|
||||
}
|
||||
|
||||
inline bool dMsgObject_isFukidashiCheck() {
|
||||
#if TARGET_PC
|
||||
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
|
||||
return msgObject != NULL && msgObject->getScrnDrawPtr() != NULL;
|
||||
#else
|
||||
return dMsgObject_getMsgObjectClass()->getScrnDrawPtr() == NULL ? false : true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void* dMsgObject_getTalkHeap() {
|
||||
|
||||
@@ -521,13 +521,13 @@ private:
|
||||
/* 0x019 */ u8 field_0x19;
|
||||
/* 0x01A */ u8 field_0x1a;
|
||||
/* 0x01B */ u8 field_0x1b;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x01C */ dPa_simpleEcallBack field_0x1c[48];
|
||||
#else
|
||||
/* 0x01C */ dPa_simpleEcallBack field_0x1c[25];
|
||||
#endif
|
||||
/* 0x210 */ level_c field_0x210;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
u8 mSceneCount;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ private:
|
||||
/* 0x18 */ JKRHeap* heap;
|
||||
/* 0x1C */ JKRSolidHeap* mDataHeap;
|
||||
/* 0x20 */ void** mRes;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x24 */ int mSize;
|
||||
#endif
|
||||
}; // Size: 0x24
|
||||
|
||||
+1
-4
@@ -1008,7 +1008,7 @@ public:
|
||||
|
||||
static const int ZONE_MAX = 0x20;
|
||||
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x000 */ u8 unk_0x0;
|
||||
/* 0x001 */ char unk_0x1;
|
||||
/* 0x000 */ u8 unk_0x2[0x48 - 0x2];
|
||||
@@ -1029,9 +1029,6 @@ public:
|
||||
/* 0xF30 */ s64 mSaveTotalTime;
|
||||
#if DEBUG
|
||||
/* 0xF80 */ flagFile_c mFlagFile;
|
||||
#elif PARTIAL_DEBUG
|
||||
// flagFile_c's ctor/virtuals are only defined under #if DEBUG (d_save.cpp)
|
||||
alignas(flagFile_c) u8 mFlagFile[sizeof(flagFile_c)];
|
||||
#endif
|
||||
}; // Size: 0xF38
|
||||
|
||||
|
||||
+4
-4
@@ -538,7 +538,7 @@ public:
|
||||
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const = 0;
|
||||
/* vt[87] */ virtual void setDoor(stage_tgsc_class*) = 0;
|
||||
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const = 0;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
virtual void setUnit(void*) = 0;
|
||||
virtual void* getUnit() = 0;
|
||||
#endif
|
||||
@@ -796,7 +796,7 @@ public:
|
||||
virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
||||
virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
||||
virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
virtual void setUnit(void* i_Unit) { mUnit = i_Unit; }
|
||||
virtual void* getUnit() { return mUnit; }
|
||||
#endif
|
||||
@@ -845,7 +845,7 @@ public:
|
||||
/* 0x54 */ stage_tgsc_class* mDrTg;
|
||||
/* 0x58 */ stage_tgsc_class* mDoor;
|
||||
/* 0x5C */ dStage_FloorInfo_c* mFloorInfo;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x60 */ void* mUnit;
|
||||
#endif
|
||||
/* 0x60 */ u16 mPlayerNum;
|
||||
@@ -990,7 +990,7 @@ public:
|
||||
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
||||
/* vt[87] */ virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
||||
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
virtual void setUnit(void* i_Unit) {
|
||||
UNUSED(i_Unit);
|
||||
OSReport("stage non unit list data !!\n");
|
||||
|
||||
@@ -97,9 +97,6 @@ public:
|
||||
private:
|
||||
#if DEBUG
|
||||
/* 0x00 */ dVibTest_c mVibTest;
|
||||
#elif PARTIAL_DEBUG
|
||||
// dVibTest_c's ctor/virtuals are only defined under #if DEBUG (d_vibration.cpp)
|
||||
alignas(dVibTest_c) u8 mVibTest[sizeof(dVibTest_c)];
|
||||
#endif
|
||||
|
||||
class {
|
||||
|
||||
+7
-71
@@ -1,11 +1,9 @@
|
||||
#ifndef DUSK_CONFIG_HPP
|
||||
#define DUSK_CONFIG_HPP
|
||||
|
||||
#include <concepts>
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "config_var.hpp"
|
||||
|
||||
namespace dusk::config {
|
||||
@@ -42,7 +40,7 @@ public:
|
||||
[[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0;
|
||||
};
|
||||
|
||||
template <ConfigValue T>
|
||||
template<ConfigValue T>
|
||||
class ConfigImpl : public ConfigImplBase {
|
||||
// Just downcasting the references...
|
||||
void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final {
|
||||
@@ -91,29 +89,20 @@ public:
|
||||
*/
|
||||
void Register(ConfigVarBase& configVar);
|
||||
|
||||
/**
|
||||
* \brief Unregister a CVar, detaching it from the config system.
|
||||
*
|
||||
* If the CVar carries a user-set value (Value or Speedrun layer), it is stashed as an
|
||||
* unregistered key: Save() keeps writing it, and a later Register() of the same name restores
|
||||
* it through the normal back-fill path. The CVar may be destroyed after this returns.
|
||||
*/
|
||||
void unregister(ConfigVarBase& configVar);
|
||||
|
||||
/**
|
||||
* \brief Load config from the standard user preferences location.
|
||||
*/
|
||||
void load_from_user_preferences();
|
||||
void load_from_file_name(const char* path);
|
||||
void LoadFromUserPreferences();
|
||||
void LoadFromFileName(const char* path);
|
||||
|
||||
void load_arg_override(std::string_view name, std::string_view value);
|
||||
void LoadArgOverride(std::string_view name, std::string_view value);
|
||||
|
||||
void shutdown();
|
||||
void Shutdown();
|
||||
|
||||
/**
|
||||
* \brief Save the config to file.
|
||||
*/
|
||||
void save();
|
||||
void Save();
|
||||
|
||||
/**
|
||||
* \brief Get a registered CVar by name.
|
||||
@@ -134,59 +123,6 @@ void ClearAllActionBindings(int port);
|
||||
*/
|
||||
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
|
||||
|
||||
/**
|
||||
* \brief Type-erased change callback. previousValue points at the value before the mutation
|
||||
* (a `const T*` for a `ConfigVar<T>`) and is valid only for the duration of the call.
|
||||
*/
|
||||
using ChangeCallback = std::function<void(ConfigVarBase& cVar, const void* previousValue)>;
|
||||
|
||||
/**
|
||||
* \brief Token identifying a change subscription. 0 is never a valid token.
|
||||
*/
|
||||
using Subscription = u64;
|
||||
|
||||
/**
|
||||
* \brief Subscribe to changes of the named CVar (registered or not yet registered).
|
||||
*
|
||||
* Fired synchronously on the mutating thread (in practice the game thread) whenever the CVar's
|
||||
* effective value changes at runtime: setValue, override/speedrun setters and clears. Values
|
||||
* applied by config load or launch arguments do *not* notify: loads happen during startup
|
||||
* before the subsystems callbacks push values into are initialized, and each subsystem reads
|
||||
* its initial value itself at its own init. Callbacks may mutate other CVars; a nested
|
||||
* mutation of the same CVar applies but does not re-notify.
|
||||
*/
|
||||
Subscription subscribe(std::string_view name, ChangeCallback callback);
|
||||
|
||||
/**
|
||||
* \brief Typed convenience overload: the callback receives the current and previous values.
|
||||
*/
|
||||
template <ConfigValue T, typename Callback>
|
||||
requires std::invocable<Callback, const T&, const T&> Subscription subscribe(
|
||||
ConfigVar<T>& cVar, Callback&& callback) {
|
||||
return subscribe(cVar.getName(),
|
||||
[&cVar, cb = std::forward<Callback>(callback)](ConfigVarBase&, const void* previousValue) {
|
||||
cb(cVar.getValue(), *static_cast<const T*>(previousValue));
|
||||
});
|
||||
}
|
||||
|
||||
void unsubscribe(Subscription token);
|
||||
|
||||
/**
|
||||
* \brief Register a CVar and attach a change callback in one step.
|
||||
*
|
||||
* Useful for pushing settings into external systems (e.g. aurora) from one place instead of
|
||||
* every UI setter. The callback fires only for runtime changes (see subscribe); the value
|
||||
* loaded from config or launch arguments does not fire it — the external system reads its
|
||||
* initial value itself at its own initialization.
|
||||
*/
|
||||
template <ConfigValue T, typename Callback>
|
||||
requires std::invocable<Callback, const T&, const T&> Subscription Register(
|
||||
ConfigVar<T>& cVar, Callback&& onChange) {
|
||||
auto subscription = subscribe(cVar, std::forward<Callback>(onChange));
|
||||
Register(static_cast<ConfigVarBase&>(cVar));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
const ConfigImplBase* GetConfigImpl() {
|
||||
static ConfigImpl<T> config;
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
#define DUSK_CONFIG_VAR_HPP
|
||||
|
||||
#include "dolphin/types.h"
|
||||
#include <concepts>
|
||||
#include <type_traits>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* The configuration system.
|
||||
@@ -89,6 +87,8 @@ protected:
|
||||
*/
|
||||
const ConfigImplBase* impl;
|
||||
|
||||
// The configuration system stores a direct pointer to the ConfigVar instance.
|
||||
// It is not legal to move or copy it.
|
||||
ConfigVarBase(const ConfigVarBase&) = delete;
|
||||
ConfigVarBase(std::string name, const ConfigImplBase* impl);
|
||||
|
||||
@@ -100,19 +100,6 @@ protected:
|
||||
abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any change subscriber (see config::subscribe) is attached to this CVar's name.
|
||||
*/
|
||||
[[nodiscard]] bool has_subscribers() const;
|
||||
|
||||
/**
|
||||
* Notify change subscribers (see config::subscribe) that the effective value of this CVar
|
||||
* changed. Called by mutators after the change has been applied; previousValue points at
|
||||
* the old value (a `const T*` for a `ConfigVar<T>`), valid only for the duration of the
|
||||
* call.
|
||||
*/
|
||||
void notify_changed(const void* previousValue);
|
||||
|
||||
public:
|
||||
virtual ~ConfigVarBase();
|
||||
|
||||
@@ -173,7 +160,6 @@ template <typename T>
|
||||
concept ConfigValue =
|
||||
!std::is_const_v<T>
|
||||
&& !std::is_volatile_v<T>
|
||||
&& std::equality_comparable<T>
|
||||
&& (std::is_same_v<T, bool>
|
||||
|| ConfigValueInteger<T>
|
||||
|| std::is_same_v<T, f32>
|
||||
@@ -185,9 +171,6 @@ concept ConfigValue =
|
||||
template <ConfigValue T>
|
||||
const ConfigImplBase* GetConfigImpl();
|
||||
|
||||
template <ConfigValue T>
|
||||
class ConfigImpl;
|
||||
|
||||
template <typename T>
|
||||
struct ConfigEnumRange {
|
||||
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
|
||||
@@ -258,7 +241,6 @@ public:
|
||||
*/
|
||||
void setValue(T newValue, bool replaceOverride = true) {
|
||||
checkRegistered();
|
||||
const auto previous = previous_for_notify();
|
||||
value = std::move(newValue);
|
||||
|
||||
if (replaceOverride) {
|
||||
@@ -267,7 +249,6 @@ public:
|
||||
} else if (layer != ConfigVarLayer::Override) {
|
||||
layer = ConfigVarLayer::Value;
|
||||
}
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
|
||||
operator const T&() {
|
||||
@@ -284,10 +265,8 @@ public:
|
||||
*/
|
||||
void setOverrideValue(T newValue) {
|
||||
checkRegistered();
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Override;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,31 +280,25 @@ public:
|
||||
void setSpeedrunValue(T newValue) {
|
||||
checkRegistered();
|
||||
if (layer != ConfigVarLayer::Override) {
|
||||
const auto previous = previous_for_notify();
|
||||
priorLayer = layer;
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Speedrun;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
void clearOverride() {
|
||||
checkRegistered();
|
||||
if (layer == ConfigVarLayer::Override) {
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = {};
|
||||
layer = ConfigVarLayer::Value;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
void clearSpeedrunOverride() override {
|
||||
checkRegistered();
|
||||
if (layer == ConfigVarLayer::Speedrun) {
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = {};
|
||||
layer = priorLayer;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,48 +312,6 @@ public:
|
||||
const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer;
|
||||
return effectiveLayer == ConfigVarLayer::Default ? defaultValue : value;
|
||||
}
|
||||
|
||||
private:
|
||||
// The config loader applies values through the silent load_* methods below.
|
||||
friend class ConfigImpl<T>;
|
||||
|
||||
/**
|
||||
* Copy of the effective value before a mutation, taken only when someone is subscribed.
|
||||
*/
|
||||
[[nodiscard]] std::optional<T> previous_for_notify() const {
|
||||
return has_subscribers() ? std::optional<T>{getValue()} : std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify subscribers if the effective value actually changed across a mutation.
|
||||
*/
|
||||
void notify_if_changed(const std::optional<T>& previous) {
|
||||
if (previous.has_value() && !(getValue() == *previous)) {
|
||||
notify_changed(&*previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* setValue(newValue, false) without notifying change subscribers. Used when loading config:
|
||||
* loads happen during startup before the subsystems change callbacks push values into are
|
||||
* initialized, and each subsystem applies the loaded value itself at its own init.
|
||||
*/
|
||||
void load_value(T newValue) {
|
||||
checkRegistered();
|
||||
value = std::move(newValue);
|
||||
if (layer != ConfigVarLayer::Override) {
|
||||
layer = ConfigVarLayer::Value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* setOverrideValue without notifying change subscribers (see load_value).
|
||||
*/
|
||||
void load_override_value(T newValue) {
|
||||
checkRegistered();
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Override;
|
||||
}
|
||||
};
|
||||
|
||||
using ActionBindConfigVar = ConfigVar<int>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "f_op/f_op_actor_mng.h"
|
||||
#include "f_pc/f_pc_layer.h"
|
||||
#include "f_pc/f_pc_manager.h"
|
||||
#include "f_pc/f_pc_node.h"
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
|
||||
// Remove a button from this frame's trigger state so the game won't see it
|
||||
// Call after detecting a combo in mod_tick to prevent double-processing
|
||||
inline void consumeInput(u32 pad, u32 buttonMask) {
|
||||
mDoCPd_c::getCpadInfo(pad).mPressedButtonFlags &= ~buttonMask;
|
||||
}
|
||||
|
||||
// Spawn an actor in the play scene layer
|
||||
// calling fopAcM_create directly outside game simulation context creates the actor in the wrong
|
||||
// layer, corrupting its first-frame rendering setup
|
||||
inline fpc_ProcID fopAcM_createInPlayScene(s16 proc_name, u32 params, const cXyz* pos, int room_no,
|
||||
const csXyz* angle, const cXyz* scale, s8 argument) {
|
||||
layer_class* savedLayer = fpcLy_CurrentLayer();
|
||||
base_process_class* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e);
|
||||
if (playScene != nullptr) {
|
||||
fpcLy_SetCurrentLayer(&((process_node_class*)playScene)->layer);
|
||||
}
|
||||
fpc_ProcID result = fopAcM_create(proc_name, params, pos, room_no, angle, scale, argument);
|
||||
fpcLy_SetCurrentLayer(savedLayer);
|
||||
return result;
|
||||
}
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
namespace dusk::mouse {
|
||||
void read();
|
||||
void get_aim_deltas(float& out_yaw, float& out_pitch);
|
||||
void get_camera_deltas(float& out_yaw, float& out_pitch);
|
||||
void getAimDeltas(float& out_yaw, float& out_pitch);
|
||||
void getCameraDeltas(float& out_yaw, float& out_pitch);
|
||||
void handle_event(const SDL_Event& event) noexcept;
|
||||
void on_focus_lost();
|
||||
void on_focus_gained();
|
||||
void onFocusLost();
|
||||
void onFocusGained();
|
||||
} // namespace dusk::mouse
|
||||
|
||||
+1
-15
@@ -7,8 +7,7 @@
|
||||
|
||||
namespace dusk {
|
||||
|
||||
using config::ConfigVar;
|
||||
using config::ActionBindConfigVar;
|
||||
using namespace config;
|
||||
|
||||
enum class BloomMode : int {
|
||||
Off = 0,
|
||||
@@ -47,12 +46,6 @@ enum class FrameInterpMode : u8 {
|
||||
Unlimited = 2,
|
||||
};
|
||||
|
||||
enum class TouchTargeting : u8 {
|
||||
Hybrid = 0,
|
||||
Hold = 1,
|
||||
Switch = 2,
|
||||
};
|
||||
|
||||
enum class MenuScaling : u8 {
|
||||
GameCube = 0,
|
||||
Wii = 1,
|
||||
@@ -104,12 +97,6 @@ struct ConfigEnumRange<FrameInterpMode> {
|
||||
static constexpr auto max = FrameInterpMode::Unlimited;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ConfigEnumRange<TouchTargeting> {
|
||||
static constexpr auto min = TouchTargeting::Hybrid;
|
||||
static constexpr auto max = TouchTargeting::Switch;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ConfigEnumRange<MenuScaling> {
|
||||
static constexpr auto min = MenuScaling::GameCube;
|
||||
@@ -229,7 +216,6 @@ struct UserSettings {
|
||||
ConfigVar<bool> invertMouseY;
|
||||
ConfigVar<bool> freeCamera;
|
||||
ConfigVar<bool> enableTouchControls;
|
||||
ConfigVar<TouchTargeting> touchTargeting;
|
||||
ConfigVar<bool> enableMenuPointer;
|
||||
ConfigVar<ui::ControlLayout> touchControlsLayout;
|
||||
ConfigVar<bool> invertCameraXAxis;
|
||||
|
||||
@@ -36,7 +36,7 @@ typedef struct node_create_request {
|
||||
/* 0x58 */ s16 name;
|
||||
/* 0x5C */ void* data;
|
||||
/* 0x60 */ s16 unk_0x60;
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x64 */ int unk_0x64;
|
||||
/* 0x68 */ int unk_0x68;
|
||||
#endif
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -11,13 +11,13 @@ class mDoAud_zelAudio_c : public Z2AudioMgr {
|
||||
public:
|
||||
void reset();
|
||||
mDoAud_zelAudio_c() {
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
setMode(2);
|
||||
#endif
|
||||
}
|
||||
~mDoAud_zelAudio_c() {}
|
||||
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
u8 getMode() { return field_0x13bd; }
|
||||
void setMode(u8 mode) { field_0x13bd = mode; }
|
||||
|
||||
@@ -33,7 +33,7 @@ public:
|
||||
static void onBgmSet() { mBgmSet = true; }
|
||||
static void offBgmSet() { mBgmSet = false; }
|
||||
|
||||
static u8 mInitFlag;
|
||||
static DUSK_GAME_DATA u8 mInitFlag;
|
||||
static u8 mResetFlag;
|
||||
static u8 mBgmSet;
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -35,11 +35,6 @@ public:
|
||||
/* 0x4 */ s8 mNo;
|
||||
/* 0x5 */ u8 mCount;
|
||||
#else
|
||||
#if PARTIAL_DEBUG
|
||||
// Initialized here since the DEBUG ctor doesn't run.
|
||||
/* 0x4 */ s8 mNo = -1;
|
||||
/* 0x5 */ u8 mCount = 0;
|
||||
#endif
|
||||
virtual ~mDoHIO_entry_c() {}
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -12,23 +12,12 @@ struct JORNodeEvent;
|
||||
class JORMContext;
|
||||
class JORServer;
|
||||
|
||||
// NOTE (stable game ABI): these classes stay non-polymorphic outside DEBUG
|
||||
// on purpose. Making them polymorphic under PARTIAL_DEBUG would give every one of the ~250
|
||||
// derived HIO classes a vptr and turn their plain `void genMessage(JORMContext*);`
|
||||
// declarations into implicit virtual overrides whose definitions are #if DEBUG-gated; every
|
||||
// instantiated one then fails to link (missing vtable). Closure types shared with DEBUG TUs
|
||||
// either declare their own unconditional virtuals (vptr in all TUs anyway) or add a
|
||||
// PARTIAL_DEBUG-only virtual dtor for vptr parity (see dAttParam_c).
|
||||
class JOREventListener {
|
||||
public:
|
||||
#if DEBUG
|
||||
JOREventListener() {}
|
||||
#if TARGET_PC
|
||||
virtual void listenPropertyEvent(const JORPropertyEvent*) {}
|
||||
#else
|
||||
virtual void listenPropertyEvent(const JORPropertyEvent*) = 0;
|
||||
#endif
|
||||
#endif
|
||||
};
|
||||
|
||||
class JORReflexible : public JOREventListener {
|
||||
@@ -41,11 +30,7 @@ public:
|
||||
virtual void listenPropertyEvent(const JORPropertyEvent*);
|
||||
virtual void listen(u32, const JOREvent*);
|
||||
virtual void genObjectInfo(const JORGenEvent*);
|
||||
#if TARGET_PC
|
||||
virtual void genMessage(JORMContext*) {}
|
||||
#else
|
||||
virtual void genMessage(JORMContext*) = 0;
|
||||
#endif
|
||||
virtual void listenNodeEvent(const JORNodeEvent*);
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <types.h>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
#include "global.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -141,9 +142,9 @@ struct TAsinAcosTable {
|
||||
}
|
||||
};
|
||||
|
||||
extern TSinCosTable<13, f32> sincosTable_;
|
||||
extern TAtanTable<1024, f32> atanTable_;
|
||||
extern TAsinAcosTable<1024, f32> asinAcosTable_;
|
||||
DUSK_GAME_EXTERN TSinCosTable<13, f32> sincosTable_;
|
||||
DUSK_GAME_EXTERN TAtanTable<1024, f32> atanTable_;
|
||||
DUSK_GAME_EXTERN TAsinAcosTable<1024, f32> asinAcosTable_;
|
||||
|
||||
inline f32 acosDegree(f32 x) {
|
||||
return asinAcosTable_.acosDegree(x);
|
||||
|
||||
@@ -314,7 +314,7 @@ public:
|
||||
>
|
||||
{
|
||||
TIterator_data_(const TFunctionValue_list_parameter& rParent, const f32* value) {
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
pOwn_ = &rParent;
|
||||
#endif
|
||||
pf_ = value;
|
||||
@@ -372,7 +372,7 @@ public:
|
||||
return (r1.pf_ - r2.pf_) / suData_size;
|
||||
}
|
||||
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x00 */ const TFunctionValue_list_parameter* pOwn_;
|
||||
#endif
|
||||
/* 0x00 */ const f32* pf_;
|
||||
@@ -425,7 +425,7 @@ public:
|
||||
>
|
||||
{
|
||||
TIterator_data_(const TFunctionValue_hermite& rParent, const f32* value) {
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
pOwn_ = &rParent;
|
||||
#endif
|
||||
pf_ = value;
|
||||
@@ -491,7 +491,7 @@ public:
|
||||
return (r1.pf_ - r2.pf_) / r1.uSize_;
|
||||
}
|
||||
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
/* 0x00 */ const TFunctionValue_hermite* pOwn_;
|
||||
/* 0x04 */ const f32* pf_;
|
||||
/* 0x08 */ u32 uSize_;
|
||||
|
||||
@@ -16,10 +16,10 @@ inline f64 getConst2() {
|
||||
return 9.765625E-4;
|
||||
}
|
||||
|
||||
TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
|
||||
DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
|
||||
|
||||
TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
|
||||
DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
|
||||
|
||||
TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
|
||||
DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
|
||||
|
||||
} // namespace JMath
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience.
|
||||
</p>
|
||||
</description>
|
||||
|
||||
|
||||
<provides>
|
||||
<binary>dusklight</binary>
|
||||
<id>dev.twilitrealm.dusk.desktop</id>
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "os_report.h"
|
||||
|
||||
Z2SoundObjBase::Z2SoundObjBase()
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
#if DEBUG
|
||||
: JSULink<Z2SoundObjBase>(this)
|
||||
#endif
|
||||
{
|
||||
|
||||
@@ -122,11 +122,18 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
||||
var_f31 /= dComIfGp_getCameraZoomScale(field_0x317c);
|
||||
}
|
||||
|
||||
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
|
||||
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
|
||||
|
||||
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMouseAim && checkAimInputContext()) {
|
||||
sp8 = mBodyAngle.x;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
|
||||
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
|
||||
|
||||
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
|
||||
sp8 = mBodyAngle.x;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sp8 = mBodyAngle.x;
|
||||
@@ -149,7 +156,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
||||
f32 final_yaw = 0.f;
|
||||
f32 final_pitch = 0.f;
|
||||
if (dusk::getSettings().game.enableMouseAim) {
|
||||
dusk::mouse::get_aim_deltas(final_yaw, final_pitch);
|
||||
dusk::mouse::getAimDeltas(final_yaw, final_pitch);
|
||||
}
|
||||
if (dusk::getSettings().game.enableGyroAim) {
|
||||
f32 gyro_yaw = 0.f;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include "d/d_com_inf_game.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include <aurora/texture.hpp>
|
||||
#include "dusk/dvd_asset.hpp"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
|
||||
@@ -41,8 +40,6 @@ static f32* l_texCoord_get() { alignas(32) static f32 buf[338]; static bool _
|
||||
//#define l_pos (l_pos_get())
|
||||
#define l_normal (l_normal_get())
|
||||
#define l_texCoord (l_texCoord_get())
|
||||
|
||||
static bool l_Egnd_mantTEX_hasReplacement = false;
|
||||
#else
|
||||
#include "assets/l_Egnd_mantTEX.h"
|
||||
|
||||
@@ -226,7 +223,6 @@ void daMant_packet_c::draw() {
|
||||
GXInitTexObjCI(
|
||||
&undersideTexObj, l_Egnd_mantTEX_U, 0x80, 0x80, GX_TF_C8, GX_CLAMP, GX_CLAMP, 0, 0);
|
||||
GXInitTexObjLOD(&undersideTexObj, GX_LINEAR, GX_LINEAR, 0.0, 0.0, 0.0, 0, 0, GX_ANISO_1);
|
||||
l_Egnd_mantTEX_hasReplacement = aurora::texture::has_replacement(&mainTexObj, &tlutObj);
|
||||
textureObjsInitialized = true;
|
||||
}
|
||||
#else
|
||||
@@ -640,11 +636,7 @@ static int daMant_Execute(mant_class* i_this) {
|
||||
iVar8 = 0;
|
||||
|
||||
if (i_this->field_0x3967 != 0) {
|
||||
#if TARGET_PC
|
||||
mant_cut_type = l_Egnd_mantTEX_hasReplacement ? 1 : i_this->field_0x3967;
|
||||
#else
|
||||
mant_cut_type = i_this->field_0x3967;
|
||||
#endif
|
||||
|
||||
if (i_this->field_0x3968 < 15) {
|
||||
i_this->field_0x3968++;
|
||||
@@ -656,18 +648,9 @@ static int daMant_Execute(mant_class* i_this) {
|
||||
iVar8 = 20;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
if (l_Egnd_mantTEX_hasReplacement) {
|
||||
unaff_r29 = i_this->mMantRng.getF(65536.0f);
|
||||
var_f31 = i_this->mMantRng.getFX(32.0f);
|
||||
var_f30 = i_this->mMantRng.getFX(32.0f);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
unaff_r29 = cM_rndF(65536.0f);
|
||||
var_f31 = cM_rndFX(32.0f);
|
||||
var_f30 = cM_rndFX(32.0f);
|
||||
}
|
||||
unaff_r29 = cM_rndF(65536.0f);
|
||||
var_f31 = cM_rndFX(32.0f);
|
||||
var_f30 = cM_rndFX(32.0f);
|
||||
}
|
||||
|
||||
i_this->field_0x3967 = 0;
|
||||
@@ -777,8 +760,6 @@ static int daMant_Create(fopAc_ac_c* i_this) {
|
||||
if(textureObjsInitialized) {
|
||||
GXInitTlutObjData(&tlutObj, l_Egnd_mantPAL); // make sure the cached textures are updated
|
||||
}
|
||||
|
||||
m_this->mMantRng.init(66, 16983, 855);
|
||||
#endif
|
||||
|
||||
lbl_277_bss_0 = 0;
|
||||
|
||||
+1
-5
@@ -7602,10 +7602,6 @@ bool dCamera_c::executeDebugFlyCam() {
|
||||
sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f};
|
||||
}
|
||||
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
stickX *= -1.0f;
|
||||
}
|
||||
|
||||
f32 verticalDisp = 0.0f;
|
||||
if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
|
||||
verticalDisp += trigR;
|
||||
@@ -7716,7 +7712,7 @@ bool dCamera_c::freeCamera() {
|
||||
|
||||
f32 yaw_rad = 0.0f;
|
||||
f32 pitch_rad = 0.0f;
|
||||
dusk::mouse::get_camera_deltas(yaw_rad, pitch_rad);
|
||||
dusk::mouse::getCameraDeltas(yaw_rad, pitch_rad);
|
||||
if (dusk::getSettings().game.enableMouseCamera && (yaw_rad != 0.0f || pitch_rad != 0.0f) &&
|
||||
!dComIfGp_checkCameraAttentionStatus(dComIfGp_getPlayerCameraID(0), 0x8))
|
||||
{
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ static int dKyeff_Create(kankyo_class* i_this) {
|
||||
|
||||
if (strcmp(dComIfGp_getStartStageName(), "Name") == 0) {
|
||||
camera_process_class* camera = dComIfGp_getCamera(0);
|
||||
OSTime time = DUSK_IF_ELSE(OSGetSystemTime(), OSGetTime());
|
||||
OSTime time = OSGetTime();
|
||||
OSTicksToCalendarTime(time, &calendar);
|
||||
|
||||
g_env_light.global_wind_influence.vec.x = 1.0f;
|
||||
|
||||
+1
-6
@@ -1640,7 +1640,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
|
||||
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX;
|
||||
mCenterZ -= mPackZ;
|
||||
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -field_0x64 : ) field_0x64;
|
||||
mCenterX += field_0x64;
|
||||
mCenterZ += mPackPlusZ;
|
||||
}
|
||||
|
||||
@@ -1649,8 +1649,6 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
{
|
||||
#if DEBUG
|
||||
field_0x64 = 33830.0f;
|
||||
#elif TARGET_PC
|
||||
field_0x64 = dusk::getSettings().game.enableMirrorMode ? 33830.0f : 0.0f;
|
||||
#else
|
||||
field_0x64 = 0.0f;
|
||||
#endif
|
||||
@@ -1659,9 +1657,6 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
f32 temp = (field_0x58 * (f32)(field_0x74 + 4)) * 0.5f;
|
||||
#if DEBUG
|
||||
mRightEdgePlus = -(((dMpath_c::getMinZ() - (-127103.67f)) - temp) / field_0x58);
|
||||
|
||||
#elif TARGET_PC
|
||||
mRightEdgePlus = dusk::getSettings().game.enableMirrorMode ? -(((dMpath_c::getMinX() - (-127103.67f)) - temp) / field_0x58) : 0.0f;
|
||||
#else
|
||||
mRightEdgePlus = 0.0f;
|
||||
#endif
|
||||
|
||||
+1
-24
@@ -914,20 +914,6 @@ void dMenu_DmapBg_c::dMapBgWide() {
|
||||
void dMenu_DmapBg_c::draw() {
|
||||
#if TARGET_PC
|
||||
dMapBgWide();
|
||||
|
||||
static bool prevMirror = false; // default state of panes is not mirrored
|
||||
if(prevMirror != dusk::getSettings().game.enableMirrorMode) {
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
static_cast<J2DPicture*>(mFloorScreen->search(MULTI_CHAR('rink')))->setMirror(J2DMirror_X);
|
||||
static_cast<J2DPicture*>(mBaseScreen->search(MULTI_CHAR('map000')))->setMirror(J2DMirror_X);
|
||||
}
|
||||
else {
|
||||
static_cast<J2DPicture*>(mFloorScreen->search(MULTI_CHAR('rink')))->setMirror(MIRROR0);
|
||||
static_cast<J2DPicture*>(mBaseScreen->search(MULTI_CHAR('map000')))->setMirror(MIRROR0);
|
||||
}
|
||||
|
||||
prevMirror = dusk::getSettings().game.enableMirrorMode;
|
||||
}
|
||||
#endif
|
||||
|
||||
u32 scissor_left;
|
||||
@@ -974,15 +960,6 @@ void dMenu_DmapBg_c::draw() {
|
||||
mpBackTexture->setAlpha(dVar17 * (field_0xdbc * field_0xd9c));
|
||||
|
||||
f32 local_28c = mpBackTexture->getBounds().i.x;
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
CPaneMgr mgr;
|
||||
Vec local_94 = mgr.getGlobalVtxCenter(mMapPane, true, 0);
|
||||
local_28c = (local_94.x * 2.0f) - (local_28c + 0.5f * mpBackTexture->getWidth()) - 0.5f * mpBackTexture->getWidth();
|
||||
}
|
||||
#endif
|
||||
|
||||
mpBackTexture->setBlackWhite(color_black, color_white);
|
||||
mpBackTexture->draw(local_28c, field_0xd94 + mpBackTexture->getBounds().i.y, mpBackTexture->getWidth(),
|
||||
mpBackTexture->getHeight(),
|
||||
@@ -2004,7 +1981,7 @@ void dMenu_Dmap_c::mapControl() {
|
||||
f32 temp_f28 = (var_f29 / 100.0f) * var_f31;
|
||||
f32 sp18 = temp_f28 * cM_ssin(stick_angle);
|
||||
f32 sp14 = temp_f28 * cM_scos(stick_angle);
|
||||
mMapCtrl->setPlusZoomCenterX(IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -sp18 :) sp18);
|
||||
mMapCtrl->setPlusZoomCenterX(sp18);
|
||||
mMapCtrl->setPlusZoomCenterZ(sp14);
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,14 @@ f32 dMenu_StageMapCtrl_c::getPixelStageSizeZ() const {
|
||||
|
||||
f32 dMenu_StageMapCtrl_c::getPixelCenterX() const {
|
||||
f32 var_f31 = dMpath_c::getCenterX();
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
return (1.0f / field_0xbc) * (field_0x9c + var_f31);
|
||||
}
|
||||
else return (1.0f / field_0xbc) * (field_0x9c - var_f31);
|
||||
#else
|
||||
return (1.0f / field_0xbc) * (field_0x9c - var_f31);
|
||||
#endif
|
||||
}
|
||||
|
||||
f32 dMenu_StageMapCtrl_c::getPixelCenterZ() const {
|
||||
@@ -418,7 +425,18 @@ inline static f32 rightModeCnvPos(f32 param_0) {
|
||||
void dMenu_StageMapCtrl_c::cnvPosTo2Dpos(f32 param_0, f32 param_1, f32* param_2,
|
||||
f32* param_3) const {
|
||||
if (param_2 != NULL) {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
*param_2 =
|
||||
(0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (field_0x9c + param_0));
|
||||
} else {
|
||||
*param_2 =
|
||||
(0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (param_0 - field_0x9c));
|
||||
}
|
||||
#else
|
||||
*param_2 = (0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (param_0 - field_0x9c));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
if (param_3 != NULL) {
|
||||
@@ -915,7 +933,8 @@ void dMenu_StageMapCtrl_c::move() {
|
||||
|
||||
void dMenu_DmapMapCtrl_c::draw() {
|
||||
if (field_0xef != 0) {
|
||||
setPos(field_0xeb, field_0xec, field_0x9c,
|
||||
setPos(field_0xeb, field_0xec,
|
||||
IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -field_0x9c :) field_0x9c,
|
||||
field_0xa0, field_0xbc, true, field_0xd8);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-10
@@ -931,8 +931,17 @@ void dMenu_Fmap_c::region_map_proc() {
|
||||
mpDraw2DBack->regionMapMove(mpStick);
|
||||
int stage_no, room_no;
|
||||
|
||||
#if TARGET_PC
|
||||
f32 arrow_pos_x = mpDraw2DBack->getArrowPos2DX();
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
arrow_pos_x = mpDraw2DBack->getMirrorPosX(arrow_pos_x, 0.0f);
|
||||
}
|
||||
|
||||
f32 pos_x = arrow_pos_x - mDoGph_gInf_c::getMinXF() - mDoGph_gInf_c::getWidthF() * 0.5f;
|
||||
#else
|
||||
f32 pos_x = mpDraw2DBack->getArrowPos2DX() - mDoGph_gInf_c::getMinXF()
|
||||
- mDoGph_gInf_c::getWidthF() * 0.5f;
|
||||
#endif
|
||||
f32 pos_y = mpDraw2DBack->getArrowPos2DY() - mDoGph_gInf_c::getHeightF() * 0.5f;
|
||||
|
||||
mpMenuFmapMap->getPointStagePathInnerNo(getNowFmapRegionData(), pos_x, pos_y,
|
||||
@@ -2477,6 +2486,12 @@ void dMenu_Fmap_c::portalWarpMapMove(STControl* i_stick) {
|
||||
f32 arrow_y = mpDraw2DBack->getArrowPos2DY();
|
||||
u8 uVar6 = 0xff;
|
||||
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
arrow_x = mpDraw2DBack->getMirrorPosX(arrow_x, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
for (int i = 0; i < portal_dat->mCount; i++) {
|
||||
if (portals[i].mRegionNo == mpDraw2DBack->getRegionCursor() + 1
|
||||
@@ -2546,11 +2561,6 @@ void dMenu_Fmap_c::drawIcon(f32 param_0, bool param_1) {
|
||||
if (mProcess == PROC_PORTAL_DEMO1) {
|
||||
is_portal_demo1 = 1;
|
||||
}
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
angle = 0x10000 - angle;
|
||||
}
|
||||
#endif
|
||||
mpDraw2DBack->setIcon2DPos(0x11, stage_name, pos.x, pos.z, cM_sht2d(angle),
|
||||
is_portal_demo1, param_1);
|
||||
|
||||
@@ -2639,11 +2649,6 @@ void dMenu_Fmap_c::drawPlayEnterIcon() {
|
||||
angle = dComIfGs_getPlayerFieldLastStayAngleY();
|
||||
SAFE_STRCPY(stage_name, dComIfGs_getPlayerFieldLastStayName());
|
||||
}
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
angle = 0x10000 - angle;
|
||||
}
|
||||
#endif
|
||||
mpDraw2DBack->setIcon2DPos(0x15, stage_name, pos.x, pos.z, cM_sht2d(angle), 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-73
@@ -426,15 +426,7 @@ void dMenu_Fmap2DBack_c::draw() {
|
||||
}
|
||||
|
||||
mpPointParent->setAlphaRate(mArrowAlpha * mSpotTextureFadeAlpha);
|
||||
|
||||
f32 drawX = mArrowPos2DX + mTransX;
|
||||
#ifdef TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
drawX = getMirrorPosX(drawX, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
mpPointParent->translate(drawX, mArrowPos2DY + mTransZ);
|
||||
mpPointParent->translate(mArrowPos2DX + mTransX, mArrowPos2DY + mTransZ);
|
||||
mpPointScreen->draw(0.0f, 0.0f, grafPort);
|
||||
}
|
||||
|
||||
@@ -753,7 +745,7 @@ void dMenu_Fmap2DBack_c::zoomMapCalc(f32 i_zoom) {
|
||||
|
||||
f32 tmp2 = (dVar12 + (i_zoom * (centerX - dVar12)));
|
||||
f32 tmp2_ = (dVar11 + (i_zoom * (centerY - dVar11)));
|
||||
|
||||
|
||||
field_0xf0c[mRegionCursor] =
|
||||
((tmp2 + (tmp3 * mZoom)) - mRegionMapSizeX[mRegionCursor] * mZoom * 0.5f) -
|
||||
mRegionMinMapX[mRegionCursor];
|
||||
@@ -1026,11 +1018,6 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
|
||||
f32 delta_y = speed * cM_ssin(angle);
|
||||
f32 delta_x = speed * cM_scos(angle);
|
||||
|
||||
#ifdef TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
delta_y = -delta_y;
|
||||
}
|
||||
#endif
|
||||
control_xpos = control_xpos + delta_y;
|
||||
control_ypos = control_ypos + delta_x;
|
||||
}
|
||||
@@ -1059,6 +1046,11 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
|
||||
calcAllMapPos2D((mArrowPos3DX + control_xpos) - mStageTransX,
|
||||
(mArrowPos3DZ + control_ypos) - mStageTransZ, &sp14, &sp10);
|
||||
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
sp14 = getMirrorPosX(sp14, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
mSelectRegion = 0xff;
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
@@ -1473,12 +1465,6 @@ void dMenu_Fmap2DBack_c::worldGridDraw() {
|
||||
f32 dVar8 = -mStageTransZ;
|
||||
calcAllMapPos2D(dVar9, dVar8, &local_74, &local_78);
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_74 = getMirrorPosX(local_74, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||
JUtility::TColor(255, 255, 255, 255), 6);
|
||||
@@ -1487,11 +1473,6 @@ void dMenu_Fmap2DBack_c::worldGridDraw() {
|
||||
while (true) {
|
||||
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
|
||||
if (local_74 >= getMapScissorAreaLX()) {
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_74 = getMirrorPosX(local_74, 0.0f);
|
||||
}
|
||||
#endif
|
||||
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||
JUtility::TColor(255, 255, 255, 255), 6);
|
||||
@@ -1553,12 +1534,6 @@ void dMenu_Fmap2DBack_c::regionGridDraw() {
|
||||
f32 dVar8 = mRegionOriginZ[mRegionCursor] - mStageTransZ;
|
||||
calcAllMapPos2D(dVar9, dVar8, &local_74, &local_78);
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_74 = getMirrorPosX(local_74, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||
JUtility::TColor(180, 0, 0, 255), 6);
|
||||
@@ -1567,12 +1542,6 @@ void dMenu_Fmap2DBack_c::regionGridDraw() {
|
||||
while (true) {
|
||||
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
|
||||
if (local_74 >= getMapScissorAreaLX()) {
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_74 = getMirrorPosX(local_74, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||
JUtility::TColor(180, 0, 0, 255), 6);
|
||||
@@ -1635,12 +1604,6 @@ void dMenu_Fmap2DBack_c::worldOriginDraw() {
|
||||
f32 local_44, local_48;
|
||||
calcAllMapPos2D(-mStageTransX, -mStageTransZ, &local_44, &local_48);
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_44 = getMirrorPosX(local_44, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(mDoGph_gInf_c::getMinXF(), local_48 - local_44 + mDoGph_gInf_c::getMinXF(),
|
||||
mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF(),
|
||||
local_48 - local_44 + (mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF()),
|
||||
@@ -1675,13 +1638,6 @@ void dMenu_Fmap2DBack_c::scrollAreaDraw() {
|
||||
calcAllMapPos2D(x_min - mStageTransX, z_min - mStageTransZ, &local_4c, &local_50);
|
||||
calcAllMapPos2D(x_max - mStageTransX, z_max - mStageTransZ, &local_54, &local_58);
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_4c = getMirrorPosX(local_4c, 0.0f);
|
||||
local_54 = getMirrorPosX(local_54, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(local_4c, local_50, local_4c, local_58,
|
||||
JUtility::TColor(255, 255, 255, 255), 6);
|
||||
J2DDrawLine(local_54, local_50, local_54, local_58,
|
||||
@@ -1702,11 +1658,6 @@ void dMenu_Fmap2DBack_c::regionOriginDraw() {
|
||||
f32 center_x, center_y;
|
||||
calcAllMapPos2D(mRegionOriginX[i] - mStageTransX, mRegionOriginZ[i] - mStageTransZ,
|
||||
¢er_x, ¢er_y);
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
center_x = getMirrorPosX(center_x - 3.0f, 3.0f);
|
||||
}
|
||||
#endif
|
||||
J2DFillBox(center_x - 3.0f, center_y - 3.0f, 6.0f, 6.0f, JUtility::TColor(255, 0, 0, 255));
|
||||
}
|
||||
}
|
||||
@@ -1724,11 +1675,6 @@ void dMenu_Fmap2DBack_c::stageOriginDraw() {
|
||||
f32 v1 = mRegionOriginX[mRegionCursor] + stage_data[i].mOffsetX - mStageTransX;
|
||||
f32 v2 = mRegionOriginZ[mRegionCursor] + stage_data[i].mOffsetZ - mStageTransZ;
|
||||
calcAllMapPos2D(v1, v2, ¢er_x, ¢er_y);
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
center_x = getMirrorPosX(center_x - 3.0f, 3.0f);
|
||||
}
|
||||
#endif
|
||||
J2DFillBox(center_x - 3.0f, center_y - 3.0f, 6.0f, 6.0f,
|
||||
JUtility::TColor(0, 0, 255, 255));
|
||||
}
|
||||
@@ -1975,7 +1921,7 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) {
|
||||
f32 speed = base_speed / 100.0f * local_78;
|
||||
f32 speed_y = speed * cM_ssin(angle);
|
||||
f32 speed_x = speed * cM_scos(angle);
|
||||
control_xpos += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_y :) speed_y;
|
||||
control_xpos += speed_y;
|
||||
control_ypos += speed_x;
|
||||
}
|
||||
}
|
||||
@@ -2000,6 +1946,11 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) {
|
||||
calcAllMapPos2D(mArrowPos3DX + control_xpos - mStageTransX,
|
||||
mArrowPos3DZ + control_ypos - mStageTransZ, &pos_x, &pos_y);
|
||||
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
pos_x = getMirrorPosX(pos_x, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
mSelectRegion = 0xff;
|
||||
int region = mRegionCursor;
|
||||
@@ -2031,6 +1982,11 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param
|
||||
if (stick_value >= slow_bound && param_2 && field_0x1238 != 2) {
|
||||
bVar6 = true;
|
||||
s16 angle = i_stick->getAngleStick();
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
angle = -angle;
|
||||
}
|
||||
#endif
|
||||
f32 local_68 = mTexMaxX - mTexMinX;
|
||||
f32 spot_zoom = getSpotMapZoomRate();
|
||||
f32 region_zoom = getRegionMapZoomRate(mRegionCursor);
|
||||
@@ -2045,7 +2001,7 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param
|
||||
f32 speed = base_speed / 100.0f * local_78;
|
||||
f32 speed_x = speed * cM_ssin(angle);
|
||||
f32 speed_z = speed * cM_scos(angle);
|
||||
mStageTransX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_x :) speed_x;
|
||||
mStageTransX += speed_x;
|
||||
mStageTransZ += speed_z;
|
||||
} else if (!param_2) {
|
||||
return;
|
||||
@@ -2139,11 +2095,6 @@ void dMenu_Fmap2DBack_c::drawDebugStageArea() {
|
||||
if (stage_no >= 0) {
|
||||
f32 v = i + mDoGph_gInf_c::getMinXF();
|
||||
f32 v2 = j;
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
v = getMirrorPosX(v - 3.0f, 3.0f);
|
||||
}
|
||||
#endif
|
||||
J2DFillBox(v - 3.0f, v2 - 3.0f, 6.0f, 6.0f, colors[stage_no % 6]);
|
||||
}
|
||||
}
|
||||
@@ -2179,11 +2130,6 @@ void dMenu_Fmap2DBack_c::drawDebugRegionArea() {
|
||||
mRegionMapSizeX[region] * mZoom, mRegionMapSizeY[region] * mZoom,
|
||||
mpAreaTex[region]->getTexture(0)->getTexInfo());
|
||||
if (u) {
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
pos_x = getMirrorPosX(pos_x - 3.0f, 3.0f);
|
||||
}
|
||||
#endif
|
||||
J2DFillBox(pos_x - 3.0f, pos_y - 3.0f, 6.0f, 6.0f, colors[region]);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -394,8 +394,21 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
|
||||
icon_size_y *= _c7c;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
f32 rotation = mIconInfo[info_idx].rotation;
|
||||
if (dusk::getSettings().game.enableMirrorMode &&
|
||||
(mIconInfo[info_idx].icon_no == ICON_LINK_e ||
|
||||
mIconInfo[info_idx].icon_no == ICON_LINK_ENTER_e))
|
||||
{
|
||||
rotation = -rotation;
|
||||
}
|
||||
|
||||
mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z,
|
||||
rotation);
|
||||
#else
|
||||
mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z,
|
||||
mIconInfo[info_idx].rotation);
|
||||
#endif
|
||||
|
||||
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
|
||||
mPictures[mIconInfo[info_idx].icon_no]->setAlpha((180.0f * _c80) / 255.0f);
|
||||
@@ -410,15 +423,10 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
|
||||
}
|
||||
|
||||
f32 pos_x = i_posX + (icon_pos_x - (icon_size_x / 2));
|
||||
bool r4 = false;
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
pos_x = getMirrorCenterPosX(i_posX + (icon_pos_x - (icon_size_x / 2)), icon_size_x / 2);
|
||||
}
|
||||
|
||||
if(mIconInfo[info_idx].icon_no == ICON_GOLD_WOLF_e) {
|
||||
r4 = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
mPictures[mIconInfo[info_idx].icon_no]->draw(pos_x, (i_posY + (icon_pos_y - icon_size_y / 2)),
|
||||
@@ -427,7 +435,7 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
|
||||
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
|
||||
mLightDropPic->draw((pos_x + (icon_size_x / 2)) - (var_f29 / 2),
|
||||
((icon_size_y / 2) + (i_posY + (icon_pos_y - icon_size_y / 2))) - (var_f28 / 2),
|
||||
var_f29, var_f28, r4, false, false);
|
||||
var_f29, var_f28, false, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -861,4 +869,4 @@ void dMenuMapCommon_c::getFmapPoeCount(const int regionNo, int& nowCount, int& t
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -437,12 +437,7 @@ void dMeter2_c::checkStatus() {
|
||||
|
||||
field_0x128 = daPy_py_c::checkNowWolf();
|
||||
|
||||
#if TARGET_PC
|
||||
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
|
||||
if (!dComIfGp_2dShowCheck() || (msgObject != NULL && msgObject->isPlaceMessage())) {
|
||||
#else
|
||||
if (!dComIfGp_2dShowCheck() || dMsgObject_getMsgObjectClass()->isPlaceMessage()) {
|
||||
#endif
|
||||
mStatus |= 0x4000;
|
||||
} else if (dComIfGp_checkPlayerStatus1(0, 1) && dComIfGp_getAStatus() == 0x12) {
|
||||
mStatus |= 0x200000;
|
||||
@@ -2875,14 +2870,8 @@ void dMeter2_c::alphaAnimeButton() {
|
||||
u8 var_31;
|
||||
var_31 = 0;
|
||||
|
||||
#if TARGET_PC
|
||||
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
|
||||
if ((mStatus & 0x4000) ||
|
||||
((mStatus & 0x100) && (msgObject != NULL && msgObject->isAutoMessageFlag())) ||
|
||||
#else
|
||||
if ((mStatus & 0x4000) ||
|
||||
((mStatus & 0x100) && dMsgObject_getMsgObjectClass()->isAutoMessageFlag()) ||
|
||||
#endif
|
||||
((mStatus & 0x40000000) && !(mStatus & 0x100)) || (mStatus & 0x80000000) || (mStatus & 8) ||
|
||||
(mStatus & 0x10) || (mStatus & 0x20) || (mStatus & 0x04000000) || (mStatus & 0x10000000))
|
||||
{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -428,6 +428,17 @@ static void dummyStrings() {
|
||||
dMsgObject_HIO_c g_MsgObject_HIO_c;
|
||||
|
||||
int dMsgObject_c::_execute() {
|
||||
// TODO: enabling wii message overrides fixes direction text, but gives wrong item control text
|
||||
/*#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
// enable wii message index override
|
||||
g_MsgObject_HIO_c.mMessageDisplay = 1;
|
||||
} else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) {
|
||||
g_MsgObject_HIO_c.mMessageDisplay = 0;
|
||||
}
|
||||
#endif*/
|
||||
|
||||
|
||||
field_0x4c7 = 0;
|
||||
if (mpTalkHeap != NULL) {
|
||||
field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap);
|
||||
|
||||
+1
-1
@@ -1823,7 +1823,7 @@ int dSv_info_c::memory_to_card(char* card_ptr, int dataNum) {
|
||||
savedata->getPlayer().getPlayerInfo().setTotalTime(play_time);
|
||||
}
|
||||
|
||||
savedata->getPlayer().getPlayerStatusB().setDateIpl(DUSK_IF_ELSE(OSGetSystemTime(), OSGetTime()));
|
||||
savedata->getPlayer().getPlayerStatusB().setDateIpl(OSGetTime());
|
||||
|
||||
memcpy(card_ptr, savedata, sizeof(dSv_save_c));
|
||||
card_ptr += 0x958;
|
||||
|
||||
+152
-222
@@ -7,7 +7,6 @@
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
@@ -16,90 +15,79 @@
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/action_bindings.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
|
||||
namespace dusk::config {
|
||||
namespace {
|
||||
using namespace dusk::config;
|
||||
|
||||
constexpr auto ConfigFileName = "config.json";
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
aurora::Module DuskConfigLog("dusk::config");
|
||||
|
||||
absl::flat_hash_map<std::string, ConfigVarBase*> RegisteredConfigVars;
|
||||
absl::flat_hash_map<std::string, nlohmann::json> UnregisteredConfigVars;
|
||||
absl::flat_hash_map<std::string, std::string> UnregisteredConfigVarOverrides;
|
||||
static absl::flat_hash_map<std::string, ConfigVarBase*> RegisteredConfigVars;
|
||||
static absl::flat_hash_map<std::string, nlohmann::json> UnregisteredConfigVars;
|
||||
static absl::flat_hash_map<std::string, std::string> UnregisteredConfigVarOverrides;
|
||||
|
||||
struct ChangeSubscription {
|
||||
Subscription token;
|
||||
ChangeCallback callback;
|
||||
};
|
||||
absl::flat_hash_map<std::string, std::vector<ChangeSubscription> > s_changeSubscriptions;
|
||||
absl::flat_hash_map<Subscription, std::string> s_changeTokenNames;
|
||||
Subscription s_nextChangeToken = 1;
|
||||
// Names currently being notified; guards against a callback re-notifying its own CVar.
|
||||
std::vector<std::string> s_activeChangeNotifications;
|
||||
|
||||
std::optional<ui::ControlAnchor> parse_control_anchor(std::string_view value) {
|
||||
static std::optional<dusk::ui::ControlAnchor> parse_control_anchor(std::string_view value) {
|
||||
if (value == "none") {
|
||||
return ui::ControlAnchor::None;
|
||||
return dusk::ui::ControlAnchor::None;
|
||||
}
|
||||
if (value == "top") {
|
||||
return ui::ControlAnchor::Top;
|
||||
return dusk::ui::ControlAnchor::Top;
|
||||
}
|
||||
if (value == "left") {
|
||||
return ui::ControlAnchor::Left;
|
||||
return dusk::ui::ControlAnchor::Left;
|
||||
}
|
||||
if (value == "bottom") {
|
||||
return ui::ControlAnchor::Bottom;
|
||||
return dusk::ui::ControlAnchor::Bottom;
|
||||
}
|
||||
if (value == "right") {
|
||||
return ui::ControlAnchor::Right;
|
||||
return dusk::ui::ControlAnchor::Right;
|
||||
}
|
||||
if (value == "topLeft") {
|
||||
return ui::ControlAnchor::TopLeft;
|
||||
return dusk::ui::ControlAnchor::TopLeft;
|
||||
}
|
||||
if (value == "topRight") {
|
||||
return ui::ControlAnchor::TopRight;
|
||||
return dusk::ui::ControlAnchor::TopRight;
|
||||
}
|
||||
if (value == "bottomLeft") {
|
||||
return ui::ControlAnchor::BottomLeft;
|
||||
return dusk::ui::ControlAnchor::BottomLeft;
|
||||
}
|
||||
if (value == "bottomRight") {
|
||||
return ui::ControlAnchor::BottomRight;
|
||||
return dusk::ui::ControlAnchor::BottomRight;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char* control_anchor_value(ui::ControlAnchor anchor) {
|
||||
static const char* control_anchor_value(dusk::ui::ControlAnchor anchor) {
|
||||
switch (anchor) {
|
||||
case ui::ControlAnchor::None:
|
||||
case dusk::ui::ControlAnchor::None:
|
||||
return "none";
|
||||
case ui::ControlAnchor::Top:
|
||||
case dusk::ui::ControlAnchor::Top:
|
||||
return "top";
|
||||
case ui::ControlAnchor::Left:
|
||||
case dusk::ui::ControlAnchor::Left:
|
||||
return "left";
|
||||
case ui::ControlAnchor::Bottom:
|
||||
case dusk::ui::ControlAnchor::Bottom:
|
||||
return "bottom";
|
||||
case ui::ControlAnchor::Right:
|
||||
case dusk::ui::ControlAnchor::Right:
|
||||
return "right";
|
||||
case ui::ControlAnchor::TopLeft:
|
||||
case dusk::ui::ControlAnchor::TopLeft:
|
||||
return "topLeft";
|
||||
case ui::ControlAnchor::TopRight:
|
||||
case dusk::ui::ControlAnchor::TopRight:
|
||||
return "topRight";
|
||||
case ui::ControlAnchor::BottomLeft:
|
||||
case dusk::ui::ControlAnchor::BottomLeft:
|
||||
return "bottomLeft";
|
||||
case ui::ControlAnchor::BottomRight:
|
||||
case dusk::ui::ControlAnchor::BottomRight:
|
||||
return "bottomRight";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
std::optional<float> json_finite_float(const json& object, const char* key) {
|
||||
static std::optional<float> json_finite_float(const json& object, const char* key) {
|
||||
const auto iter = object.find(key);
|
||||
if (iter == object.end() || !iter->is_number()) {
|
||||
return std::nullopt;
|
||||
@@ -113,7 +101,7 @@ std::optional<float> json_finite_float(const json& object, const char* key) {
|
||||
return value;
|
||||
}
|
||||
|
||||
std::optional<ui::ControlProps> parse_control_props(const json& value) {
|
||||
static std::optional<dusk::ui::ControlProps> parse_control_props(const json& value) {
|
||||
if (!value.is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -132,7 +120,7 @@ std::optional<ui::ControlProps> parse_control_props(const json& value) {
|
||||
if (!anchor || *w <= 0.0f || *h <= 0.0f || *scale <= 0.0f) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return ui::ControlProps{
|
||||
return dusk::ui::ControlProps{
|
||||
.x = *x,
|
||||
.y = *y,
|
||||
.w = *w,
|
||||
@@ -142,17 +130,17 @@ std::optional<ui::ControlProps> parse_control_props(const json& value) {
|
||||
};
|
||||
}
|
||||
|
||||
std::filesystem::path GetConfigJsonPath() {
|
||||
return ConfigPath / ConfigFileName;
|
||||
static std::filesystem::path GetConfigJsonPath() {
|
||||
return dusk::ConfigPath / ConfigFileName;
|
||||
}
|
||||
|
||||
std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) {
|
||||
static std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) {
|
||||
auto tempPath = configJsonPath;
|
||||
tempPath.replace_filename(fmt::format(".{}.tmp", configJsonPath.filename().string()));
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) {
|
||||
static void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) {
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(source, target, ec);
|
||||
if (ec) {
|
||||
@@ -162,84 +150,9 @@ void ReplaceFile(const std::filesystem::path& source, const std::filesystem::pat
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const Underlying raw = static_cast<Underlying>(value);
|
||||
const Underlying min = static_cast<Underlying>(ConfigEnumRange<T>::min);
|
||||
const Underlying max = static_cast<Underlying>(ConfigEnumRange<T>::max);
|
||||
if (raw < min || raw > max) {
|
||||
return cVar.getDefaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
ConfigVarBase::ConfigVarBase(std::string name, const ConfigImplBase* impl) : name(std::move(name)), registered(false), layer(ConfigVarLayer::Default), impl(impl) {
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_integral_v<T>&& std::is_signed_v<T> T parse_arg_value(
|
||||
const ConfigVar<T>&, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<T>::min() && result <= std::numeric_limits<T>::max()) {
|
||||
return static_cast<T>(result);
|
||||
}
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_integral_v<T>&& std::is_unsigned_v<T> T parse_arg_value(
|
||||
const ConfigVar<T>&, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<T>::max()) {
|
||||
return static_cast<T>(result);
|
||||
}
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
|
||||
f32 parse_arg_value(const ConfigVar<f32>&, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
return std::stof(str);
|
||||
}
|
||||
|
||||
f64 parse_arg_value(const ConfigVar<f64>&, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
return std::stod(str);
|
||||
}
|
||||
|
||||
std::string parse_arg_value(const ConfigVar<std::string>&, const std::string_view stringValue) {
|
||||
return std::string(stringValue);
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_enum_v<T> T parse_arg_value(
|
||||
const ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const std::string str(stringValue);
|
||||
|
||||
if constexpr (std::is_signed_v<Underlying>) {
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<Underlying>::min() &&
|
||||
result <= std::numeric_limits<Underlying>::max())
|
||||
{
|
||||
return sanitizeEnumValue(cVar, static_cast<T>(result));
|
||||
}
|
||||
throw std::out_of_range("Value is too large");
|
||||
} else {
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<Underlying>::max()) {
|
||||
return sanitizeEnumValue(cVar, static_cast<T>(result));
|
||||
}
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ConfigVarBase::ConfigVarBase(std::string name, const ConfigImplBase* impl)
|
||||
: name(std::move(name)), registered(false), layer(ConfigVarLayer::Default), impl(impl) {}
|
||||
|
||||
const char* ConfigVarBase::getName() const noexcept {
|
||||
return name.c_str();
|
||||
}
|
||||
@@ -254,6 +167,21 @@ ConfigVarBase::~ConfigVarBase() {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const Underlying raw = static_cast<Underlying>(value);
|
||||
const Underlying min = static_cast<Underlying>(ConfigEnumRange<T>::min);
|
||||
const Underlying max = static_cast<Underlying>(ConfigEnumRange<T>::max);
|
||||
if (raw < min || raw > max) {
|
||||
return cVar.getDefaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
@@ -267,12 +195,12 @@ void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
|
||||
|
||||
const Underlying raw = b ? static_cast<Underlying>(1) : static_cast<Underlying>(0);
|
||||
|
||||
cVar.load_value(sanitizeEnumValue(cVar, static_cast<T>(raw)));
|
||||
cVar.setValue(sanitizeEnumValue(cVar, static_cast<T>(raw)), false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get<T>()));
|
||||
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<T>()), false);
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
@@ -280,9 +208,74 @@ nlohmann::json ConfigImpl<T>::dumpToJson(const ConfigVar<T>& cVar) {
|
||||
return cVar.getValueForSave();
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_integral_v<T>&& std::is_signed_v<T> static void loadFromArgImpl(
|
||||
ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<T>::min() && result <= std::numeric_limits<T>::max()) {
|
||||
cVar.setOverrideValue(result);
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_integral_v<T>&& std::is_unsigned_v<T> static void loadFromArgImpl(
|
||||
ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<T>::max()) {
|
||||
cVar.setOverrideValue(result);
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
|
||||
static void loadFromArgImpl(ConfigVar<f32>& cVar, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stof(str);
|
||||
cVar.setOverrideValue(result);
|
||||
}
|
||||
|
||||
static void loadFromArgImpl(ConfigVar<f64>& cVar, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stod(str);
|
||||
cVar.setOverrideValue(result);
|
||||
}
|
||||
|
||||
static void loadFromArgImpl(ConfigVar<std::string>& cVar, const std::string_view stringValue) {
|
||||
cVar.setOverrideValue(std::string(stringValue));
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_enum_v<T> static void loadFromArgImpl(
|
||||
ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const std::string str(stringValue);
|
||||
|
||||
if constexpr (std::is_signed_v<Underlying>) {
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<Underlying>::min() &&
|
||||
result <= std::numeric_limits<Underlying>::max())
|
||||
{
|
||||
cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast<T>(result)));
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
} else {
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<Underlying>::max()) {
|
||||
cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast<T>(result)));
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
void ConfigImpl<T>::loadFromArg(ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
cVar.load_override_value(parse_arg_value(cVar, stringValue));
|
||||
loadFromArgImpl(cVar, stringValue);
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -290,16 +283,18 @@ void ConfigImpl<bool>::loadFromArg(ConfigVar<bool>& cVar, const std::string_view
|
||||
if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" ||
|
||||
stringValue == "True")
|
||||
{
|
||||
cVar.load_override_value(true);
|
||||
cVar.setOverrideValue(true);
|
||||
} else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" ||
|
||||
stringValue == "False")
|
||||
{
|
||||
cVar.load_override_value(false);
|
||||
cVar.setOverrideValue(false);
|
||||
} else {
|
||||
throw InvalidConfigError("Value cannot be parsed as boolean");
|
||||
}
|
||||
}
|
||||
|
||||
// My IDE is convinced this namespace is necessary. It shouldn't be AFAICT?
|
||||
namespace dusk::config {
|
||||
template class ConfigImpl<bool>;
|
||||
template class ConfigImpl<s8>;
|
||||
template class ConfigImpl<u8>;
|
||||
@@ -312,10 +307,10 @@ template class ConfigImpl<u64>;
|
||||
template class ConfigImpl<f32>;
|
||||
template class ConfigImpl<f64>;
|
||||
template class ConfigImpl<std::string>;
|
||||
template class ConfigImpl<BloomMode>;
|
||||
template class ConfigImpl<DepthOfFieldMode>;
|
||||
template class ConfigImpl<DiscVerificationState>;
|
||||
template class ConfigImpl<GameLanguage>;
|
||||
template class ConfigImpl<dusk::BloomMode>;
|
||||
template class ConfigImpl<dusk::DepthOfFieldMode>;
|
||||
template class ConfigImpl<dusk::DiscVerificationState>;
|
||||
template class ConfigImpl<dusk::GameLanguage>;
|
||||
|
||||
template <>
|
||||
void ConfigImpl<FrameInterpMode>::loadFromJson(
|
||||
@@ -325,11 +320,11 @@ void ConfigImpl<FrameInterpMode>::loadFromJson(
|
||||
|
||||
const FrameInterpMode mode = b ? FrameInterpMode::Unlimited : FrameInterpMode::Off;
|
||||
|
||||
cVar.load_value(sanitizeEnumValue(cVar, mode));
|
||||
cVar.setValue(sanitizeEnumValue(cVar, mode), false);
|
||||
return;
|
||||
}
|
||||
|
||||
cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get<FrameInterpMode>()));
|
||||
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<FrameInterpMode>()), false);
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -360,7 +355,7 @@ void ConfigImpl<ui::ControlLayout>::loadFromJson(
|
||||
}
|
||||
}
|
||||
|
||||
cVar.load_value(std::move(layout));
|
||||
cVar.setValue(std::move(layout), false);
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -390,14 +385,14 @@ nlohmann::json ConfigImpl<ui::ControlLayout>::dumpToJson(const ConfigVar<ui::Con
|
||||
};
|
||||
}
|
||||
|
||||
template class ConfigImpl<FrameInterpMode>;
|
||||
template class ConfigImpl<TouchTargeting>;
|
||||
template class ConfigImpl<MenuScaling>;
|
||||
template class ConfigImpl<Resampler>;
|
||||
template class ConfigImpl<MagicArmorMode>;
|
||||
template class ConfigImpl<ui::ControlLayout>;
|
||||
template class ConfigImpl<dusk::FrameInterpMode>;
|
||||
template class ConfigImpl<dusk::MenuScaling>;
|
||||
template class ConfigImpl<dusk::Resampler>;
|
||||
template class ConfigImpl<dusk::MagicArmorMode>;
|
||||
template class ConfigImpl<dusk::ui::ControlLayout>;
|
||||
} // namespace dusk::config
|
||||
|
||||
void Register(ConfigVarBase& configVar) {
|
||||
void dusk::config::Register(ConfigVarBase& configVar) {
|
||||
const std::string_view name = configVar.getName();
|
||||
if (RegisteredConfigVars.contains(name)) {
|
||||
DuskConfigLog.fatal("Tried to register CVar {} twice!", name);
|
||||
@@ -420,31 +415,17 @@ void Register(ConfigVarBase& configVar) {
|
||||
|
||||
const auto overridePair = UnregisteredConfigVarOverrides.find(name);
|
||||
if (overridePair != UnregisteredConfigVarOverrides.end()) {
|
||||
const auto value = std::move(overridePair->second);
|
||||
UnregisteredConfigVars.erase(name);
|
||||
|
||||
try {
|
||||
configVar.getImpl()->loadFromArg(configVar, overridePair->second);
|
||||
configVar.getImpl()->loadFromArg(configVar, value);
|
||||
} catch (std::exception& e) {
|
||||
DuskConfigLog.error("Failed to load key '{}' from override arg: {}", name, e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void unregister(ConfigVarBase& configVar) {
|
||||
const std::string_view name = configVar.getName();
|
||||
const auto it = RegisteredConfigVars.find(name);
|
||||
if (it == RegisteredConfigVars.end() || it->second != &configVar) {
|
||||
DuskConfigLog.fatal("Tried to unregister CVar '{}' that is not registered!", name);
|
||||
}
|
||||
|
||||
const auto layer = configVar.getLayer();
|
||||
if (layer == ConfigVarLayer::Value || layer == ConfigVarLayer::Speedrun) {
|
||||
UnregisteredConfigVars.insert_or_assign(
|
||||
std::string{name}, configVar.getImpl()->dumpToJson(configVar));
|
||||
}
|
||||
|
||||
RegisteredConfigVars.erase(it);
|
||||
configVar.unmarkRegistered();
|
||||
}
|
||||
|
||||
void ConfigVarBase::markRegistered() {
|
||||
if (registered)
|
||||
abort();
|
||||
@@ -459,17 +440,17 @@ void ConfigVarBase::unmarkRegistered() {
|
||||
registered = false;
|
||||
}
|
||||
|
||||
void load_from_user_preferences() {
|
||||
void dusk::config::LoadFromUserPreferences() {
|
||||
const auto configJsonPath = GetConfigJsonPath();
|
||||
if (configJsonPath.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto configPathString = io::fs_path_to_string(configJsonPath);
|
||||
load_from_file_name(configPathString.c_str());
|
||||
LoadFromFileName(configPathString.c_str());
|
||||
}
|
||||
|
||||
static void LoadFromPath(const char* path) {
|
||||
auto data = io::FileStream::ReadAllBytes(path);
|
||||
auto data = dusk::io::FileStream::ReadAllBytes(path);
|
||||
|
||||
json j = json::parse(data);
|
||||
if (!j.is_object()) {
|
||||
@@ -483,8 +464,8 @@ static void LoadFromPath(const char* path) {
|
||||
const auto& key = el.key();
|
||||
auto configVar = RegisteredConfigVars.find(key);
|
||||
if (configVar == RegisteredConfigVars.end()) {
|
||||
DuskConfigLog.debug("Unknown key '{}' found in config! If this gets registered later, "
|
||||
"that's acceptable!",
|
||||
DuskConfigLog.debug(
|
||||
"Unknown key '{}' found in config! If this gets registered later, that's acceptable!",
|
||||
key);
|
||||
UnregisteredConfigVars.emplace(key, el.value());
|
||||
continue;
|
||||
@@ -498,7 +479,7 @@ static void LoadFromPath(const char* path) {
|
||||
}
|
||||
}
|
||||
|
||||
void load_from_file_name(const char* path) {
|
||||
void dusk::config::LoadFromFileName(const char* path) {
|
||||
DuskConfigLog.info("Loading config from '{}'", path);
|
||||
|
||||
try {
|
||||
@@ -516,10 +497,10 @@ void load_from_file_name(const char* path) {
|
||||
}
|
||||
}
|
||||
|
||||
void load_arg_override(std::string_view name, std::string_view value) {
|
||||
void dusk::config::LoadArgOverride(std::string_view name, std::string_view value) {
|
||||
const auto cVar = GetConfigVar(name);
|
||||
if (!cVar) {
|
||||
UnregisteredConfigVarOverrides.emplace(name, value);
|
||||
UnregisteredConfigVarOverrides.emplace(name, name);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -530,7 +511,7 @@ void load_arg_override(std::string_view name, std::string_view value) {
|
||||
}
|
||||
}
|
||||
|
||||
void save() {
|
||||
void dusk::config::Save() {
|
||||
const auto configJsonPath = GetConfigJsonPath();
|
||||
if (configJsonPath.empty()) {
|
||||
return;
|
||||
@@ -561,14 +542,14 @@ void save() {
|
||||
}
|
||||
}
|
||||
|
||||
void ClearAllActionBindings(int port) {
|
||||
void dusk::config::ClearAllActionBindings(int port) {
|
||||
for (auto& actionBinding : getActionBinds() | std::views::values) {
|
||||
actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID);
|
||||
}
|
||||
save();
|
||||
Save();
|
||||
}
|
||||
|
||||
ConfigVarBase* GetConfigVar(std::string_view name) {
|
||||
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
|
||||
const auto configVar = RegisteredConfigVars.find(name);
|
||||
if (configVar != RegisteredConfigVars.end()) {
|
||||
return configVar->second;
|
||||
@@ -577,59 +558,13 @@ ConfigVarBase* GetConfigVar(std::string_view name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
|
||||
void dusk::config::EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
|
||||
for (auto& pair : RegisteredConfigVars) {
|
||||
callback(*pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
Subscription subscribe(std::string_view name, ChangeCallback callback) {
|
||||
const auto token = s_nextChangeToken++;
|
||||
s_changeSubscriptions[std::string{name}].push_back({token, std::move(callback)});
|
||||
s_changeTokenNames.emplace(token, std::string{name});
|
||||
return token;
|
||||
}
|
||||
|
||||
void unsubscribe(Subscription token) {
|
||||
const auto nameIt = s_changeTokenNames.find(token);
|
||||
if (nameIt == s_changeTokenNames.end()) {
|
||||
DuskConfigLog.fatal("Tried to unsubscribe unknown change token {}!", token);
|
||||
}
|
||||
|
||||
const auto subsIt = s_changeSubscriptions.find(nameIt->second);
|
||||
auto& subscriptions = subsIt->second;
|
||||
std::erase_if(
|
||||
subscriptions, [token](const ChangeSubscription& sub) { return sub.token == token; });
|
||||
if (subscriptions.empty()) {
|
||||
s_changeSubscriptions.erase(subsIt);
|
||||
}
|
||||
s_changeTokenNames.erase(nameIt);
|
||||
}
|
||||
|
||||
bool ConfigVarBase::has_subscribers() const {
|
||||
return s_changeSubscriptions.contains(name);
|
||||
}
|
||||
|
||||
void ConfigVarBase::notify_changed(const void* previousValue) {
|
||||
const auto subsIt = s_changeSubscriptions.find(name);
|
||||
if (subsIt == s_changeSubscriptions.end()) {
|
||||
return;
|
||||
}
|
||||
if (std::ranges::find(s_activeChangeNotifications, name) != s_activeChangeNotifications.end()) {
|
||||
DuskConfigLog.error("Recursive change notification for CVar '{}' suppressed", name);
|
||||
return;
|
||||
}
|
||||
|
||||
s_activeChangeNotifications.push_back(name);
|
||||
// Copied so callbacks can subscribe/unsubscribe safely.
|
||||
const auto subscriptions = subsIt->second;
|
||||
for (const auto& sub : subscriptions) {
|
||||
sub.callback(*this, previousValue);
|
||||
}
|
||||
s_activeChangeNotifications.pop_back();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
void dusk::config::Shutdown() {
|
||||
for (auto& pair : RegisteredConfigVars) {
|
||||
pair.second->unmarkRegistered();
|
||||
}
|
||||
@@ -637,9 +572,4 @@ void shutdown() {
|
||||
RegisteredConfigVars.clear();
|
||||
UnregisteredConfigVars.clear();
|
||||
UnregisteredConfigVarOverrides.clear();
|
||||
s_changeSubscriptions.clear();
|
||||
s_changeTokenNames.clear();
|
||||
s_activeChangeNotifications.clear();
|
||||
}
|
||||
|
||||
} // namespace dusk::config
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -9,7 +9,7 @@ namespace dusk::config {
|
||||
bool copy = var.getValue();
|
||||
if (ImGui::Checkbox(title, ©)) {
|
||||
var.setValue(copy);
|
||||
save();
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace dusk::config {
|
||||
float val = var;
|
||||
if (ImGui::SliderFloat(label, &val, v_min, v_max, format, flags)) {
|
||||
var.setValue(val);
|
||||
save();
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace dusk::config {
|
||||
int val = var;
|
||||
if (ImGui::SliderInt(label, &val, v_min, v_max, format, flags)) {
|
||||
var.setValue(val);
|
||||
save();
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace dusk::config {
|
||||
bool copy = p_selected.getValue();
|
||||
if (ImGui::MenuItem(label, shortcut, ©, enabled)) {
|
||||
p_selected.setValue(copy);
|
||||
save();
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ namespace dusk {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
|
||||
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||
config::save();
|
||||
config::Save();
|
||||
}
|
||||
|
||||
if (getSettings().game.enableResetKeybind && ImGui::GetIO().KeyCtrl &&
|
||||
@@ -336,7 +336,7 @@ namespace dusk {
|
||||
if constexpr (SupportsProcessRestart) {
|
||||
if (ImGui::Button("Retry (Auto backend)")) {
|
||||
getSettings().backend.graphicsBackend.setValue("auto");
|
||||
config::save();
|
||||
config::Save();
|
||||
RestartRequested = true;
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace dusk {
|
||||
bool disableWaterRefraction = getSettings().game.disableWaterRefraction;
|
||||
if (ImGui::Checkbox("Disable Water Refraction", &disableWaterRefraction)) {
|
||||
getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction);
|
||||
config::save();
|
||||
config::Save();
|
||||
}
|
||||
ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias);
|
||||
ImGui::EndMenu();
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -314,7 +314,7 @@ void end_game_frame() noexcept {
|
||||
}
|
||||
|
||||
void begin_context(Context context) noexcept {
|
||||
if (context == Context::None || !enabled()) {
|
||||
if (context == Context::None) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 += "&"; break;
|
||||
case '<': out += "<"; break;
|
||||
case '>': out += ">"; 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
+50
-72
@@ -1,69 +1,54 @@
|
||||
#include "dusk/mouse.h"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "dusk/menu_pointer.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <imgui.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
namespace dusk::mouse {
|
||||
namespace {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
constexpr float kMousePixelToRad = 0.0025f;
|
||||
constexpr auto kCursorIdleDuration = std::chrono::seconds(1);
|
||||
constexpr int kIdleHideFrames = 99; // Approx. 3 seconds with 33ms ticks
|
||||
|
||||
float s_aim_yaw_rad = 0.0f;
|
||||
float s_aim_pitch_rad = 0.0f;
|
||||
float s_camera_yaw_rad = 0.0f;
|
||||
float s_aim_yaw_rad = 0.0f;
|
||||
float s_aim_pitch_rad = 0.0f;
|
||||
float s_camera_yaw_rad = 0.0f;
|
||||
float s_camera_pitch_rad = 0.0f;
|
||||
Clock::time_point s_last_cursor_motion = Clock::now();
|
||||
int s_idle_frames = 0;
|
||||
|
||||
void reset_deltas() {
|
||||
s_aim_yaw_rad = s_aim_pitch_rad = 0.0f;
|
||||
s_camera_yaw_rad = s_camera_pitch_rad = 0.0f;
|
||||
}
|
||||
|
||||
bool query_mouse_aim_context() {
|
||||
bool queryMouseAimContext() {
|
||||
return getSettings().game.enableMouseAim.getValue() && dCamera_c::isAimActive();
|
||||
}
|
||||
|
||||
bool want_mouse_capture() {
|
||||
return getSettings().game.enableMouseCamera.getValue() || query_mouse_aim_context();
|
||||
bool wantMouseCapture() {
|
||||
return getSettings().game.enableMouseCamera.getValue() || queryMouseAimContext();
|
||||
}
|
||||
|
||||
bool mouse_input_enabled() {
|
||||
const auto& game = getSettings().game;
|
||||
return game.enableMouseAim.getValue() || game.enableMouseCamera.getValue();
|
||||
}
|
||||
|
||||
bool is_window_focused(SDL_Window* window) {
|
||||
bool isWindowFocused(SDL_Window* window) {
|
||||
if (window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return (SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS) != 0;
|
||||
}
|
||||
|
||||
bool imgui_windows_visible() {
|
||||
return ImGui::GetIO().MetricsRenderWindows > 0;
|
||||
}
|
||||
|
||||
bool should_capture_mouse(SDL_Window* window) {
|
||||
if (window == nullptr || ui::any_document_visible() || imgui_windows_visible() ||
|
||||
menu_pointer::active())
|
||||
{
|
||||
bool shouldCaptureMouse(SDL_Window* window) {
|
||||
if (window == nullptr || ui::any_document_visible() || menu_pointer::active()) {
|
||||
return false;
|
||||
}
|
||||
return want_mouse_capture() && is_window_focused(window);
|
||||
return wantMouseCapture() && isWindowFocused(window);
|
||||
}
|
||||
|
||||
bool sync_capture_state(SDL_Window* window, bool should_capture) {
|
||||
bool syncCaptureState(SDL_Window* window, bool should_capture) {
|
||||
if (window == nullptr) {
|
||||
reset_deltas();
|
||||
return false;
|
||||
@@ -93,7 +78,7 @@ bool sync_capture_state(SDL_Window* window, bool should_capture) {
|
||||
return is_captured;
|
||||
}
|
||||
|
||||
void accumulate_deltas(float mx_rel, float my_rel, bool camera_active, bool aim_active) {
|
||||
void accumulateDeltas(float mx_rel, float my_rel, bool camera_active, bool aim_active) {
|
||||
const auto& game = getSettings().game;
|
||||
const bool mirror_mode = game.enableMirrorMode.getValue();
|
||||
const bool invert_y = game.invertMouseY.getValue();
|
||||
@@ -129,62 +114,57 @@ void set_cursor_visible(bool visible) {
|
||||
}
|
||||
}
|
||||
|
||||
bool cursor_idle() {
|
||||
return Clock::now() - s_last_cursor_motion >= kCursorIdleDuration;
|
||||
}
|
||||
|
||||
bool should_show_cursor(bool captured) {
|
||||
if (captured) {
|
||||
return false;
|
||||
}
|
||||
if (ui::any_document_visible()) {
|
||||
return true;
|
||||
}
|
||||
if (imgui_windows_visible()) {
|
||||
return true;
|
||||
}
|
||||
if (menu_pointer::enabled() && menu_pointer::active()) {
|
||||
return true;
|
||||
}
|
||||
if (mouse_input_enabled()) {
|
||||
return false;
|
||||
}
|
||||
return !cursor_idle();
|
||||
}
|
||||
|
||||
void update_cursor_visibility(SDL_Window* window, bool captured) {
|
||||
if (window == nullptr || !is_window_focused(window)) {
|
||||
if (window == nullptr || !isWindowFocused(window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
set_cursor_visible(should_show_cursor(captured));
|
||||
if (captured) {
|
||||
s_idle_frames = 0;
|
||||
set_cursor_visible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) {
|
||||
s_idle_frames = 0;
|
||||
set_cursor_visible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_idle_frames < kIdleHideFrames) {
|
||||
++s_idle_frames;
|
||||
set_cursor_visible(true);
|
||||
} else {
|
||||
set_cursor_visible(false);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void read() {
|
||||
SDL_Window* window = aurora::window::get_sdl_window();
|
||||
const bool capture_active = sync_capture_state(window, should_capture_mouse(window));
|
||||
const bool capture_active = syncCaptureState(window, shouldCaptureMouse(window));
|
||||
update_cursor_visibility(window, capture_active);
|
||||
|
||||
if (!capture_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool aim_active = capture_active && query_mouse_aim_context();
|
||||
const bool aim_active = capture_active && queryMouseAimContext();
|
||||
const bool camera_active = capture_active && getSettings().game.enableMouseCamera;
|
||||
|
||||
float mx_rel = 0.0f;
|
||||
float my_rel = 0.0f;
|
||||
SDL_GetRelativeMouseState(&mx_rel, &my_rel);
|
||||
accumulate_deltas(mx_rel, my_rel, camera_active, aim_active);
|
||||
accumulateDeltas(mx_rel, my_rel, camera_active, aim_active);
|
||||
}
|
||||
|
||||
void get_aim_deltas(float& out_yaw, float& out_pitch) {
|
||||
void getAimDeltas(float& out_yaw, float& out_pitch) {
|
||||
out_yaw = s_aim_yaw_rad;
|
||||
out_pitch = s_aim_pitch_rad;
|
||||
}
|
||||
|
||||
void get_camera_deltas(float& out_yaw, float& out_pitch) {
|
||||
void getCameraDeltas(float& out_yaw, float& out_pitch) {
|
||||
out_yaw = 0.0f;
|
||||
out_pitch = 0.0f;
|
||||
|
||||
@@ -198,28 +178,26 @@ void get_camera_deltas(float& out_yaw, float& out_pitch) {
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept {
|
||||
switch (event.type) {
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
s_last_cursor_motion = Clock::now();
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
on_focus_lost();
|
||||
onFocusLost();
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
on_focus_gained();
|
||||
onFocusGained();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void on_focus_lost() {
|
||||
void onFocusLost() {
|
||||
SDL_Window* window = aurora::window::get_sdl_window();
|
||||
if (window != nullptr) {
|
||||
sync_capture_state(window, false);
|
||||
syncCaptureState(window, false);
|
||||
}
|
||||
s_idle_frames = 0;
|
||||
set_cursor_visible(true);
|
||||
}
|
||||
|
||||
void on_focus_gained() {
|
||||
void onFocusGained() {
|
||||
SDL_Window* window = aurora::window::get_sdl_window();
|
||||
sync_capture_state(window, should_capture_mouse(window));
|
||||
syncCaptureState(window, shouldCaptureMouse(window));
|
||||
}
|
||||
} // namespace dusk::mouse
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/config.hpp"
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
@@ -97,7 +96,6 @@ UserSettings g_userSettings = {
|
||||
.invertMouseY {"game.invertMouseY", false},
|
||||
.freeCamera {"game.freeCamera", false},
|
||||
.enableTouchControls {"game.enableTouchControls", false},
|
||||
.touchTargeting {"game.touchTargeting", TouchTargeting::Hybrid},
|
||||
.enableMenuPointer {"game.enableMenuPointer", true},
|
||||
.touchControlsLayout {"game.touchControlsLayout", ui::ControlLayout{}},
|
||||
.invertCameraXAxis {"game.invertCameraXAxis", false},
|
||||
@@ -268,8 +266,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.touchCameraYSensitivity);
|
||||
Register(g_userSettings.game.minimalHUD);
|
||||
Register(g_userSettings.game.hudScale);
|
||||
Register(g_userSettings.game.pauseOnFocusLost,
|
||||
[](const bool& value, const bool&) { aurora_set_pause_on_focus_lost(value); });
|
||||
Register(g_userSettings.game.pauseOnFocusLost);
|
||||
Register(g_userSettings.game.enableDiscordPresence);
|
||||
Register(g_userSettings.game.bloomMode);
|
||||
Register(g_userSettings.game.bloomMultiplier);
|
||||
@@ -332,7 +329,6 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.invertMouseY);
|
||||
Register(g_userSettings.game.freeCamera);
|
||||
Register(g_userSettings.game.enableTouchControls);
|
||||
Register(g_userSettings.game.touchTargeting);
|
||||
Register(g_userSettings.game.enableMenuPointer);
|
||||
Register(g_userSettings.game.touchControlsLayout);
|
||||
Register(g_userSettings.game.debugFlyCam);
|
||||
|
||||
@@ -34,9 +34,9 @@ void resetForSpeedrunMode() {
|
||||
getSettings().game.fastRoll.setSpeedrunValue(false);
|
||||
getSettings().game.fastSpinner.setSpeedrunValue(false);
|
||||
getSettings().game.armorRupeeDrain.setSpeedrunValue(MagicArmorMode::NORMAL);
|
||||
getSettings().game.invincibleEnemies.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.pauseOnFocusLost.setSpeedrunValue(false);
|
||||
aurora_set_pause_on_focus_lost(false);
|
||||
|
||||
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
|
||||
getSettings().game.recordingMode.setSpeedrunValue(false);
|
||||
|
||||
@@ -7,9 +7,6 @@ float s_pitch_dp = 0.0f;
|
||||
} // namespace
|
||||
|
||||
void add_delta(float yaw_dp, float pitch_dp) noexcept {
|
||||
if (getSettings().game.enableMirrorMode) {
|
||||
yaw_dp *= -1.0;
|
||||
}
|
||||
s_yaw_dp += yaw_dp;
|
||||
s_pitch_dp += pitch_dp;
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ ControllerConfigWindow::ControllerConfigWindow(bool prelaunch) {
|
||||
void ControllerConfigWindow::hide(bool close) {
|
||||
stop_rumble_test();
|
||||
cancel_pending_binding();
|
||||
config::save();
|
||||
config::Save();
|
||||
Window::hide(close);
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
PADClearPort(port);
|
||||
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
|
||||
PADSerializeMappings();
|
||||
config::ClearAllActionBindings(port);
|
||||
ClearAllActionBindings(port);
|
||||
refresh_controller_page();
|
||||
});
|
||||
|
||||
@@ -417,7 +417,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
PADClearPort(port);
|
||||
PADSetKeyboardActive(static_cast<u32>(port), TRUE);
|
||||
PADSerializeMappings();
|
||||
config::ClearAllActionBindings(port);
|
||||
ClearAllActionBindings(port);
|
||||
});
|
||||
|
||||
const u32 controllerCount = PADCount();
|
||||
@@ -439,7 +439,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
|
||||
PADSetPortForIndex(i, port);
|
||||
PADSerializeMappings();
|
||||
config::ClearAllActionBindings(port);
|
||||
ClearAllActionBindings(port);
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -1125,7 +1125,7 @@ void ControllerConfigWindow::poll_pending_binding() {
|
||||
return;
|
||||
}
|
||||
mPendingActionBinding->setValue(button);
|
||||
config::save();
|
||||
config::Save();
|
||||
finish_pending_binding(completedPort);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -51,8 +51,6 @@ struct ControlProps {
|
||||
float h = 0.0f;
|
||||
float scale = 1.0f;
|
||||
ControlAnchor anchor = ControlAnchor::None;
|
||||
|
||||
bool operator==(const ControlProps&) const = default;
|
||||
};
|
||||
|
||||
struct ControlRect {
|
||||
@@ -78,8 +76,6 @@ struct ControlLayout {
|
||||
|
||||
int version = Version;
|
||||
std::map<std::string, ControlProps, std::less<> > controls;
|
||||
|
||||
bool operator==(const ControlLayout&) const = default;
|
||||
};
|
||||
|
||||
constexpr std::array<std::string_view, 9> kControlLayoutIds = {
|
||||
|
||||
@@ -300,7 +300,7 @@ void GraphicsTuner::show() {
|
||||
}
|
||||
|
||||
void GraphicsTuner::hide(bool close) {
|
||||
config::save();
|
||||
config::Save();
|
||||
mRoot->RemoveAttribute("open");
|
||||
if (close) {
|
||||
mPendingClose = true;
|
||||
|
||||
@@ -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(); };
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+6
-15
@@ -197,19 +197,10 @@ void remove_element(Rml::Element*& elem) noexcept {
|
||||
|
||||
} // namespace
|
||||
|
||||
static std::string FormatElapsedTime(OSTime ticksElapsed) {
|
||||
using namespace std::chrono;
|
||||
|
||||
milliseconds ms{OSTicksToMilliseconds(ticksElapsed)};
|
||||
|
||||
const hours hr = duration_cast<hours>(ms);
|
||||
ms -= hr;
|
||||
const minutes min = duration_cast<minutes>(ms);
|
||||
ms -= min;
|
||||
const seconds sec = duration_cast<seconds>(ms);
|
||||
ms -= sec;
|
||||
|
||||
return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", hr.count(), min.count(), sec.count(), ms.count());
|
||||
static std::string FormatTime(OSTime ticks) {
|
||||
OSCalendarTime t;
|
||||
OSTicksToCalendarTime(ticks, &t);
|
||||
return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", t.hour, t.min, t.sec, t.msec);
|
||||
}
|
||||
|
||||
Overlay::Overlay() : Document(kDocumentSource, true) {
|
||||
@@ -326,13 +317,13 @@ void Overlay::update() {
|
||||
|
||||
if (getSettings().game.showSpeedrunRTATimer) {
|
||||
mSpeedrunRta->SetAttribute("open", "");
|
||||
mSpeedrunRta->SetInnerRML(escape(fmt::format("RTA {}", FormatElapsedTime(elapsedTime))));
|
||||
mSpeedrunRta->SetInnerRML(escape(fmt::format("RTA {}", FormatTime(elapsedTime))));
|
||||
} else {
|
||||
mSpeedrunRta->RemoveAttribute("open");
|
||||
}
|
||||
|
||||
mSpeedrunIgt->SetInnerRML(
|
||||
escape(fmt::format("IGT {}", FormatElapsedTime(m_speedrunInfo.m_igtTimer))));
|
||||
escape(fmt::format("IGT {}", FormatTime(m_speedrunInfo.m_igtTimer))));
|
||||
} else {
|
||||
mSpeedrunTimer->RemoveAttribute("open");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -309,7 +310,7 @@ void persist_disc_choice(const std::string& path, iso::ValidationError validatio
|
||||
|
||||
getSettings().backend.isoPath.setValue(path);
|
||||
getSettings().backend.isoVerification.setValue(verification);
|
||||
config::save();
|
||||
config::Save();
|
||||
|
||||
if (previousPath != path || previousVerification != verification) {
|
||||
iso::log_verification_state(path, verification);
|
||||
@@ -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");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user