mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-10 12:54:50 -04:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6849baac06 | |||
| f3ca94b338 | |||
| 9b3b827054 | |||
| 4de3c20431 | |||
| 2840843e3c | |||
| 312195920a | |||
| bc11bf3563 | |||
| 36635f8c62 | |||
| 6136d9bbf7 | |||
| 16c573a170 | |||
| e9e16a8ac1 | |||
| 4339e60ba6 | |||
| af5635dd42 | |||
| c876ea558f | |||
| 9b75dc5fd2 | |||
| 5c9c76cc0b | |||
| b474aafe9d | |||
| 84c5729c4e | |||
| e0ab36b7d9 | |||
| d40a30ee13 | |||
| 074b43a089 | |||
| 8e8dc305e6 | |||
| 41389f4a8e | |||
| 8145dec2a1 | |||
| 4089a80a17 | |||
| b772b6d952 | |||
| 42910ab2fd | |||
| f54892b2c2 | |||
| f32e069c4b | |||
| 09f087656a | |||
| fe15366912 | |||
| cfadf7607a | |||
| f81d25b425 | |||
| ebf6f31719 | |||
| 590d209f76 | |||
| ed21cd4fd0 | |||
| 277538bb81 | |||
| 5418b1831d | |||
| 2d4e69466b | |||
| 427dcfab82 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
Language: Cpp
|
Language: Cpp
|
||||||
Standard: C++03
|
Standard: c++20
|
||||||
AccessModifierOffset: -4
|
AccessModifierOffset: -4
|
||||||
AlignAfterOpenBracket: DontAlign
|
AlignAfterOpenBracket: DontAlign
|
||||||
AlignConsecutiveAssignments: false
|
AlignConsecutiveAssignments: false
|
||||||
|
|||||||
+142
-135
@@ -5,96 +5,8 @@ if (NOT CMAKE_BUILD_TYPE)
|
|||||||
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
|
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
|
include(cmake/DetectVersion.cmake)
|
||||||
|
detect_version()
|
||||||
if (DUSK_VERSION_OVERRIDE)
|
|
||||||
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
|
|
||||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
|
||||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
|
||||||
set(DUSK_VERSION_CODE "1")
|
|
||||||
set(DUSK_WC_REVISION "")
|
|
||||||
set(DUSK_WC_BRANCH "")
|
|
||||||
set(DUSK_WC_DATE "")
|
|
||||||
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
|
|
||||||
else ()
|
|
||||||
# obtain revision info from git
|
|
||||||
find_package(Git)
|
|
||||||
if (GIT_FOUND)
|
|
||||||
# make sure version information gets re-run when the current Git HEAD changes
|
|
||||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
|
||||||
OUTPUT_VARIABLE dusk_git_head_filename
|
|
||||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
|
||||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
|
|
||||||
|
|
||||||
execute_process(WORKING_DIRECTORY ${CMAKE_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}
|
|
||||||
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
|
|
||||||
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*"
|
|
||||||
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
|
|
||||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
|
||||||
|
|
||||||
# remove the git hash, then collapse a clean "-0" suffix only
|
|
||||||
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
|
||||||
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
|
||||||
|
|
||||||
# defines DUSK_WC_BRANCH
|
|
||||||
execute_process(WORKING_DIRECTORY ${CMAKE_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
|
|
||||||
OUTPUT_VARIABLE DUSK_WC_DATE
|
|
||||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
|
||||||
else ()
|
|
||||||
message(STATUS "Unable to find git, commit information will not be available")
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
|
|
||||||
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
|
|
||||||
set(_ver_major ${CMAKE_MATCH_1})
|
|
||||||
set(_ver_minor ${CMAKE_MATCH_2})
|
|
||||||
set(_ver_patch ${CMAKE_MATCH_3})
|
|
||||||
set(DUSK_VERSION_TWEAK "0")
|
|
||||||
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
|
|
||||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
|
||||||
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
|
|
||||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
|
||||||
endif ()
|
|
||||||
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
|
|
||||||
if(DUSK_VERSION_TWEAK GREATER 999)
|
|
||||||
set(_tweak 999)
|
|
||||||
else()
|
|
||||||
set(_tweak ${DUSK_VERSION_TWEAK})
|
|
||||||
endif()
|
|
||||||
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
|
|
||||||
math(EXPR DUSK_VERSION_CODE
|
|
||||||
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
|
|
||||||
else ()
|
|
||||||
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
|
|
||||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
|
||||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
|
||||||
set(DUSK_VERSION_CODE "1")
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
# Add version information to CI environment variables
|
|
||||||
if(DEFINED ENV{GITHUB_ENV})
|
|
||||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
|
|
||||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
|
|
||||||
endif()
|
|
||||||
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
|
|
||||||
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
||||||
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
|
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
@@ -177,12 +89,12 @@ option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
|
|||||||
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
|
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
|
||||||
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
|
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
|
||||||
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
|
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
|
||||||
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
|
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
|
||||||
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
|
|
||||||
|
|
||||||
# Edit & Continue
|
# Edit & Continue
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug"
|
||||||
|
AND CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
|
||||||
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue")
|
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue")
|
||||||
endif ()
|
endif ()
|
||||||
if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue")
|
if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue")
|
||||||
@@ -299,7 +211,47 @@ FetchContent_Declare(json
|
|||||||
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
|
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
|
||||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||||
)
|
)
|
||||||
FetchContent_MakeAvailable(cxxopts json)
|
message(STATUS "dusklight: Fetching miniz")
|
||||||
|
FetchContent_Declare(miniz
|
||||||
|
URL https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||||
|
EXCLUDE_FROM_ALL
|
||||||
|
)
|
||||||
|
|
||||||
|
set(_fetch_content_deps cxxopts json miniz)
|
||||||
|
if (DUSK_ENABLE_CODE_MODS)
|
||||||
|
message(STATUS "dusklight: Fetching funchook")
|
||||||
|
# cmake/PatchFunchook.cmake patches funchook's cmake/capstone.cmake.in to inject a
|
||||||
|
# PATCH_COMMAND into capstone's inner ExternalProject. That PATCH_COMMAND runs
|
||||||
|
# cmake/PatchCapstone.cmake after capstone is cloned, which removes the
|
||||||
|
# cmake_policy(SET CMP0048 OLD) line that CMake >= 3.27 rejects.
|
||||||
|
# This is incredibly scuffed and we should probably think of a better way to do this
|
||||||
|
set(CAPSTONE_FIX_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/PatchCapstone.cmake")
|
||||||
|
FetchContent_Declare(funchook
|
||||||
|
GIT_REPOSITORY https://github.com/kubo/funchook.git
|
||||||
|
GIT_TAG v1.1.3
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
GIT_PROGRESS TRUE
|
||||||
|
PATCH_COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=<SOURCE_DIR> -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/PatchFunchook.cmake
|
||||||
|
EXCLUDE_FROM_ALL
|
||||||
|
)
|
||||||
|
set(FUNCHOOK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(FUNCHOOK_BUILD_SHARED OFF CACHE BOOL "" FORCE)
|
||||||
|
set(FUNCHOOK_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
|
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||||
|
list(LENGTH CMAKE_OSX_ARCHITECTURES _osx_arch_count)
|
||||||
|
if (_osx_arch_count EQUAL 1)
|
||||||
|
list(GET CMAKE_OSX_ARCHITECTURES 0 _osx_arch)
|
||||||
|
if (_osx_arch MATCHES "^(arm64|aarch64|ARM64)$")
|
||||||
|
set(FUNCHOOK_CPU arm64 CACHE STRING "" FORCE)
|
||||||
|
elseif (_osx_arch MATCHES "^(x86_64|AMD64|amd64|i[3-6]86|x86)$")
|
||||||
|
set(FUNCHOOK_CPU x86 CACHE STRING "" FORCE)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
list(APPEND _fetch_content_deps funchook)
|
||||||
|
endif ()
|
||||||
|
FetchContent_MakeAvailable(${_fetch_content_deps})
|
||||||
|
|
||||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||||
message(STATUS "dusklight: Fetching sentry-native")
|
message(STATUS "dusklight: Fetching sentry-native")
|
||||||
@@ -333,21 +285,7 @@ if(_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQU
|
|||||||
add_compile_options(-fsigned-char)
|
add_compile_options(-fsigned-char)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
configure_version_header()
|
||||||
set(PLATFORM_NAME win32)
|
|
||||||
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
|
|
||||||
if (IOS)
|
|
||||||
set(PLATFORM_NAME ios)
|
|
||||||
elseif (TVOS)
|
|
||||||
set(PLATFORM_NAME tvos)
|
|
||||||
else ()
|
|
||||||
set(PLATFORM_NAME macos)
|
|
||||||
endif ()
|
|
||||||
else ()
|
|
||||||
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
|
|
||||||
endif ()
|
|
||||||
|
|
||||||
configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_BINARY_DIR}/version.h)
|
|
||||||
|
|
||||||
include(files.cmake)
|
include(files.cmake)
|
||||||
|
|
||||||
@@ -363,22 +301,15 @@ set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
|
|||||||
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
|
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
|
||||||
source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
|
source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
|
||||||
|
|
||||||
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
|
include(cmake/GameABIConfig.cmake)
|
||||||
|
|
||||||
set(GAME_INCLUDE_DIRS
|
|
||||||
include
|
|
||||||
src
|
|
||||||
assets/GZ2E01 # TODO: make this dynamic if needed?
|
|
||||||
libs/JSystem/include
|
|
||||||
libs
|
|
||||||
extern/aurora/include/dolphin
|
|
||||||
extern
|
|
||||||
${CMAKE_BINARY_DIR})
|
|
||||||
|
|
||||||
find_package(Threads REQUIRED)
|
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
|
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
|
||||||
Threads::Threads zstd::libzstd)
|
Threads::Threads zstd::libzstd dusklight_game_headers)
|
||||||
|
if (DUSK_ENABLE_CODE_MODS)
|
||||||
|
list(APPEND GAME_LIBS funchook-static)
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||||
list(APPEND GAME_LIBS sentry)
|
list(APPEND GAME_LIBS sentry)
|
||||||
@@ -446,8 +377,8 @@ if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
|
|||||||
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
|
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if(ANDROID)
|
if (DUSK_ENABLE_CODE_MODS)
|
||||||
list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1)
|
list(APPEND GAME_COMPILE_DEFS DUSK_CODE_MODS=1)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (DUSK_PACKAGE_INSTALL)
|
if (DUSK_PACKAGE_INSTALL)
|
||||||
@@ -474,7 +405,7 @@ set(GAME_DEBUG_FILES
|
|||||||
set_source_files_properties(
|
set_source_files_properties(
|
||||||
${GAME_DEBUG_FILES}
|
${GAME_DEBUG_FILES}
|
||||||
PROPERTIES
|
PROPERTIES
|
||||||
COMPILE_DEFINITIONS "$<$<CONFIG:Debug>:DEBUG=1>;$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>"
|
COMPILE_DEFINITIONS "$<$<CONFIG:Debug>:DEBUG=1>"
|
||||||
)
|
)
|
||||||
|
|
||||||
# game_base is for all other game code files
|
# game_base is for all other game code files
|
||||||
@@ -488,16 +419,11 @@ set(GAME_BASE_FILES
|
|||||||
set_source_files_properties(
|
set_source_files_properties(
|
||||||
${GAME_BASE_FILES}
|
${GAME_BASE_FILES}
|
||||||
PROPERTIES
|
PROPERTIES
|
||||||
COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0;$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>"
|
COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0"
|
||||||
)
|
)
|
||||||
|
|
||||||
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
|
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||||
target_compile_definitions(${jsystem_lib} PRIVATE
|
target_compile_definitions(${jsystem_lib} PRIVATE ${GAME_COMPILE_DEFS} $<$<CONFIG:Debug>:DEBUG=1>)
|
||||||
${GAME_COMPILE_DEFS}
|
|
||||||
$<$<CONFIG:Debug>:DEBUG=1>
|
|
||||||
$<$<CONFIG:Debug>:PARTIAL_DEBUG=1>
|
|
||||||
)
|
|
||||||
target_include_directories(${jsystem_lib} PRIVATE ${GAME_INCLUDE_DIRS})
|
|
||||||
target_link_libraries(${jsystem_lib} PRIVATE ${GAME_LIBS})
|
target_link_libraries(${jsystem_lib} PRIVATE ${GAME_LIBS})
|
||||||
set_target_properties(${jsystem_lib} PROPERTIES FOLDER "JSystem")
|
set_target_properties(${jsystem_lib} PROPERTIES FOLDER "JSystem")
|
||||||
endforeach()
|
endforeach()
|
||||||
@@ -509,7 +435,7 @@ if (CMAKE_CXX_LINK_GROUP_USING_RESCAN_SUPPORTED OR CMAKE_LINK_GROUP_USING_RESCAN
|
|||||||
set(JSYSTEM_LINK_LIBRARIES "$<LINK_GROUP:RESCAN,${JSYSTEM_LIBRARIES}>")
|
set(JSYSTEM_LINK_LIBRARIES "$<LINK_GROUP:RESCAN,${JSYSTEM_LIBRARIES}>")
|
||||||
endif ()
|
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)
|
if(ANDROID)
|
||||||
add_library(dusklight SHARED ${DUSK_FILES})
|
add_library(dusklight SHARED ${DUSK_FILES})
|
||||||
set_target_properties(dusklight PROPERTIES OUTPUT_NAME main)
|
set_target_properties(dusklight PROPERTIES OUTPUT_NAME main)
|
||||||
@@ -521,9 +447,55 @@ if (ENABLE_ASAN)
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
|
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
|
||||||
target_include_directories(dusklight PRIVATE ${GAME_INCLUDE_DIRS})
|
target_include_directories(dusklight PRIVATE ${miniz_SOURCE_DIR})
|
||||||
target_link_libraries(dusklight PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
|
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>")
|
target_precompile_headers(dusklight PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
|
||||||
|
|
||||||
|
if (DUSK_ENABLE_CODE_MODS)
|
||||||
|
include(cmake/SymbolManifest.cmake)
|
||||||
|
if (WIN32)
|
||||||
|
# Game ABI exports & import library for mod linking
|
||||||
|
include(cmake/WindowsExports.cmake)
|
||||||
|
setup_windows_exports(dusklight)
|
||||||
|
endif ()
|
||||||
|
# Post-link symbol manifest: hookable-surface name->address map keyed to the build.
|
||||||
|
setup_symbol_manifest(dusklight)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
# Hook reliability: guaranteed patchable entries on the game ABI surface, and no identical-code folding.
|
||||||
|
if (MSVC)
|
||||||
|
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
|
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:/hotpatch>)
|
||||||
|
endif ()
|
||||||
|
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64")
|
||||||
|
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN:16 /OPT:NOICF)
|
||||||
|
else ()
|
||||||
|
target_link_options(dusklight PRIVATE /FUNCTIONPADMIN /OPT:NOICF)
|
||||||
|
endif ()
|
||||||
|
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
|
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
|
||||||
|
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=2,1>)
|
||||||
|
else ()
|
||||||
|
set(DUSK_PATCHABLE_ENTRY_FLAG $<$<COMPILE_LANGUAGE:C,CXX>:-fpatchable-function-entry=10,5>)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
if (DEFINED DUSK_PATCHABLE_ENTRY_FLAG)
|
||||||
|
target_compile_options(dusklight PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||||
|
foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||||
|
target_compile_options(${jsystem_lib} PRIVATE ${DUSK_PATCHABLE_ENTRY_FLAG})
|
||||||
|
endforeach()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
target_link_libraries(dusklight PRIVATE Psapi)
|
||||||
|
endif ()
|
||||||
|
if (APPLE)
|
||||||
|
# Mods resolve game symbols from the executable at dlopen time.
|
||||||
|
target_link_options(dusklight PRIVATE -Wl,-export_dynamic)
|
||||||
|
elseif (UNIX AND NOT ANDROID)
|
||||||
|
target_link_options(dusklight PRIVATE -rdynamic)
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (TARGET crashpad_handler)
|
if (TARGET crashpad_handler)
|
||||||
add_dependencies(dusklight crashpad_handler)
|
add_dependencies(dusklight crashpad_handler)
|
||||||
add_custom_command(TARGET dusklight POST_BUILD
|
add_custom_command(TARGET dusklight POST_BUILD
|
||||||
@@ -542,6 +514,7 @@ endif ()
|
|||||||
|
|
||||||
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||||
target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1")
|
target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1")
|
||||||
|
target_link_libraries(dusklight PRIVATE dl)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (NOT APPLE)
|
if (NOT APPLE)
|
||||||
@@ -582,6 +555,13 @@ if (WIN32)
|
|||||||
endif ()
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
include(cmake/ModSDK.cmake)
|
||||||
|
|
||||||
|
if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
|
add_custom_target(dusklight_mods) # Aggregate target for all in-tree mods
|
||||||
|
add_subdirectory(tools/mod_template)
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
if (IOS)
|
if (IOS)
|
||||||
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios)
|
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios)
|
||||||
@@ -589,6 +569,7 @@ if (APPLE)
|
|||||||
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/tvos)
|
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/tvos)
|
||||||
else ()
|
else ()
|
||||||
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos)
|
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos)
|
||||||
|
set(DUSK_ENTITLEMENTS ${DUSK_RESOURCE_DIR}/Dusklight.entitlements)
|
||||||
endif ()
|
endif ()
|
||||||
set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in)
|
set(DUSK_INFO_PLIST ${DUSK_RESOURCE_DIR}/Info.plist.in)
|
||||||
file(GLOB_RECURSE DUSK_RESOURCE_FILES
|
file(GLOB_RECURSE DUSK_RESOURCE_FILES
|
||||||
@@ -608,8 +589,7 @@ if (APPLE)
|
|||||||
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
|
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
|
||||||
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
|
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
|
||||||
endforeach ()
|
endforeach ()
|
||||||
set_target_properties(
|
set(_apple_bundle_properties
|
||||||
dusklight PROPERTIES
|
|
||||||
MACOSX_BUNDLE TRUE
|
MACOSX_BUNDLE TRUE
|
||||||
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
|
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
|
||||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
|
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
|
||||||
@@ -620,6 +600,31 @@ if (APPLE)
|
|||||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
|
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
|
||||||
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
|
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
|
||||||
)
|
)
|
||||||
|
if (DUSK_ENTITLEMENTS)
|
||||||
|
list(APPEND _apple_bundle_properties
|
||||||
|
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${DUSK_ENTITLEMENTS})
|
||||||
|
endif ()
|
||||||
|
if (NOT IOS AND NOT TVOS)
|
||||||
|
list(APPEND _apple_bundle_properties
|
||||||
|
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES")
|
||||||
|
endif ()
|
||||||
|
set_target_properties(dusklight PROPERTIES ${_apple_bundle_properties})
|
||||||
|
|
||||||
|
if (NOT IOS AND NOT TVOS AND NOT "${CMAKE_GENERATOR}" STREQUAL "Xcode")
|
||||||
|
set(_sign_nested_commands)
|
||||||
|
if (TARGET crashpad_handler)
|
||||||
|
list(APPEND _sign_nested_commands
|
||||||
|
COMMAND /usr/bin/codesign --force --sign -
|
||||||
|
"$<TARGET_FILE_DIR:dusklight>/crashpad_handler")
|
||||||
|
endif ()
|
||||||
|
add_custom_command(TARGET dusklight POST_BUILD
|
||||||
|
${_sign_nested_commands}
|
||||||
|
COMMAND /usr/bin/codesign --force --sign - --entitlements
|
||||||
|
"${DUSK_ENTITLEMENTS}" "$<TARGET_BUNDLE_DIR:dusklight>"
|
||||||
|
COMMENT "Signing Dusklight.app with entitlements"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (APPLE AND NOT IOS AND NOT TVOS)
|
if (APPLE AND NOT IOS AND NOT TVOS)
|
||||||
@@ -743,3 +748,5 @@ foreach (target IN LISTS BINARY_TARGETS)
|
|||||||
endif ()
|
endif ()
|
||||||
endforeach ()
|
endforeach ()
|
||||||
endforeach ()
|
endforeach ()
|
||||||
|
|
||||||
|
install_bundled_mods()
|
||||||
|
|||||||
+18
-2
@@ -72,7 +72,11 @@
|
|||||||
"type": "BOOL",
|
"type": "BOOL",
|
||||||
"value": false
|
"value": false
|
||||||
},
|
},
|
||||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
|
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
||||||
|
"DUSK_ENABLE_CODE_MODS": {
|
||||||
|
"type": "BOOL",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vendor": {
|
"vendor": {
|
||||||
"microsoft.com/VisualStudioSettings/CMake/1.0": {
|
"microsoft.com/VisualStudioSettings/CMake/1.0": {
|
||||||
@@ -159,6 +163,10 @@
|
|||||||
"CMAKE_C_COMPILER": "cl",
|
"CMAKE_C_COMPILER": "cl",
|
||||||
"CMAKE_CXX_COMPILER": "cl",
|
"CMAKE_CXX_COMPILER": "cl",
|
||||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
||||||
|
"DUSK_ENABLE_CODE_MODS": {
|
||||||
|
"type": "BOOL",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
|
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
|
||||||
"type": "BOOL",
|
"type": "BOOL",
|
||||||
"value": true
|
"value": true
|
||||||
@@ -262,7 +270,11 @@
|
|||||||
"type": "BOOL",
|
"type": "BOOL",
|
||||||
"value": false
|
"value": false
|
||||||
},
|
},
|
||||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
|
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
||||||
|
"DUSK_ENABLE_CODE_MODS": {
|
||||||
|
"type": "BOOL",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"vendor": {
|
"vendor": {
|
||||||
"microsoft.com/VisualStudioSettings/CMake/1.0": {
|
"microsoft.com/VisualStudioSettings/CMake/1.0": {
|
||||||
@@ -394,6 +406,10 @@
|
|||||||
"type": "BOOL",
|
"type": "BOOL",
|
||||||
"value": false
|
"value": false
|
||||||
},
|
},
|
||||||
|
"DUSK_ENABLE_CODE_MODS": {
|
||||||
|
"type": "BOOL",
|
||||||
|
"value": true
|
||||||
|
},
|
||||||
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
|
"CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": {
|
||||||
"type": "BOOL",
|
"type": "BOOL",
|
||||||
"value": true
|
"value": true
|
||||||
|
|||||||
@@ -20,31 +20,17 @@ 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.
|
> Dusklight does *not* provide any copyrighted assets. You must provide your own copy of the original game.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 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.
|
> 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.
|
||||||
|
|
||||||
### 1. Dump your game
|
### 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 a `.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 `.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.
|
Currently, only the GameCube USA and EUR releases are supported. Support for other versions of the game is planned in the future.
|
||||||
|
|
||||||
### 2. Download [Dusklight](https://github.com/TwilitRealm/dusklight/releases)
|
### 2. Install Dusklight
|
||||||
|
|
||||||
### 3. Setup the game
|
Visit the [official installation guide](https://twilitrealm.dev/install/) for full instructions.
|
||||||
**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
|
# Building
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt)
|
||||||
|
include_guard(GLOBAL)
|
||||||
|
|
||||||
|
get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||||
|
|
||||||
|
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
|
||||||
|
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
|
||||||
|
|
||||||
|
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
|
||||||
|
|
||||||
|
macro(detect_version)
|
||||||
|
if (DUSK_VERSION_OVERRIDE)
|
||||||
|
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
|
||||||
|
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||||
|
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||||
|
set(DUSK_VERSION_CODE "1")
|
||||||
|
set(DUSK_WC_REVISION "")
|
||||||
|
set(DUSK_WC_BRANCH "")
|
||||||
|
set(DUSK_WC_DATE "")
|
||||||
|
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
|
||||||
|
else ()
|
||||||
|
# obtain revision info from git
|
||||||
|
find_package(Git)
|
||||||
|
if (GIT_FOUND)
|
||||||
|
# make sure version information gets re-run when the current Git HEAD changes
|
||||||
|
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
||||||
|
OUTPUT_VARIABLE dusk_git_head_filename
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||||
|
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
|
||||||
|
|
||||||
|
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
|
||||||
|
OUTPUT_VARIABLE dusk_git_head_symbolic
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT}
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
|
||||||
|
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||||
|
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
|
||||||
|
|
||||||
|
# defines DUSK_WC_REVISION
|
||||||
|
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||||
|
OUTPUT_VARIABLE DUSK_WC_REVISION
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
# defines DUSK_WC_DESCRIBE
|
||||||
|
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
|
||||||
|
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
|
||||||
|
# remove the git hash, then collapse a clean "-0" suffix only
|
||||||
|
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||||
|
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||||
|
|
||||||
|
# defines DUSK_WC_BRANCH
|
||||||
|
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||||
|
OUTPUT_VARIABLE DUSK_WC_BRANCH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
# defines DUSK_WC_DATE
|
||||||
|
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
|
||||||
|
OUTPUT_VARIABLE DUSK_WC_DATE
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
else ()
|
||||||
|
message(STATUS "Unable to find git, commit information will not be available")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
|
||||||
|
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
|
||||||
|
set(_ver_major ${CMAKE_MATCH_1})
|
||||||
|
set(_ver_minor ${CMAKE_MATCH_2})
|
||||||
|
set(_ver_patch ${CMAKE_MATCH_3})
|
||||||
|
set(DUSK_VERSION_TWEAK "0")
|
||||||
|
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
|
||||||
|
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||||
|
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
|
||||||
|
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||||
|
endif ()
|
||||||
|
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
|
||||||
|
if (DUSK_VERSION_TWEAK GREATER 999)
|
||||||
|
set(_tweak 999)
|
||||||
|
else ()
|
||||||
|
set(_tweak ${DUSK_VERSION_TWEAK})
|
||||||
|
endif ()
|
||||||
|
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
|
||||||
|
math(EXPR DUSK_VERSION_CODE
|
||||||
|
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
|
||||||
|
else ()
|
||||||
|
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
|
||||||
|
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||||
|
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||||
|
set(DUSK_VERSION_CODE "1")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
# Add version information to CI environment variables
|
||||||
|
if (DEFINED ENV{GITHUB_ENV})
|
||||||
|
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
|
||||||
|
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
|
||||||
|
endif ()
|
||||||
|
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
|
||||||
|
endmacro()
|
||||||
|
|
||||||
|
# Sets PLATFORM_NAME and configures version.h into the caller's binary dir.
|
||||||
|
macro(configure_version_header)
|
||||||
|
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
||||||
|
set(PLATFORM_NAME win32)
|
||||||
|
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
|
||||||
|
if (IOS)
|
||||||
|
set(PLATFORM_NAME ios)
|
||||||
|
elseif (TVOS)
|
||||||
|
set(PLATFORM_NAME tvos)
|
||||||
|
else ()
|
||||||
|
set(PLATFORM_NAME macos)
|
||||||
|
endif ()
|
||||||
|
else ()
|
||||||
|
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||||
|
endmacro()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# The game ABI surface shared by the main build and the mod SDK (sdk/CMakeLists.txt)
|
||||||
|
include_guard(GLOBAL)
|
||||||
|
|
||||||
|
get_filename_component(_game_root "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||||
|
|
||||||
|
# PARTIAL_DEBUG makes debug and release share one struct/vtable ABI so a mod binary loads into either
|
||||||
|
set(_game_compile_defs TARGET_PC=1 WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1 PARTIAL_DEBUG=1)
|
||||||
|
if (ANDROID)
|
||||||
|
list(APPEND _game_compile_defs TARGET_ANDROID=1)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(_game_include_dirs
|
||||||
|
${_game_root}/include
|
||||||
|
${_game_root}/src
|
||||||
|
${_game_root}/assets/GZ2E01 # TODO: make this dynamic if needed?
|
||||||
|
${_game_root}/libs/JSystem/include
|
||||||
|
${_game_root}/libs
|
||||||
|
${_game_root}/extern/aurora/include/dolphin
|
||||||
|
${_game_root}/extern/aurora/include
|
||||||
|
${_game_root}/extern
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Interface target for mods and sub-projects to inherit game headers/defines.
|
||||||
|
add_library(dusklight_game_headers INTERFACE)
|
||||||
|
target_include_directories(dusklight_game_headers INTERFACE ${_game_include_dirs})
|
||||||
|
target_compile_definitions(dusklight_game_headers INTERFACE ${_game_compile_defs})
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
# add_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>] [OVERLAY_DIR <overlay>]
|
||||||
|
# [TEXTURES_DIR <textures>] [OUTPUT_DIR <dir>] [BUNDLE])
|
||||||
|
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_BINARY_DIR}/mods" CACHE PATH "Directory to write mod packages into")
|
||||||
|
|
||||||
|
function(_mod_lib_name out_var)
|
||||||
|
set(_arch "${CMAKE_SYSTEM_PROCESSOR}")
|
||||||
|
if (APPLE AND CMAKE_OSX_ARCHITECTURES)
|
||||||
|
list(LENGTH CMAKE_OSX_ARCHITECTURES _count)
|
||||||
|
if (_count GREATER 1)
|
||||||
|
message(FATAL_ERROR "add_mod: universal binaries are not supported")
|
||||||
|
endif ()
|
||||||
|
set(_arch "${CMAKE_OSX_ARCHITECTURES}")
|
||||||
|
endif ()
|
||||||
|
string(TOLOWER "${CMAKE_SYSTEM_NAME}" _platform)
|
||||||
|
string(TOLOWER "${_arch}" _arch)
|
||||||
|
if (_arch MATCHES "^(i[3-6]86|x86)$")
|
||||||
|
set(_arch "x86")
|
||||||
|
endif ()
|
||||||
|
if (WIN32)
|
||||||
|
set(_ext ".dll")
|
||||||
|
elseif (APPLE)
|
||||||
|
set(_ext ".dylib")
|
||||||
|
else ()
|
||||||
|
set(_ext ".so")
|
||||||
|
endif ()
|
||||||
|
set(${out_var} "${_platform}-${_arch}${_ext}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(_mod_resolve_source_path out_var path)
|
||||||
|
if (IS_ABSOLUTE "${path}")
|
||||||
|
set(_path "${path}")
|
||||||
|
else ()
|
||||||
|
set(_path "${CMAKE_CURRENT_SOURCE_DIR}/${path}")
|
||||||
|
endif ()
|
||||||
|
set(${out_var} "${_path}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(_mod_collect_assets out_var dir)
|
||||||
|
if (NOT IS_DIRECTORY "${dir}")
|
||||||
|
message(FATAL_ERROR "add_mod: asset directory does not exist: ${dir}")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
file(GLOB_RECURSE _files CONFIGURE_DEPENDS LIST_DIRECTORIES false "${dir}/*")
|
||||||
|
set(${out_var} ${_files} PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(add_mod target_name)
|
||||||
|
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR" "SOURCES" ${ARGN})
|
||||||
|
if (NOT ARG_MOD_JSON)
|
||||||
|
message(FATAL_ERROR "add_mod: MOD_JSON is required")
|
||||||
|
endif ()
|
||||||
|
_mod_resolve_source_path(_mod_json "${ARG_MOD_JSON}")
|
||||||
|
if (NOT EXISTS "${_mod_json}")
|
||||||
|
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
add_library(${target_name} SHARED ${ARG_SOURCES})
|
||||||
|
_mod_lib_name(_lib_name)
|
||||||
|
set_target_properties(${target_name} PROPERTIES
|
||||||
|
PREFIX ""
|
||||||
|
C_VISIBILITY_PRESET hidden
|
||||||
|
CXX_VISIBILITY_PRESET hidden
|
||||||
|
VISIBILITY_INLINES_HIDDEN ON
|
||||||
|
WINDOWS_EXPORT_ALL_SYMBOLS OFF)
|
||||||
|
target_compile_features(${target_name} PRIVATE cxx_std_20)
|
||||||
|
target_link_libraries(${target_name} PRIVATE dusklight_game_headers)
|
||||||
|
|
||||||
|
if (NOT TARGET dusklight)
|
||||||
|
# Apply global compile options for out-of-tree mod builds
|
||||||
|
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||||
|
target_compile_options(${target_name} PRIVATE
|
||||||
|
-Wno-multichar -Wno-trigraphs -Wno-deprecated-declarations)
|
||||||
|
elseif (APPLE)
|
||||||
|
target_compile_options(${target_name} PRIVATE
|
||||||
|
-Wno-declaration-after-statement -Wno-non-pod-varargs)
|
||||||
|
elseif (MSVC)
|
||||||
|
target_compile_options(${target_name} PRIVATE
|
||||||
|
"$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>"
|
||||||
|
"$<$<COMPILE_LANGUAGE:C,CXX>:/utf-8>")
|
||||||
|
endif ()
|
||||||
|
# Use signed char on ARM to match the original game (and x86)
|
||||||
|
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _mod_arch)
|
||||||
|
if (_mod_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
|
||||||
|
target_compile_options(${target_name} PRIVATE -fsigned-char)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
# Game symbols resolve against the host executable at dlopen time.
|
||||||
|
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
|
||||||
|
elseif (ANDROID)
|
||||||
|
if (TARGET dusklight)
|
||||||
|
target_link_libraries(${target_name} PRIVATE dusklight)
|
||||||
|
elseif (DUSK_GAME_SOLIB)
|
||||||
|
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_SOLIB}")
|
||||||
|
else ()
|
||||||
|
message(FATAL_ERROR "add_mod: DUSK_GAME_SOLIB is not set (libmain.so)")
|
||||||
|
endif ()
|
||||||
|
elseif (UNIX)
|
||||||
|
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
|
||||||
|
elseif (WIN32)
|
||||||
|
# Link against the generated import library (game ABI surface). Function calls
|
||||||
|
# resolve through import thunks. Data is toolchain dependent:
|
||||||
|
# - clang-cl: lld's mingw mode auto-imports data references, fixed up at load by
|
||||||
|
# the mod SDK's pseudo-relocation runtime (pseudo_reloc.cpp).
|
||||||
|
# - cl (MSVC): only DUSK_GAME_DATA-annotated data is reachable. Un-annotated
|
||||||
|
# references fail to link.
|
||||||
|
if (NOT DUSK_GAME_IMPLIB)
|
||||||
|
message(FATAL_ERROR "add_mod: DUSK_GAME_IMPLIB is not set.")
|
||||||
|
endif ()
|
||||||
|
target_link_libraries(${target_name} PRIVATE "${DUSK_GAME_IMPLIB}")
|
||||||
|
set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||||
|
target_compile_definitions(${target_name} PRIVATE _ITERATOR_DEBUG_LEVEL=0)
|
||||||
|
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
|
target_compile_options(${target_name} PRIVATE "$<$<COMPILE_LANGUAGE:C,CXX>:/clang:-mcmodel=large>")
|
||||||
|
target_sources(${target_name} PRIVATE "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../sdk/pseudo_reloc.cpp")
|
||||||
|
# lld mingw mode rewrites /DEFAULTLIB directives to -l style and skips %LIB%, so
|
||||||
|
# the CRT libraries and search paths are spelled out explicitly.
|
||||||
|
target_link_options(${target_name} PRIVATE -lldmingw /nodefaultlib /INCREMENTAL:NO)
|
||||||
|
target_link_libraries(${target_name} PRIVATE
|
||||||
|
msvcrt.lib msvcprt.lib vcruntime.lib ucrt.lib
|
||||||
|
oldnames.lib uuid.lib kernel32.lib user32.lib)
|
||||||
|
set(_lib_dirs "$ENV{LIB}")
|
||||||
|
if ("${_lib_dirs}" STREQUAL "")
|
||||||
|
message(FATAL_ERROR "add_mod: %LIB% is empty; configure from a VS dev shell")
|
||||||
|
endif ()
|
||||||
|
foreach (_libdir IN LISTS _lib_dirs)
|
||||||
|
target_link_options(${target_name} PRIVATE "/libpath:${_libdir}")
|
||||||
|
endforeach ()
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(_output_dir "${DUSK_MODS_OUTPUT_DIR}")
|
||||||
|
if (ARG_OUTPUT_DIR)
|
||||||
|
set(_output_dir "${ARG_OUTPUT_DIR}")
|
||||||
|
endif ()
|
||||||
|
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage")
|
||||||
|
set(_out "${_output_dir}/${target_name}.dusk")
|
||||||
|
|
||||||
|
set(_zip_args "${_lib_name}" mod.json)
|
||||||
|
set(_package_deps "${_mod_json}")
|
||||||
|
set(_package_inputs "${_mod_json}")
|
||||||
|
set(_extra_cmds "")
|
||||||
|
if (ARG_RES_DIR)
|
||||||
|
_mod_resolve_source_path(_res_dir "${ARG_RES_DIR}")
|
||||||
|
_mod_collect_assets(_res_deps "${_res_dir}")
|
||||||
|
list(APPEND _package_deps ${_res_deps})
|
||||||
|
list(APPEND _package_inputs "${_res_dir}" ${_res_deps})
|
||||||
|
list(APPEND _zip_args res)
|
||||||
|
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${_res_dir}" "${_stage}/res")
|
||||||
|
endif ()
|
||||||
|
if (ARG_OVERLAY_DIR)
|
||||||
|
_mod_resolve_source_path(_overlay_dir "${ARG_OVERLAY_DIR}")
|
||||||
|
_mod_collect_assets(_overlay_deps "${_overlay_dir}")
|
||||||
|
list(APPEND _package_deps ${_overlay_deps})
|
||||||
|
list(APPEND _package_inputs "${_overlay_dir}" ${_overlay_deps})
|
||||||
|
list(APPEND _zip_args overlay)
|
||||||
|
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${_overlay_dir}" "${_stage}/overlay")
|
||||||
|
endif ()
|
||||||
|
if (ARG_TEXTURES_DIR)
|
||||||
|
_mod_resolve_source_path(_textures_dir "${ARG_TEXTURES_DIR}")
|
||||||
|
_mod_collect_assets(_textures_deps "${_textures_dir}")
|
||||||
|
list(APPEND _package_deps ${_textures_deps})
|
||||||
|
list(APPEND _package_inputs "${_textures_dir}" ${_textures_deps})
|
||||||
|
list(APPEND _zip_args textures)
|
||||||
|
list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${_textures_dir}" "${_stage}/textures")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(_bundle_cmds "")
|
||||||
|
if (ARG_BUNDLE AND TARGET dusklight)
|
||||||
|
file(READ "${_mod_json}" _mod_json_text)
|
||||||
|
string(JSON _mod_id GET "${_mod_json_text}" id)
|
||||||
|
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_TARGETS "${target_name}")
|
||||||
|
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_IDS "${_mod_id}")
|
||||||
|
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_STAGES "${_stage}")
|
||||||
|
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_PACKAGES "${_out}")
|
||||||
|
set_property(GLOBAL APPEND PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES "${_lib_name}")
|
||||||
|
set(_bundle_cmds
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundled_mods"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_out}" "${CMAKE_BINARY_DIR}/bundled_mods/${target_name}.dusk")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(_package_target "${target_name}_package")
|
||||||
|
set(_package_inputs_file "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_package_inputs.txt")
|
||||||
|
list(SORT _package_inputs)
|
||||||
|
set(_package_inputs_text "")
|
||||||
|
foreach (_package_input IN LISTS _package_inputs)
|
||||||
|
string(APPEND _package_inputs_text "${_package_input}\n")
|
||||||
|
endforeach ()
|
||||||
|
file(GENERATE OUTPUT "${_package_inputs_file}" CONTENT "${_package_inputs_text}")
|
||||||
|
add_custom_command(OUTPUT "${_out}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_stage}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${_output_dir}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${target_name}>" "${_stage}/${_lib_name}"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_mod_json}" "${_stage}/mod.json"
|
||||||
|
${_extra_cmds}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E chdir "${_stage}" ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
|
||||||
|
${_bundle_cmds}
|
||||||
|
DEPENDS ${target_name} ${_package_deps} "${_package_inputs_file}"
|
||||||
|
COMMENT "Packaging ${target_name} -> ${_out}"
|
||||||
|
COMMAND_EXPAND_LISTS
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(${_package_target} ALL DEPENDS "${_out}")
|
||||||
|
if (TARGET dusklight_mods)
|
||||||
|
add_dependencies(dusklight_mods ${_package_target})
|
||||||
|
endif ()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Install rules for BUNDLE mods.
|
||||||
|
# - Windows: the .dusk archives into <install>/mods (the loader extracts native libs to the
|
||||||
|
# user cache).
|
||||||
|
# - Linux: pre-extracted stage dirs into <install>/mods so native libs dlopen in place from
|
||||||
|
# read-only installs.
|
||||||
|
# - macOS: pre-extracted stage dirs into the installed app's Contents/Resources/mods, dylibs
|
||||||
|
# ad-hoc signed in place, then the whole bundle re-signed.
|
||||||
|
# - iOS/tvOS: assets into <app>/mods/<id> and the dylib into Frameworks/<id>.dylib.
|
||||||
|
# - Android: nothing here; gradle packs ${CMAKE_BINARY_DIR}/bundled_mods into APK assets.
|
||||||
|
function(install_bundled_mods)
|
||||||
|
get_property(_targets GLOBAL PROPERTY DUSK_BUNDLED_MOD_TARGETS)
|
||||||
|
if (NOT _targets OR ANDROID)
|
||||||
|
return ()
|
||||||
|
endif ()
|
||||||
|
get_property(_ids GLOBAL PROPERTY DUSK_BUNDLED_MOD_IDS)
|
||||||
|
get_property(_stages GLOBAL PROPERTY DUSK_BUNDLED_MOD_STAGES)
|
||||||
|
get_property(_lib_names GLOBAL PROPERTY DUSK_BUNDLED_MOD_LIB_NAMES)
|
||||||
|
list(LENGTH _targets _count)
|
||||||
|
math(EXPR _last "${_count} - 1")
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
get_target_property(_app_name dusklight OUTPUT_NAME)
|
||||||
|
if (NOT _app_name)
|
||||||
|
set(_app_name dusklight)
|
||||||
|
endif ()
|
||||||
|
set(_bundle_dir "${CMAKE_INSTALL_PREFIX}/${_app_name}.app")
|
||||||
|
if (IOS OR TVOS)
|
||||||
|
foreach (_i RANGE ${_last})
|
||||||
|
list(GET _targets ${_i} _target)
|
||||||
|
list(GET _ids ${_i} _id)
|
||||||
|
list(GET _stages ${_i} _stage)
|
||||||
|
list(GET _lib_names ${_i} _lib_name)
|
||||||
|
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/mods/${_id}"
|
||||||
|
PATTERN "${_lib_name}" EXCLUDE)
|
||||||
|
install(PROGRAMS "$<TARGET_FILE:${_target}>"
|
||||||
|
DESTINATION "${_bundle_dir}/Frameworks" RENAME "${_id}.dylib")
|
||||||
|
endforeach ()
|
||||||
|
else ()
|
||||||
|
foreach (_i RANGE ${_last})
|
||||||
|
list(GET _ids ${_i} _id)
|
||||||
|
list(GET _stages ${_i} _stage)
|
||||||
|
list(GET _lib_names ${_i} _lib_name)
|
||||||
|
install(DIRECTORY "${_stage}/" DESTINATION "${_bundle_dir}/Contents/Resources/mods/${_id}")
|
||||||
|
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - \"${_bundle_dir}/Contents/Resources/mods/${_id}/${_lib_name}\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||||
|
endforeach ()
|
||||||
|
install(CODE "execute_process(COMMAND /usr/bin/codesign --force --sign - --entitlements \"${DUSK_ENTITLEMENTS}\" \"${_bundle_dir}\" COMMAND_ERROR_IS_FATAL ANY)")
|
||||||
|
endif ()
|
||||||
|
return ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (DUSK_PACKAGE_INSTALL)
|
||||||
|
set(_mods_dest "${CMAKE_INSTALL_DATAROOTDIR}/dusklight/mods")
|
||||||
|
else ()
|
||||||
|
set(_mods_dest "${CMAKE_INSTALL_PREFIX}/mods")
|
||||||
|
endif ()
|
||||||
|
if (WIN32)
|
||||||
|
foreach (_target IN LISTS _targets)
|
||||||
|
install(FILES "${CMAKE_BINARY_DIR}/bundled_mods/${_target}.dusk" DESTINATION "${_mods_dest}")
|
||||||
|
endforeach ()
|
||||||
|
else ()
|
||||||
|
foreach (_i RANGE ${_last})
|
||||||
|
list(GET _ids ${_i} _id)
|
||||||
|
list(GET _stages ${_i} _stage)
|
||||||
|
install(DIRECTORY "${_stage}/" DESTINATION "${_mods_dest}/${_id}")
|
||||||
|
endforeach ()
|
||||||
|
endif ()
|
||||||
|
endfunction()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Patches capstone's CMakeLists.txt for compatibility with CMake >= 4.0:
|
||||||
|
# - Bumps cmake_minimum_required to 3.10 (CMake >= 4.0 dropped < 3.5 support; < 3.10 warns)
|
||||||
|
# - Removes cmake_policy(SET CMP0048 OLD) (rejected by CMake >= 3.27)
|
||||||
|
file(READ "${DIR}/CMakeLists.txt" _content)
|
||||||
|
string(REGEX REPLACE
|
||||||
|
"cmake_minimum_required[ \t]*\\([ \t]*VERSION[ \t]+[0-9]+\\.[0-9]+(\\.[0-9]+)?[ \t]*\\)"
|
||||||
|
"cmake_minimum_required(VERSION 3.10)"
|
||||||
|
_content "${_content}")
|
||||||
|
string(REGEX REPLACE
|
||||||
|
"cmake_policy[ \t]*\\([ \t]*SET[ \t]+CMP0048[ \t]+OLD[ \t]*\\)"
|
||||||
|
"# cmake_policy(SET CMP0048 OLD)"
|
||||||
|
_content "${_content}")
|
||||||
|
file(WRITE "${DIR}/CMakeLists.txt" "${_content}")
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
file(READ "${SOURCE_DIR}/cmake/capstone.cmake.in" _content)
|
||||||
|
|
||||||
|
# Insert PATCH_COMMAND before CONFIGURE_COMMAND in the ExternalProject_Add.
|
||||||
|
# Bracket args prevent cmake from substituting ${...} while writing this file.
|
||||||
|
string(REPLACE
|
||||||
|
" CONFIGURE_COMMAND \"\""
|
||||||
|
[=[ PATCH_COMMAND "${CMAKE_COMMAND}" -DDIR=${CMAKE_CURRENT_BINARY_DIR}/capstone-src -P "${CAPSTONE_FIX_SCRIPT}"
|
||||||
|
CONFIGURE_COMMAND ""]=]
|
||||||
|
_content "${_content}")
|
||||||
|
|
||||||
|
file(WRITE "${SOURCE_DIR}/cmake/capstone.cmake.in" "${_content}")
|
||||||
|
|
||||||
|
file(READ "${SOURCE_DIR}/src/funchook_unix.c" _unix_content)
|
||||||
|
|
||||||
|
# macOS rejects the POSIX mprotect RWX/RW transition for executable image pages on arm64.
|
||||||
|
# Use Mach VM_PROT_COPY for the short patch window, then restore RX permissions.
|
||||||
|
if (NOT _unix_content MATCHES "VM_PROT_READ \\| VM_PROT_WRITE \\| VM_PROT_COPY")
|
||||||
|
string(REPLACE
|
||||||
|
[=[ rv = mprotect(mstate->addr, mstate->size, prot);]=]
|
||||||
|
[=[#ifdef __APPLE__
|
||||||
|
kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr,
|
||||||
|
(vm_size_t)mstate->size, FALSE,
|
||||||
|
VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY);
|
||||||
|
if (kr == KERN_SUCCESS) {
|
||||||
|
funchook_log(funchook, " unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR")\n",
|
||||||
|
mstate->addr, mstate->size, start, len);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
funchook_set_error_message(funchook, "Failed to unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR", error=%s)",
|
||||||
|
mstate->addr, mstate->size, start, len,
|
||||||
|
mach_error_string(kr));
|
||||||
|
return FUNCHOOK_ERROR_MEMORY_FUNCTION;
|
||||||
|
#endif
|
||||||
|
rv = mprotect(mstate->addr, mstate->size, prot);]=]
|
||||||
|
_unix_content "${_unix_content}")
|
||||||
|
|
||||||
|
string(REPLACE
|
||||||
|
[=[ char errbuf[128];
|
||||||
|
int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=]
|
||||||
|
[=[ char errbuf[128];
|
||||||
|
#ifdef __APPLE__
|
||||||
|
kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr,
|
||||||
|
(vm_size_t)mstate->size, FALSE,
|
||||||
|
VM_PROT_READ | VM_PROT_EXECUTE);
|
||||||
|
|
||||||
|
if (kr == KERN_SUCCESS) {
|
||||||
|
funchook_log(funchook, " protect memory %p (size=%"PRIuPTR", prot=read,exec)\n",
|
||||||
|
mstate->addr, mstate->size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
funchook_set_error_message(funchook, "Failed to protect memory %p (size=%"PRIuPTR", prot=read,exec, error=%s)",
|
||||||
|
mstate->addr, mstate->size,
|
||||||
|
mach_error_string(kr));
|
||||||
|
return FUNCHOOK_ERROR_MEMORY_FUNCTION;
|
||||||
|
#endif
|
||||||
|
int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=]
|
||||||
|
_unix_content "${_unix_content}")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
file(WRITE "${SOURCE_DIR}/src/funchook_unix.c" "${_unix_content}")
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
include_guard(GLOBAL)
|
||||||
|
|
||||||
|
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||||
|
|
||||||
|
set(_SYMGEN_VERSION "1.1.0")
|
||||||
|
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
|
||||||
|
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
|
||||||
|
mark_as_advanced(SYMGEN_PATH)
|
||||||
|
|
||||||
|
function(symgen_host_asset out_name)
|
||||||
|
string(TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" _host_processor)
|
||||||
|
set(_asset "")
|
||||||
|
|
||||||
|
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
|
||||||
|
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||||
|
set(_asset "symgen-macos-arm64")
|
||||||
|
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||||
|
set(_asset "symgen-macos-x86_64")
|
||||||
|
endif ()
|
||||||
|
elseif (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
if (_host_processor MATCHES "^(aarch64|arm64)$")
|
||||||
|
set(_asset "symgen-linux-aarch64")
|
||||||
|
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||||
|
set(_asset "symgen-linux-x86_64")
|
||||||
|
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||||
|
set(_asset "symgen-linux-i686")
|
||||||
|
endif ()
|
||||||
|
elseif (CMAKE_HOST_WIN32)
|
||||||
|
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||||
|
set(_asset "symgen-windows-arm64.exe")
|
||||||
|
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||||
|
set(_asset "symgen-windows-x86_64.exe")
|
||||||
|
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||||
|
set(_asset "symgen-windows-x86.exe")
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(${out_name} "${_asset}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(ensure_symgen required)
|
||||||
|
if (TARGET symgen)
|
||||||
|
return()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (SYMGEN_PATH)
|
||||||
|
get_filename_component(_symgen "${SYMGEN_PATH}" ABSOLUTE)
|
||||||
|
if (NOT EXISTS "${_symgen}")
|
||||||
|
if (required)
|
||||||
|
message(FATAL_ERROR "symgen: SYMGEN_PATH does not exist: ${_symgen}")
|
||||||
|
endif ()
|
||||||
|
message(STATUS "symgen: SYMGEN_PATH does not exist, symbol manifest generation "
|
||||||
|
"skipped (by-name hook resolution will be unavailable)")
|
||||||
|
return()
|
||||||
|
endif ()
|
||||||
|
else ()
|
||||||
|
symgen_host_asset(_asset)
|
||||||
|
if (_asset STREQUAL "")
|
||||||
|
if (required)
|
||||||
|
message(FATAL_ERROR "symgen: no prebuilt binary for host "
|
||||||
|
"${CMAKE_HOST_SYSTEM_NAME}/${CMAKE_HOST_SYSTEM_PROCESSOR} "
|
||||||
|
"(configure with -DDUSK_ENABLE_CODE_MODS=OFF)")
|
||||||
|
endif ()
|
||||||
|
message(STATUS "symgen: no prebuilt binary for host "
|
||||||
|
"${CMAKE_HOST_SYSTEM_NAME}/${CMAKE_HOST_SYSTEM_PROCESSOR}; "
|
||||||
|
"symbol manifest generation skipped (by-name hook resolution will be unavailable)")
|
||||||
|
return()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(_symgen_dir "${CMAKE_BINARY_DIR}/_deps/symgen")
|
||||||
|
set(_symgen "${_symgen_dir}/${_asset}")
|
||||||
|
set(_url "${_SYMGEN_RELEASE_BASE_URL}/${_asset}")
|
||||||
|
message(STATUS "dusklight: Fetching symgen ${_SYMGEN_VERSION} (${_asset})")
|
||||||
|
file(MAKE_DIRECTORY "${_symgen_dir}")
|
||||||
|
file(DOWNLOAD "${_url}" "${_symgen}"
|
||||||
|
TLS_VERIFY ON
|
||||||
|
STATUS _download_status
|
||||||
|
SHOW_PROGRESS)
|
||||||
|
list(GET _download_status 0 _download_code)
|
||||||
|
if (NOT _download_code EQUAL 0)
|
||||||
|
list(GET _download_status 1 _download_message)
|
||||||
|
file(REMOVE "${_symgen}")
|
||||||
|
if (required)
|
||||||
|
message(FATAL_ERROR "symgen: failed to download ${_url}: ${_download_message}")
|
||||||
|
endif ()
|
||||||
|
message(STATUS "symgen: failed to download ${_url}: ${_download_message}; "
|
||||||
|
"symbol manifest generation skipped (by-name hook resolution will be unavailable)")
|
||||||
|
return()
|
||||||
|
endif ()
|
||||||
|
if (NOT CMAKE_HOST_WIN32)
|
||||||
|
file(CHMOD "${_symgen}" PERMISSIONS
|
||||||
|
OWNER_READ OWNER_WRITE OWNER_EXECUTE
|
||||||
|
GROUP_READ GROUP_EXECUTE
|
||||||
|
WORLD_READ WORLD_EXECUTE)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
add_custom_target(symgen DEPENDS "${_symgen}")
|
||||||
|
set(SYMGEN_EXE "${_symgen}" CACHE INTERNAL "symgen executable" FORCE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
function(setup_symbol_manifest target)
|
||||||
|
ensure_symgen(TRUE)
|
||||||
|
if (NOT TARGET symgen)
|
||||||
|
return()
|
||||||
|
endif ()
|
||||||
|
add_dependencies(${target} symgen)
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
set(_input --pdb "$<TARGET_PDB_FILE:${target}>")
|
||||||
|
set(_out "$<TARGET_FILE_DIR:${target}>/dusklight.symdb")
|
||||||
|
else ()
|
||||||
|
set(_input --binary "$<TARGET_FILE:${target}>")
|
||||||
|
if (APPLE)
|
||||||
|
set(_out "$<TARGET_BUNDLE_CONTENT_DIR:${target}>/Resources/dusklight.symdb")
|
||||||
|
else ()
|
||||||
|
set(_out "$<TARGET_FILE_DIR:${target}>/dusklight.symdb")
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
add_custom_command(TARGET ${target} POST_BUILD
|
||||||
|
COMMAND "${SYMGEN_EXE}" manifest ${_input} --out "${_out}"
|
||||||
|
COMMENT "Generating symbol manifest"
|
||||||
|
VERBATIM)
|
||||||
|
endfunction()
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
include_guard(GLOBAL)
|
||||||
|
|
||||||
|
get_filename_component(_DUSK_WINDOWS_EXPORTS_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||||
|
|
||||||
|
# Windows mod linking: generate the curated export surface for the game executable and the
|
||||||
|
# import library mods link against. symgen scans the built objects, filters by source, and
|
||||||
|
# writes a .def used by the main link and import library generation.
|
||||||
|
function(setup_windows_exports target)
|
||||||
|
if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||||
|
message(WARNING "dusklight: Windows code-mod exports are x64-only for now; skipping")
|
||||||
|
return()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
include("${_DUSK_WINDOWS_EXPORTS_CMAKE_DIR}/SymbolManifest.cmake")
|
||||||
|
ensure_symgen(TRUE)
|
||||||
|
set(_symgen "${SYMGEN_EXE}")
|
||||||
|
add_dependencies(${target} symgen)
|
||||||
|
|
||||||
|
set(_config_subdir "")
|
||||||
|
if (CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(_config_subdir "$<CONFIG>/")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(_rsp_lines "$<TARGET_OBJECTS:${target}>")
|
||||||
|
foreach (_lib IN LISTS JSYSTEM_LIBRARIES)
|
||||||
|
list(APPEND _rsp_lines "$<TARGET_FILE:${_lib}>")
|
||||||
|
endforeach ()
|
||||||
|
list(JOIN _rsp_lines "\n" _rsp_content)
|
||||||
|
set(_rsp "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports_input.rsp")
|
||||||
|
file(GENERATE OUTPUT "${_rsp}" CONTENT "${_rsp_content}")
|
||||||
|
|
||||||
|
set(_sdk_args)
|
||||||
|
foreach (_lib aurora_card aurora_core aurora_dvd aurora_gd aurora_gx aurora_mtx
|
||||||
|
aurora_os aurora_pad aurora_si aurora_vi)
|
||||||
|
if (TARGET ${_lib})
|
||||||
|
list(APPEND _sdk_args --sdk-lib "$<TARGET_FILE:${_lib}>")
|
||||||
|
endif ()
|
||||||
|
endforeach ()
|
||||||
|
|
||||||
|
# Generate curated exports list from the main binary
|
||||||
|
set(_def "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_exports.def")
|
||||||
|
add_custom_command(TARGET ${target} PRE_LINK
|
||||||
|
# TODO: src/dusk/ is NOT excluded: inline code in game headers
|
||||||
|
# currently call into it (e.g. dusk::frame_interp::lookup_replacement).
|
||||||
|
COMMAND "${_symgen}" def
|
||||||
|
--rsp "${_rsp}"
|
||||||
|
--out "${_def}"
|
||||||
|
--exclude cmake_pch
|
||||||
|
--exclude miniz
|
||||||
|
--exclude asan_options
|
||||||
|
--max-exports 58000
|
||||||
|
${_sdk_args}
|
||||||
|
COMMENT "Generating dusklight exports"
|
||||||
|
VERBATIM)
|
||||||
|
target_link_options(${target} PRIVATE "/DEF:${_def}")
|
||||||
|
|
||||||
|
# Generate import library for mods to link against.
|
||||||
|
set(_implib "${CMAKE_BINARY_DIR}/${_config_subdir}dusklight_imports.lib")
|
||||||
|
get_filename_component(_compiler_dir "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||||
|
find_program(DUSK_LLVM_DLLTOOL llvm-dlltool HINTS "${_compiler_dir}")
|
||||||
|
if (DUSK_LLVM_DLLTOOL)
|
||||||
|
set(_implib_cmd "${DUSK_LLVM_DLLTOOL}" -d "${_def}" -D dusklight.exe -m i386:x86-64
|
||||||
|
-l "${_implib}")
|
||||||
|
else ()
|
||||||
|
set(_implib_cmd "${CMAKE_AR}" /nologo "/def:${_def}" /machine:x64 /name:dusklight.exe
|
||||||
|
"/out:${_implib}")
|
||||||
|
endif ()
|
||||||
|
add_custom_command(TARGET ${target} POST_BUILD
|
||||||
|
COMMAND ${_implib_cmd}
|
||||||
|
BYPRODUCTS "${_implib}"
|
||||||
|
COMMENT "Generating dusklight import library"
|
||||||
|
VERBATIM)
|
||||||
|
set(DUSK_GAME_IMPLIB "${_implib}" CACHE INTERNAL "Import library for Windows mod linking")
|
||||||
|
set(DUSK_GAME_DEF "${_def}" CACHE INTERNAL "Curated export .def for the game executable")
|
||||||
|
|
||||||
|
# Ship the import library as sdk/dusklight.lib in the install tree: mods may use it to
|
||||||
|
# compile against Dusklight without having to build the whole game. (See DUSK_GAME_IMPLIB)
|
||||||
|
install(FILES "${_implib}" DESTINATION sdk RENAME dusklight.lib)
|
||||||
|
endfunction()
|
||||||
+716
@@ -0,0 +1,716 @@
|
|||||||
|
# Dusklight Mod API
|
||||||
|
|
||||||
|
Mods are `.dusk` bundles: zip archives that can contain code (in the form of native libraries), resources, DVD overlay
|
||||||
|
files, and texture replacements. Mods may be enabled, disabled and reloaded at runtime.
|
||||||
|
|
||||||
|
When code mods are loaded, they get dynamically linked by the operating system to the running game process. The mod
|
||||||
|
exports lifecycle functions that Dusklight calls into (`mod_initialize`, `mod_update`, `mod_shutdown`), and the mod
|
||||||
|
communicates with the host via **services**: plain C APIs, individually versioned. Dusklight exports several built-in
|
||||||
|
services, and mods may export services of their own, permitting framework mods and cross-mod integration.
|
||||||
|
|
||||||
|
Beyond services, mods have full access to the original game's code: include game headers, call directly into any public
|
||||||
|
function, read and write data fields, and hook the vast majority of game functions.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Getting Started](#getting-started)
|
||||||
|
2. [mod.json](#modjson)
|
||||||
|
3. [Anatomy of a Code Mod](#anatomy-of-a-code-mod)
|
||||||
|
4. [Services](#services)
|
||||||
|
5. [Built-in Services](#built-in-services)
|
||||||
|
6. [Hooking Game Functions](#hooking-game-functions)
|
||||||
|
7. [Asset Overlays](#asset-overlays)
|
||||||
|
8. [Runtime Lifecycle](#runtime-lifecycle)
|
||||||
|
9. [Error Handling](#error-handling)
|
||||||
|
10. [Advanced: Exporting Services](#advanced-exporting-services)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Fork the [mod template](../tools/mod_template/), a self-contained CMake project that uses the Dusklight mod SDK.
|
||||||
|
|
||||||
|
```
|
||||||
|
my_mod/
|
||||||
|
├── CMakeLists.txt
|
||||||
|
├── mod.json
|
||||||
|
├── src/mod.cpp
|
||||||
|
├── res/ (optional bundled resources)
|
||||||
|
├── overlay/ (optional game file overrides)
|
||||||
|
└── textures/ (optional texture replacements)
|
||||||
|
```
|
||||||
|
|
||||||
|
**CMakeLists.txt:**
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
cmake_minimum_required(VERSION 3.25)
|
||||||
|
project(my_mod CXX)
|
||||||
|
|
||||||
|
set(DUSKLIGHT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusklight" CACHE PATH "Path to dusklight source root")
|
||||||
|
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
|
add_mod(my_mod
|
||||||
|
SOURCES src/mod.cpp
|
||||||
|
MOD_JSON mod.json
|
||||||
|
RES_DIR res # optional
|
||||||
|
OVERLAY_DIR overlay # optional
|
||||||
|
TEXTURES_DIR textures # optional
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Building produces `my_mod.dusk` in `build/<preset>/mods/` (configurable via the `DUSK_MODS_OUTPUT_DIR` cache variable).
|
||||||
|
Dusklight searches a `mods/` directory next to the app in addition to the user directory, so a dev build launched from
|
||||||
|
`build/<preset>/` picks these up automatically: rebuild, relaunch (or click Reload), done.
|
||||||
|
|
||||||
|
For a regular game install, copy the `.dusk` into the user mods folder:
|
||||||
|
|
||||||
|
- Windows: `%APPDATA%\TwilitRealm\Dusklight\mods`
|
||||||
|
- Linux: `~/.local/share/TwilitRealm/Dusklight/mods`
|
||||||
|
- macOS: `~/Library/Application Support/TwilitRealm/Dusklight/mods`
|
||||||
|
|
||||||
|
Passing `--mods <dir>` on the command line replaces the user directory with one of your choosing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## mod.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "com.example.my_mod",
|
||||||
|
"name": "My Mod",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Your Name",
|
||||||
|
"description": "A short description shown in the mod manager.",
|
||||||
|
"icon": "res/my_icon.png",
|
||||||
|
"banner": "res/my_banner.png"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`id` is required: a unique, stable identifier (reverse-DNS style; periods, underscores, and alphanumerics). Everything
|
||||||
|
else is optional but recommended.
|
||||||
|
|
||||||
|
`icon` and `banner` are bundle-relative paths to PNG images for the in-game mod manager: the square icon (e.g.
|
||||||
|
512x512), the banner (~3.5:1). If omitted, `res/icon.png` and `res/banner.png` are used automatically when present.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anatomy of a Code Mod
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
#include "mods/svc/log.h"
|
||||||
|
|
||||||
|
DEFINE_MOD(); // once, in exactly one translation unit
|
||||||
|
IMPORT_SERVICE(LogService, svc_log); // resolved by the loader before mod_initialize
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||||
|
svc_log->info(mod_ctx, "hello from my_mod");
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_EXPORT ModResult mod_update(ModError* error) { // called every frame
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_EXPORT ModResult mod_shutdown(ModError* error) {
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All three lifecycle exports are required. `mod_ctx` is your mod's identity token, set by the loader before
|
||||||
|
`mod_initialize` runs. Pass it as the first argument to every service call.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
A service is a struct of C function pointers with a version header. You declare what you use at file scope, and the
|
||||||
|
loader resolves it before your mod initializes:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(LogService, svc_log); // required, any minor version
|
||||||
|
IMPORT_SERVICE_VERSION(LogService, svc_log, 2); // required, minor version >= 2
|
||||||
|
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
|
||||||
|
```
|
||||||
|
|
||||||
|
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
|
||||||
|
allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major**
|
||||||
|
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
|
||||||
|
functions to the end of the struct without breaking existing callers and simply bumps the minor version. Mods that
|
||||||
|
want the newer functions may use `IMPORT_SERVICE_VERSION` to require that minor at **load time**, or `SERVICE_HAS` to
|
||||||
|
check at **runtime** whether a specific function is available.
|
||||||
|
|
||||||
|
The contract (see `include/mods/api.h` for the full version):
|
||||||
|
|
||||||
|
- **A required import is guaranteed valid.** If the service is missing or too old, the mod fails to load with a clear
|
||||||
|
error. No need to null check at call sites.
|
||||||
|
- **Anything at or below the minor version you imported can be called unconditionally.**
|
||||||
|
- Optional imports may be null; check once in `mod_initialize`.
|
||||||
|
- Fields newer than your imported minor must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a null
|
||||||
|
check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Built-in Services
|
||||||
|
|
||||||
|
### LogService (`mods/svc/log.h`)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(LogService, svc_log);
|
||||||
|
|
||||||
|
svc_log->info(mod_ctx, "spawned the thing");
|
||||||
|
svc_log->warn(mod_ctx, "that looks wrong");
|
||||||
|
svc_log->error(mod_ctx, "very bad");
|
||||||
|
svc_log->write(mod_ctx, LOG_LEVEL_DEBUG, "verbose details");
|
||||||
|
```
|
||||||
|
|
||||||
|
Messages appear in the console prefixed with your mod ID. Messages are plain UTF-8 strings and are copied before the
|
||||||
|
call returns; use `snprintf` or `fmt::format` for formatting.
|
||||||
|
|
||||||
|
### ResourceService (`mods/svc/resource.h`)
|
||||||
|
|
||||||
|
Loads files from the `res/` tree of your `.dusk` archive. Paths are relative to `res/` (pass `"config.txt"`, not
|
||||||
|
`"res/config.txt"`); absolute paths and `..` are rejected.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(ResourceService, svc_resource);
|
||||||
|
|
||||||
|
ResourceBuffer buf = RESOURCE_BUFFER_INIT;
|
||||||
|
if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
|
||||||
|
// buf.data / buf.size
|
||||||
|
svc_resource->free(mod_ctx, &buf);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. Note that the bundle is read-only; for writable
|
||||||
|
storage, use the directory from `svc_host->mod_dir(mod_ctx)`.
|
||||||
|
|
||||||
|
### HostService (`mods/svc/host.h`)
|
||||||
|
|
||||||
|
Mod metadata and runtime interaction with the loader:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(HostService, svc_host);
|
||||||
|
|
||||||
|
const char* id = svc_host->mod_id(mod_ctx);
|
||||||
|
const char* dir = svc_host->mod_dir(mod_ctx); // writable per-mod directory
|
||||||
|
svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); // disables the mod
|
||||||
|
```
|
||||||
|
|
||||||
|
`get_service`/`publish_service` provide dynamic service lookup; see [Advanced](#advanced-exporting-services).
|
||||||
|
|
||||||
|
**Lifecycle watches.** If your mod provides a service that hands out per-caller state (registrations, callbacks,
|
||||||
|
handles), watch other mods' lifecycle and drop what you hold for a mod when it detaches.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE_VERSION(HostService, svc_host, 1);
|
||||||
|
|
||||||
|
void on_mod_lifecycle(ModContext* ctx, ModContext* subject, const char* subject_id,
|
||||||
|
ModLifecycleEvent event, void* user_data) {
|
||||||
|
if (event == MOD_LIFECYCLE_DETACHED) {
|
||||||
|
drop_state_for(subject); // same ModContext* the subject passed into your service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t watch = 0;
|
||||||
|
svc_host->watch_mod_lifecycle(mod_ctx, on_mod_lifecycle, nullptr, &watch);
|
||||||
|
```
|
||||||
|
|
||||||
|
`MOD_LIFECYCLE_DETACHED` fires on the game thread at a lifecycle safe point, after the subject's `mod_shutdown` ran and
|
||||||
|
every service dropped its state. For your own mod's teardown, use `mod_shutdown` instead.
|
||||||
|
|
||||||
|
### HookService (`mods/svc/hook.h`)
|
||||||
|
|
||||||
|
Installs hooks on game functions and resolves symbols by name. You'll rarely call it directly; use the typed helpers in
|
||||||
|
`mods/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||||
|
|
||||||
|
### OverlayService (`mods/svc/overlay.h`)
|
||||||
|
|
||||||
|
Registers DVD file overlays at runtime: the dynamic counterpart to the static `overlay/` directory (see
|
||||||
|
[Asset Overlays](#asset-overlays)). Overlay a disc path with a file from your bundle, or with a caller-owned buffer
|
||||||
|
(copied on registration):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(OverlayService, svc_overlay);
|
||||||
|
|
||||||
|
OverlayHandle handle = 0;
|
||||||
|
svc_overlay->add_file(mod_ctx, "/res/Msgus.arc", "res/replacement.arc", &handle);
|
||||||
|
svc_overlay->add_buffer(mod_ctx, "/generated.txt", data, size, nullptr);
|
||||||
|
svc_overlay->remove(mod_ctx, handle);
|
||||||
|
```
|
||||||
|
|
||||||
|
`disc_path` must be absolute (leading `/`) and is matched against the disc case-insensitively. Paths that don't exist
|
||||||
|
on the disc are added as new files. Changes are applied at the next frame boundary, and data the game already read
|
||||||
|
stays in memory until the file is re-read: sometimes a scene reload, and in the worst case, a full restart.
|
||||||
|
|
||||||
|
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
|
||||||
|
|
||||||
|
### TextureService (`mods/svc/texture.h`)
|
||||||
|
|
||||||
|
Registers texture replacements at runtime: the dynamic counterpart to the static `textures/` directory (see
|
||||||
|
[Asset Overlays](#asset-overlays)). Two forms: raw texel data with an explicit key, or an encoded `.dds`/`.png` from
|
||||||
|
your bundle whose filename encodes the key:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(TextureService, svc_texture);
|
||||||
|
|
||||||
|
// Encoded file; filename follows the replacement naming convention.
|
||||||
|
TextureReplacementHandle handle = 0;
|
||||||
|
svc_texture->register_file(mod_ctx, "res/tex1_32x32_$_6.png", &handle);
|
||||||
|
|
||||||
|
// Raw data: match by texel-data pointer or by content hash (TEXTURE_KEY_SOURCE).
|
||||||
|
TextureKey key = TEXTURE_KEY_INIT;
|
||||||
|
key.kind = TEXTURE_KEY_POINTER;
|
||||||
|
key.pointer = someTexObj.data;
|
||||||
|
TextureData data = TEXTURE_DATA_INIT;
|
||||||
|
data.data = pixels; data.size = pixelsSize;
|
||||||
|
data.width = 32; data.height = 32; data.gx_format = GX_TF_RGBA8_PC;
|
||||||
|
svc_texture->register_data(mod_ctx, &key, &data, nullptr);
|
||||||
|
|
||||||
|
svc_texture->unregister(mod_ctx, handle);
|
||||||
|
```
|
||||||
|
|
||||||
|
Filenames use the same Dolphin-style convention as the user's `texture_replacements` directory:
|
||||||
|
`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, where hashes may be `$` (wildcard). `_mipN` sidecar files next to
|
||||||
|
a registered file are picked up automatically. Files are decoded lazily on first use by the renderer; raw data is copied
|
||||||
|
at registration. Registrations follow your mod's lifecycle.
|
||||||
|
|
||||||
|
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
|
||||||
|
|
||||||
|
### ConfigService (`mods/svc/config.h`)
|
||||||
|
|
||||||
|
Persistent, mod-scoped configuration variables. Each var is stored in the user's `config.json` under
|
||||||
|
`mod.<escaped mod id>.<name>` (escaping: `.` → `_`, `_` → `__`, so `com.example.my_mod` becomes `com_example_my__mod`),
|
||||||
|
next to the host's own settings:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(ConfigService, svc_config);
|
||||||
|
|
||||||
|
ConfigVarDesc desc = CONFIG_VAR_DESC_INIT;
|
||||||
|
desc.name = "speedMultiplier"; // 1-64 chars from [A-Za-z0-9_-]; "enabled" is reserved
|
||||||
|
desc.type = CONFIG_VAR_FLOAT;
|
||||||
|
desc.default_float = 1.0;
|
||||||
|
ConfigVarHandle var = 0;
|
||||||
|
svc_config->register_var(mod_ctx, &desc, &var);
|
||||||
|
|
||||||
|
double speed = 1.0;
|
||||||
|
svc_config->get_float(mod_ctx, var, &speed);
|
||||||
|
svc_config->set_float(mod_ctx, var, 2.0);
|
||||||
|
|
||||||
|
// Optional: get notified when the value changes.
|
||||||
|
void on_speed_changed(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value,
|
||||||
|
const ConfigVarValue* previous, void* user_data) {
|
||||||
|
/* value->float_value is the new value, previous->float_value the old one */
|
||||||
|
}
|
||||||
|
svc_config->subscribe(mod_ctx, var, on_speed_changed, nullptr, nullptr);
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `CONFIG_VAR_BOOL` (`bool`), `CONFIG_VAR_INT` (`int64_t`), `CONFIG_VAR_FLOAT` (`double`), `CONFIG_VAR_STRING`
|
||||||
|
(UTF-8; `get_string` copies into a caller buffer, pass a `NULL` buffer with size 0 to query the length). Accessors are
|
||||||
|
typed and must match the registration.
|
||||||
|
|
||||||
|
Change callbacks fire on the game thread whenever the value changes at runtime (your own `set_*` calls included).
|
||||||
|
Writes that store the same value are silent. Values applied from `config.json` or `--cvar` at registration do
|
||||||
|
**not** fire callbacks; read the value after `register_var` for the starting state.
|
||||||
|
|
||||||
|
### UiService (`mods/svc/ui.h`)
|
||||||
|
|
||||||
|
Integrate seamlessly with Dusklight's UI system: add controls and buttons to your mod's detail pane in the Mods window,
|
||||||
|
create custom windows and modal dialogs, apply custom RCSS stylesheets (anywhere!), and add menu bar tabs.
|
||||||
|
|
||||||
|
**Mod panel:** Registers or replaces the panel rendered in your mod's detail pane; `build` runs every time the detail
|
||||||
|
content is rebuilt, and `update` runs every frame while that mod is selected. While your mod is selected, the detail
|
||||||
|
pane carries your mod's id as a `mod-id` attribute (like custom window roots), so scoped RCSS can target it (e.g.
|
||||||
|
`[mod-id="com.example.mod"]`).
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(UiService, svc_ui);
|
||||||
|
|
||||||
|
UiElementHandle statusText = 0;
|
||||||
|
|
||||||
|
ModResult build(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||||
|
svc_ui->pane_add_section(mod_ctx, panel, "Status");
|
||||||
|
svc_ui->pane_add_text(mod_ctx, panel, "starting...", &statusText);
|
||||||
|
svc_ui->pane_add_progress(mod_ctx, panel, 0.5f, nullptr);
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModResult update(ModContext*, void*, ModError*) {
|
||||||
|
svc_ui->elem_set_text(mod_ctx, statusText, "running");
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiModsPanelDesc panel = UI_MODS_PANEL_DESC_INIT;
|
||||||
|
panel.build = build;
|
||||||
|
panel.update = update;
|
||||||
|
svc_ui->register_mods_panel(mod_ctx, &panel);
|
||||||
|
```
|
||||||
|
|
||||||
|
Element setters must match the element kind: `elem_set_text`/`elem_set_rml` on text rows, and `elem_set_progress` on
|
||||||
|
progress bars. `elem_set_class` sets or clears an RCSS class on any element handle, for styling via scoped or
|
||||||
|
per-window RCSS. A non-`MOD_OK` result from `build`/`update` fails your mod, as do exceptions thrown from any UI
|
||||||
|
callback.
|
||||||
|
|
||||||
|
**Controls:** `pane_add_control` adds an input row described by a `UiControlDesc`: `UI_CONTROL_BUTTON`,
|
||||||
|
`UI_CONTROL_TOGGLE`, `UI_CONTROL_NUMBER`, `UI_CONTROL_STRING`, or `UI_CONTROL_SELECT`. Values bind with callbacks or
|
||||||
|
directly to a config var.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||||
|
control.kind = UI_CONTROL_TOGGLE;
|
||||||
|
control.label = "Enable rainbows";
|
||||||
|
control.help_rml = "Shown in the help pane while focused.";
|
||||||
|
control.binding = UI_BINDING_CONFIG_VAR;
|
||||||
|
control.config_var = myBoolVar; // from svc_config->register_var
|
||||||
|
svc_ui->pane_add_control(mod_ctx, leftPane, &control, nullptr);
|
||||||
|
```
|
||||||
|
|
||||||
|
`UI_BINDING_CONFIG_VAR` wires persistence, change notifications, and the modified indicator automatically. The var
|
||||||
|
type must match the control: `TOGGLE` = bool, `NUMBER` and `SELECT` = int, `STRING` = string. Float vars are not
|
||||||
|
bindable; use callbacks and convert. `help_rml` and `SELECT` option lists render in a help pane, so `SELECT` controls
|
||||||
|
are only available inside window tabs.
|
||||||
|
|
||||||
|
**Windows:** `window_push` pushes a tabbed two-pane window onto the document stack and shows it. Each tab's `build`
|
||||||
|
receives the window handle plus fresh left and right pane handles on every activation. The optional per-tab `update`
|
||||||
|
runs each frame while that tab is active. `on_closed` fires when the window is destroyed. `desc.rcss` optionally styles
|
||||||
|
that window's document only; custom windows carry the owning mod's id as a `mod-id` attribute on the window root, so
|
||||||
|
scoped RCSS can target your specific mod's windows (e.g. `window[mod-id="com.example.mod"]`).
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
UiTabDesc tabs[1] = {UI_TAB_DESC_INIT};
|
||||||
|
tabs[0].title = "Options";
|
||||||
|
tabs[0].build = build_options_tab;
|
||||||
|
|
||||||
|
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
|
||||||
|
desc.tabs = tabs;
|
||||||
|
desc.tab_count = 1;
|
||||||
|
desc.on_closed = options_window_closed;
|
||||||
|
UiWindowHandle window = 0;
|
||||||
|
svc_ui->window_push(mod_ctx, &desc, &window);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dialogs:** `dialog_push` shows a modal dialog. `variant` picks the style, `icon` optionally overrides the variant's
|
||||||
|
default icon, and actions become buttons. After an action's `on_pressed` returns, the dialog closes unless the action
|
||||||
|
sets `keep_open`. A `keep_open` action can close it later (or immediately) with `dialog_close`. Cancel fires
|
||||||
|
`on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live
|
||||||
|
dialog.
|
||||||
|
|
||||||
|
**Menu bar tabs:** `register_menu_tab` adds a tab to the in-game menu bar. `on_selected` fires when the user activates
|
||||||
|
the tab: typically you'd push a window from it. The tab is removed by `unregister_menu_tab`, or automatically when the
|
||||||
|
mod is disabled.
|
||||||
|
|
||||||
|
**Custom styles:** `register_styles(scope, rcss, &handle)` applies an RCSS stylesheet to every document of a scope:
|
||||||
|
existing documents restyle immediately, and future ones pick it up when created. `register_styles_file(scope, path,
|
||||||
|
&handle)` reads the sheet from your bundle's `res/` directory. Scopes are `UI_SCOPE_PRELAUNCH`, `UI_SCOPE_WINDOW`,
|
||||||
|
`UI_SCOPE_MENU_BAR`, `UI_SCOPE_OVERLAY`, `UI_SCOPE_TOUCH_CONTROLS`, and `UI_SCOPE_GRAPHICS_TUNER`. Sheets apply after
|
||||||
|
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
|
||||||
|
unless changing host UI is intentional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hooking Game Functions
|
||||||
|
|
||||||
|
Mods may hook the vast majority of game functions, including file-local static, private and virtual functions.
|
||||||
|
`mods/hook.hpp` provides typed helpers over the hook service:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "mods/hook.hpp"
|
||||||
|
#include "mods/svc/hook.h"
|
||||||
|
|
||||||
|
IMPORT_SERVICE(HookService, svc_hook);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pre-hooks
|
||||||
|
|
||||||
|
Run before the original. Return `HOOK_SKIP_ORIGINAL` to cancel it (post-hooks still run).
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata) {
|
||||||
|
daAlink_c* link = dusk::mods::arg<daAlink_c*>(args, 0); // arg 0 is `this`
|
||||||
|
if (link->shape_angle.y > 10000) {
|
||||||
|
return HOOK_SKIP_ORIGINAL;
|
||||||
|
}
|
||||||
|
return HOOK_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
dusk::mods::hook_add_pre<&daAlink_c::posMove>(svc_hook, on_pos_move_pre);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Post-hooks
|
||||||
|
|
||||||
|
Run after the original (or after a replace-hook, or after a cancelled original). `retval` points to the return value,
|
||||||
|
if any.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... }
|
||||||
|
|
||||||
|
dusk::mods::hook_add_post<&daAlink_c::posMove>(svc_hook, on_pos_move_post);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replace-hooks
|
||||||
|
|
||||||
|
Substitute the original entirely. Call through to it via `Hook<...>::g_orig` if needed:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using ExecuteEntry = dusk::mods::Hook<&daAlink_c::execute>;
|
||||||
|
|
||||||
|
void on_execute_replace(ModContext*, void* args, void* retval, void*) {
|
||||||
|
int result = ExecuteEntry::g_orig(dusk::mods::arg<daAlink_c*>(args, 0));
|
||||||
|
if (retval != nullptr) {
|
||||||
|
*static_cast<int*>(retval) = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dusk::mods::hook_replace<&daAlink_c::execute>(svc_hook, on_execute_replace);
|
||||||
|
```
|
||||||
|
|
||||||
|
By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`,
|
||||||
|
`userdata`) controls this and callback ordering. Multiple mods can attach pre/post hooks to the same function
|
||||||
|
independently.
|
||||||
|
|
||||||
|
### Hooking by name
|
||||||
|
|
||||||
|
Functions you can't name in C++ (file-local statics, private class members, anything not in a header) can be hooked by
|
||||||
|
symbol name instead. You must supply the signature along with the name.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// static callback used by Link's hookshot collider in d_a_alink_hook.inc
|
||||||
|
using HookshotHit = dusk::mods::NamedHook<
|
||||||
|
"daAlink_hookshotAtHitCallBack",
|
||||||
|
void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>;
|
||||||
|
|
||||||
|
dusk::mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit_pre);
|
||||||
|
...
|
||||||
|
HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the original
|
||||||
|
```
|
||||||
|
|
||||||
|
Class member functions must include `Class*` as the first argument.
|
||||||
|
|
||||||
|
The install fails with the resolve error when the name is missing (`MOD_UNAVAILABLE`), ambiguous (`MOD_CONFLICT`),
|
||||||
|
or the manifest is absent (`MOD_UNSUPPORTED`). Unlike `Hook<&Class::method>`, the signature is **not**
|
||||||
|
compiler-checked: a mismatched signature will corrupt the call.
|
||||||
|
|
||||||
|
### Reading and writing arguments
|
||||||
|
|
||||||
|
`args` is an array of pointers to the arguments. For member functions, index 0 is `this`; parameters follow in
|
||||||
|
declaration order.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
T value = dusk::mods::arg<T>(args, n); // copy
|
||||||
|
T& ref = dusk::mods::arg_ref<T>(args, n); // read/write reference
|
||||||
|
```
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// fpc_ProcID fopAcM_createItem(..., int itemNo, ...): turn heart drops into green rupees
|
||||||
|
HookAction on_create_item_pre(ModContext*, void* args, void*, void*) {
|
||||||
|
int& itemNo = dusk::mods::arg_ref<int>(args, 1);
|
||||||
|
if (itemNo == dItemNo_HEART_e) {
|
||||||
|
itemNo = dItemNo_GREEN_RUPEE_e;
|
||||||
|
}
|
||||||
|
return HOOK_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
dusk::mods::hook_add_pre<&fopAcM_createItem>(svc_hook, on_create_item_pre);
|
||||||
|
```
|
||||||
|
|
||||||
|
For reference parameters (e.g. `const cXyz& pos`), `arg_ref<cXyz>` yields a direct reference.
|
||||||
|
|
||||||
|
### Resolving symbols by name
|
||||||
|
|
||||||
|
`resolve` looks a symbol up in the **symbol manifest**: a name→address map generated alongside every game build and
|
||||||
|
keyed to that exact binary. It covers the whole image, including functions that aren't exported (file-local statics),
|
||||||
|
which makes them hookable:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(HookService, svc_hook);
|
||||||
|
|
||||||
|
void* addr = nullptr;
|
||||||
|
uint32_t flags = 0;
|
||||||
|
if (svc_hook->resolve(mod_ctx, "GXSetProjection", &addr, &flags) == MOD_OK) {
|
||||||
|
// addr is the function's real address in the running game; hook or call it.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Two spellings work on every platform:
|
||||||
|
|
||||||
|
- **Display names** (`daAlink_c::posMove`, `fapGm_Before`): the qualified name with no parameter list. They carry no
|
||||||
|
signature, so overload sets (and file-local statics sharing a name) return `MOD_CONFLICT`.
|
||||||
|
- **Decorated names** (`_ZN9daAlink_c7posMoveEv` / `?posMove@daAlink_c@@...`): the platform's mangled spelling in
|
||||||
|
dlsym convention (no Mach-O leading underscore). The escape hatch for overloads.
|
||||||
|
|
||||||
|
`MOD_UNSUPPORTED` means the manifest is missing or was built for a different game binary.
|
||||||
|
|
||||||
|
### Game code ABI contract
|
||||||
|
|
||||||
|
A primary consideration when letting mods link against the game is maintaining ABI stability across Dusklight
|
||||||
|
versions. If your mod calls or hooks game code directly (anything beyond the service APIs), import `GameService`
|
||||||
|
(`mods/svc/game.h`):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(GameService, svc_game);
|
||||||
|
```
|
||||||
|
|
||||||
|
Its major version is the game code ABI epoch: it's bumped when game struct or vtable layouts change incompatibly, and
|
||||||
|
the ordinary service version check then rejects your mod with a clear error instead of letting it corrupt memory in a
|
||||||
|
version it wasn't built for. Service-only and asset-only mods should *not* import it and will continue to work across
|
||||||
|
game ABI changes.
|
||||||
|
|
||||||
|
The more you can do through services, the better: a mod that avoids touching game code directly sidesteps future ABI
|
||||||
|
breaks entirely and plays nicer with other enabled mods.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Asset Overlays
|
||||||
|
|
||||||
|
Files placed under `overlay/` in the `.dusk` archive override game files at the corresponding path, equivalent to
|
||||||
|
replacing files in the .iso. This requires no code: an archive with just `mod.json` and `overlay/` is a complete mod.
|
||||||
|
|
||||||
|
Files placed under `textures/` register as texture replacements, and act just like the user's general
|
||||||
|
`texture_replacements/` directory: Dolphin-style naming, matched by texture hash
|
||||||
|
(`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, `$` as a hash wildcard). Subdirectories are scanned recursively;
|
||||||
|
only the filename needs to match.
|
||||||
|
|
||||||
|
Both mechanisms are tied to the mod's lifecycle: disabling the mod removes its overrides (files revert to the disc
|
||||||
|
contents on their next open; added files stop existing), and reloading serves the new bundle's content. However, game
|
||||||
|
data the engine already read stays as-is until it is loaded again, which may require a scene change or, in the worst
|
||||||
|
case, a full restart. Texture replacements usually take effect immediately.
|
||||||
|
|
||||||
|
If multiple sources replace the same file or texture, the last one wins: runtime registrations override static
|
||||||
|
`textures/` or `overlay/` files, and later-loaded mods override earlier ones. Cross-mod conflicts log warnings.
|
||||||
|
**All** mod-provided texture replacements override the user's `texture_replacements/`.
|
||||||
|
|
||||||
|
To configure overlays and texture replacements at runtime instead, see [OverlayService](#overlayservice-modssvcoverlayh)
|
||||||
|
and [TextureService](#textureservice-modssvctextureh).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Runtime Lifecycle
|
||||||
|
|
||||||
|
Mods can be disabled, re-enabled, and reloaded at runtime without restarting the game (the enabled state persists as the
|
||||||
|
`mod.<escaped id>.enabled` config var). Write your mod assuming this happens:
|
||||||
|
|
||||||
|
- **Disable** calls `mod_shutdown`, removes your hooks, services, overlays, and texture replacements (both static and
|
||||||
|
runtime-registered), and unloads your library.
|
||||||
|
- **Enable** and **Reload** load a *fresh copy* of your library, imports are re-resolved, and `mod_initialize` runs
|
||||||
|
again. You never see a second `mod_initialize` on the same image, so just make `mod_shutdown` release anything the
|
||||||
|
loader doesn't manage for you (threads, files, game-side state you mutated).
|
||||||
|
- **Reload** additionally re-reads the `.dusk` from disk, picking up a rebuilt library and changed assets. This is the
|
||||||
|
fast iteration loop during development: rebuild, click Reload.
|
||||||
|
|
||||||
|
**Dependents restart too.** Disabling or reloading a mod that exports services shuts down the mods importing them
|
||||||
|
first (in reverse dependency order) and brings them back afterward. A mod whose *required* provider is disabled stays
|
||||||
|
suspended and resumes automatically when the provider returns. Mods with an *optional* import of a disabled provider
|
||||||
|
restart with that import null.
|
||||||
|
|
||||||
|
**One caution for hooks:** lifecycle changes are applied between frames, which is safe for hooks on functions
|
||||||
|
that return every frame (effectively everything you'd normally hook). Avoid hooking a function that stays on
|
||||||
|
the stack for the whole session (e.g. the outermost main loop); a mod that does cannot be safely unloaded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Service calls report failure through `ModResult` return values (`MOD_OK`, `MOD_UNAVAILABLE`,
|
||||||
|
`MOD_INVALID_ARGUMENT`, ...). Lifecycle exports additionally receive a `ModError*`: fill it (e.g. with
|
||||||
|
`dusk::mods::set_error(error, code, "message")`) and return the code, and the loader disables the mod and shows the
|
||||||
|
message to the user.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||||
|
if (!load_my_data()) {
|
||||||
|
return dusk::mods::set_error(error, MOD_ERROR, "failed to load data");
|
||||||
|
}
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Throwing exceptions out of lifecycle functions also disables the mod (they are caught by the loader), but prefer
|
||||||
|
explicit results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced: Exporting Services
|
||||||
|
|
||||||
|
Mods may export services of their own, permitting framework mods and cross-mod integration. Define the interface in a
|
||||||
|
header both mods share:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// my_mod_api.h
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
#define MY_MOD_SERVICE_ID "com.example.my_mod.api"
|
||||||
|
#define MY_MOD_SERVICE_MAJOR 1u
|
||||||
|
#define MY_MOD_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
typedef struct MyModService {
|
||||||
|
ServiceHeader header;
|
||||||
|
ModResult (*do_thing)(ModContext* ctx, int value);
|
||||||
|
} MyModService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<MyModService> {
|
||||||
|
static constexpr const char* id = MY_MOD_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = MY_MOD_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
**Provider:**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ModResult do_thing(ModContext* ctx, int value) { ... }
|
||||||
|
|
||||||
|
constexpr MyModService g_service{
|
||||||
|
.header = SERVICE_HEADER(MyModService, MY_MOD_SERVICE_MAJOR, MY_MOD_SERVICE_MINOR),
|
||||||
|
.do_thing = do_thing,
|
||||||
|
};
|
||||||
|
EXPORT_SERVICE(g_service);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Consumer:**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
IMPORT_SERVICE(MyModService, svc_my_mod);
|
||||||
|
// or IMPORT_OPTIONAL_SERVICE if the dependency is optional
|
||||||
|
|
||||||
|
svc_my_mod->do_thing(mod_ctx, 42);
|
||||||
|
```
|
||||||
|
|
||||||
|
The loader registers all exports before resolving any imports, so declaration order between mods doesn't matter. Note
|
||||||
|
that the `ctx` a provider receives identifies the *calling* mod.
|
||||||
|
|
||||||
|
### Dependencies between mods
|
||||||
|
|
||||||
|
Service imports are also dependency declarations: the loader initializes mods in dependency order, so by the time your
|
||||||
|
`mod_initialize` runs, every mod you import services from (required *or* optional) has already finished its own
|
||||||
|
`mod_initialize`. This includes deferred services: a service the provider publishes during its initialization resolves
|
||||||
|
into your import slot just like a static export.
|
||||||
|
|
||||||
|
Consequences of that contract:
|
||||||
|
|
||||||
|
- If a provider fails to load, every mod that *requires* one of its services is disabled too, with an error naming the
|
||||||
|
provider. Optional imports of a failed provider simply resolve to `NULL`.
|
||||||
|
- Mods whose **required** imports form a cycle all fail to load. If the cycle runs through an **optional** import, the
|
||||||
|
loader breaks it there: the optional import still resolves, but its provider may not be initialized yet when you run.
|
||||||
|
- `svc_host->get_service(...)` is outside this system. It sees whatever is published at call time and gives no
|
||||||
|
initialization-order guarantee, which also makes it the escape hatch for intentionally cyclic designs.
|
||||||
|
|
||||||
|
Mods shut down in reverse initialization order, so services you import remain safe to call from `mod_shutdown`.
|
||||||
|
|
||||||
|
Rules for providers:
|
||||||
|
|
||||||
|
- Service IDs are global and use reverse-DNS names (e.g. `com.mydomain.mod.service`)
|
||||||
|
- Every function pointer covered by your declared minor version must be populated.
|
||||||
|
- Within a major version, only append fields; never reorder, remove, or repurpose them. Breaking changes require a major
|
||||||
|
bump (which is, in effect, a new service).
|
||||||
|
- Only one provider per `(id, major)` pair may be registered; duplicates are load errors.
|
||||||
|
|
||||||
|
For services whose construction can't happen at static-init time, declare the export with `EXPORT_DEFERRED_SERVICE(...)`
|
||||||
|
and publish the pointer later via `svc_host->publish_service(...)`. Consumers can fetch services dynamically with
|
||||||
|
`svc_host->get_service(...)`; prefer manifest imports whenever possible, since they give the loader dependency
|
||||||
|
information and fail fast with good errors.
|
||||||
Vendored
+1
-1
Submodule extern/aurora updated: 22351fb0b7...0a5a5d90ef
+34
@@ -1496,6 +1496,12 @@ set(DUSK_FILES
|
|||||||
src/dusk/ui/input.hpp
|
src/dusk/ui/input.hpp
|
||||||
src/dusk/ui/icon_provider.cpp
|
src/dusk/ui/icon_provider.cpp
|
||||||
src/dusk/ui/icon_provider.hpp
|
src/dusk/ui/icon_provider.hpp
|
||||||
|
src/dusk/ui/logs_window.cpp
|
||||||
|
src/dusk/ui/logs_window.hpp
|
||||||
|
src/dusk/ui/mod_texture_provider.cpp
|
||||||
|
src/dusk/ui/mod_texture_provider.hpp
|
||||||
|
src/dusk/ui/mod_window.cpp
|
||||||
|
src/dusk/ui/mod_window.hpp
|
||||||
src/dusk/ui/modal.cpp
|
src/dusk/ui/modal.cpp
|
||||||
src/dusk/ui/modal.hpp
|
src/dusk/ui/modal.hpp
|
||||||
src/dusk/ui/nav_types.hpp
|
src/dusk/ui/nav_types.hpp
|
||||||
@@ -1507,6 +1513,8 @@ set(DUSK_FILES
|
|||||||
src/dusk/ui/pane.hpp
|
src/dusk/ui/pane.hpp
|
||||||
src/dusk/ui/menu_bar.cpp
|
src/dusk/ui/menu_bar.cpp
|
||||||
src/dusk/ui/menu_bar.hpp
|
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.cpp
|
||||||
src/dusk/ui/prelaunch.hpp
|
src/dusk/ui/prelaunch.hpp
|
||||||
src/dusk/ui/preset.cpp
|
src/dusk/ui/preset.cpp
|
||||||
@@ -1541,6 +1549,32 @@ set(DUSK_FILES
|
|||||||
src/dusk/OSReport.cpp
|
src/dusk/OSReport.cpp
|
||||||
src/dusk/OSThread.cpp
|
src/dusk/OSThread.cpp
|
||||||
src/dusk/OSMutex.cpp
|
src/dusk/OSMutex.cpp
|
||||||
|
src/dusk/mods/log_buffer.cpp
|
||||||
|
src/dusk/mods/log_buffer.hpp
|
||||||
|
src/dusk/mods/manifest.cpp
|
||||||
|
src/dusk/mods/manifest.hpp
|
||||||
|
src/dusk/mods/loader/bundle_disk.cpp
|
||||||
|
src/dusk/mods/loader/bundle_zip.cpp
|
||||||
|
src/dusk/mods/loader/context.cpp
|
||||||
|
src/dusk/mods/loader/depgraph.cpp
|
||||||
|
src/dusk/mods/loader/depgraph.hpp
|
||||||
|
src/dusk/mods/loader/loader.cpp
|
||||||
|
src/dusk/mods/loader/loader.hpp
|
||||||
|
src/dusk/mods/loader/native_module.cpp
|
||||||
|
src/dusk/mods/loader/native_module.hpp
|
||||||
|
src/dusk/mods/svc/config.cpp
|
||||||
|
src/dusk/mods/svc/config.hpp
|
||||||
|
src/dusk/mods/svc/game.cpp
|
||||||
|
src/dusk/mods/svc/hook.cpp
|
||||||
|
src/dusk/mods/svc/host.cpp
|
||||||
|
src/dusk/mods/svc/log.cpp
|
||||||
|
src/dusk/mods/svc/overlay.cpp
|
||||||
|
src/dusk/mods/svc/resource.cpp
|
||||||
|
src/dusk/mods/svc/texture.cpp
|
||||||
|
src/dusk/mods/svc/ui.cpp
|
||||||
|
src/dusk/mods/svc/ui.hpp
|
||||||
|
src/dusk/mods/svc/registry.cpp
|
||||||
|
src/dusk/mods/svc/registry.hpp
|
||||||
src/dusk/discord.cpp
|
src/dusk/discord.cpp
|
||||||
src/dusk/discord.hpp
|
src/dusk/discord.hpp
|
||||||
src/dusk/discord_presence.cpp
|
src/dusk/discord_presence.cpp
|
||||||
|
|||||||
@@ -16,37 +16,37 @@
|
|||||||
];
|
];
|
||||||
forAllSystems = lib.genAttrs supportedSystems;
|
forAllSystems = lib.genAttrs supportedSystems;
|
||||||
|
|
||||||
dawnVersion = "v20260423.175430";
|
dawnVersion = "v20260618.032059";
|
||||||
nodVersion = "v2.0.0-alpha.8";
|
nodVersion = "v2.0.0-alpha.10";
|
||||||
versionSuffix = "nix-" + (self.shortRev or self.dirtyShortRev or "dirty");
|
versionSuffix = "nix-" + (self.shortRev or self.dirtyShortRev or "dirty");
|
||||||
|
|
||||||
dawnInfo = {
|
dawnInfo = {
|
||||||
"x86_64-linux" = {
|
"x86_64-linux" = {
|
||||||
triple = "linux-x86_64";
|
triple = "linux-x86_64";
|
||||||
hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs=";
|
hash = "sha256-GFSd573b+VQx/VmFdNQgWDd0V9ayQlcw0Zuopke12ak=";
|
||||||
};
|
};
|
||||||
"aarch64-linux" = {
|
"aarch64-linux" = {
|
||||||
triple = "linux-aarch64";
|
triple = "linux-aarch64";
|
||||||
hash = "sha256-34yyFpfqBZUwoFXQ41F0AwAU78FaNihOSY0oriwn6B0=";
|
hash = "sha256-ZaoP7BAjBMnfAv2/AMRi3FNH2ZtyqASCSFyU/oB2Mzg=";
|
||||||
};
|
};
|
||||||
"aarch64-darwin" = {
|
"aarch64-darwin" = {
|
||||||
triple = "darwin-arm64";
|
triple = "darwin-arm64";
|
||||||
hash = "sha256-eQnzrBp6gjiBek1VYQ9A5W13ClYWrDDKjIqv/7eNTR4=";
|
hash = "sha256-HT+qtlLaSHyoXPrUcXgcTGa877X5YfzbxRD4bJb7i1Y=";
|
||||||
};
|
};
|
||||||
"x86_64-darwin" = {
|
"x86_64-darwin" = {
|
||||||
triple = "darwin-x86_64";
|
triple = "darwin-x86_64";
|
||||||
hash = "sha256-QGWiGdxiI9kci3NPXH6QFFirxn16851zB/w3jqhIBJ4=";
|
hash = "sha256-cUNaCbA7rlKSukDVKGaVEVw0Zt1+mSbaHbmUCMvMVWc=";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
nodPrebuiltInfo = {
|
nodPrebuiltInfo = {
|
||||||
"x86_64-linux" = {
|
"x86_64-linux" = {
|
||||||
triple = "linux-x86_64";
|
triple = "linux-x86_64";
|
||||||
hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE=";
|
hash = "sha256-FVQWECVA2gWdc+n5OQ/Tvwn8z0qdgjSd1WlFt5HKOec=";
|
||||||
};
|
};
|
||||||
"aarch64-darwin" = {
|
"aarch64-darwin" = {
|
||||||
triple = "macos-arm64";
|
triple = "macos-arm64";
|
||||||
hash = "sha256-UPy1ywCcv0K6VJOU3uUelJuUdBh3UNaPRlyP5LOBeDw=";
|
hash = "sha256-8ZEejxksVgShNKUVRCBYaLOp9x/qOC9pAeVrElQUGUk=";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
dawn = pkgs.fetchzip {
|
dawn = pkgs.fetchzip {
|
||||||
url = "https://github.com/encounter/dawn-build/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
|
url = "https://github.com/encounter/dawn/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
|
||||||
hash = dawnInfo.${system}.hash;
|
hash = dawnInfo.${system}.hash;
|
||||||
stripRoot = false;
|
stripRoot = false;
|
||||||
};
|
};
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
owner = "encounter";
|
owner = "encounter";
|
||||||
repo = "nod";
|
repo = "nod";
|
||||||
rev = nodVersion;
|
rev = nodVersion;
|
||||||
hash = "sha256-+zrtVzjo0+X/6uMcNUn1+FaSR+jOhrcQSDNBFjw0NDs=";
|
hash = "sha256-r8qDlOVxv5iKiFjJQrcBuL9HVoOM3yEjRVnQIMqaICs=";
|
||||||
};
|
};
|
||||||
patches = [ ./fix-cmake-paths.patch ];
|
patches = [ ./fix-cmake-paths.patch ];
|
||||||
cargoDeps = pkgs.rustPlatform.importCargoLock {
|
cargoDeps = pkgs.rustPlatform.importCargoLock {
|
||||||
@@ -141,12 +141,12 @@
|
|||||||
XXHASH = pkgs.xxhash.src;
|
XXHASH = pkgs.xxhash.src;
|
||||||
ZSTD = pkgs.zstd.src;
|
ZSTD = pkgs.zstd.src;
|
||||||
FMT = pkgs.fetchzip {
|
FMT = pkgs.fetchzip {
|
||||||
url = "https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz";
|
url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz";
|
||||||
hash = "sha256-sUbxlYi/Aupaox3JjWFqXIjcaQa0LFjclQAOleT+FRA=";
|
hash = "sha256-ZmI1Dv0ZabPlxa02OpERI47jp7zFfjpeWCy1WyuPYZ0=";
|
||||||
};
|
};
|
||||||
TRACY = pkgs.fetchzip {
|
TRACY = pkgs.fetchzip {
|
||||||
url = "https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz";
|
url = "https://github.com/wolfpld/tracy/archive/6789e7d6f9a65ec98926b602097a33a9676d2606.tar.gz";
|
||||||
hash = "sha256-hbNGOsGeyGSvCJ2No8RkwOib1lX2on3vNZSzyVkZdXw=";
|
hash = "sha256-Xxyd7G/mnXEPpN+ehmwl0AkAhS3CwObpJNDgcqbdUJg=";
|
||||||
};
|
};
|
||||||
IMGUI = pkgs.fetchFromGitHub {
|
IMGUI = pkgs.fetchFromGitHub {
|
||||||
owner = "ocornut";
|
owner = "ocornut";
|
||||||
@@ -269,6 +269,12 @@
|
|||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
postFixup = lib.optionalString (!isDarwin) ''
|
||||||
|
patchelf \
|
||||||
|
--add-needed "${pkgs.vulkan-loader}/lib/libvulkan.so" \
|
||||||
|
$out/bin/dusklight
|
||||||
|
'';
|
||||||
|
|
||||||
dontStrip = true;
|
dontStrip = true;
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public:
|
|||||||
|
|
||||||
/* 0x14 */ cM3dGAab mM3dGAab;
|
/* 0x14 */ cM3dGAab mM3dGAab;
|
||||||
/* 0x30 */ cBgS_ShdwDraw_Callback mCallbackFun;
|
/* 0x30 */ cBgS_ShdwDraw_Callback mCallbackFun;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x34 */ int field_0x34;
|
/* 0x34 */ int field_0x34;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ public:
|
|||||||
/* 0x1370 */ Z2FxLineMgr mFxLineMgr;
|
/* 0x1370 */ Z2FxLineMgr mFxLineMgr;
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
/* 0x13BC */ Z2DebugSys mDebugSys;
|
/* 0x13BC */ Z2DebugSys mDebugSys;
|
||||||
|
#elif PARTIAL_DEBUG
|
||||||
|
alignas(Z2DebugSys) u8 mDebugSys[sizeof(Z2DebugSys)];
|
||||||
#endif
|
#endif
|
||||||
}; // Size: 0x138C
|
}; // Size: 0x138C
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ public:
|
|||||||
JAISoundHandle* getMainBgmHandle() { return &mMainBgmHandle; }
|
JAISoundHandle* getMainBgmHandle() { return &mMainBgmHandle; }
|
||||||
JAISoundHandle* getSubBgmHandle() { return &mSubBgmHandle; }
|
JAISoundHandle* getSubBgmHandle() { return &mSubBgmHandle; }
|
||||||
|
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
f32 field_0x00_debug;
|
f32 field_0x00_debug;
|
||||||
u8 field_0x04_debug;
|
u8 field_0x04_debug;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -100,13 +100,13 @@ public:
|
|||||||
bool isForceBattle() { return forceBattle_; }
|
bool isForceBattle() { return forceBattle_; }
|
||||||
JSUList<Z2CreatureEnemy>* getEnemyList() { return &field_0x0; }
|
JSUList<Z2CreatureEnemy>* getEnemyList() { return &field_0x0; }
|
||||||
|
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
JSUList<Z2SoundObjBase>* getAllList() { return &allList_; }
|
JSUList<Z2SoundObjBase>* getAllList() { return &allList_; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/* 0x00 */ JSUList<Z2CreatureEnemy> field_0x0;
|
/* 0x00 */ JSUList<Z2CreatureEnemy> field_0x0;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x0C */ JSUList<Z2SoundObjBase> allList_;
|
/* 0x0C */ JSUList<Z2SoundObjBase> allList_;
|
||||||
#endif
|
#endif
|
||||||
/* 0x0C */ Z2EnemyArea enemyArea_;
|
/* 0x0C */ Z2EnemyArea enemyArea_;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
struct Z2SoundStarter;
|
struct Z2SoundStarter;
|
||||||
|
|
||||||
class Z2SoundObjBase : public Z2SoundHandles
|
class Z2SoundObjBase : public Z2SoundHandles
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
, public JSULink<Z2SoundObjBase>
|
, public JSULink<Z2SoundObjBase>
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,9 +88,14 @@ public:
|
|||||||
/* 0x396A */ u8 field_0x396A[0x399E - 0x396A];
|
/* 0x396A */ u8 field_0x396A[0x399E - 0x396A];
|
||||||
/* 0x399E */ s16 field_0x399e;
|
/* 0x399E */ s16 field_0x399e;
|
||||||
/* 0x39A0 */ u8 field_0x39A0[0x39A4 - 0x39A0];
|
/* 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);
|
STATIC_ASSERT(sizeof(mant_class) == 0x39a4);
|
||||||
|
#endif
|
||||||
|
|
||||||
#endif /* D_A_MANT_H */
|
#endif /* D_A_MANT_H */
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ private:
|
|||||||
|
|
||||||
class dAttParam_c : public JORReflexible {
|
class dAttParam_c : public JORReflexible {
|
||||||
public:
|
public:
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x04 */ s8 mHIOChildNo;
|
/* 0x04 */ s8 mHIOChildNo;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ public:
|
|||||||
/* 0x35 */ u8 mAttnCursorDisappearFrames;
|
/* 0x35 */ u8 mAttnCursorDisappearFrames;
|
||||||
/* 0x38 */ f32 field_0x38;
|
/* 0x38 */ f32 field_0x38;
|
||||||
/* 0x3C */ f32 field_0x3c;
|
/* 0x3C */ f32 field_0x3c;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x44 */ s32 mDebugDispPosX;
|
/* 0x44 */ s32 mDebugDispPosX;
|
||||||
/* 0x48 */ s32 mDebugDispPosY;
|
/* 0x48 */ s32 mDebugDispPosY;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ private:
|
|||||||
/* 0x02C */ u32 m_flags;
|
/* 0x02C */ u32 m_flags;
|
||||||
/* 0x030 */ cXyz* pm_pos;
|
/* 0x030 */ cXyz* pm_pos;
|
||||||
/* 0x034 */ cXyz* pm_old_pos;
|
/* 0x034 */ cXyz* pm_old_pos;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x038 */ cXyz unk_0x38;
|
/* 0x038 */ cXyz unk_0x38;
|
||||||
#endif
|
#endif
|
||||||
/* 0x038 */ cXyz* pm_speed;
|
/* 0x038 */ cXyz* pm_speed;
|
||||||
@@ -229,7 +229,7 @@ private:
|
|||||||
/* 0x0CC */ f32 field_0xcc;
|
/* 0x0CC */ f32 field_0xcc;
|
||||||
/* 0x0D0 */ f32 m_wtr_chk_offset;
|
/* 0x0D0 */ f32 m_wtr_chk_offset;
|
||||||
/* 0x0D4 */ cBgS_PolyInfo* pm_out_poly_info;
|
/* 0x0D4 */ cBgS_PolyInfo* pm_out_poly_info;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x0E4 */ cXyz unk_0xe4;
|
/* 0x0E4 */ cXyz unk_0xe4;
|
||||||
#endif
|
#endif
|
||||||
/* 0x0D8 */ f32 field_0xd8;
|
/* 0x0D8 */ f32 field_0xd8;
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ public:
|
|||||||
|
|
||||||
// /* 0x0000 */ cCcS mCCcS;
|
// /* 0x0000 */ cCcS mCCcS;
|
||||||
/* 0x284C */ dCcMassS_Mng mMass_Mng;
|
/* 0x284C */ dCcMassS_Mng mMass_Mng;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x2AD0 */ u8 m_is_mass_all_timer;
|
/* 0x2AD0 */ u8 m_is_mass_all_timer;
|
||||||
#endif
|
#endif
|
||||||
}; // Size = 0x2AC4
|
}; // Size = 0x2AC4
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
#include "m_Do/m_Do_graphic.h"
|
#include "m_Do/m_Do_graphic.h"
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include "tracy/Tracy.hpp"
|
#include "dusk/profiling.hpp"
|
||||||
|
|
||||||
enum dComIfG_ButtonStatus {
|
enum dComIfG_ButtonStatus {
|
||||||
/* 0x00 */ BUTTON_STATUS_NONE,
|
/* 0x00 */ BUTTON_STATUS_NONE,
|
||||||
@@ -1037,7 +1037,7 @@ public:
|
|||||||
/* 0x1DE09 */ u8 field_0x1de09;
|
/* 0x1DE09 */ u8 field_0x1de09;
|
||||||
/* 0x1DE0A */ u8 field_0x1de0a;
|
/* 0x1DE0A */ u8 field_0x1de0a;
|
||||||
/* 0x1DE0B */ u8 mIsDebugMode;
|
/* 0x1DE0B */ u8 mIsDebugMode;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x1DE0C */ OSStopwatch mStopwatch;
|
/* 0x1DE0C */ OSStopwatch mStopwatch;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public:
|
|||||||
BASE_ROOM5,
|
BASE_ROOM5,
|
||||||
BASE_DEMO,
|
BASE_DEMO,
|
||||||
|
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
BASE_DEBUG,
|
BASE_DEBUG,
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ public:
|
|||||||
/* 0x09B8 */ DUNGEON_LIGHT dungeonlight[8];
|
/* 0x09B8 */ DUNGEON_LIGHT dungeonlight[8];
|
||||||
/* 0x0C18 */ BOSS_LIGHT field_0x0c18[8];
|
/* 0x0C18 */ BOSS_LIGHT field_0x0c18[8];
|
||||||
/* 0x0D58 */ BOSS_LIGHT field_0x0d58[6];
|
/* 0x0D58 */ BOSS_LIGHT field_0x0d58[6];
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x0E48 */ NAVYCHAN navy;
|
/* 0x0E48 */ NAVYCHAN navy;
|
||||||
/* 0x0E58 */ u8 field_0xe58[0xE68 - 0xE58]; // part of NAVYCHAN?
|
/* 0x0E58 */ u8 field_0xe58[0xE68 - 0xE58]; // part of NAVYCHAN?
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ private:
|
|||||||
bool mCursorInterpPrevAngular;
|
bool mCursorInterpPrevAngular;
|
||||||
bool mCursorInterpCurrAngular;
|
bool mCursorInterpCurrAngular;
|
||||||
bool mCursorInterpInit;
|
bool mCursorInterpInit;
|
||||||
|
bool mPointerTouchPressHoveredCurrent;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -360,7 +360,12 @@ inline void dMsgObject_demoMessageGroup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline bool dMsgObject_isTalkNowCheck() {
|
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;
|
return dMsgObject_getMsgObjectClass()->getStatus() == 1 ? false : true;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool dMsgObject_isKillMessageFlag() {
|
inline bool dMsgObject_isKillMessageFlag() {
|
||||||
@@ -497,7 +502,12 @@ inline void dMsgObject_onMsgSend() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline bool dMsgObject_isFukidashiCheck() {
|
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;
|
return dMsgObject_getMsgObjectClass()->getScrnDrawPtr() == NULL ? false : true;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void* dMsgObject_getTalkHeap() {
|
inline void* dMsgObject_getTalkHeap() {
|
||||||
|
|||||||
@@ -521,13 +521,13 @@ private:
|
|||||||
/* 0x019 */ u8 field_0x19;
|
/* 0x019 */ u8 field_0x19;
|
||||||
/* 0x01A */ u8 field_0x1a;
|
/* 0x01A */ u8 field_0x1a;
|
||||||
/* 0x01B */ u8 field_0x1b;
|
/* 0x01B */ u8 field_0x1b;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x01C */ dPa_simpleEcallBack field_0x1c[48];
|
/* 0x01C */ dPa_simpleEcallBack field_0x1c[48];
|
||||||
#else
|
#else
|
||||||
/* 0x01C */ dPa_simpleEcallBack field_0x1c[25];
|
/* 0x01C */ dPa_simpleEcallBack field_0x1c[25];
|
||||||
#endif
|
#endif
|
||||||
/* 0x210 */ level_c field_0x210;
|
/* 0x210 */ level_c field_0x210;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
u8 mSceneCount;
|
u8 mSceneCount;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ private:
|
|||||||
/* 0x18 */ JKRHeap* heap;
|
/* 0x18 */ JKRHeap* heap;
|
||||||
/* 0x1C */ JKRSolidHeap* mDataHeap;
|
/* 0x1C */ JKRSolidHeap* mDataHeap;
|
||||||
/* 0x20 */ void** mRes;
|
/* 0x20 */ void** mRes;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x24 */ int mSize;
|
/* 0x24 */ int mSize;
|
||||||
#endif
|
#endif
|
||||||
}; // Size: 0x24
|
}; // Size: 0x24
|
||||||
|
|||||||
+4
-1
@@ -1008,7 +1008,7 @@ public:
|
|||||||
|
|
||||||
static const int ZONE_MAX = 0x20;
|
static const int ZONE_MAX = 0x20;
|
||||||
|
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x000 */ u8 unk_0x0;
|
/* 0x000 */ u8 unk_0x0;
|
||||||
/* 0x001 */ char unk_0x1;
|
/* 0x001 */ char unk_0x1;
|
||||||
/* 0x000 */ u8 unk_0x2[0x48 - 0x2];
|
/* 0x000 */ u8 unk_0x2[0x48 - 0x2];
|
||||||
@@ -1029,6 +1029,9 @@ public:
|
|||||||
/* 0xF30 */ s64 mSaveTotalTime;
|
/* 0xF30 */ s64 mSaveTotalTime;
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
/* 0xF80 */ flagFile_c mFlagFile;
|
/* 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
|
#endif
|
||||||
}; // Size: 0xF38
|
}; // Size: 0xF38
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -538,7 +538,7 @@ public:
|
|||||||
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const = 0;
|
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const = 0;
|
||||||
/* vt[87] */ virtual void setDoor(stage_tgsc_class*) = 0;
|
/* vt[87] */ virtual void setDoor(stage_tgsc_class*) = 0;
|
||||||
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const = 0;
|
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const = 0;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
virtual void setUnit(void*) = 0;
|
virtual void setUnit(void*) = 0;
|
||||||
virtual void* getUnit() = 0;
|
virtual void* getUnit() = 0;
|
||||||
#endif
|
#endif
|
||||||
@@ -796,7 +796,7 @@ public:
|
|||||||
virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
||||||
virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
||||||
virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
virtual void setUnit(void* i_Unit) { mUnit = i_Unit; }
|
virtual void setUnit(void* i_Unit) { mUnit = i_Unit; }
|
||||||
virtual void* getUnit() { return mUnit; }
|
virtual void* getUnit() { return mUnit; }
|
||||||
#endif
|
#endif
|
||||||
@@ -845,7 +845,7 @@ public:
|
|||||||
/* 0x54 */ stage_tgsc_class* mDrTg;
|
/* 0x54 */ stage_tgsc_class* mDrTg;
|
||||||
/* 0x58 */ stage_tgsc_class* mDoor;
|
/* 0x58 */ stage_tgsc_class* mDoor;
|
||||||
/* 0x5C */ dStage_FloorInfo_c* mFloorInfo;
|
/* 0x5C */ dStage_FloorInfo_c* mFloorInfo;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x60 */ void* mUnit;
|
/* 0x60 */ void* mUnit;
|
||||||
#endif
|
#endif
|
||||||
/* 0x60 */ u16 mPlayerNum;
|
/* 0x60 */ u16 mPlayerNum;
|
||||||
@@ -990,7 +990,7 @@ public:
|
|||||||
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
/* 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[87] */ virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
||||||
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
virtual void setUnit(void* i_Unit) {
|
virtual void setUnit(void* i_Unit) {
|
||||||
UNUSED(i_Unit);
|
UNUSED(i_Unit);
|
||||||
OSReport("stage non unit list data !!\n");
|
OSReport("stage non unit list data !!\n");
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ public:
|
|||||||
private:
|
private:
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
/* 0x00 */ dVibTest_c mVibTest;
|
/* 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
|
#endif
|
||||||
|
|
||||||
class {
|
class {
|
||||||
|
|||||||
+69
-7
@@ -1,9 +1,11 @@
|
|||||||
#ifndef DUSK_CONFIG_HPP
|
#ifndef DUSK_CONFIG_HPP
|
||||||
#define DUSK_CONFIG_HPP
|
#define DUSK_CONFIG_HPP
|
||||||
|
|
||||||
|
#include <concepts>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include "nlohmann/json.hpp"
|
|
||||||
#include "config_var.hpp"
|
#include "config_var.hpp"
|
||||||
|
|
||||||
namespace dusk::config {
|
namespace dusk::config {
|
||||||
@@ -40,7 +42,7 @@ public:
|
|||||||
[[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0;
|
[[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
template<ConfigValue T>
|
template <ConfigValue T>
|
||||||
class ConfigImpl : public ConfigImplBase {
|
class ConfigImpl : public ConfigImplBase {
|
||||||
// Just downcasting the references...
|
// Just downcasting the references...
|
||||||
void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final {
|
void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final {
|
||||||
@@ -90,20 +92,28 @@ public:
|
|||||||
void Register(ConfigVarBase& configVar);
|
void Register(ConfigVarBase& configVar);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Indicate that all registrations have happened and everything should lock in.
|
* \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 FinishRegistration();
|
void unregister(ConfigVarBase& configVar);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Load config from the standard user preferences location.
|
* \brief Load config from the standard user preferences location.
|
||||||
*/
|
*/
|
||||||
void LoadFromUserPreferences();
|
void load_from_user_preferences();
|
||||||
void LoadFromFileName(const char* path);
|
void load_from_file_name(const char* path);
|
||||||
|
|
||||||
|
void load_arg_override(std::string_view name, std::string_view value);
|
||||||
|
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Save the config to file.
|
* \brief Save the config to file.
|
||||||
*/
|
*/
|
||||||
void Save();
|
void save();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Get a registered CVar by name.
|
* \brief Get a registered CVar by name.
|
||||||
@@ -124,6 +134,58 @@ void ClearAllActionBindings(int port);
|
|||||||
*/
|
*/
|
||||||
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
|
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Type-erased change callback. previousValue points at the value before the mutation
|
||||||
|
* (a `const T*` for a `ConfigVar<T>`) and is valid only for the duration of the call.
|
||||||
|
*/
|
||||||
|
using ChangeCallback = std::function<void(ConfigVarBase& cVar, const void* previousValue)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Token identifying a change subscription. 0 is never a valid token.
|
||||||
|
*/
|
||||||
|
using Subscription = u64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Subscribe to changes of the named CVar (registered or not yet registered).
|
||||||
|
*
|
||||||
|
* Fired synchronously on the mutating thread (in practice the game thread) whenever the CVar's
|
||||||
|
* effective value changes at runtime: setValue, override/speedrun setters and clears. Values
|
||||||
|
* applied by config load or launch arguments do *not* notify: loads happen during startup
|
||||||
|
* before the subsystems callbacks push values into are initialized, and each subsystem reads
|
||||||
|
* its initial value itself at its own init. Callbacks may mutate other CVars; a nested
|
||||||
|
* mutation of the same CVar applies but does not re-notify.
|
||||||
|
*/
|
||||||
|
Subscription subscribe(std::string_view name, ChangeCallback callback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Typed convenience overload: the callback receives the current and previous values.
|
||||||
|
*/
|
||||||
|
template <ConfigValue T, typename Callback>
|
||||||
|
requires std::invocable<Callback, const T&, const T&> Subscription subscribe(
|
||||||
|
ConfigVar<T>& cVar, Callback&& callback) {
|
||||||
|
return subscribe(cVar.getName(),
|
||||||
|
[&cVar, cb = std::forward<Callback>(callback)](ConfigVarBase&, const void* previousValue) {
|
||||||
|
cb(cVar.getValue(), *static_cast<const T*>(previousValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void unsubscribe(Subscription token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Register a CVar and attach a change callback in one step.
|
||||||
|
*
|
||||||
|
* Useful for pushing settings into external systems (e.g. aurora) from one place instead of
|
||||||
|
* every UI setter. The callback fires only for runtime changes (see subscribe); not when
|
||||||
|
* loaded from config or launch arguments.
|
||||||
|
*/
|
||||||
|
template <ConfigValue T, typename Callback>
|
||||||
|
requires std::invocable<Callback, const T&, const T&> Subscription Register(
|
||||||
|
ConfigVar<T>& cVar, Callback&& onChange) {
|
||||||
|
auto subscription = subscribe(cVar, std::forward<Callback>(onChange));
|
||||||
|
Register(static_cast<ConfigVarBase&>(cVar));
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
template <ConfigValue T>
|
template <ConfigValue T>
|
||||||
const ConfigImplBase* GetConfigImpl() {
|
const ConfigImplBase* GetConfigImpl() {
|
||||||
static ConfigImpl<T> config;
|
static ConfigImpl<T> config;
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
#define DUSK_CONFIG_VAR_HPP
|
#define DUSK_CONFIG_VAR_HPP
|
||||||
|
|
||||||
#include "dolphin/types.h"
|
#include "dolphin/types.h"
|
||||||
#include <type_traits>
|
#include <concepts>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The configuration system.
|
* The configuration system.
|
||||||
@@ -69,7 +71,7 @@ protected:
|
|||||||
/**
|
/**
|
||||||
* The name of this CVar, used in the configuration file.
|
* The name of this CVar, used in the configuration file.
|
||||||
*/
|
*/
|
||||||
const char* name;
|
std::string name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this CVar has been registered with the global managing logic.
|
* Whether this CVar has been registered with the global managing logic.
|
||||||
@@ -87,8 +89,8 @@ protected:
|
|||||||
*/
|
*/
|
||||||
const ConfigImplBase* impl;
|
const ConfigImplBase* impl;
|
||||||
|
|
||||||
ConfigVarBase(const char* name, const ConfigImplBase* impl);
|
ConfigVarBase(const ConfigVarBase&) = delete;
|
||||||
virtual ~ConfigVarBase() = default;
|
ConfigVarBase(std::string name, const ConfigImplBase* impl);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that the CVar is registered, aborting if this is not the case.
|
* Check that the CVar is registered, aborting if this is not the case.
|
||||||
@@ -98,7 +100,22 @@ protected:
|
|||||||
abort();
|
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:
|
public:
|
||||||
|
virtual ~ConfigVarBase();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name of this CVar, used in the configuration file.
|
* Get the name of this CVar, used in the configuration file.
|
||||||
*/
|
*/
|
||||||
@@ -121,6 +138,7 @@ public:
|
|||||||
* This is necessary to make it legal to access.
|
* This is necessary to make it legal to access.
|
||||||
*/
|
*/
|
||||||
void markRegistered();
|
void markRegistered();
|
||||||
|
void unmarkRegistered();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear a speedrun-mode override if one is active on this CVar.
|
* Clear a speedrun-mode override if one is active on this CVar.
|
||||||
@@ -155,6 +173,7 @@ template <typename T>
|
|||||||
concept ConfigValue =
|
concept ConfigValue =
|
||||||
!std::is_const_v<T>
|
!std::is_const_v<T>
|
||||||
&& !std::is_volatile_v<T>
|
&& !std::is_volatile_v<T>
|
||||||
|
&& std::equality_comparable<T>
|
||||||
&& (std::is_same_v<T, bool>
|
&& (std::is_same_v<T, bool>
|
||||||
|| ConfigValueInteger<T>
|
|| ConfigValueInteger<T>
|
||||||
|| std::is_same_v<T, f32>
|
|| std::is_same_v<T, f32>
|
||||||
@@ -166,6 +185,9 @@ concept ConfigValue =
|
|||||||
template <ConfigValue T>
|
template <ConfigValue T>
|
||||||
const ConfigImplBase* GetConfigImpl();
|
const ConfigImplBase* GetConfigImpl();
|
||||||
|
|
||||||
|
template <ConfigValue T>
|
||||||
|
class ConfigImpl;
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
struct ConfigEnumRange {
|
struct ConfigEnumRange {
|
||||||
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
|
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
|
||||||
@@ -192,10 +214,12 @@ public:
|
|||||||
* @param arg Arguments to forward to construct the default value.
|
* @param arg Arguments to forward to construct the default value.
|
||||||
*/
|
*/
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
ConfigVar(const char* name, Args&&... arg)
|
ConfigVar(std::string name, Args&&... arg)
|
||||||
: ConfigVarBase(name, GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
|
: ConfigVarBase(std::move(name), GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
|
||||||
value(), overrideValue() {}
|
value(), overrideValue() {}
|
||||||
|
|
||||||
|
ConfigVar(ConfigVar const&) = delete;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Get the current value of the CVar.
|
* \brief Get the current value of the CVar.
|
||||||
*
|
*
|
||||||
@@ -234,6 +258,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
void setValue(T newValue, bool replaceOverride = true) {
|
void setValue(T newValue, bool replaceOverride = true) {
|
||||||
checkRegistered();
|
checkRegistered();
|
||||||
|
const auto previous = previous_for_notify();
|
||||||
value = std::move(newValue);
|
value = std::move(newValue);
|
||||||
|
|
||||||
if (replaceOverride) {
|
if (replaceOverride) {
|
||||||
@@ -242,6 +267,7 @@ public:
|
|||||||
} else if (layer != ConfigVarLayer::Override) {
|
} else if (layer != ConfigVarLayer::Override) {
|
||||||
layer = ConfigVarLayer::Value;
|
layer = ConfigVarLayer::Value;
|
||||||
}
|
}
|
||||||
|
notify_if_changed(previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
operator const T&() {
|
operator const T&() {
|
||||||
@@ -258,8 +284,10 @@ public:
|
|||||||
*/
|
*/
|
||||||
void setOverrideValue(T newValue) {
|
void setOverrideValue(T newValue) {
|
||||||
checkRegistered();
|
checkRegistered();
|
||||||
|
const auto previous = previous_for_notify();
|
||||||
overrideValue = std::move(newValue);
|
overrideValue = std::move(newValue);
|
||||||
layer = ConfigVarLayer::Override;
|
layer = ConfigVarLayer::Override;
|
||||||
|
notify_if_changed(previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,25 +301,31 @@ public:
|
|||||||
void setSpeedrunValue(T newValue) {
|
void setSpeedrunValue(T newValue) {
|
||||||
checkRegistered();
|
checkRegistered();
|
||||||
if (layer != ConfigVarLayer::Override) {
|
if (layer != ConfigVarLayer::Override) {
|
||||||
|
const auto previous = previous_for_notify();
|
||||||
priorLayer = layer;
|
priorLayer = layer;
|
||||||
overrideValue = std::move(newValue);
|
overrideValue = std::move(newValue);
|
||||||
layer = ConfigVarLayer::Speedrun;
|
layer = ConfigVarLayer::Speedrun;
|
||||||
|
notify_if_changed(previous);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearOverride() {
|
void clearOverride() {
|
||||||
checkRegistered();
|
checkRegistered();
|
||||||
if (layer == ConfigVarLayer::Override) {
|
if (layer == ConfigVarLayer::Override) {
|
||||||
|
const auto previous = previous_for_notify();
|
||||||
overrideValue = {};
|
overrideValue = {};
|
||||||
layer = ConfigVarLayer::Value;
|
layer = ConfigVarLayer::Value;
|
||||||
|
notify_if_changed(previous);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSpeedrunOverride() override {
|
void clearSpeedrunOverride() override {
|
||||||
checkRegistered();
|
checkRegistered();
|
||||||
if (layer == ConfigVarLayer::Speedrun) {
|
if (layer == ConfigVarLayer::Speedrun) {
|
||||||
|
const auto previous = previous_for_notify();
|
||||||
overrideValue = {};
|
overrideValue = {};
|
||||||
layer = priorLayer;
|
layer = priorLayer;
|
||||||
|
notify_if_changed(previous);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,6 +339,48 @@ public:
|
|||||||
const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer;
|
const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer;
|
||||||
return effectiveLayer == ConfigVarLayer::Default ? defaultValue : value;
|
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>;
|
using ActionBindConfigVar = ConfigVar<int>;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
#include <dolphin/gx/GXAurora.h>
|
#include <dolphin/gx/GXAurora.h>
|
||||||
#include <dolphin/gx/GXExtra.h>
|
#include <dolphin/gx/GXExtra.h>
|
||||||
#include "tracy/Tracy.hpp"
|
|
||||||
|
#include "profiling.hpp"
|
||||||
|
|
||||||
#if DUSK_GFX_DEBUG_GROUPS
|
#if DUSK_GFX_DEBUG_GROUPS
|
||||||
#define GX_DEBUG_GROUP(name, ...) \
|
#define GX_DEBUG_GROUP(name, ...) \
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ class CPaneMgr;
|
|||||||
|
|
||||||
namespace dusk::menu_pointer {
|
namespace dusk::menu_pointer {
|
||||||
|
|
||||||
|
using TargetId = u16;
|
||||||
|
constexpr TargetId InvalidTarget = 0xffff;
|
||||||
|
|
||||||
enum class Context {
|
enum class Context {
|
||||||
None,
|
None,
|
||||||
FileSelect,
|
FileSelect,
|
||||||
@@ -43,12 +46,14 @@ bool active() noexcept;
|
|||||||
bool enabled() noexcept;
|
bool enabled() noexcept;
|
||||||
bool mouse_capture_active() noexcept;
|
bool mouse_capture_active() noexcept;
|
||||||
const State& state() noexcept;
|
const State& state() noexcept;
|
||||||
|
void set_hover_target(TargetId target) noexcept;
|
||||||
bool consume_click() noexcept;
|
bool consume_click() noexcept;
|
||||||
|
bool peek_click() noexcept;
|
||||||
void set_dialog_choice(u8 choice, bool clicked) noexcept;
|
void set_dialog_choice(u8 choice, bool clicked) noexcept;
|
||||||
bool get_dialog_choice(u8& choice) noexcept;
|
bool get_dialog_choice(u8& choice) noexcept;
|
||||||
bool consume_dialog_click(u8& choice) noexcept;
|
bool consume_dialog_click(u8& choice) noexcept;
|
||||||
void defer_activation(Context context, u8 target) noexcept;
|
void defer_activation(Context context, TargetId target) noexcept;
|
||||||
bool consume_deferred_activation(Context context, u8 target) noexcept;
|
bool consume_deferred_activation(Context context, TargetId target) noexcept;
|
||||||
void clear_deferred_activation(Context context) noexcept;
|
void clear_deferred_activation(Context context) noexcept;
|
||||||
u32 suppressed_pad_buttons(u32 port) noexcept;
|
u32 suppressed_pad_buttons(u32 port) noexcept;
|
||||||
void finish_pad_suppression_read(u32 port) noexcept;
|
void finish_pad_suppression_read(u32 port) noexcept;
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
|
#include <ranges>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "dusk/config.hpp"
|
||||||
|
#include "dusk/config_var.hpp"
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
namespace dusk::mods {
|
||||||
|
struct LoadedMod;
|
||||||
|
class ModBundle;
|
||||||
|
} // namespace dusk::mods
|
||||||
|
|
||||||
|
struct ModContext {
|
||||||
|
dusk::mods::LoadedMod* mod = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace dusk::mods::loader {
|
||||||
|
class NativeModule;
|
||||||
|
} // namespace dusk::mods::loader
|
||||||
|
|
||||||
|
namespace dusk::mods {
|
||||||
|
|
||||||
|
struct ModDependencyEdge {
|
||||||
|
LoadedMod* mod = nullptr;
|
||||||
|
bool required = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModManifestInfo {
|
||||||
|
struct Import {
|
||||||
|
std::string id;
|
||||||
|
uint16_t major = 0;
|
||||||
|
bool required = false;
|
||||||
|
bool operator==(const Import&) const = default;
|
||||||
|
};
|
||||||
|
struct Export {
|
||||||
|
std::string id;
|
||||||
|
uint16_t major = 0;
|
||||||
|
bool operator==(const Export&) const = default;
|
||||||
|
};
|
||||||
|
std::vector<Import> imports;
|
||||||
|
std::vector<Export> exports;
|
||||||
|
bool operator==(const ModManifestInfo&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModMetadata {
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
std::string version;
|
||||||
|
std::string author;
|
||||||
|
std::string description;
|
||||||
|
std::string iconPath;
|
||||||
|
std::string bannerPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ModSearchDir {
|
||||||
|
std::filesystem::path path;
|
||||||
|
// Directory bundles dlopen their native lib in place instead of extracting it to the cache.
|
||||||
|
// Required where extracted code cannot run (iOS), desirable for signed/read-only installs.
|
||||||
|
bool inPlaceNative = false;
|
||||||
|
// Native library location for platforms that restrict placement (e.g. iOS/tvOS Frameworks/)
|
||||||
|
std::filesystem::path nativeLibDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NativeMod {
|
||||||
|
std::unique_ptr<loader::NativeModule> handle;
|
||||||
|
const ModManifest* manifest = nullptr;
|
||||||
|
ModContext** contextSymbol = nullptr;
|
||||||
|
|
||||||
|
ModInitializeFn fn_initialize = nullptr;
|
||||||
|
ModUpdateFn fn_update = nullptr;
|
||||||
|
ModShutdownFn fn_shutdown = 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 ships native libraries, but none matches this build's platform and architecture.
|
||||||
|
*/
|
||||||
|
ModMissingPlatform,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mod is built for a different ABI version than this build of the game.
|
||||||
|
*/
|
||||||
|
ApiVersionMismatch,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mod is missing a required native API export.
|
||||||
|
*/
|
||||||
|
MissingExport,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unknown error loading the native mod.
|
||||||
|
*/
|
||||||
|
Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LoadedMod {
|
||||||
|
ModMetadata metadata;
|
||||||
|
std::string modPath;
|
||||||
|
std::string dir;
|
||||||
|
|
||||||
|
uint32_t searchDirIndex = 0;
|
||||||
|
// Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported.
|
||||||
|
bool inPlace = false;
|
||||||
|
|
||||||
|
std::unique_ptr<ConfigVar<bool> > cvarIsEnabled;
|
||||||
|
config::Subscription enabledSubscription = 0;
|
||||||
|
|
||||||
|
bool active = false;
|
||||||
|
bool loadFailed = false;
|
||||||
|
std::string failureReason;
|
||||||
|
|
||||||
|
// mod_initialize succeeded; a mod_shutdown is owed on deactivation.
|
||||||
|
bool initialized = false;
|
||||||
|
// Static service exports are currently present in the registry.
|
||||||
|
bool servicesRegistered = false;
|
||||||
|
// Lifecycle state last applied by the loader; diffed against cvarIsEnabled to pick up
|
||||||
|
// runtime enable/disable requests.
|
||||||
|
bool enabledApplied = false;
|
||||||
|
// Deactivated because a provider it imports from was disabled, not by its own cvar.
|
||||||
|
bool suspendedByProvider = false;
|
||||||
|
// Bumped per native lib extraction so every dlopen sees a fresh path (and thus a fresh
|
||||||
|
// image with fresh statics; a previous dlclose may not fully unmap). Also bumped by
|
||||||
|
// asset-only reloads, so it doubles as a generation for anything caching per-mod content.
|
||||||
|
uint32_t cacheGeneration = 0;
|
||||||
|
// Currently extracted native library, empty if none.
|
||||||
|
std::string nativePath;
|
||||||
|
|
||||||
|
NativeModStatus nativeStatus = NativeModStatus::None;
|
||||||
|
std::unique_ptr<NativeMod> native;
|
||||||
|
std::unique_ptr<ModContext> context;
|
||||||
|
|
||||||
|
// Shared with overlay file registrations so in-flight DVD reads survive disable/reload.
|
||||||
|
std::shared_ptr<ModBundle> bundle;
|
||||||
|
|
||||||
|
ModManifestInfo manifestInfo;
|
||||||
|
|
||||||
|
// Mods this mod imports services from, and mods importing services from this mod.
|
||||||
|
std::vector<ModDependencyEdge> dependencies;
|
||||||
|
std::vector<ModDependencyEdge> dependents;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ModLoader {
|
||||||
|
public:
|
||||||
|
static ModLoader& instance();
|
||||||
|
|
||||||
|
void set_search_dirs(std::vector<ModSearchDir> dirs) { m_searchDirs = std::move(dirs); }
|
||||||
|
void set_cache_dir(std::filesystem::path dir) { m_cacheDir = std::move(dir); }
|
||||||
|
void init();
|
||||||
|
void tick();
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
void request_enable(std::string_view id);
|
||||||
|
void request_disable(std::string_view id);
|
||||||
|
void request_reload(std::string_view id);
|
||||||
|
void notify_mod_failure(LoadedMod& mod, bool firstFailure);
|
||||||
|
|
||||||
|
[[nodiscard]] auto mods() const {
|
||||||
|
return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; });
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] auto active_mods() const {
|
||||||
|
return mods() | std::views::filter([](const auto& m) { return m.active; });
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class RequestKind : u8 { Enable, Disable, Reload };
|
||||||
|
struct Request {
|
||||||
|
std::string modId;
|
||||||
|
RequestKind kind;
|
||||||
|
};
|
||||||
|
// ModLoader::tick runs inside fapGm_Execute, so code from an unloading mod can still be
|
||||||
|
// live on the stack (its frame unwinds after the tick). dlclose is therefore deferred to
|
||||||
|
// the next tick, by which point every per-frame entry into the mod should have returned.
|
||||||
|
struct RetiredNative {
|
||||||
|
std::unique_ptr<NativeMod> native;
|
||||||
|
std::string path;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<LoadedMod> > m_mods;
|
||||||
|
std::vector<ModSearchDir> m_searchDirs;
|
||||||
|
std::filesystem::path m_cacheDir;
|
||||||
|
std::vector<Request> m_pendingRequests;
|
||||||
|
std::vector<std::string> m_pendingFailures;
|
||||||
|
std::vector<RetiredNative> m_retiredNatives;
|
||||||
|
bool m_initialized = false;
|
||||||
|
bool m_startupComplete = false;
|
||||||
|
|
||||||
|
void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex);
|
||||||
|
void load_native(LoadedMod& mod, const std::string& dllEntry);
|
||||||
|
// Resolved <nativeLibDir>/<mod id><ext> if it exists on disk, empty otherwise.
|
||||||
|
[[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const;
|
||||||
|
void unload_native(LoadedMod& mod);
|
||||||
|
// Registers exports (if needed), resolves imports and runs mod_initialize.
|
||||||
|
// Returns whether the mod ended up active; failures go through fail_mod.
|
||||||
|
bool activate_mod(LoadedMod& mod);
|
||||||
|
// Runs mod_shutdown (if needed), detaches the mod from every service, and unloads the
|
||||||
|
// native lib. Must only run with no mod code on the stack (startup, shutdown, or top of tick).
|
||||||
|
void deactivate_mod(LoadedMod& mod);
|
||||||
|
void init_services();
|
||||||
|
bool register_static_service_exports(LoadedMod& mod);
|
||||||
|
bool resolve_service_imports(LoadedMod& mod);
|
||||||
|
[[nodiscard]] std::string describe_missing_import(
|
||||||
|
const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const;
|
||||||
|
|
||||||
|
LoadedMod* find_mod(std::string_view id) const;
|
||||||
|
void drain_retired_natives();
|
||||||
|
void apply_pending_requests();
|
||||||
|
void flush_toasts();
|
||||||
|
void on_enabled_changed(LoadedMod& mod);
|
||||||
|
// Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the
|
||||||
|
// bundle from disk, then reactivates whatever the current cvar/provider state allows.
|
||||||
|
void apply_lifecycle_change(LoadedMod& target, bool reload);
|
||||||
|
// `target` plus transitive active/suspended dependents, in m_mods (init) order.
|
||||||
|
std::vector<LoadedMod*> collect_lifecycle_set(LoadedMod& target);
|
||||||
|
bool reload_bundle(LoadedMod& mod);
|
||||||
|
bool ensure_native_loaded(LoadedMod& mod);
|
||||||
|
};
|
||||||
|
|
||||||
|
using ModIndex = std::ranges::range_difference_t<decltype(std::declval<ModLoader>().mods())>;
|
||||||
|
|
||||||
|
} // namespace dusk::mods
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
namespace dusk::mouse {
|
namespace dusk::mouse {
|
||||||
void read();
|
void read();
|
||||||
void getAimDeltas(float& out_yaw, float& out_pitch);
|
void get_aim_deltas(float& out_yaw, float& out_pitch);
|
||||||
void getCameraDeltas(float& out_yaw, float& out_pitch);
|
void get_camera_deltas(float& out_yaw, float& out_pitch);
|
||||||
void handle_event(const SDL_Event& event) noexcept;
|
void handle_event(const SDL_Event& event) noexcept;
|
||||||
void onFocusLost();
|
void on_focus_lost();
|
||||||
void onFocusGained();
|
void on_focus_gained();
|
||||||
} // namespace dusk::mouse
|
} // namespace dusk::mouse
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if defined(__has_include)
|
||||||
|
#if __has_include(<tracy/Tracy.hpp>)
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ZoneScoped
|
||||||
|
#define ZoneScoped
|
||||||
|
#define ZoneScopedN(name)
|
||||||
|
#endif
|
||||||
+15
-2
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
namespace dusk {
|
namespace dusk {
|
||||||
|
|
||||||
using namespace config;
|
using config::ConfigVar;
|
||||||
|
using config::ActionBindConfigVar;
|
||||||
|
|
||||||
enum class BloomMode : int {
|
enum class BloomMode : int {
|
||||||
Off = 0,
|
Off = 0,
|
||||||
@@ -46,6 +47,12 @@ enum class FrameInterpMode : u8 {
|
|||||||
Unlimited = 2,
|
Unlimited = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class TouchTargeting : u8 {
|
||||||
|
Hybrid = 0,
|
||||||
|
Hold = 1,
|
||||||
|
Switch = 2,
|
||||||
|
};
|
||||||
|
|
||||||
enum class MenuScaling : u8 {
|
enum class MenuScaling : u8 {
|
||||||
GameCube = 0,
|
GameCube = 0,
|
||||||
Wii = 1,
|
Wii = 1,
|
||||||
@@ -97,6 +104,12 @@ struct ConfigEnumRange<FrameInterpMode> {
|
|||||||
static constexpr auto max = FrameInterpMode::Unlimited;
|
static constexpr auto max = FrameInterpMode::Unlimited;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct ConfigEnumRange<TouchTargeting> {
|
||||||
|
static constexpr auto min = TouchTargeting::Hybrid;
|
||||||
|
static constexpr auto max = TouchTargeting::Switch;
|
||||||
|
};
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
struct ConfigEnumRange<MenuScaling> {
|
struct ConfigEnumRange<MenuScaling> {
|
||||||
static constexpr auto min = MenuScaling::GameCube;
|
static constexpr auto min = MenuScaling::GameCube;
|
||||||
@@ -216,6 +229,7 @@ struct UserSettings {
|
|||||||
ConfigVar<bool> invertMouseY;
|
ConfigVar<bool> invertMouseY;
|
||||||
ConfigVar<bool> freeCamera;
|
ConfigVar<bool> freeCamera;
|
||||||
ConfigVar<bool> enableTouchControls;
|
ConfigVar<bool> enableTouchControls;
|
||||||
|
ConfigVar<TouchTargeting> touchTargeting;
|
||||||
ConfigVar<bool> enableMenuPointer;
|
ConfigVar<bool> enableMenuPointer;
|
||||||
ConfigVar<ui::ControlLayout> touchControlsLayout;
|
ConfigVar<ui::ControlLayout> touchControlsLayout;
|
||||||
ConfigVar<bool> invertCameraXAxis;
|
ConfigVar<bool> invertCameraXAxis;
|
||||||
@@ -275,7 +289,6 @@ struct UserSettings {
|
|||||||
ConfigVar<DiscVerificationState> isoVerification;
|
ConfigVar<DiscVerificationState> isoVerification;
|
||||||
ConfigVar<std::string> graphicsBackend;
|
ConfigVar<std::string> graphicsBackend;
|
||||||
ConfigVar<bool> skipPreLaunchUI;
|
ConfigVar<bool> skipPreLaunchUI;
|
||||||
ConfigVar<bool> showPipelineCompilation;
|
|
||||||
ConfigVar<bool> wasPresetChosen;
|
ConfigVar<bool> wasPresetChosen;
|
||||||
ConfigVar<bool> checkForUpdates;
|
ConfigVar<bool> checkForUpdates;
|
||||||
ConfigVar<int> cardFileType;
|
ConfigVar<int> cardFileType;
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
#ifndef DUSK_TEXTURE_REPLACEMENTS_HPP
|
#ifndef DUSK_TEXTURE_REPLACEMENTS_HPP
|
||||||
#define DUSK_TEXTURE_REPLACEMENTS_HPP
|
#define DUSK_TEXTURE_REPLACEMENTS_HPP
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace dusk::texture_replacements {
|
namespace dusk::texture_replacements {
|
||||||
|
|
||||||
|
// Mod replacements are prioritized *over* user replacements (<data folder>/texture_replacements/)
|
||||||
|
inline constexpr int32_t kUserTextureReplacementPriority = -1'000'000;
|
||||||
|
|
||||||
void reload();
|
void reload();
|
||||||
void set_enabled(bool enabled);
|
void set_enabled(bool enabled);
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ typedef struct node_create_request {
|
|||||||
/* 0x58 */ s16 name;
|
/* 0x58 */ s16 name;
|
||||||
/* 0x5C */ void* data;
|
/* 0x5C */ void* data;
|
||||||
/* 0x60 */ s16 unk_0x60;
|
/* 0x60 */ s16 unk_0x60;
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x64 */ int unk_0x64;
|
/* 0x64 */ int unk_0x64;
|
||||||
/* 0x68 */ int unk_0x68;
|
/* 0x68 */ int unk_0x68;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -114,6 +114,19 @@ inline int __builtin_clz(unsigned int v) {
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Data symbols exported from the main exe need dllimport on the mod side.
|
||||||
|
// DUSK_BUILDING_GAME is defined for the game build so the same headers work in both.
|
||||||
|
#if defined(TARGET_PC) && defined(_WIN32) && !defined(DUSK_BUILDING_GAME)
|
||||||
|
#define DUSK_GAME_EXTERN extern __declspec(dllimport)
|
||||||
|
#define DUSK_GAME_DATA __declspec(dllimport)
|
||||||
|
#elif defined(TARGET_PC) && defined(_WIN32) && defined(DUSK_BUILDING_GAME)
|
||||||
|
#define DUSK_GAME_EXTERN extern __declspec(dllexport)
|
||||||
|
#define DUSK_GAME_DATA __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define DUSK_GAME_EXTERN extern
|
||||||
|
#define DUSK_GAME_DATA
|
||||||
|
#endif
|
||||||
|
|
||||||
#define FAST_DIV(x, n) (x >> (n / 2))
|
#define FAST_DIV(x, n) (x >> (n / 2))
|
||||||
|
|
||||||
#define SQUARE(x) ((x) * (x))
|
#define SQUARE(x) ((x) * (x))
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ class mDoAud_zelAudio_c : public Z2AudioMgr {
|
|||||||
public:
|
public:
|
||||||
void reset();
|
void reset();
|
||||||
mDoAud_zelAudio_c() {
|
mDoAud_zelAudio_c() {
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
setMode(2);
|
setMode(2);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
~mDoAud_zelAudio_c() {}
|
~mDoAud_zelAudio_c() {}
|
||||||
|
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
u8 getMode() { return field_0x13bd; }
|
u8 getMode() { return field_0x13bd; }
|
||||||
void setMode(u8 mode) { field_0x13bd = mode; }
|
void setMode(u8 mode) { field_0x13bd = mode; }
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ public:
|
|||||||
/* 0x4 */ s8 mNo;
|
/* 0x4 */ s8 mNo;
|
||||||
/* 0x5 */ u8 mCount;
|
/* 0x5 */ u8 mCount;
|
||||||
#else
|
#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() {}
|
virtual ~mDoHIO_entry_c() {}
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
extern "C" {
|
||||||
|
#else
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define MOD_EXPORT __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define MOD_EXPORT __attribute__((visibility("default")))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#define MOD_EXTERN_C extern "C"
|
||||||
|
#else
|
||||||
|
#define MOD_EXTERN_C
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define MOD_ABI_VERSION 5u
|
||||||
|
#define MOD_ERROR_MESSAGE_SIZE 512u
|
||||||
|
|
||||||
|
typedef struct ModContext ModContext;
|
||||||
|
|
||||||
|
typedef enum ModResult {
|
||||||
|
MOD_OK = 0,
|
||||||
|
MOD_ERROR = 1,
|
||||||
|
MOD_UNAVAILABLE = 2,
|
||||||
|
MOD_UNSUPPORTED = 3,
|
||||||
|
MOD_CONFLICT = 4,
|
||||||
|
MOD_INVALID_ARGUMENT = 5,
|
||||||
|
} ModResult;
|
||||||
|
|
||||||
|
static_assert(sizeof(ModResult) == 4, "mod SDK enums must be int-sized; do not build mods with -fshort-enums");
|
||||||
|
|
||||||
|
typedef struct ModError {
|
||||||
|
uint32_t struct_size;
|
||||||
|
ModResult code;
|
||||||
|
char message[MOD_ERROR_MESSAGE_SIZE];
|
||||||
|
} ModError;
|
||||||
|
|
||||||
|
#define MOD_ERROR_INIT {sizeof(ModError), MOD_OK, {0}}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Opaque per-mod context, populated by the host before mod_initialize is called.
|
||||||
|
* Pass it as the first argument to every service call; it identifies the calling
|
||||||
|
* mod for attribution (logging, resource lookup, hook ownership, etc.).
|
||||||
|
*/
|
||||||
|
MOD_EXPORT extern ModContext* mod_ctx;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Service versioning contract:
|
||||||
|
*
|
||||||
|
* A service is a struct of function pointers, beginning with a ServiceHeader.
|
||||||
|
* Compatibility is tracked with a major/minor version pair:
|
||||||
|
*
|
||||||
|
* - A major version bump is a breaking change. Different majors are distinct
|
||||||
|
* services; the registry never matches an import against a different major.
|
||||||
|
* - A minor version bump may only append fields to the end of the struct.
|
||||||
|
* Existing fields must keep their offsets and semantics.
|
||||||
|
*
|
||||||
|
* Providers: exporting minor N means every function pointer introduced at or
|
||||||
|
* below N is populated (non-NULL). struct_size reflects the compiled struct.
|
||||||
|
*
|
||||||
|
* Importers: importing with min_minor_version N guarantees (enforced at load
|
||||||
|
* time) that the resolved service is at least minor N, so any field introduced
|
||||||
|
* at or below N may be used unconditionally, with no availability checks.
|
||||||
|
* Fields newer than the declared min_minor_version must be gated behind
|
||||||
|
* SERVICE_HAS plus a NULL check on the pointer itself.
|
||||||
|
*
|
||||||
|
* Load ordering: a manifest import of another mod's service (required or
|
||||||
|
* optional) guarantees that the provider's mod_initialize completed before the
|
||||||
|
* importer's runs, and deferred services published during the provider's
|
||||||
|
* initialization resolve into import slots just like static exports. If a
|
||||||
|
* provider fails to load, mods that required its services fail in turn. Mods
|
||||||
|
* whose required imports form a cycle all fail to load; a cycle involving an
|
||||||
|
* optional import is broken by dropping the ordering guarantee (not the
|
||||||
|
* resolution) of that optional import. Dynamic lookups via
|
||||||
|
* HostService::get_service carry no ordering guarantee: they see whatever has
|
||||||
|
* been published at call time.
|
||||||
|
*/
|
||||||
|
typedef struct ServiceHeader {
|
||||||
|
uint32_t struct_size;
|
||||||
|
uint16_t major_version;
|
||||||
|
uint16_t minor_version;
|
||||||
|
} ServiceHeader;
|
||||||
|
|
||||||
|
#define SERVICE_HEADER(service_type, major, minor) {sizeof(service_type), (major), (minor)}
|
||||||
|
|
||||||
|
#define SERVICE_HAS(service, service_type, field) \
|
||||||
|
((service) != NULL && \
|
||||||
|
(service)->header.struct_size >= \
|
||||||
|
(uint32_t)(offsetof(service_type, field) + sizeof(((service_type*)0)->field)))
|
||||||
|
|
||||||
|
typedef enum ServiceImportFlags {
|
||||||
|
SERVICE_IMPORT_REQUIRED = 0u,
|
||||||
|
SERVICE_IMPORT_OPTIONAL = 1u << 0u,
|
||||||
|
} ServiceImportFlags;
|
||||||
|
|
||||||
|
typedef enum ServiceExportFlags {
|
||||||
|
SERVICE_EXPORT_STATIC = 0u,
|
||||||
|
SERVICE_EXPORT_DEFERRED = 1u << 0u,
|
||||||
|
} ServiceExportFlags;
|
||||||
|
|
||||||
|
typedef struct ServiceImport {
|
||||||
|
uint32_t struct_size;
|
||||||
|
const char* service_id;
|
||||||
|
uint16_t major_version;
|
||||||
|
uint16_t min_minor_version;
|
||||||
|
uint32_t flags;
|
||||||
|
void* slot;
|
||||||
|
} ServiceImport;
|
||||||
|
|
||||||
|
typedef struct ServiceExport {
|
||||||
|
uint32_t struct_size;
|
||||||
|
const char* service_id;
|
||||||
|
uint16_t major_version;
|
||||||
|
uint16_t minor_version;
|
||||||
|
uint32_t flags;
|
||||||
|
const void* service;
|
||||||
|
} ServiceExport;
|
||||||
|
|
||||||
|
typedef struct ModManifest {
|
||||||
|
uint32_t struct_size;
|
||||||
|
uint32_t abi_version;
|
||||||
|
const ServiceImport* imports;
|
||||||
|
size_t import_count;
|
||||||
|
const ServiceExport* exports;
|
||||||
|
size_t export_count;
|
||||||
|
} ModManifest;
|
||||||
|
|
||||||
|
typedef const ModManifest* (*ModGetManifestFn)(void);
|
||||||
|
|
||||||
|
typedef ModResult (*ModInitializeFn)(ModError* out_error);
|
||||||
|
typedef ModResult (*ModUpdateFn)(ModError* out_error);
|
||||||
|
typedef ModResult (*ModShutdownFn)(ModError* out_error);
|
||||||
|
|
||||||
|
MOD_EXPORT const ModManifest* mod_get_manifest(void);
|
||||||
|
|
||||||
|
MOD_EXPORT ModResult mod_initialize(ModError* out_error);
|
||||||
|
MOD_EXPORT ModResult mod_update(ModError* out_error);
|
||||||
|
MOD_EXPORT ModResult mod_shutdown(ModError* out_error);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,464 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/svc/hook.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace dusk::mods {
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
T arg(void* argsRaw, int n) noexcept {
|
||||||
|
void** args = static_cast<void**>(argsRaw);
|
||||||
|
return *static_cast<std::add_pointer_t<std::remove_reference_t<T> > >(args[n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
|
||||||
|
void** args = static_cast<void**>(argsRaw);
|
||||||
|
return *static_cast<std::add_pointer_t<std::remove_reference_t<T> > >(args[n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class F>
|
||||||
|
void* mfp_addr(F fn) noexcept {
|
||||||
|
void* p = nullptr;
|
||||||
|
static_assert(sizeof(fn) >= sizeof(void*), "unexpected function pointer size");
|
||||||
|
std::memcpy(&p, &fn, sizeof(void*));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A string usable as a template argument: carries the hook target's symbol name and
|
||||||
|
* makes each NamedHook instantiation's static state unique. */
|
||||||
|
template <size_t N>
|
||||||
|
struct FixedString {
|
||||||
|
char chars[N]{};
|
||||||
|
constexpr FixedString(const char (&s)[N]) noexcept {
|
||||||
|
for (size_t i = 0; i < N; ++i) {
|
||||||
|
chars[i] = s[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
constexpr std::string_view class_name() {
|
||||||
|
#if defined(__clang__) || defined(__GNUC__)
|
||||||
|
// "... class_name() [T = daAlink_c]" / "... [with T = daAlink_c; ...]"
|
||||||
|
constexpr std::string_view fn = __PRETTY_FUNCTION__;
|
||||||
|
constexpr size_t start = fn.find("T = ") + 4;
|
||||||
|
return fn.substr(start, fn.find_first_of(";]", start) - start);
|
||||||
|
#elif defined(_MSC_VER)
|
||||||
|
// "... class_name<class daAlink_c>(void)"
|
||||||
|
constexpr std::string_view fn = __FUNCSIG__;
|
||||||
|
constexpr size_t start = fn.find("class_name<") + 11;
|
||||||
|
constexpr std::string_view name = fn.substr(start, fn.rfind(">(") - start);
|
||||||
|
if constexpr (name.starts_with("class ")) {
|
||||||
|
return name.substr(6);
|
||||||
|
} else if constexpr (name.starts_with("struct ")) {
|
||||||
|
return name.substr(7);
|
||||||
|
} else {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
#error "unsupported compiler"
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The manifest name of C's vtable. Only unscoped, non-template class names are
|
||||||
|
* supported (an empty result fails resolution and the install reports it). */
|
||||||
|
template <class C>
|
||||||
|
constexpr auto vtable_symbol() {
|
||||||
|
constexpr std::string_view name = class_name<C>();
|
||||||
|
constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos;
|
||||||
|
// "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated
|
||||||
|
std::array<char, name.size() + 12> out{};
|
||||||
|
if constexpr (!simple) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
size_t n = 0;
|
||||||
|
#if defined(_WIN32)
|
||||||
|
for (char c : {'?', '?', '_', '7'}) {
|
||||||
|
out[n++] = c;
|
||||||
|
}
|
||||||
|
for (char c : name) {
|
||||||
|
out[n++] = c;
|
||||||
|
}
|
||||||
|
for (char c : {'@', '@', '6', 'B', '@'}) {
|
||||||
|
out[n++] = c;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
for (char c : {'_', 'Z', 'T', 'V'}) {
|
||||||
|
out[n++] = c;
|
||||||
|
}
|
||||||
|
size_t len = name.size();
|
||||||
|
char digits[8]{};
|
||||||
|
size_t d = 0;
|
||||||
|
while (len != 0) {
|
||||||
|
digits[d++] = static_cast<char>('0' + len % 10);
|
||||||
|
len /= 10;
|
||||||
|
}
|
||||||
|
while (d != 0) {
|
||||||
|
out[n++] = digits[--d];
|
||||||
|
}
|
||||||
|
for (char c : name) {
|
||||||
|
out[n++] = c;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at.
|
||||||
|
* Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */
|
||||||
|
inline size_t vcall_slot_offset(const void*& fn) noexcept {
|
||||||
|
constexpr size_t npos = static_cast<size_t>(-1);
|
||||||
|
#if defined(_M_X64) || defined(__x86_64__)
|
||||||
|
const auto* p = static_cast<const uint8_t*>(fn);
|
||||||
|
for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs
|
||||||
|
int32_t rel;
|
||||||
|
std::memcpy(&rel, p + 1, 4);
|
||||||
|
p += 5 + rel;
|
||||||
|
}
|
||||||
|
fn = p;
|
||||||
|
// The vptr load. Unoptimized clang-cl thunks spill/reload rcx first
|
||||||
|
// (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window.
|
||||||
|
const uint8_t* q = nullptr;
|
||||||
|
for (int i = 0; i <= 12; ++i) {
|
||||||
|
if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx]
|
||||||
|
q = p + i + 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (q == nullptr) {
|
||||||
|
return npos;
|
||||||
|
}
|
||||||
|
if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8]
|
||||||
|
return static_cast<int8_t>(q[2]);
|
||||||
|
}
|
||||||
|
if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32]
|
||||||
|
int32_t off;
|
||||||
|
std::memcpy(&off, q + 2, 4);
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
// clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax
|
||||||
|
// distinguishes the thunk from an ordinary getter that begins the same way.
|
||||||
|
if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) {
|
||||||
|
size_t off = 0;
|
||||||
|
const uint8_t* r = q + 3;
|
||||||
|
if (q[2] == 0x40) {
|
||||||
|
off = static_cast<int8_t>(q[3]);
|
||||||
|
r = q + 4;
|
||||||
|
} else if (q[2] == 0x80) {
|
||||||
|
int32_t off32;
|
||||||
|
std::memcpy(&off32, q + 3, 4);
|
||||||
|
off = off32;
|
||||||
|
r = q + 7;
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= 8; ++i) {
|
||||||
|
if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional)
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return npos;
|
||||||
|
#elif defined(_M_ARM64) || defined(__aarch64__)
|
||||||
|
const auto* p = static_cast<const uint8_t*>(fn);
|
||||||
|
uint32_t insn[3];
|
||||||
|
for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs
|
||||||
|
std::memcpy(insn, p, 4);
|
||||||
|
if ((insn[0] & 0xFC000000u) != 0x14000000u) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const auto imm26 = static_cast<int32_t>(insn[0] << 6) >> 6;
|
||||||
|
p += static_cast<intptr_t>(imm26) * 4;
|
||||||
|
}
|
||||||
|
fn = p;
|
||||||
|
std::memcpy(insn, p, 12);
|
||||||
|
// ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs
|
||||||
|
if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) {
|
||||||
|
return npos;
|
||||||
|
}
|
||||||
|
const uint32_t t = insn[0] & 0x1Fu;
|
||||||
|
if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) {
|
||||||
|
return npos;
|
||||||
|
}
|
||||||
|
const uint32_t s = insn[1] & 0x1Fu;
|
||||||
|
if (insn[2] != (0xD61F0000u | (s << 5))) {
|
||||||
|
return npos;
|
||||||
|
}
|
||||||
|
return ((insn[1] >> 10) & 0xFFFu) * 8;
|
||||||
|
#else
|
||||||
|
(void)fn;
|
||||||
|
return npos;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Code address of the member function a mfp designates. Virtual mfps don't carry
|
||||||
|
* one; recover it from the class's vtable (resolved from the symbol manifest), so
|
||||||
|
* Hook works uniformly on virtual and non-virtual members. */
|
||||||
|
template <class C, class F>
|
||||||
|
ModResult member_target(const HookService* hooks, F mfp, void** out) {
|
||||||
|
*out = nullptr;
|
||||||
|
uintptr_t words[sizeof(F) > sizeof(uintptr_t) ? 2 : 1] = {};
|
||||||
|
std::memcpy(words, &mfp, sizeof(words) < sizeof(F) ? sizeof(words) : sizeof(F));
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const void* fn = reinterpret_cast<const void*>(words[0]);
|
||||||
|
const size_t slot = vcall_slot_offset(fn);
|
||||||
|
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
|
||||||
|
*out = const_cast<void*>(fn);
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
void* vtable = nullptr;
|
||||||
|
const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol<C>().data(), &vtable, nullptr);
|
||||||
|
if (resolved != MOD_OK) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
// ??_7 points at the first slot.
|
||||||
|
*out = *reinterpret_cast<void**>(static_cast<char*>(vtable) + slot);
|
||||||
|
#else
|
||||||
|
#if defined(__aarch64__) || defined(__arm__)
|
||||||
|
// AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function
|
||||||
|
// addresses can't spare their low bit), and ptr holds the slot offset directly.
|
||||||
|
const bool isVirtual = (words[1] & 1) != 0;
|
||||||
|
const uintptr_t thisAdjust = words[1] >> 1;
|
||||||
|
const uintptr_t slotOffset = words[0];
|
||||||
|
#else
|
||||||
|
// Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1.
|
||||||
|
const bool isVirtual = (words[0] & 1) != 0;
|
||||||
|
const uintptr_t thisAdjust = words[1];
|
||||||
|
const uintptr_t slotOffset = words[0] - 1;
|
||||||
|
#endif
|
||||||
|
if (!isVirtual) { // non-virtual: the address itself
|
||||||
|
*out = reinterpret_cast<void*>(words[0]);
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
if (thisAdjust != 0) {
|
||||||
|
// this-adjusting mfp (member of a secondary base): the slot offset is
|
||||||
|
// relative to a vtable we can't locate. Hook the overrider by name instead.
|
||||||
|
return MOD_UNSUPPORTED;
|
||||||
|
}
|
||||||
|
void* vtable = nullptr;
|
||||||
|
const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol<C>().data(), &vtable, nullptr);
|
||||||
|
if (resolved != MOD_OK) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
// _ZTV points at the offset-to-top slot; the address point mfps index from is
|
||||||
|
// two pointers in (past offset-to-top and the typeinfo pointer).
|
||||||
|
*out = *reinterpret_cast<void**>(static_cast<char*>(vtable) + 2 * sizeof(void*) + slotOffset);
|
||||||
|
#endif
|
||||||
|
return *out != nullptr ? MOD_OK : MOD_UNAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
/* Trampoline generator + per-target state shared by Hook and NamedHook. Tag makes
|
||||||
|
* each hooked target's statics distinct; the target address is filled in at install. */
|
||||||
|
template <class Tag, class R, class... A>
|
||||||
|
struct HookImpl {
|
||||||
|
static inline R (*g_orig)(A...) = nullptr;
|
||||||
|
static inline const HookService* hooks = nullptr;
|
||||||
|
static inline void* target = nullptr;
|
||||||
|
|
||||||
|
static bool dispatch_pre(void* args, void* retval) {
|
||||||
|
if (hooks == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int skipOriginal = 0;
|
||||||
|
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
|
||||||
|
return result == MOD_OK && skipOriginal != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dispatch_post(void* args, void* retval) {
|
||||||
|
if (hooks != nullptr) {
|
||||||
|
hooks->dispatch_post(mod_ctx, target, args, retval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static R trampoline(A... args) {
|
||||||
|
if constexpr (sizeof...(A) == 0) {
|
||||||
|
if constexpr (std::is_void_v<R>) {
|
||||||
|
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
|
||||||
|
if (!skipOriginal) {
|
||||||
|
g_orig(args...);
|
||||||
|
}
|
||||||
|
dispatch_post(nullptr, nullptr);
|
||||||
|
} else {
|
||||||
|
R result{};
|
||||||
|
const bool skipOriginal =
|
||||||
|
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
|
||||||
|
if (!skipOriginal) {
|
||||||
|
result = g_orig(args...);
|
||||||
|
}
|
||||||
|
dispatch_post(nullptr, static_cast<void*>(std::addressof(result)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
|
||||||
|
if constexpr (std::is_void_v<R>) {
|
||||||
|
const bool skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
|
||||||
|
if (!skipOriginal) {
|
||||||
|
g_orig(args...);
|
||||||
|
}
|
||||||
|
dispatch_post(static_cast<void*>(ptrs), nullptr);
|
||||||
|
} else {
|
||||||
|
R result{};
|
||||||
|
const bool skipOriginal = dispatch_pre(
|
||||||
|
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||||
|
if (!skipOriginal) {
|
||||||
|
result = g_orig(args...);
|
||||||
|
}
|
||||||
|
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
template <auto Target>
|
||||||
|
using TargetTag = std::integral_constant<decltype(Target), Target>;
|
||||||
|
template <FixedString Name>
|
||||||
|
struct NameTag {};
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Typed hook on a function named at compile time (&daAlink_c::execute, &free_fn).
|
||||||
|
* Member functions may be virtual: the install decodes the member function pointer and hooks the
|
||||||
|
* class's own overrider.
|
||||||
|
*/
|
||||||
|
template <auto Target>
|
||||||
|
struct Hook;
|
||||||
|
|
||||||
|
template <class C, class R, class... A, R (C::*Target)(A...)>
|
||||||
|
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {
|
||||||
|
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||||
|
return detail::member_target<C>(hooks, Target, out);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class C, class R, class... A, R (C::*Target)(A...) const>
|
||||||
|
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {
|
||||||
|
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||||
|
return detail::member_target<C>(hooks, Target, out);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class R, class... A, R (*Target)(A...)>
|
||||||
|
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {
|
||||||
|
static ModResult resolve_target(const HookService*, void** out) {
|
||||||
|
*out = mfp_addr(Target);
|
||||||
|
return MOD_OK;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Typed hook on a function by its symbol name, for targets you can't name in C++: file-local
|
||||||
|
* statics, private members, or symbols without a header. The signature is written free-style with
|
||||||
|
* the receiver first and is *not* compiler-checked.
|
||||||
|
*
|
||||||
|
* using HookshotHit = dusk::mods::NamedHook<
|
||||||
|
* "daAlink_hookshotAtHitCallBack",
|
||||||
|
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>;
|
||||||
|
* dusk::mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit);
|
||||||
|
*/
|
||||||
|
template <FixedString Name, class Sig>
|
||||||
|
struct NamedHook;
|
||||||
|
|
||||||
|
template <FixedString Name, class R, class... A>
|
||||||
|
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, R, A...> {
|
||||||
|
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||||
|
HookSymbolFlags flags{};
|
||||||
|
const ModResult resolved = hooks->resolve(mod_ctx, Name.chars, out, &flags);
|
||||||
|
if (resolved == MOD_OK && (flags & HOOK_SYMBOL_CODE) == 0) {
|
||||||
|
*out = nullptr;
|
||||||
|
return MOD_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class Entry>
|
||||||
|
ModResult hook_install(const HookService* hooks) {
|
||||||
|
if (hooks == nullptr) {
|
||||||
|
return MOD_UNAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry::hooks = hooks;
|
||||||
|
if (Entry::target == nullptr) {
|
||||||
|
const ModResult resolved = Entry::resolve_target(hooks, &Entry::target);
|
||||||
|
if (resolved != MOD_OK) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
|
||||||
|
reinterpret_cast<void**>(&Entry::g_orig));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <auto Target>
|
||||||
|
ModResult hook_install(const HookService* hooks) {
|
||||||
|
return hook_install<Hook<Target> >(hooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Entry>
|
||||||
|
ModResult hook_add_pre(
|
||||||
|
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||||
|
const ModResult installed = hook_install<Entry>(hooks);
|
||||||
|
if (installed != MOD_OK) {
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <auto Target>
|
||||||
|
ModResult hook_add_pre(
|
||||||
|
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||||
|
return hook_add_pre<Hook<Target> >(hooks, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Entry>
|
||||||
|
ModResult hook_add_post(
|
||||||
|
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||||
|
const ModResult installed = hook_install<Entry>(hooks);
|
||||||
|
if (installed != MOD_OK) {
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooks->add_post(mod_ctx, Entry::target, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <auto Target>
|
||||||
|
ModResult hook_add_post(
|
||||||
|
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||||
|
return hook_add_post<Hook<Target> >(hooks, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Entry>
|
||||||
|
ModResult hook_replace(
|
||||||
|
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||||
|
const ModResult installed = hook_install<Entry>(hooks);
|
||||||
|
if (installed != MOD_OK) {
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hooks->replace(mod_ctx, Entry::target, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <auto Target>
|
||||||
|
ModResult hook_replace(
|
||||||
|
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||||
|
return hook_replace<Hook<Target> >(hooks, callback, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dusk::mods
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dusk::mods {
|
||||||
|
|
||||||
|
template <class Service>
|
||||||
|
struct ServiceTraits;
|
||||||
|
|
||||||
|
namespace detail {
|
||||||
|
|
||||||
|
inline std::vector<ServiceImport>& imports() {
|
||||||
|
static std::vector<ServiceImport> entries;
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<ServiceExport>& exports() {
|
||||||
|
static std::vector<ServiceExport> entries;
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int register_import(ServiceImport entry) {
|
||||||
|
imports().push_back(entry);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int register_export(ServiceExport entry) {
|
||||||
|
exports().push_back(entry);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const ModManifest* manifest() {
|
||||||
|
static ModManifest manifest{
|
||||||
|
sizeof(ModManifest),
|
||||||
|
MOD_ABI_VERSION,
|
||||||
|
nullptr,
|
||||||
|
0,
|
||||||
|
nullptr,
|
||||||
|
0,
|
||||||
|
};
|
||||||
|
|
||||||
|
auto& importEntries = imports();
|
||||||
|
auto& exportEntries = exports();
|
||||||
|
manifest.imports = importEntries.data();
|
||||||
|
manifest.import_count = importEntries.size();
|
||||||
|
manifest.exports = exportEntries.data();
|
||||||
|
manifest.export_count = exportEntries.size();
|
||||||
|
return &manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
inline ModResult set_error(ModError* outError, ModResult code, const char* message) {
|
||||||
|
if (outError != nullptr && outError->struct_size >= sizeof(ModError)) {
|
||||||
|
outError->code = code;
|
||||||
|
outError->message[0] = '\0';
|
||||||
|
if (message != nullptr) {
|
||||||
|
std::snprintf(outError->message, sizeof(outError->message), "%s", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dusk::mods
|
||||||
|
|
||||||
|
#define DEFINE_MOD() \
|
||||||
|
extern "C" { \
|
||||||
|
MOD_EXPORT ModContext* mod_ctx = nullptr; \
|
||||||
|
MOD_EXPORT const ModManifest* mod_get_manifest(void) { \
|
||||||
|
return ::dusk::mods::detail::manifest(); \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declares `static const service_type* variable`, filled in by the host before mod_initialize.
|
||||||
|
// Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports
|
||||||
|
// must be checked against nullptr before use.
|
||||||
|
#define IMPORT_SERVICE_EX( \
|
||||||
|
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
|
||||||
|
static const service_type* variable = nullptr; \
|
||||||
|
[[maybe_unused]] static const int mod_import_registration_##variable = \
|
||||||
|
::dusk::mods::detail::register_import(ServiceImport{ \
|
||||||
|
sizeof(ServiceImport), \
|
||||||
|
(service_id_value), \
|
||||||
|
static_cast<uint16_t>(major_value), \
|
||||||
|
static_cast<uint16_t>(min_minor_value), \
|
||||||
|
static_cast<uint32_t>(flags_value), \
|
||||||
|
&(variable), \
|
||||||
|
})
|
||||||
|
|
||||||
|
#define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \
|
||||||
|
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
|
||||||
|
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
|
||||||
|
SERVICE_IMPORT_REQUIRED)
|
||||||
|
|
||||||
|
#define IMPORT_SERVICE(service_type, variable) IMPORT_SERVICE_VERSION(service_type, variable, 0)
|
||||||
|
|
||||||
|
#define IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, min_minor_value) \
|
||||||
|
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
|
||||||
|
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
|
||||||
|
SERVICE_IMPORT_OPTIONAL)
|
||||||
|
|
||||||
|
#define IMPORT_OPTIONAL_SERVICE(service_type, variable) \
|
||||||
|
IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, 0)
|
||||||
|
|
||||||
|
#define EXPORT_SERVICE_AS(instance, service_id_value) \
|
||||||
|
namespace { \
|
||||||
|
const int mod_export_registration_##instance = \
|
||||||
|
::dusk::mods::detail::register_export(ServiceExport{ \
|
||||||
|
sizeof(ServiceExport), \
|
||||||
|
(service_id_value), \
|
||||||
|
(instance).header.major_version, \
|
||||||
|
(instance).header.minor_version, \
|
||||||
|
SERVICE_EXPORT_STATIC, \
|
||||||
|
&(instance), \
|
||||||
|
}); \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define EXPORT_SERVICE(instance) \
|
||||||
|
EXPORT_SERVICE_AS( \
|
||||||
|
instance, ::dusk::mods::ServiceTraits<std::remove_cv_t<decltype(instance)> >::id)
|
||||||
|
|
||||||
|
#define EXPORT_DEFERRED_SERVICE(token, service_id_value, major_value, minor_value) \
|
||||||
|
namespace { \
|
||||||
|
const int mod_deferred_export_registration_##token = \
|
||||||
|
::dusk::mods::detail::register_export(ServiceExport{ \
|
||||||
|
sizeof(ServiceExport), \
|
||||||
|
(service_id_value), \
|
||||||
|
static_cast<uint16_t>(major_value), \
|
||||||
|
static_cast<uint16_t>(minor_value), \
|
||||||
|
SERVICE_EXPORT_DEFERRED, \
|
||||||
|
nullptr, \
|
||||||
|
}); \
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config"
|
||||||
|
#define CONFIG_SERVICE_MAJOR 1u
|
||||||
|
#define CONFIG_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
/* Handle for a config var registered by the calling mod. 0 is never a valid handle. */
|
||||||
|
typedef uint64_t ConfigVarHandle;
|
||||||
|
/* Handle for a change subscription. 0 is never a valid handle. */
|
||||||
|
typedef uint64_t ConfigSubscriptionHandle;
|
||||||
|
|
||||||
|
typedef enum ConfigVarType {
|
||||||
|
CONFIG_VAR_BOOL = 0, /* bool */
|
||||||
|
CONFIG_VAR_INT = 1, /* int64_t */
|
||||||
|
CONFIG_VAR_FLOAT = 2, /* double */
|
||||||
|
CONFIG_VAR_STRING = 3, /* UTF-8 */
|
||||||
|
} ConfigVarType;
|
||||||
|
|
||||||
|
typedef struct ConfigVarDesc {
|
||||||
|
uint32_t struct_size;
|
||||||
|
/* Name fragment: 1-64 characters from [A-Za-z0-9_-]. The full config key is
|
||||||
|
* "mod.<escaped mod id>.<name>", persisted in config.json alongside host settings.
|
||||||
|
* "enabled" is reserved by the loader. */
|
||||||
|
const char* name;
|
||||||
|
ConfigVarType type;
|
||||||
|
/* Default value; only the field matching `type` is read. */
|
||||||
|
bool default_bool;
|
||||||
|
int64_t default_int;
|
||||||
|
double default_float;
|
||||||
|
const char* default_string; /* NULL means "" */
|
||||||
|
} ConfigVarDesc;
|
||||||
|
|
||||||
|
#define CONFIG_VAR_DESC_INIT {sizeof(ConfigVarDesc), NULL, CONFIG_VAR_BOOL, false, 0, 0.0, NULL}
|
||||||
|
|
||||||
|
/* Snapshot of a var's value; only the field matching `type` is meaningful. */
|
||||||
|
typedef struct ConfigVarValue {
|
||||||
|
uint32_t struct_size;
|
||||||
|
ConfigVarType type;
|
||||||
|
bool bool_value;
|
||||||
|
int64_t int_value;
|
||||||
|
double float_value;
|
||||||
|
const char* string_value; /* NUL-terminated; NULL for non-string vars */
|
||||||
|
size_t string_length; /* excludes the NUL */
|
||||||
|
} ConfigVarValue;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fired on the game thread whenever the var's effective value changes at runtime: the calling mod's
|
||||||
|
* own set_* calls and any other runtime writer. Writes that leave the value unchanged do not fire,
|
||||||
|
* and neither do values applied from config.json or --cvar during registration. `value` holds the
|
||||||
|
* new (current) value and `previous` the one it replaced; both snapshots are valid only for the
|
||||||
|
* duration of the call (copy string_value if you need to keep it). Setting the same var from inside
|
||||||
|
* its own callback applies the write but is not re-notified.
|
||||||
|
*/
|
||||||
|
typedef void (*ConfigChangedFn)(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value,
|
||||||
|
const ConfigVarValue* previous, void* user_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scoped configuration variables.
|
||||||
|
*
|
||||||
|
* Registrations are owned by the calling mod and removed automatically (subscriptions included)
|
||||||
|
* when it is disabled, reloaded, or fails. Values are saved to config.json. Writes are debounced,
|
||||||
|
* not flushed per set.
|
||||||
|
*/
|
||||||
|
typedef struct ConfigService {
|
||||||
|
ServiceHeader header;
|
||||||
|
|
||||||
|
/* Register a config var. If a value for the full key was saved earlier (or set via --cvar),
|
||||||
|
* it takes effect immediately; otherwise the var starts at the default. Registering a name
|
||||||
|
* that is already live is MOD_CONFLICT. */
|
||||||
|
ModResult (*register_var)(
|
||||||
|
ModContext* ctx, const ConfigVarDesc* desc, ConfigVarHandle* out_handle);
|
||||||
|
/* Unregister a var previously registered by the calling mod. Its persisted value is kept. */
|
||||||
|
ModResult (*unregister_var)(ModContext* ctx, ConfigVarHandle var);
|
||||||
|
|
||||||
|
/* Typed accessors; the type must match the registration (MOD_INVALID_ARGUMENT otherwise). */
|
||||||
|
ModResult (*get_bool)(ModContext* ctx, ConfigVarHandle var, bool* out_value);
|
||||||
|
ModResult (*set_bool)(ModContext* ctx, ConfigVarHandle var, bool value);
|
||||||
|
ModResult (*get_int)(ModContext* ctx, ConfigVarHandle var, int64_t* out_value);
|
||||||
|
ModResult (*set_int)(ModContext* ctx, ConfigVarHandle var, int64_t value);
|
||||||
|
ModResult (*get_float)(ModContext* ctx, ConfigVarHandle var, double* out_value);
|
||||||
|
ModResult (*set_float)(ModContext* ctx, ConfigVarHandle var, double value);
|
||||||
|
/* Copies the NUL-terminated value into buffer. out_length (optional) receives the full
|
||||||
|
* length excluding the NUL regardless of buffer size; call with buffer == NULL and
|
||||||
|
* buffer_size == 0 to query the length. A non-NULL buffer that is too small fails with
|
||||||
|
* MOD_INVALID_ARGUMENT and writes nothing. */
|
||||||
|
ModResult (*get_string)(
|
||||||
|
ModContext* ctx, ConfigVarHandle var, char* buffer, size_t buffer_size, size_t* out_length);
|
||||||
|
ModResult (*set_string)(ModContext* ctx, ConfigVarHandle var, const char* value);
|
||||||
|
|
||||||
|
/* Subscribe to changes of a var registered by the calling mod. out_handle may be NULL if
|
||||||
|
* the subscription is never removed manually (cleanup on mod teardown is automatic). */
|
||||||
|
ModResult (*subscribe)(ModContext* ctx, ConfigVarHandle var, ConfigChangedFn callback,
|
||||||
|
void* user_data, ConfigSubscriptionHandle* out_handle);
|
||||||
|
ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle);
|
||||||
|
} ConfigService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<ConfigService> {
|
||||||
|
static constexpr const char* id = CONFIG_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mods that link or hook game code directly must import this service; service-only and asset-only
|
||||||
|
* mods must not.
|
||||||
|
*
|
||||||
|
* Major version is the game-code ABI epoch: it is bumped when game-visible struct or vtable layouts
|
||||||
|
* change incompatibly (e.g. a TARGET_PC field added to an existing game struct). The loader's
|
||||||
|
* ordinary version check then fails mods built against the old epoch with a clear message instead
|
||||||
|
* of letting them corrupt memory.
|
||||||
|
*/
|
||||||
|
#define GAME_SERVICE_ID "dev.twilitrealm.dusklight.game"
|
||||||
|
#define GAME_SERVICE_MAJOR 1u
|
||||||
|
#define GAME_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
typedef struct GameService {
|
||||||
|
ServiceHeader header;
|
||||||
|
} GameService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<GameService> {
|
||||||
|
static constexpr const char* id = GAME_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Intercept game functions by address. Prefer the typed helpers in mods/hook.hpp
|
||||||
|
* (hook_add_pre/hook_add_post/hook_replace over a &Class::method): they generate the
|
||||||
|
* trampoline and hide install/dispatch, which are the low-level primitives those helpers
|
||||||
|
* build. resolve() maps a symbol name to an address for targets you can't name at compile time
|
||||||
|
* (file-local statics included).
|
||||||
|
*
|
||||||
|
* Every call is game-thread-only. Install and removal must run with no hooked function on the
|
||||||
|
* stack; the loader guarantees this by applying mod lifecycle changes between frames, which is
|
||||||
|
* why hooking a function that never returns (the outermost loop) makes a mod un-unloadable.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook"
|
||||||
|
#define HOOK_SERVICE_MAJOR 1u
|
||||||
|
#define HOOK_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
/* Symbol flags reported by resolve() */
|
||||||
|
typedef enum HookSymbolFlags {
|
||||||
|
HOOK_SYMBOL_CODE = 1u << 0u,
|
||||||
|
HOOK_SYMBOL_DATA = 1u << 1u,
|
||||||
|
/* Not exported/dynamically visible: hookable, but never linkable. */
|
||||||
|
HOOK_SYMBOL_LOCAL = 1u << 2u,
|
||||||
|
/* Other names share this address (ICF fold/alias): a hook intercepts them all. */
|
||||||
|
HOOK_SYMBOL_MULTI_NAME = 1u << 3u,
|
||||||
|
/* Resolved through a demangled display-name alias rather than the real symbol. */
|
||||||
|
HOOK_SYMBOL_DISPLAY = 1u << 6u,
|
||||||
|
} HookSymbolFlags;
|
||||||
|
|
||||||
|
/* A pre-hook's return value: whether to run the original function. */
|
||||||
|
typedef enum HookAction {
|
||||||
|
HOOK_CONTINUE = 0, /* run the original (and any lower-priority pre-hooks) */
|
||||||
|
HOOK_SKIP_ORIGINAL = 1, /* cancel the original and remaining pre-hooks; post-hooks still run */
|
||||||
|
} HookAction;
|
||||||
|
|
||||||
|
/* How replace resolves a second replace-hook on a target that already has one. */
|
||||||
|
typedef enum HookReplacePolicy {
|
||||||
|
HOOK_REPLACE_CONFLICT = 0, /* refuse with MOD_CONFLICT (the default) */
|
||||||
|
HOOK_REPLACE_PRIORITY = 1, /* take over only if this options.priority is strictly higher */
|
||||||
|
HOOK_REPLACE_OVERRIDE = 2, /* take over unconditionally */
|
||||||
|
} HookReplacePolicy;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hook callbacks. `args` is an array of pointers to the call's arguments (index 0 is `this`
|
||||||
|
* for member functions); `retval` points at the return slot (NULL for void). Read and write
|
||||||
|
* them through dusk::mods::arg<T> / arg_ref<T> from mods/hook.hpp. `userdata` is the pointer
|
||||||
|
* from HookOptions. All run on the game thread, in the hooked call's own stack frame.
|
||||||
|
*/
|
||||||
|
typedef HookAction (*HookPreFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||||
|
typedef void (*HookPostFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||||
|
typedef void (*HookReplaceFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||||
|
|
||||||
|
typedef struct HookOptions {
|
||||||
|
uint32_t struct_size;
|
||||||
|
/* Higher runs first; ties break by registration order. Applies to pre/post ordering and,
|
||||||
|
* with HOOK_REPLACE_PRIORITY, to replace-hook takeover. */
|
||||||
|
int32_t priority;
|
||||||
|
HookReplacePolicy replace_policy;
|
||||||
|
void* userdata; /* passed back to the callback */
|
||||||
|
} HookOptions;
|
||||||
|
|
||||||
|
#define HOOK_OPTIONS_INIT {sizeof(HookOptions), 0, HOOK_REPLACE_CONFLICT, NULL}
|
||||||
|
|
||||||
|
typedef struct HookService {
|
||||||
|
ServiceHeader header;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Install a trampoline detour on fn_addr and return the address to call the original through in
|
||||||
|
* *out_original_fn. The typed helpers generate the trampoline and call this; mods normally
|
||||||
|
* don't. The first mod to install a given target owns the live detour; later mods register as
|
||||||
|
* candidates so a hook survives the owner unloading (the detour is handed off and every
|
||||||
|
* original pointer is rewritten). Idempotent per (mod, out slot).
|
||||||
|
*/
|
||||||
|
ModResult (*install)(
|
||||||
|
ModContext* ctx, void* fn_addr, void* trampoline_fn, void** out_original_fn);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Register a callback on an already-installed target. Pre runs before the original (and can
|
||||||
|
* cancel it), post runs after (even if cancelled). Any number of mods may add pre/post to the
|
||||||
|
* same target; they run in priority then registration order. replace installs a single
|
||||||
|
* substitute for the original, managed by options.replace_policy, MOD_CONFLICT if refused.
|
||||||
|
*/
|
||||||
|
ModResult (*add_pre)(
|
||||||
|
ModContext* ctx, void* fn_addr, HookPreFn callback, const HookOptions* options);
|
||||||
|
ModResult (*add_post)(
|
||||||
|
ModContext* ctx, void* fn_addr, HookPostFn callback, const HookOptions* options);
|
||||||
|
ModResult (*replace)(
|
||||||
|
ModContext* ctx, void* fn_addr, HookReplaceFn callback, const HookOptions* options);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run the registered callbacks for a target. The generated trampoline calls these; they
|
||||||
|
* are not a mod-facing entry point. dispatch_pre reports through *out_skip_original
|
||||||
|
* whether the original should be skipped (a pre-hook returned HOOK_SKIP_ORIGINAL, or a
|
||||||
|
* replace-hook ran).
|
||||||
|
*/
|
||||||
|
ModResult (*dispatch_pre)(
|
||||||
|
ModContext* ctx, void* fn_addr, void* args, void* retval, int* out_skip_original);
|
||||||
|
ModResult (*dispatch_post)(ModContext* ctx, void* fn_addr, void* args, void* retval);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Resolve a game symbol by name from the symbol manifest, including non-exported (static)
|
||||||
|
* functions. Names can be either the platform's mangled name (i.e. the name passed to dlopen;
|
||||||
|
* no Mach-O leading underscore) or the qualified function name without parameters (e.g.
|
||||||
|
* "daAlink_c::execute"). out_flags (optional) receives HookSymbolFlags.
|
||||||
|
*
|
||||||
|
* Results: MOD_OK; MOD_UNSUPPORTED (no manifest for this build, missing or stale);
|
||||||
|
* MOD_UNAVAILABLE (symbol not found); MOD_CONFLICT (name maps to more than one address: C++
|
||||||
|
* overloads or per-TU statics; use the mangled name).
|
||||||
|
*/
|
||||||
|
ModResult (*resolve)(
|
||||||
|
ModContext* ctx, const char* symbol, void** out_addr, HookSymbolFlags* out_flags);
|
||||||
|
} HookService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<HookService> {
|
||||||
|
static constexpr const char* id = HOOK_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The host service: the calling mod's identity and its runtime interface to the loader.
|
||||||
|
* Always available; every other service can be reached from it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host"
|
||||||
|
#define HOST_SERVICE_MAJOR 2u
|
||||||
|
#define HOST_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ignore unknown values: later service minors may add events.
|
||||||
|
*/
|
||||||
|
typedef enum ModLifecycleEvent {
|
||||||
|
/*
|
||||||
|
* The subject mod is gone: its mod_shutdown has run (when it initialized at all) and
|
||||||
|
* every service has already dropped the state it held for it. The subject's library is
|
||||||
|
* still mapped, so pointers into it are valid to compare against, but they must not be
|
||||||
|
* called or dereferenced after the callback returns. Drop everything keyed to the
|
||||||
|
* mod: callbacks it registered, its ModContext*, state indexed by it.
|
||||||
|
*/
|
||||||
|
MOD_LIFECYCLE_DETACHED = 0,
|
||||||
|
} ModLifecycleEvent;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ctx is the watching mod's own context; subject identifies the mod the event is about.
|
||||||
|
* subject_id is valid only for the duration of the call.
|
||||||
|
*/
|
||||||
|
typedef void (*ModLifecycleFn)(ModContext* ctx, ModContext* subject, const char* subject_id,
|
||||||
|
ModLifecycleEvent event, void* user_data);
|
||||||
|
|
||||||
|
typedef struct HostService {
|
||||||
|
ServiceHeader header;
|
||||||
|
|
||||||
|
/* Version string of the current Dusklight build. (e.g. "1.4.2") */
|
||||||
|
const char* version;
|
||||||
|
|
||||||
|
/* Build id of the running game binary: PDB GUID+age on Windows, LC_UUID on macOS, GNU build-id
|
||||||
|
* on Linux. May be empty (len 0) if the identity could not be determined. */
|
||||||
|
const uint8_t* build_id;
|
||||||
|
uint32_t build_id_len;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Look up a service by id at call time. Unlike a manifest import, this sees whatever is
|
||||||
|
* currently published and carries no initialization-order guarantee (see mods/api.h).
|
||||||
|
* MOD_UNAVAILABLE if no matching service is published; *out_service is null on failure.
|
||||||
|
*/
|
||||||
|
ModResult (*get_service)(ModContext* ctx, const char* service_id, uint16_t major_version,
|
||||||
|
uint16_t min_minor_version, const void** out_service);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Publish a service the calling mod declared as a deferred export in its manifest.
|
||||||
|
* Must happen during mod_initialize so importers can resolve it; `service` must stay
|
||||||
|
* valid until the mod shuts down.
|
||||||
|
*/
|
||||||
|
ModResult (*publish_service)(
|
||||||
|
ModContext* ctx, const char* service_id, uint16_t major_version, const void* service);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Report an unrecoverable failure. The calling mod's services stop resolving immediately
|
||||||
|
* and the loader fully disables it at the next safe point; `message` is shown to the user.
|
||||||
|
* Safe to call from any mod callback.
|
||||||
|
*/
|
||||||
|
void (*fail)(ModContext* ctx, ModResult code, const char* message);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The calling mod's manifest metadata. Returned strings remain valid while the mod is
|
||||||
|
* loaded.
|
||||||
|
*/
|
||||||
|
const char* (*mod_id)(ModContext* ctx);
|
||||||
|
const char* (*mod_name)(ModContext* ctx);
|
||||||
|
const char* (*mod_version)(ModContext* ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A writable scratch directory reserved for the calling mod. Contents survive disable
|
||||||
|
* and reload within a session, but the directory is wiped at game startup.
|
||||||
|
*/
|
||||||
|
const char* (*mod_dir)(ModContext* ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Observe other mods' lifecycle events. Any mod whose service hands out per-caller state
|
||||||
|
* (registrations, callbacks, handles) should watch for MOD_LIFECYCLE_DETACHED and drop what it
|
||||||
|
* holds for the subject.
|
||||||
|
*
|
||||||
|
* Callbacks fire on the game thread at a lifecycle safe point (never mid-frame), for
|
||||||
|
* every mod but the watcher itself (use mod_shutdown for self-cleanup).
|
||||||
|
*/
|
||||||
|
ModResult (*watch_mod_lifecycle)(
|
||||||
|
ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle);
|
||||||
|
ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle);
|
||||||
|
} HostService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<HostService> {
|
||||||
|
static constexpr const char* id = HOST_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = HOST_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Logging into the game's console and log files. Messages are attributed to the calling mod
|
||||||
|
* (prefixed with its ID).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define LOG_SERVICE_ID "dev.twilitrealm.dusklight.log"
|
||||||
|
#define LOG_SERVICE_MAJOR 1u
|
||||||
|
#define LOG_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
typedef enum LogLevel {
|
||||||
|
LOG_LEVEL_TRACE = 0,
|
||||||
|
LOG_LEVEL_DEBUG = 1,
|
||||||
|
LOG_LEVEL_INFO = 2,
|
||||||
|
LOG_LEVEL_WARN = 3,
|
||||||
|
LOG_LEVEL_ERROR = 4,
|
||||||
|
} LogLevel;
|
||||||
|
|
||||||
|
typedef struct LogService {
|
||||||
|
ServiceHeader header;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Write a log message at the given level.
|
||||||
|
* `message` is a plain UTF-8 string and is copied before returning.
|
||||||
|
*/
|
||||||
|
void (*write)(ModContext* ctx, LogLevel level, const char* message);
|
||||||
|
|
||||||
|
/* Per-level shorthands for write. */
|
||||||
|
void (*trace)(ModContext* ctx, const char* message);
|
||||||
|
void (*debug)(ModContext* ctx, const char* message);
|
||||||
|
void (*info)(ModContext* ctx, const char* message);
|
||||||
|
void (*warn)(ModContext* ctx, const char* message);
|
||||||
|
void (*error)(ModContext* ctx, const char* message);
|
||||||
|
} LogService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<LogService> {
|
||||||
|
static constexpr const char* id = LOG_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = LOG_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
#define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay"
|
||||||
|
#define OVERLAY_SERVICE_MAJOR 1u
|
||||||
|
#define OVERLAY_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
/* Handle for a runtime overlay registration. 0 is never a valid handle. */
|
||||||
|
typedef uint64_t OverlayHandle;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Runtime DVD file overlays.
|
||||||
|
*
|
||||||
|
* Registrations are owned by the calling mod and removed automatically when it is disabled,
|
||||||
|
* reloaded, or fails. Changes are applied at the next frame boundary; data the game has already
|
||||||
|
* read stays in memory until it re-reads the file (sometimes on scene reload, sometimes on
|
||||||
|
* restart).
|
||||||
|
*
|
||||||
|
* disc_path names the file to overlay: absolute with a leading '/', matched against the disc
|
||||||
|
* case-insensitively (e.g. "/res/Stage/R04_00.arc"). Paths that do not exist on the disc are added
|
||||||
|
* as new files.
|
||||||
|
*
|
||||||
|
* If multiple sources overlay the same path, the last one wins: a mod's runtime registrations beat
|
||||||
|
* its static overlay/ files, and later-loaded mods beat earlier ones.
|
||||||
|
*/
|
||||||
|
typedef struct OverlayService {
|
||||||
|
ServiceHeader header;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Overlay disc_path with a file from the calling mod's bundle (bundle-relative path, e.g.
|
||||||
|
* "res/replacement.arc"). The file's contents are read lazily on each open, so the bundle
|
||||||
|
* file must not change size while registered.
|
||||||
|
*/
|
||||||
|
ModResult (*add_file)(
|
||||||
|
ModContext* ctx, const char* disc_path, const char* bundle_path, OverlayHandle* out_handle);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Overlay disc_path with a caller-owned buffer. The data is copied; the caller may free it
|
||||||
|
* as soon as this returns.
|
||||||
|
*/
|
||||||
|
ModResult (*add_buffer)(ModContext* ctx, const char* disc_path, const void* data, size_t size,
|
||||||
|
OverlayHandle* out_handle);
|
||||||
|
|
||||||
|
/* Remove a runtime overlay previously added by the calling mod. */
|
||||||
|
ModResult (*remove)(ModContext* ctx, OverlayHandle handle);
|
||||||
|
} OverlayService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<OverlayService> {
|
||||||
|
static constexpr const char* id = OVERLAY_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = OVERLAY_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read-only access to the res/ tree of the calling mod's own bundle. Reload serves the new
|
||||||
|
* bundle's contents. For writable storage, use HostService::mod_dir.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define RESOURCE_SERVICE_ID "dev.twilitrealm.dusklight.resource"
|
||||||
|
#define RESOURCE_SERVICE_MAJOR 1u
|
||||||
|
#define RESOURCE_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A loaded resource, allocated by the service. Return every successful load with free;
|
||||||
|
* buffers still live when the mod is disabled or reloaded are reclaimed with a warning.
|
||||||
|
*/
|
||||||
|
typedef struct ResourceBuffer {
|
||||||
|
uint32_t struct_size;
|
||||||
|
void* data;
|
||||||
|
size_t size;
|
||||||
|
} ResourceBuffer;
|
||||||
|
|
||||||
|
#define RESOURCE_BUFFER_INIT {sizeof(ResourceBuffer), NULL, 0u}
|
||||||
|
|
||||||
|
typedef struct ResourceService {
|
||||||
|
ServiceHeader header;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load a file into a fresh allocation. `relative_path` is resolved against the bundle's
|
||||||
|
* res/ directory. Absolute paths and ".." are rejected. MOD_UNAVAILABLE if the file does not
|
||||||
|
* exist. An empty file loads as data == NULL with size 0. Previous contents of `out_buffer` are
|
||||||
|
* overwritten, not freed.
|
||||||
|
*/
|
||||||
|
ModResult (*load)(ModContext* ctx, const char* relative_path, ResourceBuffer* out_buffer);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Release a loaded buffer and reset it to the empty state. Safe to call on an empty or
|
||||||
|
* already-freed buffer.
|
||||||
|
*/
|
||||||
|
void (*free)(ModContext* ctx, ResourceBuffer* buffer);
|
||||||
|
} ResourceService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<ResourceService> {
|
||||||
|
static constexpr const char* id = RESOURCE_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = RESOURCE_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
|
||||||
|
#define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture"
|
||||||
|
#define TEXTURE_SERVICE_MAJOR 1u
|
||||||
|
#define TEXTURE_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
/* Handle for a runtime texture replacement registration. 0 is never a valid handle. */
|
||||||
|
typedef uint64_t TextureReplacementHandle;
|
||||||
|
|
||||||
|
typedef enum TextureKeyKind {
|
||||||
|
/* Match a texture by the address of its in-memory GX texel data. */
|
||||||
|
TEXTURE_KEY_POINTER = 0,
|
||||||
|
/* Match by content: XXH64 of the base mip level (and of the referenced TLUT range for
|
||||||
|
* palette formats), as encoded in replacement filenames / texture dumps. */
|
||||||
|
TEXTURE_KEY_SOURCE = 1,
|
||||||
|
} TextureKeyKind;
|
||||||
|
|
||||||
|
/* Wildcard values for TEXTURE_KEY_SOURCE hashes ("$" in the filename convention). */
|
||||||
|
#define TEXTURE_HASH_WILDCARD UINT64_C(0xFFFFFFFFFFFFFFFF)
|
||||||
|
#define TEXTURE_TLUT_WILDCARD UINT64_C(0xFFFFFFFFFFFFFFFE)
|
||||||
|
|
||||||
|
typedef struct TextureKey {
|
||||||
|
uint32_t struct_size;
|
||||||
|
TextureKeyKind kind;
|
||||||
|
const void* pointer; /* TEXTURE_KEY_POINTER only */
|
||||||
|
uint64_t texture_hash; /* TEXTURE_KEY_SOURCE */
|
||||||
|
uint64_t tlut_hash; /* TEXTURE_KEY_SOURCE, palette formats only */
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint32_t gx_format;
|
||||||
|
bool has_tlut;
|
||||||
|
} TextureKey;
|
||||||
|
|
||||||
|
#define TEXTURE_KEY_INIT {sizeof(TextureKey), TEXTURE_KEY_POINTER, NULL, 0u, 0u, 0u, 0u, 0u, false}
|
||||||
|
|
||||||
|
typedef struct TextureData {
|
||||||
|
uint32_t struct_size;
|
||||||
|
const void* data; /* texel data laid out in gx_format; copied by the service */
|
||||||
|
size_t size;
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint32_t mip_count;
|
||||||
|
uint32_t gx_format; /* any GX texture format supported by Aurora's converter */
|
||||||
|
} TextureData;
|
||||||
|
|
||||||
|
#define TEXTURE_DATA_INIT {sizeof(TextureData), NULL, 0u, 0u, 0u, 1u, 0u}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Runtime texture replacements.
|
||||||
|
*
|
||||||
|
* Registrations are owned by the calling mod and removed automatically when it is disabled,
|
||||||
|
* reloaded, or fails. When multiple sources replace the same texture, the highest priority wins:
|
||||||
|
* later-loaded mods beat earlier ones, and any mod beats the user's texture_replacements config
|
||||||
|
* directory. Files shipped in a mod's textures/ directory register automatically with the same
|
||||||
|
* ownership and priority; this service is for replacements decided at runtime.
|
||||||
|
*/
|
||||||
|
typedef struct TextureService {
|
||||||
|
ServiceHeader header;
|
||||||
|
|
||||||
|
/* Register a replacement from raw texel data. The data is copied; the caller may free it as
|
||||||
|
* soon as this returns. */
|
||||||
|
ModResult (*register_data)(ModContext* ctx, const TextureKey* key, const TextureData* data,
|
||||||
|
TextureReplacementHandle* out_handle);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Register a replacement from an encoded .dds/.png inside the calling mod's bundle. The
|
||||||
|
* filename encodes the key (same convention as the texture_replacements directory, e.g.
|
||||||
|
* "tex1_{w}x{h}_{hash}_{fmt}.dds"); "_mipN" sidecars next to it are picked up automatically.
|
||||||
|
* The file is decoded lazily on first use by the renderer.
|
||||||
|
*/
|
||||||
|
ModResult (*register_file)(ModContext* ctx, const char* bundle_path,
|
||||||
|
TextureReplacementHandle* out_handle);
|
||||||
|
|
||||||
|
/* Remove a replacement previously registered by the calling mod. */
|
||||||
|
ModResult (*unregister)(ModContext* ctx, TextureReplacementHandle handle);
|
||||||
|
} TextureService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<TextureService> {
|
||||||
|
static constexpr const char* id = TEXTURE_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = TEXTURE_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "mods/api.h"
|
||||||
|
#include "mods/svc/config.h"
|
||||||
|
|
||||||
|
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
|
||||||
|
#define UI_SERVICE_MAJOR 1u
|
||||||
|
#define UI_SERVICE_MINOR 0u
|
||||||
|
|
||||||
|
/*
|
||||||
|
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, scoped
|
||||||
|
* RCSS stylesheets and menu bar tabs.
|
||||||
|
*
|
||||||
|
* All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI
|
||||||
|
* callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with
|
||||||
|
* MOD_INVALID_ARGUMENT. Element handles die with the content that owns them: a panel or tab rebuild
|
||||||
|
* destroys the previous build's elements, so re-acquire handles inside the build callback rather
|
||||||
|
* than caching them. Strings are UTF-8 and, in both directions, only valid for the duration of the
|
||||||
|
* call.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 0 is never a valid handle. */
|
||||||
|
typedef uint64_t UiWindowHandle;
|
||||||
|
typedef uint64_t UiDialogHandle;
|
||||||
|
typedef uint64_t UiElementHandle;
|
||||||
|
typedef uint64_t UiStyleHandle;
|
||||||
|
typedef uint64_t UiMenuTabHandle;
|
||||||
|
|
||||||
|
typedef enum UiStyleScope {
|
||||||
|
UI_SCOPE_PRELAUNCH = 0, /* the pre-launch menu */
|
||||||
|
UI_SCOPE_WINDOW = 1, /* every tabbed/small window, host and mod alike */
|
||||||
|
UI_SCOPE_MENU_BAR = 2, /* the in-game menu bar */
|
||||||
|
UI_SCOPE_OVERLAY = 3, /* the passive overlay (toasts, FPS counter, timers) */
|
||||||
|
UI_SCOPE_TOUCH_CONTROLS = 4, /* touch controls and their editor */
|
||||||
|
UI_SCOPE_GRAPHICS_TUNER = 5, /* the graphics tuner overlay window */
|
||||||
|
} UiStyleScope;
|
||||||
|
|
||||||
|
typedef enum UiDialogVariant {
|
||||||
|
UI_DIALOG_NORMAL = 0,
|
||||||
|
UI_DIALOG_WARNING = 1, /* warning icon by default */
|
||||||
|
UI_DIALOG_DANGER = 2, /* red styling, error icon by default */
|
||||||
|
} UiDialogVariant;
|
||||||
|
|
||||||
|
typedef enum UiControlKind {
|
||||||
|
UI_CONTROL_BUTTON = 0, /* action button (on_pressed) */
|
||||||
|
UI_CONTROL_TOGGLE = 1, /* boolean on/off */
|
||||||
|
UI_CONTROL_NUMBER = 2, /* integer stepper with min/max/step */
|
||||||
|
UI_CONTROL_STRING = 3, /* text input */
|
||||||
|
UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */
|
||||||
|
} UiControlKind;
|
||||||
|
|
||||||
|
typedef enum UiControlBinding {
|
||||||
|
/* Values flow through the `get`/`set` callbacks. Getters are polled every frame while the
|
||||||
|
control is visible and must be cheap. */
|
||||||
|
UI_BINDING_CALLBACKS = 0,
|
||||||
|
/* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod)
|
||||||
|
* directly: persistence, change notifications and the modified indicator (value != default) are
|
||||||
|
* wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and
|
||||||
|
* SELECT = int, STRING = string. Float vars are not bindable; use callbacks. */
|
||||||
|
UI_BINDING_CONFIG_VAR = 1,
|
||||||
|
} UiControlBinding;
|
||||||
|
|
||||||
|
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
|
||||||
|
* reads string_value. string_value passed to a setter is only valid during the call; a getter
|
||||||
|
* should point it at storage owned by the mod (e.g. a static buffer) that stays valid until the
|
||||||
|
* next call into the mod — the host copies it right after the getter returns. */
|
||||||
|
typedef struct UiControlValue {
|
||||||
|
uint32_t struct_size;
|
||||||
|
bool bool_value;
|
||||||
|
int64_t int_value;
|
||||||
|
const char* string_value;
|
||||||
|
} UiControlValue;
|
||||||
|
|
||||||
|
#define UI_CONTROL_VALUE_INIT {sizeof(UiControlValue), false, 0, NULL}
|
||||||
|
|
||||||
|
typedef void (*UiControlGetFn)(ModContext* ctx, void* user_data, UiControlValue* out_value);
|
||||||
|
typedef void (*UiControlSetFn)(ModContext* ctx, void* user_data, const UiControlValue* value);
|
||||||
|
/* Polled every frame while the control is visible. */
|
||||||
|
typedef bool (*UiPredicateFn)(ModContext* ctx, void* user_data);
|
||||||
|
typedef void (*UiPressedFn)(ModContext* ctx, void* user_data);
|
||||||
|
|
||||||
|
typedef struct UiControlDesc {
|
||||||
|
uint32_t struct_size;
|
||||||
|
UiControlKind kind;
|
||||||
|
/* Row label (plain text). Required. */
|
||||||
|
const char* label;
|
||||||
|
/* Optional RML shown as contextual help when the control is focused or hovered. Only rendered
|
||||||
|
* where a help pane exists (mod window tabs). */
|
||||||
|
const char* help_rml;
|
||||||
|
UiControlBinding binding; /* ignored for BUTTON */
|
||||||
|
ConfigVarHandle config_var; /* UI_BINDING_CONFIG_VAR */
|
||||||
|
UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
|
||||||
|
UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
|
||||||
|
UiPressedFn on_pressed; /* BUTTON only. Required for BUTTON. */
|
||||||
|
UiPredicateFn is_disabled; /* optional */
|
||||||
|
/* Optional override for the modified indicator. CONFIG_VAR controls derive it from value !=
|
||||||
|
* default when this is NULL. */
|
||||||
|
UiPredicateFn is_modified;
|
||||||
|
/* Passed to every callback above. */
|
||||||
|
void* user_data;
|
||||||
|
/* NUMBER: inclusive clamp range and step. min == max means the defaults (0 .. INT32_MAX); step
|
||||||
|
* < 1 means 1. */
|
||||||
|
int64_t min;
|
||||||
|
int64_t max;
|
||||||
|
int64_t step;
|
||||||
|
const char* prefix; /* NUMBER: optional text before the value */
|
||||||
|
const char* suffix; /* NUMBER: optional text after the value */
|
||||||
|
/* SELECT: option labels (plain text). Required for SELECT. SELECT controls
|
||||||
|
* present their options in the help pane, so they are only available where
|
||||||
|
* one exists (mod window tabs); MOD_UNSUPPORTED elsewhere. */
|
||||||
|
const char* const* options;
|
||||||
|
size_t option_count;
|
||||||
|
int32_t max_length; /* STRING: maximum input length; < 1 means unlimited */
|
||||||
|
} UiControlDesc;
|
||||||
|
|
||||||
|
#define UI_CONTROL_DESC_INIT \
|
||||||
|
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
|
||||||
|
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0}
|
||||||
|
|
||||||
|
/* Build the panel contents. `panel` accepts the pane_add_* functions; it and
|
||||||
|
* every element created in it are destroyed (handles invalidated) whenever the
|
||||||
|
* panel is rebuilt, e.g. on tab switches. A non-MOD_OK result fails the mod. */
|
||||||
|
typedef ModResult (*UiPanelBuildFn)(
|
||||||
|
ModContext* ctx, UiElementHandle panel, void* user_data, ModError* out_error);
|
||||||
|
/* Called every frame while the panel is the visible tab. */
|
||||||
|
typedef ModResult (*UiPanelUpdateFn)(ModContext* ctx, void* user_data, ModError* out_error);
|
||||||
|
|
||||||
|
/* A panel rendered inside the calling mod's tab of the host Mods window. */
|
||||||
|
typedef struct UiModsPanelDesc {
|
||||||
|
uint32_t struct_size;
|
||||||
|
UiPanelBuildFn build; /* required */
|
||||||
|
UiPanelUpdateFn update;
|
||||||
|
void* user_data;
|
||||||
|
} UiModsPanelDesc;
|
||||||
|
|
||||||
|
#define UI_MODS_PANEL_DESC_INIT {sizeof(UiModsPanelDesc), NULL, NULL, NULL}
|
||||||
|
|
||||||
|
/* Build one tab of a mod window. `left_pane` is the interactive column,
|
||||||
|
* `right_pane` shows contextual help (controls' help_rml and SELECT options
|
||||||
|
* render there). Rebuilt on every tab activation. */
|
||||||
|
typedef ModResult (*UiTabBuildFn)(ModContext* ctx, UiWindowHandle window, UiElementHandle left_pane,
|
||||||
|
UiElementHandle right_pane, void* user_data, ModError* out_error);
|
||||||
|
|
||||||
|
typedef struct UiTabDesc {
|
||||||
|
uint32_t struct_size;
|
||||||
|
const char* title; /* required */
|
||||||
|
UiTabBuildFn build; /* required */
|
||||||
|
UiPanelUpdateFn update;
|
||||||
|
void* user_data;
|
||||||
|
} UiTabDesc;
|
||||||
|
|
||||||
|
#define UI_TAB_DESC_INIT {sizeof(UiTabDesc), NULL, NULL, NULL, NULL}
|
||||||
|
|
||||||
|
typedef void (*UiWindowClosedFn)(ModContext* ctx, UiWindowHandle window, void* user_data);
|
||||||
|
|
||||||
|
typedef struct UiWindowDesc {
|
||||||
|
uint32_t struct_size;
|
||||||
|
const UiTabDesc* tabs; /* at least one */
|
||||||
|
size_t tab_count;
|
||||||
|
/* Optional RCSS applied to this window's document only (on top of any
|
||||||
|
* UI_SCOPE_WINDOW sheets, which apply automatically). */
|
||||||
|
const char* rcss;
|
||||||
|
/* Fired when the window is destroyed by user close or window_close. Not
|
||||||
|
* fired during owning-mod teardown/shutdown. The handle is already invalid. */
|
||||||
|
UiWindowClosedFn on_closed;
|
||||||
|
void* user_data;
|
||||||
|
} UiWindowDesc;
|
||||||
|
|
||||||
|
#define UI_WINDOW_DESC_INIT {sizeof(UiWindowDesc), NULL, 0u, NULL, NULL, NULL}
|
||||||
|
|
||||||
|
typedef void (*UiDialogActionFn)(ModContext* ctx, UiDialogHandle dialog, void* user_data);
|
||||||
|
|
||||||
|
/* Note: array element without struct_size; a future change requires appending
|
||||||
|
* a v2 desc struct rather than growing this one. */
|
||||||
|
typedef struct UiDialogAction {
|
||||||
|
const char* label; /* required */
|
||||||
|
UiDialogActionFn on_pressed;
|
||||||
|
void* user_data;
|
||||||
|
bool keep_open; /* false = the dialog closes after on_pressed returns */
|
||||||
|
} UiDialogAction;
|
||||||
|
|
||||||
|
typedef struct UiDialogDesc {
|
||||||
|
uint32_t struct_size;
|
||||||
|
const char* title; /* plain text; required */
|
||||||
|
const char* body_rml; /* RML body; required */
|
||||||
|
UiDialogVariant variant;
|
||||||
|
/* Optional icon name overriding the variant default: "warning", "error",
|
||||||
|
* "question-mark", "verifying", "celebration". */
|
||||||
|
const char* icon;
|
||||||
|
const UiDialogAction* actions; /* at least one */
|
||||||
|
size_t action_count;
|
||||||
|
/* Fired on cancel (B/Escape) before the dialog closes; the dialog always
|
||||||
|
* closes on dismiss. */
|
||||||
|
UiDialogActionFn on_dismiss;
|
||||||
|
void* user_data; /* passed to on_dismiss */
|
||||||
|
} UiDialogDesc;
|
||||||
|
|
||||||
|
#define UI_DIALOG_DESC_INIT \
|
||||||
|
{sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL}
|
||||||
|
|
||||||
|
/* A tab added to the in-game menu bar. */
|
||||||
|
typedef struct UiMenuTabDesc {
|
||||||
|
uint32_t struct_size;
|
||||||
|
const char* label; /* plain text; required */
|
||||||
|
UiPressedFn on_selected; /* fired when the user activates the tab; required */
|
||||||
|
void* user_data;
|
||||||
|
} UiMenuTabDesc;
|
||||||
|
|
||||||
|
#define UI_MENU_TAB_DESC_INIT {sizeof(UiMenuTabDesc), NULL, NULL, NULL}
|
||||||
|
|
||||||
|
typedef struct UiService {
|
||||||
|
ServiceHeader header;
|
||||||
|
|
||||||
|
/* Register or replace the panel shown in the calling mod's Mods-window tab. */
|
||||||
|
ModResult (*register_mods_panel)(ModContext* ctx, const UiModsPanelDesc* desc);
|
||||||
|
|
||||||
|
/* Content builders. `pane` is a panel or tab pane handle; out_elem (where
|
||||||
|
* present, optional) receives a handle for later elem_set_* updates. */
|
||||||
|
ModResult (*pane_add_section)(ModContext* ctx, UiElementHandle pane, const char* title);
|
||||||
|
ModResult (*pane_add_text)(
|
||||||
|
ModContext* ctx, UiElementHandle pane, const char* text, UiElementHandle* out_elem);
|
||||||
|
ModResult (*pane_add_rml)(
|
||||||
|
ModContext* ctx, UiElementHandle pane, const char* rml, UiElementHandle* out_elem);
|
||||||
|
ModResult (*pane_add_progress)(
|
||||||
|
ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem);
|
||||||
|
ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc,
|
||||||
|
UiElementHandle* out_elem);
|
||||||
|
|
||||||
|
/* Element updates. The handle kind must match the setter (text/rml on text
|
||||||
|
* rows, progress on progress bars). */
|
||||||
|
ModResult (*elem_set_text)(ModContext* ctx, UiElementHandle elem, const char* text);
|
||||||
|
ModResult (*elem_set_rml)(ModContext* ctx, UiElementHandle elem, const char* rml);
|
||||||
|
ModResult (*elem_set_progress)(ModContext* ctx, UiElementHandle elem, float value);
|
||||||
|
/* Set or clear an RCSS class on any element handle (rows, progress bars,
|
||||||
|
* controls, panes), for styling via scoped or window RCSS. */
|
||||||
|
ModResult (*elem_set_class)(
|
||||||
|
ModContext* ctx, UiElementHandle elem, const char* name, bool active);
|
||||||
|
|
||||||
|
/* Push a tabbed two-pane window onto the document stack and show it. */
|
||||||
|
ModResult (*window_push)(ModContext* ctx, const UiWindowDesc* desc, UiWindowHandle* out_window);
|
||||||
|
ModResult (*window_close)(ModContext* ctx, UiWindowHandle window);
|
||||||
|
|
||||||
|
/* Push a modal dialog onto the document stack and show it. */
|
||||||
|
ModResult (*dialog_push)(ModContext* ctx, const UiDialogDesc* desc, UiDialogHandle* out_dialog);
|
||||||
|
ModResult (*dialog_close)(ModContext* ctx, UiDialogHandle dialog);
|
||||||
|
ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml);
|
||||||
|
/* Replace the dialog icon ("" removes it; names as in UiDialogDesc.icon). */
|
||||||
|
ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon);
|
||||||
|
/* Append one action button (same callback rules as at push). */
|
||||||
|
ModResult (*dialog_add_action)(
|
||||||
|
ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action);
|
||||||
|
|
||||||
|
/* Whether any focus-stack document is currently visible (visible documents block gamepad
|
||||||
|
* input). */
|
||||||
|
ModResult (*is_any_document_visible)(ModContext* ctx, bool* out_visible);
|
||||||
|
|
||||||
|
/* Register an RCSS sheet applied to every document of `scope`, now and in the future, until
|
||||||
|
* unregistered or the calling mod is torn down. Sheets apply in registration order after the
|
||||||
|
* host styles. Fails with MOD_INVALID_ARGUMENT if the RCSS does not parse. */
|
||||||
|
ModResult (*register_styles)(
|
||||||
|
ModContext* ctx, UiStyleScope scope, const char* rcss, UiStyleHandle* out_style);
|
||||||
|
/* Like register_styles, but reads the RCSS from the mod bundle's res/ directory (same path
|
||||||
|
* rules as ResourceService::load). MOD_UNAVAILABLE if the file does not exist. */
|
||||||
|
ModResult (*register_styles_file)(
|
||||||
|
ModContext* ctx, UiStyleScope scope, const char* path, UiStyleHandle* out_style);
|
||||||
|
ModResult (*unregister_styles)(ModContext* ctx, UiStyleHandle style);
|
||||||
|
|
||||||
|
/* Add a tab to the in-game menu bar, on_selected runs with the menu open; pushing a window from
|
||||||
|
* it stacks the window over the menu like the host tabs do. The tab is removed when
|
||||||
|
* unregistered or the mod is torn down. */
|
||||||
|
ModResult (*register_menu_tab)(
|
||||||
|
ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab);
|
||||||
|
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
|
||||||
|
} UiService;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
#include "mods/service.hpp"
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct dusk::mods::ServiceTraits<UiService> {
|
||||||
|
static constexpr const char* id = UI_SERVICE_ID;
|
||||||
|
static constexpr uint16_t major_version = UI_SERVICE_MAJOR;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -12,12 +12,23 @@ struct JORNodeEvent;
|
|||||||
class JORMContext;
|
class JORMContext;
|
||||||
class JORServer;
|
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 {
|
class JOREventListener {
|
||||||
public:
|
public:
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
JOREventListener() {}
|
JOREventListener() {}
|
||||||
|
#if TARGET_PC
|
||||||
|
virtual void listenPropertyEvent(const JORPropertyEvent*) {}
|
||||||
|
#else
|
||||||
virtual void listenPropertyEvent(const JORPropertyEvent*) = 0;
|
virtual void listenPropertyEvent(const JORPropertyEvent*) = 0;
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
class JORReflexible : public JOREventListener {
|
class JORReflexible : public JOREventListener {
|
||||||
@@ -30,7 +41,11 @@ public:
|
|||||||
virtual void listenPropertyEvent(const JORPropertyEvent*);
|
virtual void listenPropertyEvent(const JORPropertyEvent*);
|
||||||
virtual void listen(u32, const JOREvent*);
|
virtual void listen(u32, const JOREvent*);
|
||||||
virtual void genObjectInfo(const JORGenEvent*);
|
virtual void genObjectInfo(const JORGenEvent*);
|
||||||
|
#if TARGET_PC
|
||||||
|
virtual void genMessage(JORMContext*) {}
|
||||||
|
#else
|
||||||
virtual void genMessage(JORMContext*) = 0;
|
virtual void genMessage(JORMContext*) = 0;
|
||||||
|
#endif
|
||||||
virtual void listenNodeEvent(const JORNodeEvent*);
|
virtual void listenNodeEvent(const JORNodeEvent*);
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ public:
|
|||||||
>
|
>
|
||||||
{
|
{
|
||||||
TIterator_data_(const TFunctionValue_list_parameter& rParent, const f32* value) {
|
TIterator_data_(const TFunctionValue_list_parameter& rParent, const f32* value) {
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
pOwn_ = &rParent;
|
pOwn_ = &rParent;
|
||||||
#endif
|
#endif
|
||||||
pf_ = value;
|
pf_ = value;
|
||||||
@@ -372,7 +372,7 @@ public:
|
|||||||
return (r1.pf_ - r2.pf_) / suData_size;
|
return (r1.pf_ - r2.pf_) / suData_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x00 */ const TFunctionValue_list_parameter* pOwn_;
|
/* 0x00 */ const TFunctionValue_list_parameter* pOwn_;
|
||||||
#endif
|
#endif
|
||||||
/* 0x00 */ const f32* pf_;
|
/* 0x00 */ const f32* pf_;
|
||||||
@@ -425,7 +425,7 @@ public:
|
|||||||
>
|
>
|
||||||
{
|
{
|
||||||
TIterator_data_(const TFunctionValue_hermite& rParent, const f32* value) {
|
TIterator_data_(const TFunctionValue_hermite& rParent, const f32* value) {
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
pOwn_ = &rParent;
|
pOwn_ = &rParent;
|
||||||
#endif
|
#endif
|
||||||
pf_ = value;
|
pf_ = value;
|
||||||
@@ -491,7 +491,7 @@ public:
|
|||||||
return (r1.pf_ - r2.pf_) / r1.uSize_;
|
return (r1.pf_ - r2.pf_) / r1.uSize_;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
/* 0x00 */ const TFunctionValue_hermite* pOwn_;
|
/* 0x00 */ const TFunctionValue_hermite* pOwn_;
|
||||||
/* 0x04 */ const f32* pf_;
|
/* 0x04 */ const f32* pf_;
|
||||||
/* 0x08 */ u32 uSize_;
|
/* 0x08 */ u32 uSize_;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
window.logs content {
|
||||||
|
flex-flow: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.logs .log-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
flex: 0 0 64dp;
|
||||||
|
height: 64dp;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8dp;
|
||||||
|
padding-right: 72dp;
|
||||||
|
background-color: rgba(217, 217, 217, 10%);
|
||||||
|
border-bottom: 2dp #92875B;
|
||||||
|
font-family: "Fira Sans Condensed";
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.logs > close {
|
||||||
|
top: 8dp;
|
||||||
|
right: 8dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.logs .log-title {
|
||||||
|
align-self: stretch;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0 24dp;
|
||||||
|
line-height: 64dp;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 4dp #C2A42D;
|
||||||
|
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.logs .log-title-mod {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: "Fira Sans";
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 15dp;
|
||||||
|
color: rgba(224, 219, 200, 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-toolbar-spacer {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-toolbar button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-family: "Fira Sans";
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 15dp;
|
||||||
|
padding: 5dp 12dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.logs content pane.log-view {
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding: 12dp 16dp;
|
||||||
|
padding-bottom: 0dp;
|
||||||
|
gap: 0dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-lines {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-line {
|
||||||
|
display: block;
|
||||||
|
font-family: "Noto Mono";
|
||||||
|
font-size: 13dp;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-line .log-time {
|
||||||
|
color: rgba(224, 219, 200, 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-line .log-mod {
|
||||||
|
color: rgba(194, 164, 45, 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-line.lvl-trace,
|
||||||
|
.log-line.lvl-debug {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-line.lvl-warn .log-msg {
|
||||||
|
color: #ffa826;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-line.lvl-error .log-msg {
|
||||||
|
color: #cc4444;
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
window.mods content pane.mod-list {
|
||||||
|
flex: 0 0 360dp;
|
||||||
|
padding: 16dp;
|
||||||
|
padding-bottom: 0dp;
|
||||||
|
gap: 4dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 640dp) {
|
||||||
|
window.mods content pane.mod-list {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mods content pane.mod-detail {
|
||||||
|
gap: 12dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12dp;
|
||||||
|
padding: 4dp 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-info-label {
|
||||||
|
font-family: "Fira Sans Condensed";
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0.55;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-info-value {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-path {
|
||||||
|
font-size: 14dp;
|
||||||
|
word-break: break-all;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
gap: 12dp;
|
||||||
|
padding: 10dp;
|
||||||
|
border-radius: 10dp;
|
||||||
|
decorator: vertical-gradient(#c2a42d00 #c2a42d00);
|
||||||
|
transition: decorator 0.1s linear-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
focus: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry.current {
|
||||||
|
box-shadow: rgba(146, 135, 91, 40%) 0 0 0 1dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry:hover,
|
||||||
|
mod-entry:focus-visible {
|
||||||
|
decorator: vertical-gradient(#c2a42d00 #c2a42d26);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry:selected {
|
||||||
|
decorator: vertical-gradient(#c2a42d10 #c2a42d40);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 56dp;
|
||||||
|
height: 56dp;
|
||||||
|
border-radius: 8dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry icon.mod-icon {
|
||||||
|
font-size: 36dp;
|
||||||
|
background-color: rgba(17, 16, 10, 20%);
|
||||||
|
color: rgba(224, 219, 200, 45%);
|
||||||
|
decorator: text("" center center);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-entry-info {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 2dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-entry-name {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-entry-name-text {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-entry-version {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 13dp;
|
||||||
|
color: rgba(224, 219, 200, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-entry-status.active {
|
||||||
|
color: #44cc55;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-entry-status.failed {
|
||||||
|
color: #cc4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-entry-desc {
|
||||||
|
font-size: 14dp;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: rgba(224, 219, 200, 65%);
|
||||||
|
max-height: 2.6em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry .mod-entry-sub {
|
||||||
|
font-size: 13dp;
|
||||||
|
color: rgba(224, 219, 200, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry.inactive .mod-icon {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-entry.inactive .mod-entry-info {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-header {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-header.has-banner {
|
||||||
|
height: 180dp;
|
||||||
|
margin: -24dp -24dp 0dp -24dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-header .mod-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 24dp;
|
||||||
|
left: 24dp;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
gap: 8dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-header .mod-actions button {
|
||||||
|
font-size: 16dp;
|
||||||
|
padding: 6dp 14dp;
|
||||||
|
background-color: rgba(21, 22, 16, 80%);
|
||||||
|
box-shadow: rgba(146, 135, 91, 60%) 0 0 0 1dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-header.no-banner {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
mod-header.no-banner .mod-actions {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mods .mod-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28dp;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mods .mod-title .mod-title-version {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 16dp;
|
||||||
|
color: rgba(224, 219, 200, 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mods .mod-author {
|
||||||
|
display: block;
|
||||||
|
font-size: 15dp;
|
||||||
|
color: rgba(224, 219, 200, 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mods .mod-restart-note {
|
||||||
|
font-size: 15dp;
|
||||||
|
color: #ffa826;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.mods .mod-description {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 14dp;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.active,
|
||||||
|
.mod-info-label.active {
|
||||||
|
color: #44cc55;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.failed,
|
||||||
|
.mod-info-label.failed {
|
||||||
|
color: #cc4444;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
+60
-2
@@ -21,6 +21,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fps,
|
fps,
|
||||||
|
pipeline-progress,
|
||||||
toast {
|
toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border: 1dp #92875B;
|
border: 1dp #92875B;
|
||||||
@@ -98,7 +99,7 @@ toast message row.muted {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast progress {
|
progress {
|
||||||
height: 4dp;
|
height: 4dp;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -106,10 +107,50 @@ toast progress {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
toast progress fill {
|
progress fill {
|
||||||
background-color: rgba(194, 164, 45, 80%);
|
background-color: rgba(194, 164, 45, 80%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pipeline-progress {
|
||||||
|
left: 12dp;
|
||||||
|
bottom: 12dp;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
z-index: 100;
|
||||||
|
min-width: 260dp;
|
||||||
|
max-width: 90%;
|
||||||
|
padding: 10dp 16dp 12dp;
|
||||||
|
border-radius: 7dp;
|
||||||
|
overflow: hidden;
|
||||||
|
filter: opacity(0);
|
||||||
|
transition: filter 0.2s linear-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline-progress[open] {
|
||||||
|
filter: opacity(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8dp;
|
||||||
|
font-size: 18dp;
|
||||||
|
font-weight: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
icon.pipeline-spinner {
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #C2A42D;
|
||||||
|
text-align: center;
|
||||||
|
transform-origin: center;
|
||||||
|
animation: 1s linear infinite pipeline-spinner-spin;
|
||||||
|
}
|
||||||
|
|
||||||
toast.achievement {
|
toast.achievement {
|
||||||
border: 1dp #C2A42D;
|
border: 1dp #C2A42D;
|
||||||
}
|
}
|
||||||
@@ -118,6 +159,14 @@ toast.achievement heading {
|
|||||||
color: #C2A42D;
|
color: #C2A42D;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toast.warning {
|
||||||
|
border: 1dp #C2A42D;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.warning heading {
|
||||||
|
color: #C2A42D;
|
||||||
|
}
|
||||||
|
|
||||||
toast.controller-warning {
|
toast.controller-warning {
|
||||||
top: auto;
|
top: auto;
|
||||||
right: auto;
|
right: auto;
|
||||||
@@ -310,6 +359,15 @@ logo img.outer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes pipeline-spinner-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-height: 640dp) {
|
@media (max-height: 640dp) {
|
||||||
toast {
|
toast {
|
||||||
top: 20dp;
|
top: 20dp;
|
||||||
|
|||||||
@@ -362,6 +362,10 @@ body.animate-in .intro-item {
|
|||||||
transition: opacity transform 0.3s 0.6s cubic-in-out;
|
transition: opacity transform 0.3s 0.6s cubic-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delay-6 {
|
||||||
|
transition: opacity transform 0.3s 0.7s cubic-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
/* Mobile layout */
|
/* Mobile layout */
|
||||||
@media (max-height: 640dp) {
|
@media (max-height: 640dp) {
|
||||||
.gradient {
|
.gradient {
|
||||||
|
|||||||
+12
-3
@@ -50,7 +50,8 @@ tab-bar[closable] tab-end-spacer {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
tab-bar[closable] close {
|
tab-bar[closable] close,
|
||||||
|
window > close {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 8dp;
|
top: 8dp;
|
||||||
@@ -70,12 +71,20 @@ tab-bar[closable] close {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tab-bar[closable] close:hover,
|
tab-bar[closable] close:hover,
|
||||||
tab-bar[closable] close:focus-visible {
|
tab-bar[closable] close:focus-visible,
|
||||||
|
window > close:hover,
|
||||||
|
window > close:focus-visible {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: rgba(194, 164, 45, 24%);
|
background-color: rgba(194, 164, 45, 24%);
|
||||||
}
|
}
|
||||||
|
|
||||||
tab-bar[closable] close:active {
|
window > close {
|
||||||
|
top: 16dp;
|
||||||
|
right: 16dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab-bar[closable] close:active,
|
||||||
|
window > close:active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: rgba(194, 164, 45, 40%);
|
background-color: rgba(194, 164, 45, 40%);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Dusklight Mod SDK entry point
|
||||||
|
#
|
||||||
|
# Provides game/service headers, compile definitions and version.h without
|
||||||
|
# configuring the full game tree.
|
||||||
|
#
|
||||||
|
# Usage (from a mod project):
|
||||||
|
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
|
||||||
|
# add_mod(my_mod SOURCES ... MOD_JSON mod.json)
|
||||||
|
#
|
||||||
|
# On Windows, pass -DDUSK_GAME_IMPLIB=<path to sdk/dusklight.lib> from the
|
||||||
|
# matching game release. TODO: auto-download from tag
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.25)
|
||||||
|
|
||||||
|
if (TARGET dusklight_game_headers)
|
||||||
|
message(FATAL_ERROR "Mod SDK already configured")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/WindowsTargetProcessor.cmake")
|
||||||
|
|
||||||
|
if (NOT CMAKE_BUILD_TYPE)
|
||||||
|
set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING
|
||||||
|
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
|
# Version detection & version.h
|
||||||
|
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/DetectVersion.cmake")
|
||||||
|
detect_version()
|
||||||
|
configure_version_header()
|
||||||
|
|
||||||
|
# Game ABI headers & compile definitions
|
||||||
|
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GameABIConfig.cmake")
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
set(DUSK_GAME_IMPLIB "" CACHE FILEPATH "Path to the dusklight import library (sdk/dusklight.lib)")
|
||||||
|
if (DUSK_GAME_IMPLIB AND NOT EXISTS "${DUSK_GAME_IMPLIB}")
|
||||||
|
message(FATAL_ERROR "Mod SDK: DUSK_GAME_IMPLIB does not exist: ${DUSK_GAME_IMPLIB}")
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/ModSDK.cmake")
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
// Mod SDK runtime (Windows): applies lld's MinGW-style runtime pseudo-relocations on
|
||||||
|
// the plain MSVC CRT. This is what lets mods reference game data (`extern` globals) with no
|
||||||
|
// __declspec(dllimport) annotations: `lld-link -lldmingw` auto-imports the data through IAT
|
||||||
|
// slots and records fixups, and this module applies them at load time.
|
||||||
|
//
|
||||||
|
// The fixup pass runs as an early CRT initializer, and the IAT slots it reads were already
|
||||||
|
// bound by the OS loader, so even mod static initializers observe patched references.
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
extern "C" char __RUNTIME_PSEUDO_RELOC_LIST__;
|
||||||
|
extern "C" char __RUNTIME_PSEUDO_RELOC_LIST_END__;
|
||||||
|
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct HdrV2 {
|
||||||
|
uint32_t magic1;
|
||||||
|
uint32_t magic2;
|
||||||
|
uint32_t version;
|
||||||
|
};
|
||||||
|
struct ItemV2 {
|
||||||
|
uint32_t sym; // RVA of the __imp_ slot (OS-bound IAT entry)
|
||||||
|
uint32_t target; // RVA of the reference to patch
|
||||||
|
uint32_t flags; // low 8 bits: bit width of the reference
|
||||||
|
};
|
||||||
|
|
||||||
|
bool g_relocsFailed = false;
|
||||||
|
|
||||||
|
void report(const char* fmt, ...) {
|
||||||
|
char buf[512];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
fprintf(stderr, "%s\n", buf);
|
||||||
|
OutputDebugStringA(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool compute_fixup(const ItemV2& item, char* base, intptr_t* out) {
|
||||||
|
char* impSlot = base + item.sym;
|
||||||
|
const intptr_t real = *reinterpret_cast<intptr_t*>(impSlot);
|
||||||
|
char* target = base + item.target;
|
||||||
|
const int bits = static_cast<int>(item.flags & 0xff);
|
||||||
|
|
||||||
|
intptr_t reldata;
|
||||||
|
switch (bits) {
|
||||||
|
case 8:
|
||||||
|
reldata = *reinterpret_cast<int8_t*>(target);
|
||||||
|
break;
|
||||||
|
case 16:
|
||||||
|
reldata = *reinterpret_cast<int16_t*>(target);
|
||||||
|
break;
|
||||||
|
case 32:
|
||||||
|
reldata = *reinterpret_cast<int32_t*>(target);
|
||||||
|
break;
|
||||||
|
case 64:
|
||||||
|
reldata = *reinterpret_cast<int64_t*>(target);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
report("unsupported %d-bit pseudo-relocation at RVA 0x%x", bits, item.target);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
reldata -= reinterpret_cast<intptr_t>(impSlot);
|
||||||
|
reldata += real;
|
||||||
|
|
||||||
|
if (bits < 64) {
|
||||||
|
const intptr_t maxUnsigned = (intptr_t{1} << bits) - 1;
|
||||||
|
const intptr_t minSigned = -(intptr_t{1} << (bits - 1));
|
||||||
|
if (reldata > maxUnsigned || reldata < minSigned) {
|
||||||
|
report("%d-bit data fixup at RVA 0x%x is out of range (delta %+lld)", bits, item.target,
|
||||||
|
static_cast<long long>(reldata));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out = reldata;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// lld refuses to emit runtime pseudo-relocs unless a function with exactly this (mingw CRT)
|
||||||
|
// name exists in the image. It is also our actual entry point.
|
||||||
|
extern "C" void _pei386_runtime_relocator() {
|
||||||
|
char* base = reinterpret_cast<char*>(&__ImageBase);
|
||||||
|
char* start = &__RUNTIME_PSEUDO_RELOC_LIST__;
|
||||||
|
char* end = &__RUNTIME_PSEUDO_RELOC_LIST_END__;
|
||||||
|
if (end - start < static_cast<ptrdiff_t>(sizeof(HdrV2))) {
|
||||||
|
return; // no data auto-imports in this mod
|
||||||
|
}
|
||||||
|
const HdrV2* hdr = reinterpret_cast<const HdrV2*>(start);
|
||||||
|
if (hdr->magic1 != 0 || hdr->magic2 != 0 || hdr->version != 1) {
|
||||||
|
report("unexpected pseudo-relocation list format");
|
||||||
|
g_relocsFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ItemV2* items = reinterpret_cast<const ItemV2*>(hdr + 1);
|
||||||
|
const ItemV2* itemsEnd = reinterpret_cast<const ItemV2*>(end);
|
||||||
|
|
||||||
|
// Validate everything before writing anything, so a bad fixup can't leave the image
|
||||||
|
// half-patched.
|
||||||
|
intptr_t scratch;
|
||||||
|
for (const ItemV2* it = items; it < itemsEnd; ++it) {
|
||||||
|
if (!compute_fixup(*it, base, &scratch)) {
|
||||||
|
g_relocsFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ItemV2* it = items; it < itemsEnd; ++it) {
|
||||||
|
intptr_t reldata;
|
||||||
|
compute_fixup(*it, base, &reldata);
|
||||||
|
char* target = base + it->target;
|
||||||
|
const size_t len = static_cast<size_t>(it->flags & 0xff) / 8;
|
||||||
|
DWORD old = 0;
|
||||||
|
if (!VirtualProtect(target, len, PAGE_EXECUTE_READWRITE, &old)) {
|
||||||
|
report("VirtualProtect failed at RVA 0x%x", it->target);
|
||||||
|
g_relocsFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::memcpy(target, &reldata, len);
|
||||||
|
VirtualProtect(target, len, old, &old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using PVFV = void (*)();
|
||||||
|
#pragma section(".CRT$XCB", read)
|
||||||
|
extern "C" __declspec(allocate(".CRT$XCB")) PVFV dusk_mod_pseudo_reloc_init =
|
||||||
|
_pei386_runtime_relocator;
|
||||||
|
|
||||||
|
BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID) {
|
||||||
|
if (reason == DLL_PROCESS_ATTACH && g_relocsFailed) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
#include "os_report.h"
|
#include "os_report.h"
|
||||||
|
|
||||||
Z2SoundObjBase::Z2SoundObjBase()
|
Z2SoundObjBase::Z2SoundObjBase()
|
||||||
#if DEBUG
|
#if PARTIAL_DEBUG || DEBUG
|
||||||
: JSULink<Z2SoundObjBase>(this)
|
: JSULink<Z2SoundObjBase>(this)
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -122,18 +122,11 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
|||||||
var_f31 /= dComIfGp_getCameraZoomScale(field_0x317c);
|
var_f31 /= dComIfGp_getCameraZoomScale(field_0x317c);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if TARGET_PC
|
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
|
||||||
if (dusk::getSettings().game.enableMouseAim && checkAimInputContext()) {
|
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
|
||||||
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) {
|
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
|
||||||
sp8 = mBodyAngle.x;
|
sp8 = mBodyAngle.x;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sp8 = mBodyAngle.x;
|
sp8 = mBodyAngle.x;
|
||||||
@@ -156,7 +149,7 @@ BOOL daAlink_c::setBodyAngleToCamera() {
|
|||||||
f32 final_yaw = 0.f;
|
f32 final_yaw = 0.f;
|
||||||
f32 final_pitch = 0.f;
|
f32 final_pitch = 0.f;
|
||||||
if (dusk::getSettings().game.enableMouseAim) {
|
if (dusk::getSettings().game.enableMouseAim) {
|
||||||
dusk::mouse::getAimDeltas(final_yaw, final_pitch);
|
dusk::mouse::get_aim_deltas(final_yaw, final_pitch);
|
||||||
}
|
}
|
||||||
if (dusk::getSettings().game.enableGyroAim) {
|
if (dusk::getSettings().game.enableGyroAim) {
|
||||||
f32 gyro_yaw = 0.f;
|
f32 gyro_yaw = 0.f;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "d/d_com_inf_game.h"
|
#include "d/d_com_inf_game.h"
|
||||||
|
|
||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
|
#include <aurora/texture.hpp>
|
||||||
#include "dusk/dvd_asset.hpp"
|
#include "dusk/dvd_asset.hpp"
|
||||||
#include "dusk/frame_interpolation.h"
|
#include "dusk/frame_interpolation.h"
|
||||||
|
|
||||||
@@ -40,6 +41,8 @@ static f32* l_texCoord_get() { alignas(32) static f32 buf[338]; static bool _
|
|||||||
//#define l_pos (l_pos_get())
|
//#define l_pos (l_pos_get())
|
||||||
#define l_normal (l_normal_get())
|
#define l_normal (l_normal_get())
|
||||||
#define l_texCoord (l_texCoord_get())
|
#define l_texCoord (l_texCoord_get())
|
||||||
|
|
||||||
|
static bool l_Egnd_mantTEX_hasReplacement = false;
|
||||||
#else
|
#else
|
||||||
#include "assets/l_Egnd_mantTEX.h"
|
#include "assets/l_Egnd_mantTEX.h"
|
||||||
|
|
||||||
@@ -223,6 +226,7 @@ void daMant_packet_c::draw() {
|
|||||||
GXInitTexObjCI(
|
GXInitTexObjCI(
|
||||||
&undersideTexObj, l_Egnd_mantTEX_U, 0x80, 0x80, GX_TF_C8, GX_CLAMP, GX_CLAMP, 0, 0);
|
&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);
|
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;
|
textureObjsInitialized = true;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
@@ -636,7 +640,11 @@ static int daMant_Execute(mant_class* i_this) {
|
|||||||
iVar8 = 0;
|
iVar8 = 0;
|
||||||
|
|
||||||
if (i_this->field_0x3967 != 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;
|
mant_cut_type = i_this->field_0x3967;
|
||||||
|
#endif
|
||||||
|
|
||||||
if (i_this->field_0x3968 < 15) {
|
if (i_this->field_0x3968 < 15) {
|
||||||
i_this->field_0x3968++;
|
i_this->field_0x3968++;
|
||||||
@@ -648,9 +656,18 @@ static int daMant_Execute(mant_class* i_this) {
|
|||||||
iVar8 = 20;
|
iVar8 = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
unaff_r29 = cM_rndF(65536.0f);
|
#if TARGET_PC
|
||||||
var_f31 = cM_rndFX(32.0f);
|
if (l_Egnd_mantTEX_hasReplacement) {
|
||||||
var_f30 = cM_rndFX(32.0f);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i_this->field_0x3967 = 0;
|
i_this->field_0x3967 = 0;
|
||||||
@@ -760,6 +777,8 @@ static int daMant_Create(fopAc_ac_c* i_this) {
|
|||||||
if(textureObjsInitialized) {
|
if(textureObjsInitialized) {
|
||||||
GXInitTlutObjData(&tlutObj, l_Egnd_mantPAL); // make sure the cached textures are updated
|
GXInitTlutObjData(&tlutObj, l_Egnd_mantPAL); // make sure the cached textures are updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_this->mMantRng.init(66, 16983, 855);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
lbl_277_bss_0 = 0;
|
lbl_277_bss_0 = 0;
|
||||||
|
|||||||
+5
-1
@@ -7602,6 +7602,10 @@ bool dCamera_c::executeDebugFlyCam() {
|
|||||||
sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f};
|
sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dusk::getSettings().game.enableMirrorMode) {
|
||||||
|
stickX *= -1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
f32 verticalDisp = 0.0f;
|
f32 verticalDisp = 0.0f;
|
||||||
if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
|
if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
|
||||||
verticalDisp += trigR;
|
verticalDisp += trigR;
|
||||||
@@ -7712,7 +7716,7 @@ bool dCamera_c::freeCamera() {
|
|||||||
|
|
||||||
f32 yaw_rad = 0.0f;
|
f32 yaw_rad = 0.0f;
|
||||||
f32 pitch_rad = 0.0f;
|
f32 pitch_rad = 0.0f;
|
||||||
dusk::mouse::getCameraDeltas(yaw_rad, pitch_rad);
|
dusk::mouse::get_camera_deltas(yaw_rad, pitch_rad);
|
||||||
if (dusk::getSettings().game.enableMouseCamera && (yaw_rad != 0.0f || pitch_rad != 0.0f) &&
|
if (dusk::getSettings().game.enableMouseCamera && (yaw_rad != 0.0f || pitch_rad != 0.0f) &&
|
||||||
!dComIfGp_checkCameraAttentionStatus(dComIfGp_getPlayerCameraID(0), 0x8))
|
!dComIfGp_checkCameraAttentionStatus(dComIfGp_getPlayerCameraID(0), 0x8))
|
||||||
{
|
{
|
||||||
|
|||||||
+32
-21
@@ -777,6 +777,7 @@ bool dFile_select_c::pointerDataSelect() {
|
|||||||
if (!dusk::menu_pointer::hit_pane(mSelFilePanes[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mSelFilePanes[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerDataSelectTarget, i));
|
||||||
const bool clicked = dusk::menu_pointer::consume_click();
|
const bool clicked = dusk::menu_pointer::consume_click();
|
||||||
if (mSelectNum != i) {
|
if (mSelectNum != i) {
|
||||||
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
|
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
|
||||||
@@ -805,6 +806,7 @@ bool dFile_select_c::pointerMenuSelect() {
|
|||||||
if (!dusk::menu_pointer::hit_pane(m3mSelPane[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(m3mSelPane[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerMenuSelectTarget, i));
|
||||||
const bool clicked = dusk::menu_pointer::consume_click();
|
const bool clicked = dusk::menu_pointer::consume_click();
|
||||||
if (!mIsDataNew[mSelectNum] && mSelectMenuNum != i) {
|
if (!mIsDataNew[mSelectNum] && mSelectMenuNum != i) {
|
||||||
mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0);
|
mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0);
|
||||||
@@ -833,6 +835,7 @@ bool dFile_select_c::pointerCopyDataToSelect() {
|
|||||||
if (!dusk::menu_pointer::hit_pane(mCpSelPane[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mCpSelPane[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerCopySelectTarget, i));
|
||||||
const bool clicked = dusk::menu_pointer::consume_click();
|
const bool clicked = dusk::menu_pointer::consume_click();
|
||||||
if (field_0x026b != i) {
|
if (field_0x026b != i) {
|
||||||
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
|
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
|
||||||
@@ -861,6 +864,7 @@ bool dFile_select_c::pointerYesNoSelect(bool errorSelect) {
|
|||||||
if (!dusk::menu_pointer::hit_pane(mYnSelPane[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mYnSelPane[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, i));
|
||||||
const bool clicked =
|
const bool clicked =
|
||||||
(!errorSelect || field_0x0268 == i) && dusk::menu_pointer::consume_click();
|
(!errorSelect || field_0x0268 == i) && dusk::menu_pointer::consume_click();
|
||||||
if (field_0x0268 != i) {
|
if (field_0x0268 != i) {
|
||||||
@@ -1103,12 +1107,13 @@ void dFile_select_c::dataSelectAnmSet() {
|
|||||||
void dFile_select_c::dataSelectMoveAnime() {
|
void dFile_select_c::dataSelectMoveAnime() {
|
||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
|
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
|
||||||
if (mSelectNum != 0xFF && dusk::menu_pointer::hit_pane(mSelFilePanes[mSelectNum], 8.0f) &&
|
if (mSelectNum != 0xFF && dusk::menu_pointer::hit_pane(mSelFilePanes[mSelectNum], 8.0f)) {
|
||||||
dusk::menu_pointer::consume_click())
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerDataSelectTarget, mSelectNum));
|
||||||
{
|
if (dusk::menu_pointer::consume_click()) {
|
||||||
dusk::menu_pointer::defer_activation(
|
dusk::menu_pointer::defer_activation(
|
||||||
dusk::menu_pointer::Context::FileSelect,
|
dusk::menu_pointer::Context::FileSelect,
|
||||||
pointer_target(s_pointerDataSelectTarget, mSelectNum));
|
pointer_target(s_pointerDataSelectTarget, mSelectNum));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
bool iVar7 = true;
|
bool iVar7 = true;
|
||||||
@@ -1494,12 +1499,14 @@ void dFile_select_c::menuSelectMoveAnm() {
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
|
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
|
||||||
if (mSelectMenuNum != 0xFF &&
|
if (mSelectMenuNum != 0xFF &&
|
||||||
dusk::menu_pointer::hit_pane(m3mSelPane[mSelectMenuNum], 8.0f) &&
|
dusk::menu_pointer::hit_pane(m3mSelPane[mSelectMenuNum], 8.0f))
|
||||||
dusk::menu_pointer::consume_click())
|
|
||||||
{
|
{
|
||||||
dusk::menu_pointer::defer_activation(
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum));
|
||||||
dusk::menu_pointer::Context::FileSelect,
|
if (dusk::menu_pointer::consume_click()) {
|
||||||
pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum));
|
dusk::menu_pointer::defer_activation(
|
||||||
|
dusk::menu_pointer::Context::FileSelect,
|
||||||
|
pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
bool tmp1 = true;
|
bool tmp1 = true;
|
||||||
@@ -1997,12 +2004,14 @@ void dFile_select_c::copyDataToSelectMoveAnm() {
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
|
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
|
||||||
if (field_0x026b != 0xFF &&
|
if (field_0x026b != 0xFF &&
|
||||||
dusk::menu_pointer::hit_pane(mCpSelPane[field_0x026b], 8.0f) &&
|
dusk::menu_pointer::hit_pane(mCpSelPane[field_0x026b], 8.0f))
|
||||||
dusk::menu_pointer::consume_click())
|
|
||||||
{
|
{
|
||||||
dusk::menu_pointer::defer_activation(
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerCopySelectTarget, field_0x026b));
|
||||||
dusk::menu_pointer::Context::FileSelect,
|
if (dusk::menu_pointer::consume_click()) {
|
||||||
pointer_target(s_pointerCopySelectTarget, field_0x026b));
|
dusk::menu_pointer::defer_activation(
|
||||||
|
dusk::menu_pointer::Context::FileSelect,
|
||||||
|
pointer_target(s_pointerCopySelectTarget, field_0x026b));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
bool iVar7 = true;
|
bool iVar7 = true;
|
||||||
@@ -2522,12 +2531,14 @@ void dFile_select_c::yesNoCursorMoveAnm() {
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
|
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect);
|
||||||
if (field_0x0268 != 0xFF &&
|
if (field_0x0268 != 0xFF &&
|
||||||
dusk::menu_pointer::hit_pane(mYnSelPane[field_0x0268], 8.0f) &&
|
dusk::menu_pointer::hit_pane(mYnSelPane[field_0x0268], 8.0f))
|
||||||
dusk::menu_pointer::consume_click())
|
|
||||||
{
|
{
|
||||||
dusk::menu_pointer::defer_activation(
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, field_0x0268));
|
||||||
dusk::menu_pointer::Context::FileSelect,
|
if (dusk::menu_pointer::consume_click()) {
|
||||||
pointer_target(s_pointerYesNoSelectTarget, field_0x0268));
|
dusk::menu_pointer::defer_activation(
|
||||||
|
dusk::menu_pointer::Context::FileSelect,
|
||||||
|
pointer_target(s_pointerYesNoSelectTarget, field_0x0268));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
bool isYnSelMove = yesnoSelectMoveAnm();
|
bool isYnSelMove = yesnoSelectMoveAnm();
|
||||||
|
|||||||
+6
-1
@@ -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;
|
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX;
|
||||||
mCenterZ -= mPackZ;
|
mCenterZ -= mPackZ;
|
||||||
mCenterX += field_0x64;
|
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -field_0x64 : ) field_0x64;
|
||||||
mCenterZ += mPackPlusZ;
|
mCenterZ += mPackPlusZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1649,6 +1649,8 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
|||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
field_0x64 = 33830.0f;
|
field_0x64 = 33830.0f;
|
||||||
|
#elif TARGET_PC
|
||||||
|
field_0x64 = dusk::getSettings().game.enableMirrorMode ? 33830.0f : 0.0f;
|
||||||
#else
|
#else
|
||||||
field_0x64 = 0.0f;
|
field_0x64 = 0.0f;
|
||||||
#endif
|
#endif
|
||||||
@@ -1657,6 +1659,9 @@ 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;
|
f32 temp = (field_0x58 * (f32)(field_0x74 + 4)) * 0.5f;
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
mRightEdgePlus = -(((dMpath_c::getMinZ() - (-127103.67f)) - temp) / field_0x58);
|
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
|
#else
|
||||||
mRightEdgePlus = 0.0f;
|
mRightEdgePlus = 0.0f;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1960,6 +1960,7 @@ bool dMenu_Collect2D_c::pointerWait() {
|
|||||||
if (getItemTag(x, y, true) == 0 || !dusk::menu_pointer::hit_pane(mpSelPm[x][y], 8.0f)) {
|
if (getItemTag(x, y, true) == 0 || !dusk::menu_pointer::hit_pane(mpSelPm[x][y], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(static_cast<dusk::menu_pointer::TargetId>(x + y * 7));
|
||||||
if (mCursorX != x || mCursorY != y) {
|
if (mCursorX != x || mCursorY != y) {
|
||||||
mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0);
|
mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0);
|
||||||
mCursorX = x;
|
mCursorX = x;
|
||||||
|
|||||||
+24
-1
@@ -914,6 +914,20 @@ void dMenu_DmapBg_c::dMapBgWide() {
|
|||||||
void dMenu_DmapBg_c::draw() {
|
void dMenu_DmapBg_c::draw() {
|
||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
dMapBgWide();
|
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
|
#endif
|
||||||
|
|
||||||
u32 scissor_left;
|
u32 scissor_left;
|
||||||
@@ -960,6 +974,15 @@ void dMenu_DmapBg_c::draw() {
|
|||||||
mpBackTexture->setAlpha(dVar17 * (field_0xdbc * field_0xd9c));
|
mpBackTexture->setAlpha(dVar17 * (field_0xdbc * field_0xd9c));
|
||||||
|
|
||||||
f32 local_28c = mpBackTexture->getBounds().i.x;
|
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->setBlackWhite(color_black, color_white);
|
||||||
mpBackTexture->draw(local_28c, field_0xd94 + mpBackTexture->getBounds().i.y, mpBackTexture->getWidth(),
|
mpBackTexture->draw(local_28c, field_0xd94 + mpBackTexture->getBounds().i.y, mpBackTexture->getWidth(),
|
||||||
mpBackTexture->getHeight(),
|
mpBackTexture->getHeight(),
|
||||||
@@ -1981,7 +2004,7 @@ void dMenu_Dmap_c::mapControl() {
|
|||||||
f32 temp_f28 = (var_f29 / 100.0f) * var_f31;
|
f32 temp_f28 = (var_f29 / 100.0f) * var_f31;
|
||||||
f32 sp18 = temp_f28 * cM_ssin(stick_angle);
|
f32 sp18 = temp_f28 * cM_ssin(stick_angle);
|
||||||
f32 sp14 = temp_f28 * cM_scos(stick_angle);
|
f32 sp14 = temp_f28 * cM_scos(stick_angle);
|
||||||
mMapCtrl->setPlusZoomCenterX(sp18);
|
mMapCtrl->setPlusZoomCenterX(IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -sp18 :) sp18);
|
||||||
mMapCtrl->setPlusZoomCenterZ(sp14);
|
mMapCtrl->setPlusZoomCenterZ(sp14);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -359,14 +359,7 @@ f32 dMenu_StageMapCtrl_c::getPixelStageSizeZ() const {
|
|||||||
|
|
||||||
f32 dMenu_StageMapCtrl_c::getPixelCenterX() const {
|
f32 dMenu_StageMapCtrl_c::getPixelCenterX() const {
|
||||||
f32 var_f31 = dMpath_c::getCenterX();
|
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);
|
return (1.0f / field_0xbc) * (field_0x9c - var_f31);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f32 dMenu_StageMapCtrl_c::getPixelCenterZ() const {
|
f32 dMenu_StageMapCtrl_c::getPixelCenterZ() const {
|
||||||
@@ -425,18 +418,7 @@ inline static f32 rightModeCnvPos(f32 param_0) {
|
|||||||
void dMenu_StageMapCtrl_c::cnvPosTo2Dpos(f32 param_0, f32 param_1, f32* param_2,
|
void dMenu_StageMapCtrl_c::cnvPosTo2Dpos(f32 param_0, f32 param_1, f32* param_2,
|
||||||
f32* param_3) const {
|
f32* param_3) const {
|
||||||
if (param_2 != NULL) {
|
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));
|
*param_2 = (0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (param_0 - field_0x9c));
|
||||||
#endif
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param_3 != NULL) {
|
if (param_3 != NULL) {
|
||||||
@@ -933,8 +915,7 @@ void dMenu_StageMapCtrl_c::move() {
|
|||||||
|
|
||||||
void dMenu_DmapMapCtrl_c::draw() {
|
void dMenu_DmapMapCtrl_c::draw() {
|
||||||
if (field_0xef != 0) {
|
if (field_0xef != 0) {
|
||||||
setPos(field_0xeb, field_0xec,
|
setPos(field_0xeb, field_0xec, field_0x9c,
|
||||||
IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -field_0x9c :) field_0x9c,
|
|
||||||
field_0xa0, field_0xbc, true, field_0xd8);
|
field_0xa0, field_0xbc, true, field_0xd8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-15
@@ -931,17 +931,8 @@ void dMenu_Fmap_c::region_map_proc() {
|
|||||||
mpDraw2DBack->regionMapMove(mpStick);
|
mpDraw2DBack->regionMapMove(mpStick);
|
||||||
int stage_no, room_no;
|
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()
|
f32 pos_x = mpDraw2DBack->getArrowPos2DX() - mDoGph_gInf_c::getMinXF()
|
||||||
- mDoGph_gInf_c::getWidthF() * 0.5f;
|
- mDoGph_gInf_c::getWidthF() * 0.5f;
|
||||||
#endif
|
|
||||||
f32 pos_y = mpDraw2DBack->getArrowPos2DY() - mDoGph_gInf_c::getHeightF() * 0.5f;
|
f32 pos_y = mpDraw2DBack->getArrowPos2DY() - mDoGph_gInf_c::getHeightF() * 0.5f;
|
||||||
|
|
||||||
mpMenuFmapMap->getPointStagePathInnerNo(getNowFmapRegionData(), pos_x, pos_y,
|
mpMenuFmapMap->getPointStagePathInnerNo(getNowFmapRegionData(), pos_x, pos_y,
|
||||||
@@ -2486,12 +2477,6 @@ void dMenu_Fmap_c::portalWarpMapMove(STControl* i_stick) {
|
|||||||
f32 arrow_y = mpDraw2DBack->getArrowPos2DY();
|
f32 arrow_y = mpDraw2DBack->getArrowPos2DY();
|
||||||
u8 uVar6 = 0xff;
|
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++) {
|
for (int i = 0; i < portal_dat->mCount; i++) {
|
||||||
if (portals[i].mRegionNo == mpDraw2DBack->getRegionCursor() + 1
|
if (portals[i].mRegionNo == mpDraw2DBack->getRegionCursor() + 1
|
||||||
@@ -2561,6 +2546,11 @@ void dMenu_Fmap_c::drawIcon(f32 param_0, bool param_1) {
|
|||||||
if (mProcess == PROC_PORTAL_DEMO1) {
|
if (mProcess == PROC_PORTAL_DEMO1) {
|
||||||
is_portal_demo1 = 1;
|
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),
|
mpDraw2DBack->setIcon2DPos(0x11, stage_name, pos.x, pos.z, cM_sht2d(angle),
|
||||||
is_portal_demo1, param_1);
|
is_portal_demo1, param_1);
|
||||||
|
|
||||||
@@ -2649,6 +2639,11 @@ void dMenu_Fmap_c::drawPlayEnterIcon() {
|
|||||||
angle = dComIfGs_getPlayerFieldLastStayAngleY();
|
angle = dComIfGs_getPlayerFieldLastStayAngleY();
|
||||||
SAFE_STRCPY(stage_name, dComIfGs_getPlayerFieldLastStayName());
|
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);
|
mpDraw2DBack->setIcon2DPos(0x15, stage_name, pos.x, pos.z, cM_sht2d(angle), 0, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-18
@@ -426,7 +426,15 @@ void dMenu_Fmap2DBack_c::draw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mpPointParent->setAlphaRate(mArrowAlpha * mSpotTextureFadeAlpha);
|
mpPointParent->setAlphaRate(mArrowAlpha * mSpotTextureFadeAlpha);
|
||||||
mpPointParent->translate(mArrowPos2DX + mTransX, mArrowPos2DY + mTransZ);
|
|
||||||
|
f32 drawX = mArrowPos2DX + mTransX;
|
||||||
|
#ifdef TARGET_PC
|
||||||
|
if (dusk::getSettings().game.enableMirrorMode) {
|
||||||
|
drawX = getMirrorPosX(drawX, 0.0f);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
mpPointParent->translate(drawX, mArrowPos2DY + mTransZ);
|
||||||
mpPointScreen->draw(0.0f, 0.0f, grafPort);
|
mpPointScreen->draw(0.0f, 0.0f, grafPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,6 +1026,11 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
|
|||||||
f32 delta_y = speed * cM_ssin(angle);
|
f32 delta_y = speed * cM_ssin(angle);
|
||||||
f32 delta_x = speed * cM_scos(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_xpos = control_xpos + delta_y;
|
||||||
control_ypos = control_ypos + delta_x;
|
control_ypos = control_ypos + delta_x;
|
||||||
}
|
}
|
||||||
@@ -1046,11 +1059,6 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
|
|||||||
calcAllMapPos2D((mArrowPos3DX + control_xpos) - mStageTransX,
|
calcAllMapPos2D((mArrowPos3DX + control_xpos) - mStageTransX,
|
||||||
(mArrowPos3DZ + control_ypos) - mStageTransZ, &sp14, &sp10);
|
(mArrowPos3DZ + control_ypos) - mStageTransZ, &sp14, &sp10);
|
||||||
|
|
||||||
#if TARGET_PC
|
|
||||||
if (dusk::getSettings().game.enableMirrorMode) {
|
|
||||||
sp14 = getMirrorPosX(sp14, 0.0f);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
mSelectRegion = 0xff;
|
mSelectRegion = 0xff;
|
||||||
for (int i = 7; i >= 0; i--) {
|
for (int i = 7; i >= 0; i--) {
|
||||||
@@ -1465,6 +1473,12 @@ void dMenu_Fmap2DBack_c::worldGridDraw() {
|
|||||||
f32 dVar8 = -mStageTransZ;
|
f32 dVar8 = -mStageTransZ;
|
||||||
calcAllMapPos2D(dVar9, dVar8, &local_74, &local_78);
|
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,
|
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||||
JUtility::TColor(255, 255, 255, 255), 6);
|
JUtility::TColor(255, 255, 255, 255), 6);
|
||||||
@@ -1473,6 +1487,11 @@ void dMenu_Fmap2DBack_c::worldGridDraw() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
|
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
|
||||||
if (local_74 >= getMapScissorAreaLX()) {
|
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,
|
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||||
JUtility::TColor(255, 255, 255, 255), 6);
|
JUtility::TColor(255, 255, 255, 255), 6);
|
||||||
@@ -1534,6 +1553,12 @@ void dMenu_Fmap2DBack_c::regionGridDraw() {
|
|||||||
f32 dVar8 = mRegionOriginZ[mRegionCursor] - mStageTransZ;
|
f32 dVar8 = mRegionOriginZ[mRegionCursor] - mStageTransZ;
|
||||||
calcAllMapPos2D(dVar9, dVar8, &local_74, &local_78);
|
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,
|
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||||
JUtility::TColor(180, 0, 0, 255), 6);
|
JUtility::TColor(180, 0, 0, 255), 6);
|
||||||
@@ -1542,6 +1567,12 @@ void dMenu_Fmap2DBack_c::regionGridDraw() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
|
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
|
||||||
if (local_74 >= getMapScissorAreaLX()) {
|
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,
|
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||||
JUtility::TColor(180, 0, 0, 255), 6);
|
JUtility::TColor(180, 0, 0, 255), 6);
|
||||||
@@ -1604,6 +1635,12 @@ void dMenu_Fmap2DBack_c::worldOriginDraw() {
|
|||||||
f32 local_44, local_48;
|
f32 local_44, local_48;
|
||||||
calcAllMapPos2D(-mStageTransX, -mStageTransZ, &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(),
|
J2DDrawLine(mDoGph_gInf_c::getMinXF(), local_48 - local_44 + mDoGph_gInf_c::getMinXF(),
|
||||||
mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF(),
|
mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF(),
|
||||||
local_48 - local_44 + (mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF()),
|
local_48 - local_44 + (mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF()),
|
||||||
@@ -1638,6 +1675,13 @@ void dMenu_Fmap2DBack_c::scrollAreaDraw() {
|
|||||||
calcAllMapPos2D(x_min - mStageTransX, z_min - mStageTransZ, &local_4c, &local_50);
|
calcAllMapPos2D(x_min - mStageTransX, z_min - mStageTransZ, &local_4c, &local_50);
|
||||||
calcAllMapPos2D(x_max - mStageTransX, z_max - mStageTransZ, &local_54, &local_58);
|
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,
|
J2DDrawLine(local_4c, local_50, local_4c, local_58,
|
||||||
JUtility::TColor(255, 255, 255, 255), 6);
|
JUtility::TColor(255, 255, 255, 255), 6);
|
||||||
J2DDrawLine(local_54, local_50, local_54, local_58,
|
J2DDrawLine(local_54, local_50, local_54, local_58,
|
||||||
@@ -1658,6 +1702,11 @@ void dMenu_Fmap2DBack_c::regionOriginDraw() {
|
|||||||
f32 center_x, center_y;
|
f32 center_x, center_y;
|
||||||
calcAllMapPos2D(mRegionOriginX[i] - mStageTransX, mRegionOriginZ[i] - mStageTransZ,
|
calcAllMapPos2D(mRegionOriginX[i] - mStageTransX, mRegionOriginZ[i] - mStageTransZ,
|
||||||
¢er_x, ¢er_y);
|
¢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));
|
J2DFillBox(center_x - 3.0f, center_y - 3.0f, 6.0f, 6.0f, JUtility::TColor(255, 0, 0, 255));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1675,6 +1724,11 @@ void dMenu_Fmap2DBack_c::stageOriginDraw() {
|
|||||||
f32 v1 = mRegionOriginX[mRegionCursor] + stage_data[i].mOffsetX - mStageTransX;
|
f32 v1 = mRegionOriginX[mRegionCursor] + stage_data[i].mOffsetX - mStageTransX;
|
||||||
f32 v2 = mRegionOriginZ[mRegionCursor] + stage_data[i].mOffsetZ - mStageTransZ;
|
f32 v2 = mRegionOriginZ[mRegionCursor] + stage_data[i].mOffsetZ - mStageTransZ;
|
||||||
calcAllMapPos2D(v1, v2, ¢er_x, ¢er_y);
|
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,
|
J2DFillBox(center_x - 3.0f, center_y - 3.0f, 6.0f, 6.0f,
|
||||||
JUtility::TColor(0, 0, 255, 255));
|
JUtility::TColor(0, 0, 255, 255));
|
||||||
}
|
}
|
||||||
@@ -1921,7 +1975,7 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) {
|
|||||||
f32 speed = base_speed / 100.0f * local_78;
|
f32 speed = base_speed / 100.0f * local_78;
|
||||||
f32 speed_y = speed * cM_ssin(angle);
|
f32 speed_y = speed * cM_ssin(angle);
|
||||||
f32 speed_x = speed * cM_scos(angle);
|
f32 speed_x = speed * cM_scos(angle);
|
||||||
control_xpos += speed_y;
|
control_xpos += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_y :) speed_y;
|
||||||
control_ypos += speed_x;
|
control_ypos += speed_x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1946,11 +2000,6 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) {
|
|||||||
calcAllMapPos2D(mArrowPos3DX + control_xpos - mStageTransX,
|
calcAllMapPos2D(mArrowPos3DX + control_xpos - mStageTransX,
|
||||||
mArrowPos3DZ + control_ypos - mStageTransZ, &pos_x, &pos_y);
|
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;
|
mSelectRegion = 0xff;
|
||||||
int region = mRegionCursor;
|
int region = mRegionCursor;
|
||||||
@@ -1982,11 +2031,6 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param
|
|||||||
if (stick_value >= slow_bound && param_2 && field_0x1238 != 2) {
|
if (stick_value >= slow_bound && param_2 && field_0x1238 != 2) {
|
||||||
bVar6 = true;
|
bVar6 = true;
|
||||||
s16 angle = i_stick->getAngleStick();
|
s16 angle = i_stick->getAngleStick();
|
||||||
#if TARGET_PC
|
|
||||||
if (dusk::getSettings().game.enableMirrorMode) {
|
|
||||||
angle = -angle;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
f32 local_68 = mTexMaxX - mTexMinX;
|
f32 local_68 = mTexMaxX - mTexMinX;
|
||||||
f32 spot_zoom = getSpotMapZoomRate();
|
f32 spot_zoom = getSpotMapZoomRate();
|
||||||
f32 region_zoom = getRegionMapZoomRate(mRegionCursor);
|
f32 region_zoom = getRegionMapZoomRate(mRegionCursor);
|
||||||
@@ -2001,7 +2045,7 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param
|
|||||||
f32 speed = base_speed / 100.0f * local_78;
|
f32 speed = base_speed / 100.0f * local_78;
|
||||||
f32 speed_x = speed * cM_ssin(angle);
|
f32 speed_x = speed * cM_ssin(angle);
|
||||||
f32 speed_z = speed * cM_scos(angle);
|
f32 speed_z = speed * cM_scos(angle);
|
||||||
mStageTransX += speed_x;
|
mStageTransX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_x :) speed_x;
|
||||||
mStageTransZ += speed_z;
|
mStageTransZ += speed_z;
|
||||||
} else if (!param_2) {
|
} else if (!param_2) {
|
||||||
return;
|
return;
|
||||||
@@ -2095,6 +2139,11 @@ void dMenu_Fmap2DBack_c::drawDebugStageArea() {
|
|||||||
if (stage_no >= 0) {
|
if (stage_no >= 0) {
|
||||||
f32 v = i + mDoGph_gInf_c::getMinXF();
|
f32 v = i + mDoGph_gInf_c::getMinXF();
|
||||||
f32 v2 = j;
|
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]);
|
J2DFillBox(v - 3.0f, v2 - 3.0f, 6.0f, 6.0f, colors[stage_no % 6]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2130,6 +2179,11 @@ void dMenu_Fmap2DBack_c::drawDebugRegionArea() {
|
|||||||
mRegionMapSizeX[region] * mZoom, mRegionMapSizeY[region] * mZoom,
|
mRegionMapSizeX[region] * mZoom, mRegionMapSizeY[region] * mZoom,
|
||||||
mpAreaTex[region]->getTexture(0)->getTexInfo());
|
mpAreaTex[region]->getTexture(0)->getTexInfo());
|
||||||
if (u) {
|
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]);
|
J2DFillBox(pos_x - 3.0f, pos_y - 3.0f, 6.0f, 6.0f, colors[region]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,6 +320,7 @@ bool dMenu_Insect_c::pointerWait() {
|
|||||||
if (!isGetInsect(x, y) || !dusk::menu_pointer::hit_pane(mpINSParent[index], 8.0f)) {
|
if (!isGetInsect(x, y) || !dusk::menu_pointer::hit_pane(mpINSParent[index], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(index);
|
||||||
|
|
||||||
if (field_0xf4 != x || field_0xf5 != y) {
|
if (field_0xf4 != x || field_0xf5 != y) {
|
||||||
field_0xf4 = x;
|
field_0xf4 = x;
|
||||||
|
|||||||
@@ -482,6 +482,7 @@ bool dMenu_Letter_c::pointerWait() {
|
|||||||
if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(i);
|
||||||
|
|
||||||
if (mIndex != i) {
|
if (mIndex != i) {
|
||||||
mIndex = i;
|
mIndex = i;
|
||||||
|
|||||||
@@ -394,21 +394,8 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
|
|||||||
icon_size_y *= _c7c;
|
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,
|
mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z,
|
||||||
mIconInfo[info_idx].rotation);
|
mIconInfo[info_idx].rotation);
|
||||||
#endif
|
|
||||||
|
|
||||||
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
|
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
|
||||||
mPictures[mIconInfo[info_idx].icon_no]->setAlpha((180.0f * _c80) / 255.0f);
|
mPictures[mIconInfo[info_idx].icon_no]->setAlpha((180.0f * _c80) / 255.0f);
|
||||||
@@ -423,10 +410,15 @@ 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));
|
f32 pos_x = i_posX + (icon_pos_x - (icon_size_x / 2));
|
||||||
|
bool r4 = false;
|
||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
if (dusk::getSettings().game.enableMirrorMode) {
|
if (dusk::getSettings().game.enableMirrorMode) {
|
||||||
pos_x = getMirrorCenterPosX(i_posX + (icon_pos_x - (icon_size_x / 2)), icon_size_x / 2);
|
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
|
#endif
|
||||||
|
|
||||||
mPictures[mIconInfo[info_idx].icon_no]->draw(pos_x, (i_posY + (icon_pos_y - icon_size_y / 2)),
|
mPictures[mIconInfo[info_idx].icon_no]->draw(pos_x, (i_posY + (icon_pos_y - icon_size_y / 2)),
|
||||||
@@ -435,7 +427,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) {
|
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
|
||||||
mLightDropPic->draw((pos_x + (icon_size_x / 2)) - (var_f29 / 2),
|
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),
|
((icon_size_y / 2) + (i_posY + (icon_pos_y - icon_size_y / 2))) - (var_f28 / 2),
|
||||||
var_f29, var_f28, false, false, false);
|
var_f29, var_f28, r4, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-5
@@ -83,6 +83,12 @@ enum SelectType {
|
|||||||
SelectType8,
|
SelectType8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if TARGET_PC
|
||||||
|
static dusk::menu_pointer::TargetId option_yes_no_target(u8 index) noexcept {
|
||||||
|
return static_cast<dusk::menu_pointer::TargetId>(0x100 + index);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
dMenu_Option_c::dMenu_Option_c(JKRArchive* i_archive, STControl* i_stick) {
|
dMenu_Option_c::dMenu_Option_c(JKRArchive* i_archive, STControl* i_stick) {
|
||||||
mUseFlag = 0;
|
mUseFlag = 0;
|
||||||
mBarScale[0] = g_drawHIO.mOptionScreen.mBarScale[0];
|
mBarScale[0] = g_drawHIO.mOptionScreen.mBarScale[0];
|
||||||
@@ -1098,18 +1104,28 @@ void dMenu_Option_c::confirm_move_move() {
|
|||||||
if (!dusk::menu_pointer::hit_pane(mpYesNoSelBase_c[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mpYesNoSelBase_c[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(option_yes_no_target(i));
|
||||||
|
const bool clicked = dusk::menu_pointer::consume_click();
|
||||||
if (field_0x3f9 != i) {
|
if (field_0x3f9 != i) {
|
||||||
Z2GetAudioMgr()->seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0, 1.0f, 1.0f, -1.0f,
|
Z2GetAudioMgr()->seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0, 1.0f, 1.0f, -1.0f,
|
||||||
-1.0f, 0);
|
-1.0f, 0);
|
||||||
field_0x3fa = field_0x3f9;
|
field_0x3fa = field_0x3f9;
|
||||||
field_0x3f9 = i;
|
field_0x3f9 = i;
|
||||||
|
if (clicked) {
|
||||||
|
yesNoSelectStart();
|
||||||
|
field_0x3ef = SelectType7;
|
||||||
|
dMeter2Info_set2DVibrationM();
|
||||||
|
mpWarning->_move();
|
||||||
|
setAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
yesnoSelectAnmSet();
|
yesnoSelectAnmSet();
|
||||||
field_0x3ef = SelectType6;
|
field_0x3ef = SelectType6;
|
||||||
mpWarning->_move();
|
mpWarning->_move();
|
||||||
setAnimation();
|
setAnimation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (dusk::menu_pointer::consume_click()) {
|
if (clicked) {
|
||||||
yesNoSelectStart();
|
yesNoSelectStart();
|
||||||
field_0x3ef = SelectType7;
|
field_0x3ef = SelectType7;
|
||||||
dMeter2Info_set2DVibrationM();
|
dMeter2Info_set2DVibrationM();
|
||||||
@@ -1156,11 +1172,36 @@ void dMenu_Option_c::confirm_select_init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dMenu_Option_c::confirm_select_move() {
|
void dMenu_Option_c::confirm_select_move() {
|
||||||
|
#if TARGET_PC
|
||||||
|
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
|
||||||
|
if (field_0x3f9 != 0xff &&
|
||||||
|
dusk::menu_pointer::hit_pane(mpYesNoSelBase_c[field_0x3f9], 8.0f))
|
||||||
|
{
|
||||||
|
const dusk::menu_pointer::TargetId target = option_yes_no_target(field_0x3f9);
|
||||||
|
dusk::menu_pointer::set_hover_target(target);
|
||||||
|
if (dusk::menu_pointer::consume_click()) {
|
||||||
|
dusk::menu_pointer::defer_activation(dusk::menu_pointer::Context::Options, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
u8 selectMoveAnm = yesnoSelectMoveAnm();
|
u8 selectMoveAnm = yesnoSelectMoveAnm();
|
||||||
u8 wakuAlphaAnm = yesnoWakuAlpahAnm(field_0x3fa);
|
u8 wakuAlphaAnm = yesnoWakuAlpahAnm(field_0x3fa);
|
||||||
|
|
||||||
if (selectMoveAnm == 1 && wakuAlphaAnm == 1) {
|
if (selectMoveAnm == 1 && wakuAlphaAnm == 1) {
|
||||||
yesnoCursorShow();
|
yesnoCursorShow();
|
||||||
|
#if TARGET_PC
|
||||||
|
if (field_0x3f9 != 0xff &&
|
||||||
|
dusk::menu_pointer::consume_deferred_activation(
|
||||||
|
dusk::menu_pointer::Context::Options, option_yes_no_target(field_0x3f9)))
|
||||||
|
{
|
||||||
|
yesNoSelectStart();
|
||||||
|
field_0x3ef = SelectType7;
|
||||||
|
dMeter2Info_set2DVibrationM();
|
||||||
|
mpWarning->_move();
|
||||||
|
setAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
field_0x3ef = SelectType5;
|
field_0x3ef = SelectType5;
|
||||||
}
|
}
|
||||||
mpWarning->_move();
|
mpWarning->_move();
|
||||||
@@ -2196,16 +2237,14 @@ bool dMenu_Option_c::isRumbleSupported() {
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
bool dMenu_Option_c::pointerConfirmSelect() {
|
bool dMenu_Option_c::pointerConfirmSelect() {
|
||||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
|
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options);
|
||||||
if (!dusk::menu_pointer::state().clicked) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (u8 i = 0; i < SelectType3; ++i) {
|
for (u8 i = 0; i < SelectType3; ++i) {
|
||||||
if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
|
if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
|
||||||
|
dusk::menu_pointer::set_hover_target(i);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dusk::menu_pointer::set_hover_target(0x200);
|
||||||
if (!dusk::menu_pointer::consume_click()) {
|
if (!dusk::menu_pointer::consume_click()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2226,6 +2265,7 @@ bool dMenu_Option_c::dpdMenuMove() {
|
|||||||
if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(i);
|
||||||
if (getSelectType() != i) {
|
if (getSelectType() != i) {
|
||||||
field_0x3ef = i;
|
field_0x3ef = i;
|
||||||
setCursorPos(i);
|
setCursorPos(i);
|
||||||
|
|||||||
+23
-1
@@ -198,6 +198,7 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i
|
|||||||
mCursorInterpPrevAngular = false;
|
mCursorInterpPrevAngular = false;
|
||||||
mCursorInterpCurrAngular = false;
|
mCursorInterpCurrAngular = false;
|
||||||
mCursorInterpInit = false;
|
mCursorInterpInit = false;
|
||||||
|
mPointerTouchPressHoveredCurrent = false;
|
||||||
#endif
|
#endif
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
field_0x674[i] = 0;
|
field_0x674[i] = 0;
|
||||||
@@ -1561,6 +1562,10 @@ bool dMenu_Ring_c::pointerMove() {
|
|||||||
if (hoveredSlot < 0) {
|
if (hoveredSlot < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (pointer.pressed) {
|
||||||
|
mPointerTouchPressHoveredCurrent = pointer.touch && hoveredSlot == mCurrentSlot;
|
||||||
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(static_cast<dusk::menu_pointer::TargetId>(hoveredSlot));
|
||||||
|
|
||||||
if (mCurrentSlot != hoveredSlot) {
|
if (mCurrentSlot != hoveredSlot) {
|
||||||
mDirectSelectCursorPos.x = mItemSlotPosX[mCurrentSlot];
|
mDirectSelectCursorPos.x = mItemSlotPosX[mCurrentSlot];
|
||||||
@@ -1573,10 +1578,27 @@ bool dMenu_Ring_c::pointerMove() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dusk::menu_pointer::consume_click()) {
|
const bool clickOpensExplain = !pointer.touch || mPointerTouchPressHoveredCurrent;
|
||||||
|
if (clickOpensExplain && dusk::menu_pointer::consume_click()) {
|
||||||
|
const u8 item = dComIfGs_getItem(mItemSlots[mCurrentSlot], false);
|
||||||
|
if (!dMeter2Info_isTouchKeyCheck(0xe) && openExplain(item)) {
|
||||||
|
dMeter2Info_setItemExplainWindowStatus(1);
|
||||||
|
field_0x6c4 = mCurrentSlot;
|
||||||
|
setStatus(STATUS_EXPLAIN);
|
||||||
|
dMeter2Info_set2DVibration();
|
||||||
|
setDoStatus(0);
|
||||||
|
} else {
|
||||||
|
Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f,
|
||||||
|
-1.0f, 0);
|
||||||
|
}
|
||||||
|
mPointerTouchPressHoveredCurrent = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pointer.released) {
|
||||||
|
mPointerTouchPressHoveredCurrent = false;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+16
-10
@@ -1820,6 +1820,7 @@ bool dMenu_save_c::pointerSaveSelect() {
|
|||||||
if (!dusk::menu_pointer::hit_pane(mpSelData[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mpSelData[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerSaveSelectTarget, i));
|
||||||
const bool clicked = dusk::menu_pointer::consume_click();
|
const bool clicked = dusk::menu_pointer::consume_click();
|
||||||
if (mSelectedFile != i) {
|
if (mSelectedFile != i) {
|
||||||
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
|
mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0);
|
||||||
@@ -1848,6 +1849,7 @@ bool dMenu_save_c::pointerYesNoSelect(bool errorSelect, u8 errParam, u8 soundPar
|
|||||||
if (!dusk::menu_pointer::hit_pane(mpNoYes[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mpNoYes[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, i));
|
||||||
const bool clicked =
|
const bool clicked =
|
||||||
(!errorSelect || mYesNoCursor == i) && dusk::menu_pointer::consume_click();
|
(!errorSelect || mYesNoCursor == i) && dusk::menu_pointer::consume_click();
|
||||||
if (mYesNoCursor != i) {
|
if (mYesNoCursor != i) {
|
||||||
@@ -1952,12 +1954,14 @@ void dMenu_save_c::saveSelectMoveAnime() {
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save);
|
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save);
|
||||||
if (mSelectedFile != 0xFF &&
|
if (mSelectedFile != 0xFF &&
|
||||||
dusk::menu_pointer::hit_pane(mpSelData[mSelectedFile], 8.0f) &&
|
dusk::menu_pointer::hit_pane(mpSelData[mSelectedFile], 8.0f))
|
||||||
dusk::menu_pointer::consume_click())
|
|
||||||
{
|
{
|
||||||
dusk::menu_pointer::defer_activation(
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerSaveSelectTarget, mSelectedFile));
|
||||||
dusk::menu_pointer::Context::Save,
|
if (dusk::menu_pointer::consume_click()) {
|
||||||
pointer_target(s_pointerSaveSelectTarget, mSelectedFile));
|
dusk::menu_pointer::defer_activation(
|
||||||
|
dusk::menu_pointer::Context::Save,
|
||||||
|
pointer_target(s_pointerSaveSelectTarget, mSelectedFile));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
bool bookWakuAnmComplete = true;
|
bool bookWakuAnmComplete = true;
|
||||||
@@ -2130,12 +2134,14 @@ void dMenu_save_c::yesNoCursorMoveAnm() {
|
|||||||
#if TARGET_PC
|
#if TARGET_PC
|
||||||
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save);
|
dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save);
|
||||||
if (mYesNoCursor != 0xFF &&
|
if (mYesNoCursor != 0xFF &&
|
||||||
dusk::menu_pointer::hit_pane(mpNoYes[mYesNoCursor], 8.0f) &&
|
dusk::menu_pointer::hit_pane(mpNoYes[mYesNoCursor], 8.0f))
|
||||||
dusk::menu_pointer::consume_click())
|
|
||||||
{
|
{
|
||||||
dusk::menu_pointer::defer_activation(
|
dusk::menu_pointer::set_hover_target(pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor));
|
||||||
dusk::menu_pointer::Context::Save,
|
if (dusk::menu_pointer::consume_click()) {
|
||||||
pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor));
|
dusk::menu_pointer::defer_activation(
|
||||||
|
dusk::menu_pointer::Context::Save,
|
||||||
|
pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
bool selAnmComplete = yesnoSelectMoveAnm(0);
|
bool selAnmComplete = yesnoSelectMoveAnm(0);
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ bool dMenu_Skill_c::pointerWait() {
|
|||||||
if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) {
|
if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
dusk::menu_pointer::set_hover_target(i);
|
||||||
|
|
||||||
if (mIndex != i) {
|
if (mIndex != i) {
|
||||||
mIndex = i;
|
mIndex = i;
|
||||||
|
|||||||
@@ -437,7 +437,12 @@ void dMeter2_c::checkStatus() {
|
|||||||
|
|
||||||
field_0x128 = daPy_py_c::checkNowWolf();
|
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()) {
|
if (!dComIfGp_2dShowCheck() || dMsgObject_getMsgObjectClass()->isPlaceMessage()) {
|
||||||
|
#endif
|
||||||
mStatus |= 0x4000;
|
mStatus |= 0x4000;
|
||||||
} else if (dComIfGp_checkPlayerStatus1(0, 1) && dComIfGp_getAStatus() == 0x12) {
|
} else if (dComIfGp_checkPlayerStatus1(0, 1) && dComIfGp_getAStatus() == 0x12) {
|
||||||
mStatus |= 0x200000;
|
mStatus |= 0x200000;
|
||||||
@@ -2870,8 +2875,14 @@ void dMeter2_c::alphaAnimeButton() {
|
|||||||
u8 var_31;
|
u8 var_31;
|
||||||
var_31 = 0;
|
var_31 = 0;
|
||||||
|
|
||||||
|
#if TARGET_PC
|
||||||
|
dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass();
|
||||||
|
if ((mStatus & 0x4000) ||
|
||||||
|
((mStatus & 0x100) && (msgObject != NULL && msgObject->isAutoMessageFlag())) ||
|
||||||
|
#else
|
||||||
if ((mStatus & 0x4000) ||
|
if ((mStatus & 0x4000) ||
|
||||||
((mStatus & 0x100) && dMsgObject_getMsgObjectClass()->isAutoMessageFlag()) ||
|
((mStatus & 0x100) && dMsgObject_getMsgObjectClass()->isAutoMessageFlag()) ||
|
||||||
|
#endif
|
||||||
((mStatus & 0x40000000) && !(mStatus & 0x100)) || (mStatus & 0x80000000) || (mStatus & 8) ||
|
((mStatus & 0x40000000) && !(mStatus & 0x100)) || (mStatus & 0x80000000) || (mStatus & 8) ||
|
||||||
(mStatus & 0x10) || (mStatus & 0x20) || (mStatus & 0x04000000) || (mStatus & 0x10000000))
|
(mStatus & 0x10) || (mStatus & 0x20) || (mStatus & 0x04000000) || (mStatus & 0x10000000))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -428,17 +428,6 @@ static void dummyStrings() {
|
|||||||
dMsgObject_HIO_c g_MsgObject_HIO_c;
|
dMsgObject_HIO_c g_MsgObject_HIO_c;
|
||||||
|
|
||||||
int dMsgObject_c::_execute() {
|
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;
|
field_0x4c7 = 0;
|
||||||
if (mpTalkHeap != NULL) {
|
if (mpTalkHeap != NULL) {
|
||||||
field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap);
|
field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap);
|
||||||
|
|||||||
@@ -560,7 +560,8 @@ bool dMsgScrn3Select_c::pointerMove() {
|
|||||||
|
|
||||||
mDPDPoint = choice;
|
mDPDPoint = choice;
|
||||||
field_0x110 = paneIndex;
|
field_0x110 = paneIndex;
|
||||||
dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::state().clicked);
|
dusk::menu_pointer::set_hover_target(choice);
|
||||||
|
dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::peek_click());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+277
-150
@@ -7,6 +7,7 @@
|
|||||||
#include "dusk/io.hpp"
|
#include "dusk/io.hpp"
|
||||||
#include "dusk/settings.h"
|
#include "dusk/settings.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
@@ -15,77 +16,90 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "dusk/action_bindings.h"
|
#include "dusk/action_bindings.h"
|
||||||
|
#include "dusk/logging.h"
|
||||||
#include "dusk/main.h"
|
#include "dusk/main.h"
|
||||||
|
|
||||||
using namespace dusk::config;
|
namespace dusk::config {
|
||||||
|
namespace {
|
||||||
constexpr auto ConfigFileName = "config.json";
|
constexpr auto ConfigFileName = "config.json";
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
aurora::Module DuskConfigLog("dusk::config");
|
aurora::Module DuskConfigLog("dusk::config");
|
||||||
|
|
||||||
static absl::flat_hash_map<std::string_view, ConfigVarBase*> RegisteredConfigVars;
|
absl::flat_hash_map<std::string, ConfigVarBase*> RegisteredConfigVars;
|
||||||
static bool RegistrationDone = false;
|
absl::flat_hash_map<std::string, nlohmann::json> UnregisteredConfigVars;
|
||||||
|
absl::flat_hash_map<std::string, std::string> UnregisteredConfigVarOverrides;
|
||||||
|
|
||||||
static std::optional<dusk::ui::ControlAnchor> parse_control_anchor(std::string_view value) {
|
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) {
|
||||||
if (value == "none") {
|
if (value == "none") {
|
||||||
return dusk::ui::ControlAnchor::None;
|
return ui::ControlAnchor::None;
|
||||||
}
|
}
|
||||||
if (value == "top") {
|
if (value == "top") {
|
||||||
return dusk::ui::ControlAnchor::Top;
|
return ui::ControlAnchor::Top;
|
||||||
}
|
}
|
||||||
if (value == "left") {
|
if (value == "left") {
|
||||||
return dusk::ui::ControlAnchor::Left;
|
return ui::ControlAnchor::Left;
|
||||||
}
|
}
|
||||||
if (value == "bottom") {
|
if (value == "bottom") {
|
||||||
return dusk::ui::ControlAnchor::Bottom;
|
return ui::ControlAnchor::Bottom;
|
||||||
}
|
}
|
||||||
if (value == "right") {
|
if (value == "right") {
|
||||||
return dusk::ui::ControlAnchor::Right;
|
return ui::ControlAnchor::Right;
|
||||||
}
|
}
|
||||||
if (value == "topLeft") {
|
if (value == "topLeft") {
|
||||||
return dusk::ui::ControlAnchor::TopLeft;
|
return ui::ControlAnchor::TopLeft;
|
||||||
}
|
}
|
||||||
if (value == "topRight") {
|
if (value == "topRight") {
|
||||||
return dusk::ui::ControlAnchor::TopRight;
|
return ui::ControlAnchor::TopRight;
|
||||||
}
|
}
|
||||||
if (value == "bottomLeft") {
|
if (value == "bottomLeft") {
|
||||||
return dusk::ui::ControlAnchor::BottomLeft;
|
return ui::ControlAnchor::BottomLeft;
|
||||||
}
|
}
|
||||||
if (value == "bottomRight") {
|
if (value == "bottomRight") {
|
||||||
return dusk::ui::ControlAnchor::BottomRight;
|
return ui::ControlAnchor::BottomRight;
|
||||||
}
|
}
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* control_anchor_value(dusk::ui::ControlAnchor anchor) {
|
const char* control_anchor_value(ui::ControlAnchor anchor) {
|
||||||
switch (anchor) {
|
switch (anchor) {
|
||||||
case dusk::ui::ControlAnchor::None:
|
case ui::ControlAnchor::None:
|
||||||
return "none";
|
return "none";
|
||||||
case dusk::ui::ControlAnchor::Top:
|
case ui::ControlAnchor::Top:
|
||||||
return "top";
|
return "top";
|
||||||
case dusk::ui::ControlAnchor::Left:
|
case ui::ControlAnchor::Left:
|
||||||
return "left";
|
return "left";
|
||||||
case dusk::ui::ControlAnchor::Bottom:
|
case ui::ControlAnchor::Bottom:
|
||||||
return "bottom";
|
return "bottom";
|
||||||
case dusk::ui::ControlAnchor::Right:
|
case ui::ControlAnchor::Right:
|
||||||
return "right";
|
return "right";
|
||||||
case dusk::ui::ControlAnchor::TopLeft:
|
case ui::ControlAnchor::TopLeft:
|
||||||
return "topLeft";
|
return "topLeft";
|
||||||
case dusk::ui::ControlAnchor::TopRight:
|
case ui::ControlAnchor::TopRight:
|
||||||
return "topRight";
|
return "topRight";
|
||||||
case dusk::ui::ControlAnchor::BottomLeft:
|
case ui::ControlAnchor::BottomLeft:
|
||||||
return "bottomLeft";
|
return "bottomLeft";
|
||||||
case dusk::ui::ControlAnchor::BottomRight:
|
case ui::ControlAnchor::BottomRight:
|
||||||
return "bottomRight";
|
return "bottomRight";
|
||||||
}
|
}
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::optional<float> json_finite_float(const json& object, const char* key) {
|
std::optional<float> json_finite_float(const json& object, const char* key) {
|
||||||
const auto iter = object.find(key);
|
const auto iter = object.find(key);
|
||||||
if (iter == object.end() || !iter->is_number()) {
|
if (iter == object.end() || !iter->is_number()) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -99,7 +113,7 @@ static std::optional<float> json_finite_float(const json& object, const char* ke
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::optional<dusk::ui::ControlProps> parse_control_props(const json& value) {
|
std::optional<ui::ControlProps> parse_control_props(const json& value) {
|
||||||
if (!value.is_object()) {
|
if (!value.is_object()) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -118,7 +132,7 @@ static std::optional<dusk::ui::ControlProps> parse_control_props(const json& val
|
|||||||
if (!anchor || *w <= 0.0f || *h <= 0.0f || *scale <= 0.0f) {
|
if (!anchor || *w <= 0.0f || *h <= 0.0f || *scale <= 0.0f) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return dusk::ui::ControlProps{
|
return ui::ControlProps{
|
||||||
.x = *x,
|
.x = *x,
|
||||||
.y = *y,
|
.y = *y,
|
||||||
.w = *w,
|
.w = *w,
|
||||||
@@ -128,17 +142,17 @@ static std::optional<dusk::ui::ControlProps> parse_control_props(const json& val
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::filesystem::path GetConfigJsonPath() {
|
std::filesystem::path GetConfigJsonPath() {
|
||||||
return dusk::ConfigPath / ConfigFileName;
|
return ConfigPath / ConfigFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) {
|
std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) {
|
||||||
auto tempPath = configJsonPath;
|
auto tempPath = configJsonPath;
|
||||||
tempPath.replace_filename(fmt::format(".{}.tmp", configJsonPath.filename().string()));
|
tempPath.replace_filename(fmt::format(".{}.tmp", configJsonPath.filename().string()));
|
||||||
return tempPath;
|
return tempPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) {
|
void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::rename(source, target, ec);
|
std::filesystem::rename(source, target, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
@@ -148,19 +162,8 @@ static void ReplaceFile(const std::filesystem::path& source, const std::filesyst
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl)
|
|
||||||
: name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {}
|
|
||||||
|
|
||||||
const char* ConfigVarBase::getName() const noexcept {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConfigImplBase* ConfigVarBase::getImpl() const noexcept {
|
|
||||||
return impl;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
||||||
if constexpr (std::is_enum_v<T>) {
|
if constexpr (std::is_enum_v<T>) {
|
||||||
using Underlying = std::underlying_type_t<T>;
|
using Underlying = std::underlying_type_t<T>;
|
||||||
const Underlying raw = static_cast<Underlying>(value);
|
const Underlying raw = static_cast<Underlying>(value);
|
||||||
@@ -174,6 +177,83 @@ static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfigImplBase* ConfigVarBase::getImpl() const noexcept {
|
||||||
|
return impl;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigVarBase::~ConfigVarBase() {
|
||||||
|
if (registered) {
|
||||||
|
DuskLog.fatal("CVar '{}' was destroyed while still registered!", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template <ConfigValue T>
|
template <ConfigValue T>
|
||||||
void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
|
void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
|
||||||
if constexpr (std::is_enum_v<T>) {
|
if constexpr (std::is_enum_v<T>) {
|
||||||
@@ -187,12 +267,12 @@ void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
|
|||||||
|
|
||||||
const Underlying raw = b ? static_cast<Underlying>(1) : static_cast<Underlying>(0);
|
const Underlying raw = b ? static_cast<Underlying>(1) : static_cast<Underlying>(0);
|
||||||
|
|
||||||
cVar.setValue(sanitizeEnumValue(cVar, static_cast<T>(raw)), false);
|
cVar.load_value(sanitizeEnumValue(cVar, static_cast<T>(raw)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<T>()), false);
|
cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get<T>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <ConfigValue T>
|
template <ConfigValue T>
|
||||||
@@ -200,74 +280,9 @@ nlohmann::json ConfigImpl<T>::dumpToJson(const ConfigVar<T>& cVar) {
|
|||||||
return cVar.getValueForSave();
|
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>
|
template <ConfigValue T>
|
||||||
void ConfigImpl<T>::loadFromArg(ConfigVar<T>& cVar, const std::string_view stringValue) {
|
void ConfigImpl<T>::loadFromArg(ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||||
loadFromArgImpl(cVar, stringValue);
|
cVar.load_override_value(parse_arg_value(cVar, stringValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
@@ -275,18 +290,16 @@ void ConfigImpl<bool>::loadFromArg(ConfigVar<bool>& cVar, const std::string_view
|
|||||||
if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" ||
|
if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" ||
|
||||||
stringValue == "True")
|
stringValue == "True")
|
||||||
{
|
{
|
||||||
cVar.setOverrideValue(true);
|
cVar.load_override_value(true);
|
||||||
} else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" ||
|
} else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" ||
|
||||||
stringValue == "False")
|
stringValue == "False")
|
||||||
{
|
{
|
||||||
cVar.setOverrideValue(false);
|
cVar.load_override_value(false);
|
||||||
} else {
|
} else {
|
||||||
throw InvalidConfigError("Value cannot be parsed as boolean");
|
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<bool>;
|
||||||
template class ConfigImpl<s8>;
|
template class ConfigImpl<s8>;
|
||||||
template class ConfigImpl<u8>;
|
template class ConfigImpl<u8>;
|
||||||
@@ -299,10 +312,10 @@ template class ConfigImpl<u64>;
|
|||||||
template class ConfigImpl<f32>;
|
template class ConfigImpl<f32>;
|
||||||
template class ConfigImpl<f64>;
|
template class ConfigImpl<f64>;
|
||||||
template class ConfigImpl<std::string>;
|
template class ConfigImpl<std::string>;
|
||||||
template class ConfigImpl<dusk::BloomMode>;
|
template class ConfigImpl<BloomMode>;
|
||||||
template class ConfigImpl<dusk::DepthOfFieldMode>;
|
template class ConfigImpl<DepthOfFieldMode>;
|
||||||
template class ConfigImpl<dusk::DiscVerificationState>;
|
template class ConfigImpl<DiscVerificationState>;
|
||||||
template class ConfigImpl<dusk::GameLanguage>;
|
template class ConfigImpl<GameLanguage>;
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
void ConfigImpl<FrameInterpMode>::loadFromJson(
|
void ConfigImpl<FrameInterpMode>::loadFromJson(
|
||||||
@@ -312,11 +325,11 @@ void ConfigImpl<FrameInterpMode>::loadFromJson(
|
|||||||
|
|
||||||
const FrameInterpMode mode = b ? FrameInterpMode::Unlimited : FrameInterpMode::Off;
|
const FrameInterpMode mode = b ? FrameInterpMode::Unlimited : FrameInterpMode::Off;
|
||||||
|
|
||||||
cVar.setValue(sanitizeEnumValue(cVar, mode), false);
|
cVar.load_value(sanitizeEnumValue(cVar, mode));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<FrameInterpMode>()), false);
|
cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get<FrameInterpMode>()));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
@@ -347,7 +360,7 @@ void ConfigImpl<ui::ControlLayout>::loadFromJson(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cVar.setValue(std::move(layout), false);
|
cVar.load_value(std::move(layout));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
@@ -377,25 +390,59 @@ nlohmann::json ConfigImpl<ui::ControlLayout>::dumpToJson(const ConfigVar<ui::Con
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
template class ConfigImpl<dusk::FrameInterpMode>;
|
template class ConfigImpl<FrameInterpMode>;
|
||||||
template class ConfigImpl<dusk::MenuScaling>;
|
template class ConfigImpl<TouchTargeting>;
|
||||||
template class ConfigImpl<dusk::Resampler>;
|
template class ConfigImpl<MenuScaling>;
|
||||||
template class ConfigImpl<dusk::MagicArmorMode>;
|
template class ConfigImpl<Resampler>;
|
||||||
template class ConfigImpl<dusk::ui::ControlLayout>;
|
template class ConfigImpl<MagicArmorMode>;
|
||||||
} // namespace dusk::config
|
template class ConfigImpl<ui::ControlLayout>;
|
||||||
|
|
||||||
void dusk::config::Register(ConfigVarBase& configVar) {
|
|
||||||
const auto& name = configVar.getName();
|
|
||||||
if (RegistrationDone) {
|
|
||||||
DuskConfigLog.fatal("Tried to register CVar {} after registrations closed!", name);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
void Register(ConfigVarBase& configVar) {
|
||||||
|
const std::string_view name = configVar.getName();
|
||||||
if (RegisteredConfigVars.contains(name)) {
|
if (RegisteredConfigVars.contains(name)) {
|
||||||
DuskConfigLog.fatal("Tried to register CVar {} twice!", name);
|
DuskConfigLog.fatal("Tried to register CVar {} twice!", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisteredConfigVars[name] = &configVar;
|
RegisteredConfigVars[name] = &configVar;
|
||||||
configVar.markRegistered();
|
configVar.markRegistered();
|
||||||
|
|
||||||
|
const auto unregPair = UnregisteredConfigVars.find(name);
|
||||||
|
if (unregPair != UnregisteredConfigVars.end()) {
|
||||||
|
const auto value = std::move(unregPair->second);
|
||||||
|
UnregisteredConfigVars.erase(name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
configVar.getImpl()->loadFromJson(configVar, value);
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
DuskConfigLog.error("Failed to load key '{}' from config value: {}", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto overridePair = UnregisteredConfigVarOverrides.find(name);
|
||||||
|
if (overridePair != UnregisteredConfigVarOverrides.end()) {
|
||||||
|
try {
|
||||||
|
configVar.getImpl()->loadFromArg(configVar, overridePair->second);
|
||||||
|
} 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() {
|
void ConfigVarBase::markRegistered() {
|
||||||
@@ -405,21 +452,24 @@ void ConfigVarBase::markRegistered() {
|
|||||||
registered = true;
|
registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dusk::config::FinishRegistration() {
|
void ConfigVarBase::unmarkRegistered() {
|
||||||
RegistrationDone = true;
|
if (!registered)
|
||||||
|
abort();
|
||||||
|
|
||||||
|
registered = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dusk::config::LoadFromUserPreferences() {
|
void load_from_user_preferences() {
|
||||||
const auto configJsonPath = GetConfigJsonPath();
|
const auto configJsonPath = GetConfigJsonPath();
|
||||||
if (configJsonPath.empty()) {
|
if (configJsonPath.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto configPathString = io::fs_path_to_string(configJsonPath);
|
const auto configPathString = io::fs_path_to_string(configJsonPath);
|
||||||
LoadFromFileName(configPathString.c_str());
|
load_from_file_name(configPathString.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void LoadFromPath(const char* path) {
|
static void LoadFromPath(const char* path) {
|
||||||
auto data = dusk::io::FileStream::ReadAllBytes(path);
|
auto data = io::FileStream::ReadAllBytes(path);
|
||||||
|
|
||||||
json j = json::parse(data);
|
json j = json::parse(data);
|
||||||
if (!j.is_object()) {
|
if (!j.is_object()) {
|
||||||
@@ -427,11 +477,13 @@ static void LoadFromPath(const char* path) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UnregisteredConfigVars.clear();
|
||||||
|
|
||||||
for (const auto& el : j.items()) {
|
for (const auto& el : j.items()) {
|
||||||
const auto& key = el.key();
|
const auto& key = el.key();
|
||||||
auto configVar = RegisteredConfigVars.find(key);
|
auto configVar = RegisteredConfigVars.find(key);
|
||||||
if (configVar == RegisteredConfigVars.end()) {
|
if (configVar == RegisteredConfigVars.end()) {
|
||||||
DuskConfigLog.error("Unknown key '{}' found in config!", key);
|
UnregisteredConfigVars.emplace(key, el.value());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,11 +495,7 @@ static void LoadFromPath(const char* path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dusk::config::LoadFromFileName(const char* path) {
|
void load_from_file_name(const char* path) {
|
||||||
if (!RegistrationDone) {
|
|
||||||
DuskConfigLog.fatal("Registration not finished yet!");
|
|
||||||
}
|
|
||||||
|
|
||||||
DuskConfigLog.info("Loading config from '{}'", path);
|
DuskConfigLog.info("Loading config from '{}'", path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -465,7 +513,21 @@ void dusk::config::LoadFromFileName(const char* path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dusk::config::Save() {
|
void load_arg_override(std::string_view name, std::string_view value) {
|
||||||
|
const auto cVar = GetConfigVar(name);
|
||||||
|
if (!cVar) {
|
||||||
|
UnregisteredConfigVarOverrides.emplace(name, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cVar->getImpl()->loadFromArg(*cVar, value);
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
DuskLog.fatal("Unable to parse: '{}': {}", value, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void save() {
|
||||||
const auto configJsonPath = GetConfigJsonPath();
|
const auto configJsonPath = GetConfigJsonPath();
|
||||||
if (configJsonPath.empty()) {
|
if (configJsonPath.empty()) {
|
||||||
return;
|
return;
|
||||||
@@ -483,6 +545,10 @@ void dusk::config::Save() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const auto& pair : UnregisteredConfigVars) {
|
||||||
|
j[pair.first] = pair.second;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auto tempConfigJsonPath = GetTempConfigJsonPath(configJsonPath);
|
const auto tempConfigJsonPath = GetTempConfigJsonPath(configJsonPath);
|
||||||
io::FileStream::WriteAllText(tempConfigJsonPath, j.dump(4));
|
io::FileStream::WriteAllText(tempConfigJsonPath, j.dump(4));
|
||||||
@@ -492,14 +558,14 @@ void dusk::config::Save() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dusk::config::ClearAllActionBindings(int port) {
|
void ClearAllActionBindings(int port) {
|
||||||
for (auto& actionBinding : getActionBinds() | std::views::values) {
|
for (auto& actionBinding : getActionBinds() | std::views::values) {
|
||||||
actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID);
|
actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID);
|
||||||
}
|
}
|
||||||
Save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
|
ConfigVarBase* GetConfigVar(std::string_view name) {
|
||||||
const auto configVar = RegisteredConfigVars.find(name);
|
const auto configVar = RegisteredConfigVars.find(name);
|
||||||
if (configVar != RegisteredConfigVars.end()) {
|
if (configVar != RegisteredConfigVars.end()) {
|
||||||
return configVar->second;
|
return configVar->second;
|
||||||
@@ -508,8 +574,69 @@ ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dusk::config::EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
|
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
|
||||||
for (auto& pair : RegisteredConfigVars) {
|
for (auto& pair : RegisteredConfigVars) {
|
||||||
callback(*pair.second);
|
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() {
|
||||||
|
for (auto& pair : RegisteredConfigVars) {
|
||||||
|
pair.second->unmarkRegistered();
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisteredConfigVars.clear();
|
||||||
|
UnregisteredConfigVars.clear();
|
||||||
|
UnregisteredConfigVarOverrides.clear();
|
||||||
|
s_changeSubscriptions.clear();
|
||||||
|
s_changeTokenNames.clear();
|
||||||
|
s_activeChangeNotifications.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace dusk::config
|
||||||
+8
-8
@@ -121,14 +121,6 @@ std::filesystem::path active_pref_path() {
|
|||||||
return get_pref_path();
|
return get_pref_path();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
|
|
||||||
const auto* basePath = SDL_GetBasePath();
|
|
||||||
if (!basePath) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
return path_from_utf8(basePath) / path;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::path default_data_path(const std::filesystem::path& prefPath) {
|
std::filesystem::path default_data_path(const std::filesystem::path& prefPath) {
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
#if TARGET_OS_IOS && !TARGET_OS_TV
|
#if TARGET_OS_IOS && !TARGET_OS_TV
|
||||||
@@ -888,6 +880,14 @@ void ensure_data_directory(const std::filesystem::path& dataPath) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
|
||||||
|
const auto* basePath = SDL_GetBasePath();
|
||||||
|
if (!basePath) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return path_from_utf8(basePath) / path;
|
||||||
|
}
|
||||||
|
|
||||||
bool open_data_path() {
|
bool open_data_path() {
|
||||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ struct Paths {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Paths initialize_data();
|
Paths initialize_data();
|
||||||
|
std::filesystem::path base_path_relative(const std::filesystem::path& path);
|
||||||
std::filesystem::path configured_data_path();
|
std::filesystem::path configured_data_path();
|
||||||
std::filesystem::path cache_path();
|
std::filesystem::path cache_path();
|
||||||
bool open_data_path();
|
bool open_data_path();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace dusk::config {
|
|||||||
bool copy = var.getValue();
|
bool copy = var.getValue();
|
||||||
if (ImGui::Checkbox(title, ©)) {
|
if (ImGui::Checkbox(title, ©)) {
|
||||||
var.setValue(copy);
|
var.setValue(copy);
|
||||||
Save();
|
save();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ namespace dusk::config {
|
|||||||
float val = var;
|
float val = var;
|
||||||
if (ImGui::SliderFloat(label, &val, v_min, v_max, format, flags)) {
|
if (ImGui::SliderFloat(label, &val, v_min, v_max, format, flags)) {
|
||||||
var.setValue(val);
|
var.setValue(val);
|
||||||
Save();
|
save();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ namespace dusk::config {
|
|||||||
int val = var;
|
int val = var;
|
||||||
if (ImGui::SliderInt(label, &val, v_min, v_max, format, flags)) {
|
if (ImGui::SliderInt(label, &val, v_min, v_max, format, flags)) {
|
||||||
var.setValue(val);
|
var.setValue(val);
|
||||||
Save();
|
save();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ namespace dusk::config {
|
|||||||
bool copy = p_selected.getValue();
|
bool copy = p_selected.getValue();
|
||||||
if (ImGui::MenuItem(label, shortcut, ©, enabled)) {
|
if (ImGui::MenuItem(label, shortcut, ©, enabled)) {
|
||||||
p_selected.setValue(copy);
|
p_selected.setValue(copy);
|
||||||
Save();
|
save();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ namespace dusk {
|
|||||||
if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
|
if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
|
||||||
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
||||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||||
config::Save();
|
config::save();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getSettings().game.enableResetKeybind && ImGui::GetIO().KeyCtrl &&
|
if (getSettings().game.enableResetKeybind && ImGui::GetIO().KeyCtrl &&
|
||||||
@@ -336,7 +336,7 @@ namespace dusk {
|
|||||||
if constexpr (SupportsProcessRestart) {
|
if constexpr (SupportsProcessRestart) {
|
||||||
if (ImGui::Button("Retry (Auto backend)")) {
|
if (ImGui::Button("Retry (Auto backend)")) {
|
||||||
getSettings().backend.graphicsBackend.setValue("auto");
|
getSettings().backend.graphicsBackend.setValue("auto");
|
||||||
config::Save();
|
config::save();
|
||||||
RestartRequested = true;
|
RestartRequested = true;
|
||||||
IsRunning = false;
|
IsRunning = false;
|
||||||
}
|
}
|
||||||
@@ -375,7 +375,6 @@ namespace dusk {
|
|||||||
|
|
||||||
void ImGuiConsole::PostDraw() {
|
void ImGuiConsole::PostDraw() {
|
||||||
m_menuTools.afterDraw();
|
m_menuTools.afterDraw();
|
||||||
ShowPipelineProgress();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImGuiConsole::UpdateDragScroll() {
|
void ImGuiConsole::UpdateDragScroll() {
|
||||||
@@ -524,31 +523,4 @@ namespace dusk {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImGuiConsole::ShowPipelineProgress() {
|
|
||||||
const auto* stats = aurora_get_stats();
|
|
||||||
const u32 queuedPipelines = stats->queuedPipelines;
|
|
||||||
if (queuedPipelines == 0 || !getSettings().backend.showPipelineCompilation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const u32 createdPipelines = stats->createdPipelines;
|
|
||||||
const u32 totalPipelines = queuedPipelines + createdPipelines;
|
|
||||||
|
|
||||||
const auto* viewport = ImGui::GetMainViewport();
|
|
||||||
const auto padding = viewport->WorkPos.y + 10.f;
|
|
||||||
const auto halfWidth = viewport->GetWorkCenter().x;
|
|
||||||
ImGui::SetNextWindowPos(ImVec2{halfWidth, padding}, ImGuiCond_Always, ImVec2{0.5f, 0.f});
|
|
||||||
ImGui::SetNextWindowSize(ImVec2{halfWidth, 0.f}, ImGuiCond_Always);
|
|
||||||
ImGui::SetNextWindowBgAlpha(0.65f);
|
|
||||||
ImGui::Begin("Pipelines", nullptr,
|
|
||||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove |
|
|
||||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing);
|
|
||||||
const auto percent = static_cast<float>(createdPipelines) / static_cast<float>(totalPipelines);
|
|
||||||
const auto progressStr = fmt::format("Processing pipelines: {} / {}", createdPipelines, totalPipelines);
|
|
||||||
const auto textSize = ImGui::CalcTextSize(progressStr.data(), progressStr.data() + progressStr.size());
|
|
||||||
ImGui::NewLine();
|
|
||||||
ImGui::SameLine(ImGui::GetWindowWidth() / 2.f - textSize.x + textSize.x / 2.f);
|
|
||||||
ImGuiStringViewText(progressStr);
|
|
||||||
ImGui::ProgressBar(percent);
|
|
||||||
ImGui::End();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ private:
|
|||||||
// Keep always last
|
// Keep always last
|
||||||
ImGuiMenuTools m_menuTools;
|
ImGuiMenuTools m_menuTools;
|
||||||
|
|
||||||
void ShowPipelineProgress();
|
|
||||||
void UpdateDragScroll();
|
void UpdateDragScroll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ namespace dusk {
|
|||||||
bool disableWaterRefraction = getSettings().game.disableWaterRefraction;
|
bool disableWaterRefraction = getSettings().game.disableWaterRefraction;
|
||||||
if (ImGui::Checkbox("Disable Water Refraction", &disableWaterRefraction)) {
|
if (ImGui::Checkbox("Disable Water Refraction", &disableWaterRefraction)) {
|
||||||
getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction);
|
getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction);
|
||||||
config::Save();
|
config::save();
|
||||||
}
|
}
|
||||||
ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias);
|
ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias);
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
|
|||||||
+143
-15
@@ -1,16 +1,34 @@
|
|||||||
#include "dusk/menu_pointer.h"
|
#include "dusk/menu_pointer.h"
|
||||||
|
|
||||||
#include "m_Do/m_Do_graphic.h"
|
|
||||||
#include "d/d_pane_class.h"
|
#include "d/d_pane_class.h"
|
||||||
#include "dusk/settings.h"
|
#include "dusk/settings.h"
|
||||||
|
#include "m_Do/m_Do_graphic.h"
|
||||||
|
|
||||||
#include <aurora/rmlui.hpp>
|
#include <aurora/rmlui.hpp>
|
||||||
#include <dolphin/pad.h>
|
#include <dolphin/pad.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
namespace dusk::menu_pointer {
|
namespace dusk::menu_pointer {
|
||||||
namespace {
|
namespace {
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
|
constexpr auto kTapMaxDuration = std::chrono::milliseconds(300);
|
||||||
|
constexpr f32 kTapMoveThresholdDp = 12.0f;
|
||||||
|
|
||||||
|
struct Gesture {
|
||||||
|
bool active = false;
|
||||||
|
bool movedTooFar = false;
|
||||||
|
bool crossedTarget = false;
|
||||||
|
bool pressTargetValid = false;
|
||||||
|
Context pressContext = Context::None;
|
||||||
|
TargetId pressTarget = InvalidTarget;
|
||||||
|
f32 startX = 0.0f;
|
||||||
|
f32 startY = 0.0f;
|
||||||
|
Clock::time_point startedAt{};
|
||||||
|
};
|
||||||
|
|
||||||
State s_state;
|
State s_state;
|
||||||
bool s_clickConsumed = false;
|
bool s_clickConsumed = false;
|
||||||
Context s_lastContext = Context::None;
|
Context s_lastContext = Context::None;
|
||||||
@@ -27,7 +45,14 @@ s32 s_mouseButton = -1;
|
|||||||
u32 s_suppressedPadHoldMask = 0;
|
u32 s_suppressedPadHoldMask = 0;
|
||||||
u32 s_suppressedPadNextReadMask = 0;
|
u32 s_suppressedPadNextReadMask = 0;
|
||||||
Context s_deferredActivationContext = Context::None;
|
Context s_deferredActivationContext = Context::None;
|
||||||
u8 s_deferredActivationTarget = 0xFF;
|
TargetId s_deferredActivationTarget = InvalidTarget;
|
||||||
|
Gesture s_gesture;
|
||||||
|
bool s_hoverTargetValid = false;
|
||||||
|
TargetId s_hoverTarget = InvalidTarget;
|
||||||
|
bool s_clickPending = false;
|
||||||
|
Context s_clickContext = Context::None;
|
||||||
|
TargetId s_clickTarget = InvalidTarget;
|
||||||
|
bool s_clickTargetValid = false;
|
||||||
|
|
||||||
s32 scancode_from_rml_button(s32 button) noexcept {
|
s32 scancode_from_rml_button(s32 button) noexcept {
|
||||||
switch (button) {
|
switch (button) {
|
||||||
@@ -104,6 +129,37 @@ void suppress_pad_for_mouse_button(s32 button, bool held) noexcept {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f32 tap_move_threshold() noexcept {
|
||||||
|
auto* context = aurora::rmlui::get_context();
|
||||||
|
if (context == nullptr) {
|
||||||
|
return kTapMoveThresholdDp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kTapMoveThresholdDp * std::max(context->GetDensityIndependentPixelRatio(), 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_gesture_movement(f32 x, f32 y) noexcept {
|
||||||
|
if (!s_gesture.active || s_gesture.movedTooFar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const f32 dx = x - s_gesture.startX;
|
||||||
|
const f32 dy = y - s_gesture.startY;
|
||||||
|
const f32 threshold = tap_move_threshold();
|
||||||
|
if (dx * dx + dy * dy > threshold * threshold) {
|
||||||
|
s_gesture.movedTooFar = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_click_state() noexcept {
|
||||||
|
s_clickConsumed = false;
|
||||||
|
s_clickPending = false;
|
||||||
|
s_clickContext = Context::None;
|
||||||
|
s_clickTarget = InvalidTarget;
|
||||||
|
s_clickTargetValid = false;
|
||||||
|
s_state.clicked = false;
|
||||||
|
}
|
||||||
|
|
||||||
void set_position_from_rml(f32 x, f32 y) noexcept {
|
void set_position_from_rml(f32 x, f32 y) noexcept {
|
||||||
auto* context = aurora::rmlui::get_context();
|
auto* context = aurora::rmlui::get_context();
|
||||||
if (context == nullptr) {
|
if (context == nullptr) {
|
||||||
@@ -121,7 +177,7 @@ void set_position_from_rml(f32 x, f32 y) noexcept {
|
|||||||
|
|
||||||
void clear_input_state() noexcept {
|
void clear_input_state() noexcept {
|
||||||
s_state = {};
|
s_state = {};
|
||||||
s_clickConsumed = false;
|
clear_click_state();
|
||||||
s_lastDialogChoice = 0xFF;
|
s_lastDialogChoice = 0xFF;
|
||||||
s_currentDialogChoice = 0xFF;
|
s_currentDialogChoice = 0xFF;
|
||||||
s_lastDialogChoiceValid = false;
|
s_lastDialogChoiceValid = false;
|
||||||
@@ -134,7 +190,10 @@ void clear_input_state() noexcept {
|
|||||||
s_suppressedPadHoldMask = 0;
|
s_suppressedPadHoldMask = 0;
|
||||||
s_suppressedPadNextReadMask = 0;
|
s_suppressedPadNextReadMask = 0;
|
||||||
s_deferredActivationContext = Context::None;
|
s_deferredActivationContext = Context::None;
|
||||||
s_deferredActivationTarget = 0xFF;
|
s_deferredActivationTarget = InvalidTarget;
|
||||||
|
s_gesture = {};
|
||||||
|
s_hoverTargetValid = false;
|
||||||
|
s_hoverTarget = InvalidTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -144,8 +203,6 @@ bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouse
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_clickConsumed = false;
|
|
||||||
|
|
||||||
if (!touch) {
|
if (!touch) {
|
||||||
if (phase == Phase::Press) {
|
if (phase == Phase::Press) {
|
||||||
if (!mouse_button_is_menu_confirm(mouseButton)) {
|
if (!mouse_button_is_menu_confirm(mouseButton)) {
|
||||||
@@ -174,21 +231,41 @@ bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouse
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (phase != Phase::Cancel) {
|
if (phase != Phase::Cancel) {
|
||||||
|
update_gesture_movement(x, y);
|
||||||
set_position_from_rml(x, y);
|
set_position_from_rml(x, y);
|
||||||
}
|
}
|
||||||
s_state.touch = touch;
|
s_state.touch = touch;
|
||||||
|
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case Phase::Press:
|
case Phase::Press:
|
||||||
|
clear_click_state();
|
||||||
|
s_gesture = {
|
||||||
|
.active = true,
|
||||||
|
.startX = x,
|
||||||
|
.startY = y,
|
||||||
|
.startedAt = Clock::now(),
|
||||||
|
};
|
||||||
s_state.down = true;
|
s_state.down = true;
|
||||||
s_state.pressed = true;
|
s_state.pressed = true;
|
||||||
break;
|
break;
|
||||||
case Phase::Release:
|
case Phase::Release: {
|
||||||
|
const bool shortEnough =
|
||||||
|
s_gesture.active && Clock::now() - s_gesture.startedAt <= kTapMaxDuration;
|
||||||
|
const bool stillEnough = s_gesture.active && !s_gesture.movedTooFar;
|
||||||
|
const bool targetClean = s_gesture.active && !s_gesture.crossedTarget;
|
||||||
|
s_clickContext = s_gesture.pressContext;
|
||||||
|
s_clickTarget = s_gesture.pressTarget;
|
||||||
|
s_clickTargetValid = s_gesture.pressTargetValid;
|
||||||
|
s_clickPending = shortEnough && stillEnough && targetClean;
|
||||||
s_state.down = false;
|
s_state.down = false;
|
||||||
s_state.released = true;
|
s_state.released = true;
|
||||||
s_state.clicked = true;
|
s_state.clicked = s_clickPending;
|
||||||
|
s_gesture = {};
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case Phase::Cancel:
|
case Phase::Cancel:
|
||||||
|
clear_click_state();
|
||||||
|
s_gesture = {};
|
||||||
s_state.down = false;
|
s_state.down = false;
|
||||||
break;
|
break;
|
||||||
case Phase::Move:
|
case Phase::Move:
|
||||||
@@ -211,6 +288,12 @@ void begin_game_frame() noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void end_game_frame() noexcept {
|
void end_game_frame() noexcept {
|
||||||
|
if (s_gesture.active && s_gesture.pressTargetValid &&
|
||||||
|
s_currentContext == s_gesture.pressContext && !s_hoverTargetValid)
|
||||||
|
{
|
||||||
|
s_gesture.crossedTarget = true;
|
||||||
|
}
|
||||||
|
|
||||||
s_lastContext = s_currentContext;
|
s_lastContext = s_currentContext;
|
||||||
s_lastDialogChoice = s_currentDialogChoice;
|
s_lastDialogChoice = s_currentDialogChoice;
|
||||||
s_lastDialogChoiceValid = s_currentDialogChoiceValid;
|
s_lastDialogChoiceValid = s_currentDialogChoiceValid;
|
||||||
@@ -222,10 +305,16 @@ void end_game_frame() noexcept {
|
|||||||
s_state.valid = false;
|
s_state.valid = false;
|
||||||
}
|
}
|
||||||
s_clickConsumed = false;
|
s_clickConsumed = false;
|
||||||
|
s_clickPending = false;
|
||||||
|
s_clickContext = Context::None;
|
||||||
|
s_clickTarget = InvalidTarget;
|
||||||
|
s_clickTargetValid = false;
|
||||||
|
s_hoverTargetValid = false;
|
||||||
|
s_hoverTarget = InvalidTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
void begin_context(Context context) noexcept {
|
void begin_context(Context context) noexcept {
|
||||||
if (context == Context::None) {
|
if (context == Context::None || !enabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +326,11 @@ void begin_context(Context context) noexcept {
|
|||||||
s_suppressedPadHoldMask = 0;
|
s_suppressedPadHoldMask = 0;
|
||||||
s_suppressedPadNextReadMask = 0;
|
s_suppressedPadNextReadMask = 0;
|
||||||
s_deferredActivationContext = Context::None;
|
s_deferredActivationContext = Context::None;
|
||||||
s_deferredActivationTarget = 0xFF;
|
s_deferredActivationTarget = InvalidTarget;
|
||||||
|
s_gesture = {};
|
||||||
|
s_hoverTargetValid = false;
|
||||||
|
s_hoverTarget = InvalidTarget;
|
||||||
|
clear_click_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
s_currentContext = context;
|
s_currentContext = context;
|
||||||
@@ -259,15 +352,50 @@ const State& state() noexcept {
|
|||||||
return s_state;
|
return s_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_hover_target(TargetId target) noexcept {
|
||||||
|
s_hoverTargetValid = true;
|
||||||
|
s_hoverTarget = target;
|
||||||
|
|
||||||
|
if (s_gesture.active && !s_gesture.pressTargetValid && s_state.down) {
|
||||||
|
s_gesture.pressContext = s_currentContext;
|
||||||
|
s_gesture.pressTarget = target;
|
||||||
|
s_gesture.pressTargetValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_gesture.active && s_gesture.pressTargetValid &&
|
||||||
|
(s_currentContext != s_gesture.pressContext || target != s_gesture.pressTarget))
|
||||||
|
{
|
||||||
|
s_gesture.crossedTarget = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool click_matches_hover_target() noexcept {
|
||||||
|
if (!s_clickPending || !s_hoverTargetValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s_clickTargetValid) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_currentContext == s_clickContext && s_hoverTarget == s_clickTarget;
|
||||||
|
}
|
||||||
|
|
||||||
bool consume_click() noexcept {
|
bool consume_click() noexcept {
|
||||||
if (!s_state.clicked || s_clickConsumed) {
|
if (s_clickConsumed || !click_matches_hover_target()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_clickConsumed = true;
|
s_clickConsumed = true;
|
||||||
|
s_clickPending = false;
|
||||||
|
s_state.clicked = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool peek_click() noexcept {
|
||||||
|
return !s_clickConsumed && click_matches_hover_target();
|
||||||
|
}
|
||||||
|
|
||||||
void set_dialog_choice(u8 choice, bool clicked) noexcept {
|
void set_dialog_choice(u8 choice, bool clicked) noexcept {
|
||||||
s_currentDialogChoice = choice;
|
s_currentDialogChoice = choice;
|
||||||
s_currentDialogChoiceValid = true;
|
s_currentDialogChoiceValid = true;
|
||||||
@@ -300,18 +428,18 @@ bool consume_dialog_click(u8& choice) noexcept {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void defer_activation(Context context, u8 target) noexcept {
|
void defer_activation(Context context, TargetId target) noexcept {
|
||||||
s_deferredActivationContext = context;
|
s_deferredActivationContext = context;
|
||||||
s_deferredActivationTarget = target;
|
s_deferredActivationTarget = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool consume_deferred_activation(Context context, u8 target) noexcept {
|
bool consume_deferred_activation(Context context, TargetId target) noexcept {
|
||||||
if (s_deferredActivationContext != context || s_deferredActivationTarget != target) {
|
if (s_deferredActivationContext != context || s_deferredActivationTarget != target) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_deferredActivationContext = Context::None;
|
s_deferredActivationContext = Context::None;
|
||||||
s_deferredActivationTarget = 0xFF;
|
s_deferredActivationTarget = InvalidTarget;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +449,7 @@ void clear_deferred_activation(Context context) noexcept {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s_deferredActivationContext = Context::None;
|
s_deferredActivationContext = Context::None;
|
||||||
s_deferredActivationTarget = 0xFF;
|
s_deferredActivationTarget = InvalidTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 suppressed_pad_buttons(u32 port) noexcept {
|
u32 suppressed_pad_buttons(u32 port) noexcept {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "dusk/io.hpp"
|
||||||
|
#include "loader.hpp"
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace dusk::mods {
|
||||||
|
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::mods
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#include "fmt/format.h"
|
||||||
|
#include "loader.hpp"
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
namespace dusk::mods {
|
||||||
|
|
||||||
|
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) {
|
||||||
|
std::lock_guard lock{m_mutex};
|
||||||
|
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::lock_guard lock{m_mutex};
|
||||||
|
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) {
|
||||||
|
std::lock_guard lock{m_mutex};
|
||||||
|
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::mods
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user