mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-06 11:51:01 -04:00
mod loader
This commit is contained in:
@@ -49,3 +49,5 @@ compile_commands.json
|
||||
pipeline_cache.bin
|
||||
|
||||
extract
|
||||
|
||||
*.dusk
|
||||
|
||||
Vendored
+2
-2
@@ -6,12 +6,12 @@
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${command:cmake.launchTargetPath}",
|
||||
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console"],
|
||||
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console", "--mods", "${workspaceRoot}/tools/mod_template/mods"],
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "gdb",
|
||||
"symbolSearchPath": "${command:cmake.launchTargetPath}",
|
||||
"console": "integratedTerminal",
|
||||
"cwd":"${workspaceRoot}"
|
||||
"cwd":"${workspaceRoot}",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+88
-29
@@ -9,26 +9,26 @@ endif ()
|
||||
find_package(Git)
|
||||
if (GIT_FOUND)
|
||||
# make sure version information gets re-run when the current Git HEAD changes
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
|
||||
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
|
||||
|
||||
# defines DUSK_WC_REVISION
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_REVISION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DESCRIBE
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
|
||||
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
@@ -37,11 +37,11 @@ if (GIT_FOUND)
|
||||
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
|
||||
# defines DUSK_WC_BRANCH
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_BRANCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DATE
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
|
||||
execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
|
||||
OUTPUT_VARIABLE DUSK_WC_DATE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
else ()
|
||||
@@ -135,7 +135,7 @@ if (DUSK_MOVIE_SUPPORT)
|
||||
-DWITH_JAVA=OFF
|
||||
)
|
||||
if (CMAKE_TOOLCHAIN_FILE)
|
||||
get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}")
|
||||
get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
list(APPEND _jpeg_cmake_args -DCMAKE_TOOLCHAIN_FILE=${_jpeg_toolchain_file})
|
||||
endif ()
|
||||
set(_jpeg_passthrough_vars
|
||||
@@ -221,7 +221,21 @@ FetchContent_Declare(json
|
||||
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(cxxopts json)
|
||||
message(STATUS "dusk: Fetching miniz")
|
||||
FetchContent_Declare(miniz
|
||||
URL https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
)
|
||||
message(STATUS "dusk: Fetching funchook")
|
||||
FetchContent_Declare(funchook
|
||||
GIT_REPOSITORY https://github.com/kubo/funchook.git
|
||||
GIT_TAG v1.1.3
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
set(FUNCHOOK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(FUNCHOOK_BUILD_SHARED OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_MakeAvailable(cxxopts json miniz funchook)
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
message(STATUS "dusk: Fetching sentry-native")
|
||||
@@ -263,7 +277,7 @@ else ()
|
||||
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
|
||||
endif ()
|
||||
|
||||
configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_BINARY_DIR}/version.h)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
|
||||
include(files.cmake)
|
||||
|
||||
@@ -294,11 +308,19 @@ set(GAME_INCLUDE_DIRS
|
||||
libs/JSystem/include
|
||||
libs
|
||||
extern/aurora/include/dolphin
|
||||
extern/aurora/include
|
||||
extern
|
||||
${CMAKE_BINARY_DIR})
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
${miniz_SOURCE_DIR})
|
||||
|
||||
# Interface target for mods and sub-projects to inherit game headers/defines
|
||||
add_library(dusk_game_headers INTERFACE)
|
||||
target_include_directories(dusk_game_headers INTERFACE ${GAME_INCLUDE_DIRS})
|
||||
target_compile_definitions(dusk_game_headers INTERFACE TARGET_PC=1)
|
||||
target_link_libraries(dusk_game_headers INTERFACE TracyClient)
|
||||
|
||||
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
|
||||
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt)
|
||||
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt funchook-static)
|
||||
|
||||
list(APPEND GAME_LIBS libzstd_static)
|
||||
|
||||
@@ -341,13 +363,14 @@ add_library(game_debug OBJECT ${JSYSTEM_DEBUG_FILES} ${SSYSTEM_FILES}
|
||||
src/dusk/imgui/ImGuiAudio.cpp)
|
||||
|
||||
# game_base is for all other game code files
|
||||
add_library(game_base OBJECT ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${JSYSTEM_FILES} ${REL_FILES} ${DUSK_FILES} ${DOLPHIN_FILES})
|
||||
add_library(game_base OBJECT ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${JSYSTEM_FILES} ${REL_FILES} ${DUSK_FILES} ${DOLPHIN_FILES}
|
||||
${miniz_SOURCE_DIR}/miniz.c)
|
||||
|
||||
target_compile_definitions(game_debug PRIVATE ${GAME_COMPILE_DEFS} $<$<CONFIG:Debug>:DEBUG=1> $<$<CONFIG:Debug>:PARTIAL_DEBUG=1>)
|
||||
target_compile_definitions(game_base PRIVATE ${GAME_COMPILE_DEFS} NDEBUG=1 NDEBUG_DEFINED=1 DEBUG_DEFINED=0 $<$<CONFIG:Debug>:PARTIAL_DEBUG=1>)
|
||||
target_compile_definitions(game_debug PRIVATE ${GAME_COMPILE_DEFS} DUSK_BUILDING_GAME=1 $<$<CONFIG:Debug>:DEBUG=1> $<$<CONFIG:Debug>:PARTIAL_DEBUG=1>)
|
||||
target_compile_definitions(game_base PRIVATE ${GAME_COMPILE_DEFS} DUSK_BUILDING_GAME=1 NDEBUG=1 NDEBUG_DEFINED=1 DEBUG_DEFINED=0 $<$<CONFIG:Debug>:PARTIAL_DEBUG=1>)
|
||||
|
||||
# only apply PCH to game_base since not all headers are necessarily validated with DEBUG=1
|
||||
target_precompile_headers(game_base PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
|
||||
target_precompile_headers(game_base PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_SOURCE_DIR}/include/dusk_pch.hpp>")
|
||||
|
||||
target_include_directories(game_debug PRIVATE ${GAME_INCLUDE_DIRS})
|
||||
target_include_directories(game_base PRIVATE ${GAME_INCLUDE_DIRS})
|
||||
@@ -366,27 +389,56 @@ target_link_libraries(game PUBLIC ${GAME_LIBS})
|
||||
if(ANDROID)
|
||||
add_library(dusk SHARED src/dusk/main.cpp)
|
||||
set_target_properties(dusk PROPERTIES OUTPUT_NAME main)
|
||||
else ()
|
||||
add_executable(dusk src/dusk/main.cpp)
|
||||
endif ()
|
||||
set(DUSK_MAIN_TARGET dusk)
|
||||
elseif(WIN32)
|
||||
# Game DLL: mods link against dusk.lib. TARGET_OBJECTS lets WINDOWS_EXPORT_ALL_SYMBOLS see and export all game symbols
|
||||
add_library(dusk_game SHARED
|
||||
src/dusk/main.cpp
|
||||
$<TARGET_OBJECTS:game_base>
|
||||
$<TARGET_OBJECTS:game_debug>)
|
||||
set_target_properties(dusk_game PROPERTIES
|
||||
WINDOWS_EXPORT_ALL_SYMBOLS ON
|
||||
OUTPUT_NAME dusk)
|
||||
|
||||
target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0)
|
||||
target_include_directories(dusk PRIVATE include)
|
||||
target_link_libraries(dusk PRIVATE game aurora::main)
|
||||
# Launcher EXE
|
||||
add_executable(dusk WIN32 src/dusk/launcher_win32.cpp)
|
||||
target_link_libraries(dusk PRIVATE dusk_game)
|
||||
target_include_directories(dusk PRIVATE include)
|
||||
set(DUSK_MAIN_TARGET dusk_game)
|
||||
else()
|
||||
add_executable(dusk src/dusk/main.cpp)
|
||||
set(DUSK_MAIN_TARGET dusk)
|
||||
endif()
|
||||
|
||||
target_compile_definitions(${DUSK_MAIN_TARGET} PRIVATE TARGET_PC AVOID_UB=1 VERSION=0)
|
||||
target_include_directories(${DUSK_MAIN_TARGET} PRIVATE include)
|
||||
# Windows uses TARGET_OBJECTS directly so link GAME_LIBS instead of the static wrapper.
|
||||
if(WIN32)
|
||||
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE ${GAME_LIBS} aurora::main Psapi)
|
||||
else()
|
||||
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE game aurora::main)
|
||||
endif()
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE dl)
|
||||
endif()
|
||||
# -rdynamic lets mods call game functions directly
|
||||
if(UNIX AND NOT ANDROID)
|
||||
target_link_options(${DUSK_MAIN_TARGET} PRIVATE -rdynamic)
|
||||
endif()
|
||||
if (TARGET crashpad_handler)
|
||||
add_dependencies(dusk crashpad_handler)
|
||||
add_dependencies(${DUSK_MAIN_TARGET} crashpad_handler)
|
||||
endif ()
|
||||
|
||||
if (ANDROID)
|
||||
# SDLActivity loads SDL_main via dlsym on Android. Since aurora::main is a static
|
||||
# archive, force an undefined reference so the linker keeps the SDL_main object.
|
||||
target_link_options(dusk PRIVATE "-Wl,-u,SDL_main")
|
||||
target_link_options(${DUSK_MAIN_TARGET} PRIVATE "-Wl,-u,SDL_main")
|
||||
endif ()
|
||||
|
||||
if (NOT APPLE)
|
||||
add_custom_command(TARGET dusk POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_SOURCE_DIR}/res"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/res"
|
||||
"$<TARGET_FILE_DIR:dusk>/res"
|
||||
COMMENT "Copying resources"
|
||||
)
|
||||
@@ -414,13 +466,13 @@ if (WIN32)
|
||||
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.rc.in ${DUSK_WINDOWS_RC} @ONLY)
|
||||
|
||||
target_sources(dusk PRIVATE ${DUSK_WINDOWS_ICON_ICO} ${DUSK_WINDOWS_RC})
|
||||
set_target_properties(dusk PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
|
||||
if (MSVC)
|
||||
target_link_options(dusk PRIVATE /MANIFEST:NO)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
include(cmake/DuskModSDK.cmake)
|
||||
|
||||
if (APPLE)
|
||||
if (IOS)
|
||||
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios)
|
||||
@@ -470,7 +522,11 @@ if (IOS)
|
||||
endif ()
|
||||
|
||||
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
|
||||
aurora_copy_runtime_dlls(dusk)
|
||||
if(WIN32)
|
||||
aurora_copy_runtime_dlls(dusk dusk_game)
|
||||
else()
|
||||
aurora_copy_runtime_dlls(dusk)
|
||||
endif()
|
||||
|
||||
if (DUSK_SELECTED_OPT)
|
||||
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
@@ -509,6 +565,9 @@ function(get_target_prefix target result_var)
|
||||
endif ()
|
||||
endfunction()
|
||||
list(APPEND BINARY_TARGETS dusk)
|
||||
if(WIN32)
|
||||
list(APPEND BINARY_TARGETS dusk_game)
|
||||
endif()
|
||||
set(EXTRA_TARGETS "")
|
||||
if (TARGET crashpad_handler)
|
||||
list(APPEND EXTRA_TARGETS crashpad_handler)
|
||||
@@ -516,7 +575,7 @@ endif ()
|
||||
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_PREFIX})
|
||||
aurora_install_runtime_dlls(dusk ${CMAKE_INSTALL_PREFIX})
|
||||
if (NOT APPLE)
|
||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/res DESTINATION ${CMAKE_INSTALL_PREFIX})
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/res DESTINATION ${CMAKE_INSTALL_PREFIX})
|
||||
endif ()
|
||||
if (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo)
|
||||
set(DEBUG_FILES_LIST "")
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# add_dusk_mod(<target> SOURCES <file>... MOD_JSON <mod.json> [RES_DIR <res>])
|
||||
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into")
|
||||
|
||||
function(add_dusk_mod target_name)
|
||||
cmake_parse_arguments(ARG "" "MOD_JSON;RES_DIR" "SOURCES" ${ARGN})
|
||||
if(NOT ARG_MOD_JSON)
|
||||
message(FATAL_ERROR "add_dusk_mod: MOD_JSON is required")
|
||||
endif()
|
||||
|
||||
add_library(${target_name} SHARED ${ARG_SOURCES})
|
||||
set_target_properties(${target_name} PROPERTIES PREFIX "" WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
target_compile_features(${target_name} PRIVATE cxx_std_20)
|
||||
target_link_libraries(${target_name} PRIVATE dusk_game_headers)
|
||||
|
||||
if(APPLE)
|
||||
target_link_options(${target_name} PRIVATE -undefined dynamic_lookup)
|
||||
elseif(UNIX)
|
||||
target_link_options(${target_name} PRIVATE -Wl,--allow-shlib-undefined)
|
||||
elseif(WIN32)
|
||||
target_link_libraries(${target_name} PRIVATE dusk_game)
|
||||
if(MSVC)
|
||||
target_link_options(${target_name} PRIVATE /INCREMENTAL:NO)
|
||||
set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(TARGET imgui)
|
||||
if(WIN32)
|
||||
target_link_libraries(${target_name} PRIVATE imgui)
|
||||
else()
|
||||
get_target_property(_inc imgui INTERFACE_INCLUDE_DIRECTORIES)
|
||||
if(_inc)
|
||||
target_include_directories(${target_name} PRIVATE ${_inc})
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/${target_name}_stage")
|
||||
set(_out "${DUSK_MODS_OUTPUT_DIR}/${target_name}.dusk")
|
||||
file(MAKE_DIRECTORY "${_stage}") # must exist before POST_BUILD on Windows
|
||||
|
||||
set(_zip_args "$<TARGET_FILE_NAME:${target_name}>" mod.json)
|
||||
set(_extra_cmds "")
|
||||
if(ARG_RES_DIR)
|
||||
list(APPEND _zip_args res)
|
||||
set(_extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/${ARG_RES_DIR}" "${_stage}/res")
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET ${target_name} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${_stage}" "${DUSK_MODS_OUTPUT_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${target_name}>" "${_stage}/$<TARGET_FILE_NAME:${target_name}>"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/${ARG_MOD_JSON}" "${_stage}/mod.json"
|
||||
${_extra_cmds}
|
||||
COMMAND ${CMAKE_COMMAND} -E tar cvf "${_out}" --format=zip ${_zip_args}
|
||||
WORKING_DIRECTORY "${_stage}"
|
||||
COMMENT "Packaging ${target_name} -> ${_out}"
|
||||
)
|
||||
endfunction()
|
||||
@@ -1387,4 +1387,8 @@ set(DUSK_FILES
|
||||
src/dusk/OSContext.cpp
|
||||
src/dusk/OSThread.cpp
|
||||
src/dusk/OSMutex.cpp
|
||||
src/dusk/hook_system.cpp
|
||||
src/dusk/mod_loader.cpp
|
||||
src/dusk/imgui/ImGuiMenuMods.cpp
|
||||
src/dusk/gx_helper.cpp
|
||||
)
|
||||
|
||||
@@ -1049,11 +1049,11 @@ public:
|
||||
|
||||
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
|
||||
|
||||
extern dComIfG_inf_c g_dComIfG_gameInfo;
|
||||
extern GXColor g_blackColor;
|
||||
extern GXColor g_clearColor;
|
||||
extern GXColor g_whiteColor;
|
||||
extern GXColor g_saftyWhiteColor;
|
||||
DUSK_GAME_EXTERN dComIfG_inf_c g_dComIfG_gameInfo;
|
||||
DUSK_GAME_EXTERN GXColor g_blackColor;
|
||||
DUSK_GAME_EXTERN GXColor g_clearColor;
|
||||
DUSK_GAME_EXTERN GXColor g_whiteColor;
|
||||
DUSK_GAME_EXTERN GXColor g_saftyWhiteColor;
|
||||
|
||||
int dComLbG_PhaseHandler(request_of_phase_process_class*, request_of_phase_process_fn*,
|
||||
void*);
|
||||
|
||||
@@ -18,9 +18,8 @@
|
||||
class GXTexObjRAII : public GXTexObj {
|
||||
public:
|
||||
GXTexObjRAII() : GXTexObj() {}
|
||||
~GXTexObjRAII() { GXDestroyTexObj(this); }
|
||||
|
||||
void reset() { GXDestroyTexObj(this); }
|
||||
~GXTexObjRAII();
|
||||
void reset();
|
||||
|
||||
GXTexObjRAII(const GXTexObjRAII&) = delete;
|
||||
GXTexObjRAII& operator=(const GXTexObjRAII&) = delete;
|
||||
@@ -44,12 +43,8 @@ typedef GXTexObj TGXTexObj;
|
||||
#endif
|
||||
|
||||
struct GXScopedDebugGroup {
|
||||
explicit GXScopedDebugGroup(const char* text) {
|
||||
GXPushDebugGroup(text);
|
||||
}
|
||||
~GXScopedDebugGroup() {
|
||||
GXPopDebugGroup();
|
||||
}
|
||||
explicit GXScopedDebugGroup(const char* text);
|
||||
~GXScopedDebugGroup();
|
||||
};
|
||||
|
||||
#define GX_AND_TRACY_SCOPED(name) GXScopedDebugGroup scope(name); ZoneScopedN(name);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include "dusk/mod_api.h"
|
||||
|
||||
namespace dusk {
|
||||
|
||||
inline DuskModAPI* g_api = nullptr;
|
||||
inline void init(DuskModAPI* api) { g_api = api; }
|
||||
|
||||
template <class T>
|
||||
T arg(void* args_raw, int n) noexcept {
|
||||
void** a = static_cast<void**>(args_raw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(a[n]);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::remove_reference_t<T>& argRef(void* args_raw, int n) noexcept {
|
||||
void** a = static_cast<void**>(args_raw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(a[n]);
|
||||
}
|
||||
|
||||
template <class F>
|
||||
void* mfpAddr(F fn) noexcept {
|
||||
void* p = nullptr;
|
||||
static_assert(sizeof(fn) >= sizeof(void*), "unexpected MFP size");
|
||||
std::memcpy(&p, &fn, sizeof(void*));
|
||||
return p;
|
||||
}
|
||||
|
||||
template <auto MFP, class R, class Self, class Orig, class... A>
|
||||
struct HookEntryBase {
|
||||
static inline Orig g_orig = nullptr;
|
||||
|
||||
static R trampoline(Self self, A... args) {
|
||||
void* ptrs[] = {static_cast<void*>(std::addressof(self)), static_cast<void*>(std::addressof(args))...};
|
||||
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast<void*>(ptrs));
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
if (!cancel) g_orig(self, args...);
|
||||
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs));
|
||||
} else {
|
||||
R result{};
|
||||
if (!cancel) result = g_orig(self, args...);
|
||||
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template <auto MFP>
|
||||
struct HookEntry;
|
||||
|
||||
template <class C, class R, class... A, R (C::*MFP)(A...)>
|
||||
struct HookEntry<MFP> : HookEntryBase<MFP, R, C*, R(*)(C*, A...), A...> {};
|
||||
|
||||
template <class C, class R, class... A, R (C::*MFP)(A...) const>
|
||||
struct HookEntry<MFP> : HookEntryBase<MFP, R, const C*, R(*)(const C*, A...), A...> {};
|
||||
|
||||
template <auto MFP>
|
||||
void hookAddPre(int32_t (*fn)(void* args)) {
|
||||
using E = HookEntry<MFP>;
|
||||
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
|
||||
reinterpret_cast<void**>(&E::g_orig));
|
||||
g_api->hook_pre(mfpAddr(MFP), fn);
|
||||
}
|
||||
|
||||
template <auto MFP>
|
||||
void hookAddPost(void (*fn)(void* args)) {
|
||||
using E = HookEntry<MFP>;
|
||||
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
|
||||
reinterpret_cast<void**>(&E::g_orig));
|
||||
g_api->hook_post(mfpAddr(MFP), fn);
|
||||
}
|
||||
|
||||
template <auto MFP>
|
||||
void hookSetReplace(void (*fn)(void* args)) {
|
||||
using E = HookEntry<MFP>;
|
||||
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
|
||||
reinterpret_cast<void**>(&E::g_orig));
|
||||
g_api->hook_replace(mfpAddr(MFP), fn);
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store);
|
||||
|
||||
void hookRegisterPre (void* fn_addr, void* mod, int32_t (*fn)(void* args));
|
||||
void hookRegisterPost(void* fn_addr, void* mod, void (*fn)(void* args));
|
||||
void hookSetReplace (void* fn_addr, void* mod, void (*fn)(void* args));
|
||||
|
||||
bool hookDispatchPre (void* fn_addr, void* args);
|
||||
void hookDispatchPost(void* fn_addr, void* args);
|
||||
|
||||
void hookClearMod(void* mod);
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef DUSK_MOD_API_H
|
||||
#define DUSK_MOD_API_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define DUSK_MOD_API_VERSION 1
|
||||
|
||||
#if defined(_WIN32)
|
||||
# define DUSK_MOD_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
# define DUSK_MOD_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
typedef struct DuskModAPI {
|
||||
uint32_t api_version;
|
||||
const char* mod_dir;
|
||||
|
||||
void (*log_info) (const char* fmt, ...);
|
||||
void (*log_warn) (const char* fmt, ...);
|
||||
void (*log_error)(const char* fmt, ...);
|
||||
|
||||
void* (*load_resource)(const char* relative_path, size_t* out_size);
|
||||
void (*free_resource)(void* data);
|
||||
|
||||
void (*register_tab_content)(void (*draw_fn)(void* userdata), void* userdata);
|
||||
void (*register_menu_item) (void (*draw_fn)(void* userdata), void* userdata);
|
||||
|
||||
void (*hook_install)(void* fn_addr, void* tramp_fn, void** orig_store);
|
||||
void (*hook_pre) (void* fn_addr, int32_t (*fn)(void* args));
|
||||
void (*hook_post) (void* fn_addr, void (*fn)(void* args));
|
||||
void (*hook_replace)(void* fn_addr, void (*fn)(void* args));
|
||||
|
||||
bool (*hook_dispatch_pre) (void* fn_addr, void* args);
|
||||
void (*hook_dispatch_post)(void* fn_addr, void* args);
|
||||
} DuskModAPI;
|
||||
|
||||
void mod_init(DuskModAPI* api);
|
||||
void mod_tick(DuskModAPI* api);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/mod_api.h"
|
||||
|
||||
namespace dusk {
|
||||
|
||||
struct ModDrawCallback {
|
||||
void (*draw_fn)(void* userdata);
|
||||
void* userdata;
|
||||
};
|
||||
|
||||
struct LoadedMod {
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::string author;
|
||||
std::string description;
|
||||
std::string mod_path;
|
||||
std::string dir;
|
||||
|
||||
void* handle = nullptr;
|
||||
bool active = false;
|
||||
|
||||
using FnInit = void (*)(DuskModAPI*);
|
||||
using FnTick = void (*)(DuskModAPI*);
|
||||
using FnCleanup = void (*)(DuskModAPI*);
|
||||
using FnSetImguiCtx = void (*)(void*);
|
||||
|
||||
FnInit fn_init = nullptr;
|
||||
FnTick fn_tick = nullptr;
|
||||
FnCleanup fn_cleanup = nullptr;
|
||||
FnSetImguiCtx fn_set_imgui_ctx = nullptr;
|
||||
|
||||
DuskModAPI api{};
|
||||
|
||||
std::vector<ModDrawCallback> tab_content;
|
||||
std::vector<ModDrawCallback> menu_items;
|
||||
};
|
||||
|
||||
class ModLoader {
|
||||
public:
|
||||
static ModLoader& instance();
|
||||
|
||||
void setModsDir(std::filesystem::path dir) { m_modsDir = std::move(dir); }
|
||||
void init();
|
||||
void tick();
|
||||
void shutdown();
|
||||
|
||||
const std::vector<LoadedMod>& mods() const { return m_mods; }
|
||||
static void callDrawCallback(const LoadedMod& mod, const ModDrawCallback& cb);
|
||||
|
||||
private:
|
||||
std::vector<LoadedMod> m_mods;
|
||||
std::filesystem::path m_modsDir = "mods";
|
||||
bool m_initialized = false;
|
||||
|
||||
void tryLoadDusk(const std::filesystem::path& modPath);
|
||||
void buildAPI(LoadedMod& mod);
|
||||
};
|
||||
|
||||
} // namespace dusk
|
||||
@@ -104,6 +104,19 @@ inline int __builtin_clz(unsigned int v) {
|
||||
|
||||
#endif
|
||||
|
||||
// Data symbols in dusk.dll need dllimport on the mod side
|
||||
// DUSK_BUILDING_GAME is defined for the game build so the same headers work in both.
|
||||
#if defined(TARGET_PC) && defined(_WIN32) && !defined(DUSK_BUILDING_GAME)
|
||||
# define DUSK_GAME_EXTERN extern __declspec(dllimport)
|
||||
# define DUSK_GAME_DATA __declspec(dllimport)
|
||||
#elif defined(TARGET_PC) && defined(_WIN32) && defined(DUSK_BUILDING_GAME)
|
||||
# define DUSK_GAME_EXTERN extern
|
||||
# define DUSK_GAME_DATA __declspec(dllexport)
|
||||
#else
|
||||
# define DUSK_GAME_EXTERN extern
|
||||
# define DUSK_GAME_DATA
|
||||
#endif
|
||||
|
||||
#define FAST_DIV(x, n) (x >> (n / 2))
|
||||
|
||||
#define SQUARE(x) ((x) * (x))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "JSystem/JUtility/JUTGamePad.h"
|
||||
#include "SSystem/SComponent/c_API_controller_pad.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "global.h"
|
||||
|
||||
// Controller Ports 1 - 4
|
||||
enum { PAD_1, PAD_2, PAD_3, PAD_4 };
|
||||
@@ -94,7 +95,7 @@ public:
|
||||
static void stopMotorWaveHard(u32 pad) { return m_gamePad[pad]->stopMotorWaveHard(); }
|
||||
|
||||
static JUTGamePad* m_gamePad[4];
|
||||
static interface_of_controller_pad m_cpadInfo[4];
|
||||
static DUSK_GAME_DATA interface_of_controller_pad m_cpadInfo[4];
|
||||
static interface_of_controller_pad m_debugCpadInfo[4];
|
||||
};
|
||||
|
||||
|
||||
@@ -371,6 +371,7 @@ public:
|
||||
static int m_height;
|
||||
static f32 m_heightF;
|
||||
static f32 m_widthF;
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
static f32 m_safeMinXF;
|
||||
@@ -380,7 +381,6 @@ public:
|
||||
static f32 m_safeWidthF;
|
||||
static f32 m_safeHeightF;
|
||||
#endif
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif /* M_DO_M_DO_GRAPHIC_H */
|
||||
|
||||
@@ -142,6 +142,12 @@ DynamicModuleControl::DynamicModuleControl(char const* name) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
// dump() is declared but its definition is inside #if !TARGET_PC above; stub it out.
|
||||
void DynamicModuleControlBase::dump() {}
|
||||
void DynamicModuleControlBase::dump(char*) {}
|
||||
#endif
|
||||
|
||||
u32 DynamicModuleControl::sAllocBytes;
|
||||
|
||||
JKRArchive* DynamicModuleControl::sArchive;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "dusk/gx_helper.h"
|
||||
|
||||
#ifdef TARGET_PC
|
||||
GXTexObjRAII::~GXTexObjRAII() { GXDestroyTexObj(this); }
|
||||
void GXTexObjRAII::reset() { GXDestroyTexObj(this); }
|
||||
#endif
|
||||
|
||||
GXScopedDebugGroup::GXScopedDebugGroup(const char* text) {
|
||||
GXPushDebugGroup(text);
|
||||
}
|
||||
GXScopedDebugGroup::~GXScopedDebugGroup() {
|
||||
GXPopDebugGroup();
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "dusk/hook_system.hpp"
|
||||
#include "dusk/logging.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <funchook.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
extern void* g_dusk_hook_current_mod;
|
||||
|
||||
struct PreHookFn {
|
||||
void* mod;
|
||||
int32_t (*fn)(void* args);
|
||||
};
|
||||
struct VoidHookFn {
|
||||
void* mod;
|
||||
void (*fn)(void* args);
|
||||
};
|
||||
|
||||
struct HookSlot {
|
||||
std::vector<PreHookFn> pre;
|
||||
VoidHookFn replace = {};
|
||||
std::vector<VoidHookFn> post;
|
||||
};
|
||||
|
||||
static std::unordered_map<uintptr_t, HookSlot>& registry() {
|
||||
static std::unordered_map<uintptr_t, HookSlot> s;
|
||||
return s;
|
||||
}
|
||||
static std::unordered_map<uintptr_t, void*>& installed() {
|
||||
static std::unordered_map<uintptr_t, void*> s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// Follow E9/FF25 chains to skip MSVC incremental-link and import stubs
|
||||
static void* resolveImportThunk(void* addr) {
|
||||
#if _WIN32
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const auto* p = static_cast<const uint8_t*>(addr);
|
||||
if (p[0] == 0xFF && p[1] == 0x25) {
|
||||
int32_t offset;
|
||||
std::memcpy(&offset, p + 2, 4);
|
||||
addr = const_cast<void*>(*reinterpret_cast<const void* const*>(p + 6 + offset));
|
||||
break;
|
||||
} else if (p[0] == 0xE9) {
|
||||
int32_t offset;
|
||||
std::memcpy(&offset, p + 1, 4);
|
||||
addr = const_cast<uint8_t*>(p) + 5 + offset;
|
||||
} else
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
return addr;
|
||||
}
|
||||
|
||||
struct ModGuard {
|
||||
void* prev;
|
||||
explicit ModGuard(void* mod) : prev(g_dusk_hook_current_mod) { g_dusk_hook_current_mod = mod; }
|
||||
~ModGuard() { g_dusk_hook_current_mod = prev; }
|
||||
};
|
||||
|
||||
void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store) {
|
||||
fn_addr = resolveImportThunk(fn_addr);
|
||||
auto key = reinterpret_cast<uintptr_t>(fn_addr);
|
||||
auto it = installed().find(key);
|
||||
if (it != installed().end()) {
|
||||
*orig_store = it->second;
|
||||
return;
|
||||
}
|
||||
|
||||
funchook_t* fh = funchook_create();
|
||||
void* fn = fn_addr;
|
||||
int prep = funchook_prepare(fh, &fn, tramp_fn);
|
||||
int inst = (prep == 0) ? funchook_install(fh, 0) : -1;
|
||||
if (prep != 0 || inst != 0) {
|
||||
DuskLog.warn("HookSystem: funchook failed for {:p} (prepare={} install={})", fn_addr, prep,
|
||||
inst);
|
||||
funchook_destroy(fh);
|
||||
return;
|
||||
}
|
||||
|
||||
funchook_destroy(fh);
|
||||
installed()[key] = fn;
|
||||
*orig_store = fn;
|
||||
}
|
||||
|
||||
bool hookDispatchPre(void* fn_addr, void* args) {
|
||||
auto it = registry().find(reinterpret_cast<uintptr_t>(fn_addr));
|
||||
if (it == registry().end())
|
||||
return false;
|
||||
auto& slot = it->second;
|
||||
for (auto& h : slot.pre) {
|
||||
ModGuard g(h.mod);
|
||||
if (h.fn(args) != 0)
|
||||
return true;
|
||||
}
|
||||
if (slot.replace.fn) {
|
||||
ModGuard g(slot.replace.mod);
|
||||
slot.replace.fn(args);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void hookDispatchPost(void* fn_addr, void* args) {
|
||||
auto it = registry().find(reinterpret_cast<uintptr_t>(fn_addr));
|
||||
if (it == registry().end())
|
||||
return;
|
||||
for (auto& h : it->second.post) {
|
||||
if (h.fn) {
|
||||
ModGuard g(h.mod);
|
||||
h.fn(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void hookRegisterPre(void* fn_addr, void* mod, int32_t (*fn)(void* args)) {
|
||||
registry()[reinterpret_cast<uintptr_t>(fn_addr)].pre.push_back({mod, fn});
|
||||
}
|
||||
|
||||
void hookRegisterPost(void* fn_addr, void* mod, void (*fn)(void* args)) {
|
||||
registry()[reinterpret_cast<uintptr_t>(fn_addr)].post.push_back({mod, fn});
|
||||
}
|
||||
|
||||
void hookSetReplace(void* fn_addr, void* mod, void (*fn)(void* args)) {
|
||||
auto& slot = registry()[reinterpret_cast<uintptr_t>(fn_addr)];
|
||||
if (slot.replace.fn)
|
||||
DuskLog.warn("HookSystem: replace hook for {} already set — overwriting", fn_addr);
|
||||
slot.replace = {mod, fn};
|
||||
}
|
||||
|
||||
void hookClearMod(void* mod) {
|
||||
for (auto& [addr, slot] : registry()) {
|
||||
auto erase = [&](auto& v) {
|
||||
v.erase(
|
||||
std::remove_if(v.begin(), v.end(), [mod](const auto& h) { return h.mod == mod; }),
|
||||
v.end());
|
||||
};
|
||||
erase(slot.pre);
|
||||
erase(slot.post);
|
||||
if (slot.replace.mod == mod)
|
||||
slot.replace = {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -322,6 +322,7 @@ namespace dusk {
|
||||
if (showMenu && ImGui::BeginMainMenuBar()) {
|
||||
m_menuGame.draw();
|
||||
m_menuEnhancements.draw();
|
||||
m_menuMods.draw();
|
||||
m_menuTools.draw();
|
||||
|
||||
const auto fpsLabel =
|
||||
@@ -365,6 +366,7 @@ namespace dusk {
|
||||
m_menuTools.ShowPlayerInfo();
|
||||
m_menuTools.ShowAudioDebug();
|
||||
m_menuTools.ShowSaveEditor();
|
||||
m_menuMods.showModsWindow();
|
||||
}
|
||||
m_menuTools.ShowStateShare();
|
||||
DuskDebugPad(); // temporary, remove later
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "ImGuiFirstRunPreset.hpp"
|
||||
#include "ImGuiMenuEnhancements.hpp"
|
||||
#include "ImGuiMenuGame.hpp"
|
||||
#include "ImGuiMenuMods.hpp"
|
||||
#include "ImGuiMenuTools.hpp"
|
||||
#include "ImGuiPreLaunchWindow.hpp"
|
||||
#include "imgui.h"
|
||||
@@ -53,6 +54,7 @@ private:
|
||||
ImGuiFirstRunPreset m_firstRunPreset;
|
||||
ImGuiMenuGame m_menuGame;
|
||||
ImGuiMenuEnhancements m_menuEnhancements;
|
||||
ImGuiMenuMods m_menuMods;
|
||||
ImGuiPreLaunchWindow m_preLaunchWindow;
|
||||
|
||||
// Keep always last
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "ImGuiMenuMods.hpp"
|
||||
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dusk {
|
||||
|
||||
void ImGuiMenuMods::draw() {
|
||||
const auto& mods = ModLoader::instance().mods();
|
||||
if (mods.empty()) return;
|
||||
|
||||
if (ImGui::BeginMenu("Mods")) {
|
||||
if (ImGui::MenuItem("Mod Manager", nullptr, m_showWindow)) {
|
||||
m_showWindow = !m_showWindow;
|
||||
}
|
||||
|
||||
for (const auto& mod : mods) {
|
||||
if (mod.menu_items.empty()) continue;
|
||||
ImGui::Separator();
|
||||
if (ImGui::BeginMenu(mod.name.c_str())) {
|
||||
for (const auto& item : mod.menu_items) {
|
||||
ModLoader::callDrawCallback(mod, item);
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuMods::showModsWindow() {
|
||||
if (!m_showWindow) return;
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(520, 420), ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Mod Manager", &m_showWindow)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& mods = ModLoader::instance().mods();
|
||||
if (mods.empty()) {
|
||||
ImGuiTextCenter("No mods loaded.");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabBar("##ModsOuter")) {
|
||||
for (const auto& mod : mods) {
|
||||
const std::string tabLabel = mod.name + (mod.active ? "" : " [disabled]");
|
||||
|
||||
if (ImGui::BeginTabItem(tabLabel.c_str())) {
|
||||
ImGui::Text("Version: %s", mod.version.c_str());
|
||||
ImGui::Text("Author: %s", mod.author.c_str());
|
||||
ImGui::Text("Status: %s", mod.active ? "Active" : "Disabled");
|
||||
ImGui::Text("Path: %s", mod.mod_path.c_str());
|
||||
|
||||
if (!mod.description.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", mod.description.c_str());
|
||||
}
|
||||
|
||||
for (const auto& cb : mod.tab_content) {
|
||||
ImGui::Separator();
|
||||
ModLoader::callDrawCallback(mod, cb);
|
||||
}
|
||||
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
|
||||
class ImGuiMenuMods {
|
||||
public:
|
||||
void draw();
|
||||
|
||||
void showModsWindow();
|
||||
|
||||
private:
|
||||
bool m_showWindow = false;
|
||||
};
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Thin Windows launcher EXE. The game lives in dusk.dll, this just forwards
|
||||
* the Windows entry point to it. Keeping the game as a DLL lets mod .dll
|
||||
* files link against dusk.lib and resolve all game symbols at load time.
|
||||
*/
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
// see src/dusk/main.cpp
|
||||
extern "C" int WINAPI dusk_WinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR cmd, int show);
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR cmd, int show) {
|
||||
return dusk_WinMain(hInst, hPrev, cmd, show);
|
||||
}
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
#if _WIN32
|
||||
#define WINDOWS_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
@@ -120,7 +120,8 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
|
||||
// Entry point called by the launcher executable.
|
||||
extern "C" int WINAPI dusk_WinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
|
||||
return RunWindowsGuiEntryPoint();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/hook_system.hpp"
|
||||
#include "dusk/logging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdarg>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "miniz.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <Windows.h>
|
||||
|
||||
static void* pl_dlopen(const std::filesystem::path& p) {
|
||||
return LoadLibraryW(p.wstring().c_str());
|
||||
}
|
||||
static void* pl_dlsym(void* h, const char* name) {
|
||||
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(h), name));
|
||||
}
|
||||
static void pl_dlclose(void* h) {
|
||||
FreeLibrary(static_cast<HMODULE>(h));
|
||||
}
|
||||
static std::string pl_dlerror() {
|
||||
char buf[256]{};
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr,
|
||||
GetLastError(), 0, buf, sizeof(buf), nullptr);
|
||||
std::string s = buf;
|
||||
while (!s.empty() && (s.back() == '\r' || s.back() == '\n'))
|
||||
s.pop_back();
|
||||
return s;
|
||||
}
|
||||
static constexpr const char* k_libExt = ".dll";
|
||||
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
static void* pl_dlopen(const std::filesystem::path& p) {
|
||||
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL);
|
||||
}
|
||||
static void* pl_dlsym(void* h, const char* name) {
|
||||
return dlsym(h, name);
|
||||
}
|
||||
static void pl_dlclose(void* h) {
|
||||
dlclose(h);
|
||||
}
|
||||
static std::string pl_dlerror() {
|
||||
const char* e = dlerror();
|
||||
return e ? e : "(unknown error)";
|
||||
}
|
||||
#if defined(__APPLE__)
|
||||
static constexpr const char* k_libExt = ".dylib";
|
||||
#else
|
||||
static constexpr const char* k_libExt = ".so";
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static dusk::LoadedMod* g_currentMod = nullptr;
|
||||
|
||||
namespace dusk {
|
||||
void* g_dusk_hook_current_mod = nullptr;
|
||||
}
|
||||
|
||||
struct ModGuard {
|
||||
explicit ModGuard(dusk::LoadedMod* m) {
|
||||
g_currentMod = m;
|
||||
dusk::g_dusk_hook_current_mod = m;
|
||||
}
|
||||
~ModGuard() {
|
||||
g_currentMod = nullptr;
|
||||
dusk::g_dusk_hook_current_mod = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
static const char* modName() {
|
||||
return g_currentMod ? g_currentMod->name.c_str() : "mod";
|
||||
}
|
||||
|
||||
static void cb_log_info(const char* fmt, ...) {
|
||||
va_list ap, ap2; va_start(ap, fmt); va_copy(ap2, ap);
|
||||
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0'); va_end(ap2);
|
||||
vsnprintf(s.data(), s.size() + 1, fmt, ap); va_end(ap);
|
||||
DuskLog.info("[{}] {}", modName(), s);
|
||||
}
|
||||
|
||||
static void cb_log_warn(const char* fmt, ...) {
|
||||
va_list ap, ap2; va_start(ap, fmt); va_copy(ap2, ap);
|
||||
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0'); va_end(ap2);
|
||||
vsnprintf(s.data(), s.size() + 1, fmt, ap); va_end(ap);
|
||||
DuskLog.warn("[{}] {}", modName(), s);
|
||||
}
|
||||
|
||||
static void cb_log_error(const char* fmt, ...) {
|
||||
va_list ap, ap2; va_start(ap, fmt); va_copy(ap2, ap);
|
||||
std::string s(vsnprintf(nullptr, 0, fmt, ap2), '\0'); va_end(ap2);
|
||||
vsnprintf(s.data(), s.size() + 1, fmt, ap); va_end(ap);
|
||||
DuskLog.error("[{}] {}", modName(), s);
|
||||
}
|
||||
|
||||
static void* cb_load_resource(const char* relative_path, size_t* out_size) {
|
||||
if (out_size)
|
||||
*out_size = 0;
|
||||
if (!g_currentMod || !relative_path)
|
||||
return nullptr;
|
||||
|
||||
mz_zip_archive zip{};
|
||||
if (!mz_zip_reader_init_file(&zip, g_currentMod->mod_path.c_str(), 0)) {
|
||||
DuskLog.warn("[{}] load_resource: could not open {}", g_currentMod->name,
|
||||
g_currentMod->mod_path);
|
||||
return nullptr;
|
||||
}
|
||||
std::string entry = std::string("res/") + relative_path;
|
||||
size_t sz = 0;
|
||||
void* data = mz_zip_reader_extract_file_to_heap(&zip, entry.c_str(), &sz, 0);
|
||||
mz_zip_reader_end(&zip);
|
||||
if (!data) {
|
||||
DuskLog.warn("[{}] load_resource: '{}' not found in zip", g_currentMod->name, entry);
|
||||
return nullptr;
|
||||
}
|
||||
if (out_size)
|
||||
*out_size = sz;
|
||||
return data;
|
||||
}
|
||||
|
||||
static void cb_free_resource(void* data) {
|
||||
mz_free(data);
|
||||
}
|
||||
|
||||
static void cb_register_tab_content(void (*draw_fn)(void*), void* userdata) {
|
||||
if (g_currentMod && draw_fn)
|
||||
g_currentMod->tab_content.push_back({draw_fn, userdata});
|
||||
}
|
||||
|
||||
static void cb_register_menu_item(void (*draw_fn)(void*), void* userdata) {
|
||||
if (g_currentMod && draw_fn)
|
||||
g_currentMod->menu_items.push_back({draw_fn, userdata});
|
||||
}
|
||||
|
||||
static void api_hook_pre(void* addr, int32_t (*fn)(void* args)) {
|
||||
dusk::hookRegisterPre(addr, g_currentMod, fn);
|
||||
}
|
||||
|
||||
static void api_hook_post(void* addr, void (*fn)(void* args)) {
|
||||
dusk::hookRegisterPost(addr, g_currentMod, fn);
|
||||
}
|
||||
|
||||
static void api_hook_replace(void* addr, void (*fn)(void* args)) {
|
||||
dusk::hookSetReplace(addr, g_currentMod, fn);
|
||||
}
|
||||
|
||||
namespace dusk {
|
||||
|
||||
ModLoader& ModLoader::instance() {
|
||||
static ModLoader inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ModLoader::buildAPI(LoadedMod& mod) {
|
||||
mod.api.api_version = DUSK_MOD_API_VERSION;
|
||||
mod.api.mod_dir = mod.dir.c_str();
|
||||
mod.api.log_info = cb_log_info;
|
||||
mod.api.log_warn = cb_log_warn;
|
||||
mod.api.log_error = cb_log_error;
|
||||
mod.api.load_resource = cb_load_resource;
|
||||
mod.api.free_resource = cb_free_resource;
|
||||
mod.api.register_tab_content = cb_register_tab_content;
|
||||
mod.api.register_menu_item = cb_register_menu_item;
|
||||
mod.api.hook_install = hookInstallByAddr;
|
||||
mod.api.hook_pre = api_hook_pre;
|
||||
mod.api.hook_post = api_hook_post;
|
||||
mod.api.hook_replace = api_hook_replace;
|
||||
mod.api.hook_dispatch_pre = hookDispatchPre;
|
||||
mod.api.hook_dispatch_post = hookDispatchPost;
|
||||
}
|
||||
|
||||
void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::string metaName, metaVersion, metaAuthor, metaDescription;
|
||||
{
|
||||
mz_zip_archive zip{};
|
||||
if (mz_zip_reader_init_file(&zip, modPath.string().c_str(), 0)) {
|
||||
size_t jsonSize = 0;
|
||||
void* jsonData = mz_zip_reader_extract_file_to_heap(&zip, "mod.json", &jsonSize, 0);
|
||||
mz_zip_reader_end(&zip);
|
||||
if (jsonData) {
|
||||
try {
|
||||
std::string jsonStr(static_cast<char*>(jsonData), jsonSize);
|
||||
mz_free(jsonData);
|
||||
jsonData = nullptr;
|
||||
auto j = nlohmann::json::parse(jsonStr);
|
||||
metaName = j.value("name", "");
|
||||
metaVersion = j.value("version", "");
|
||||
metaAuthor = j.value("author", "");
|
||||
metaDescription = j.value("description", "");
|
||||
} catch (const std::exception& e) {
|
||||
mz_free(jsonData);
|
||||
DuskLog.warn("ModLoader: bad mod.json in {}: {}", modPath.filename().string(),
|
||||
e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mz_zip_archive zip{};
|
||||
if (!mz_zip_reader_init_file(&zip, modPath.string().c_str(), 0)) {
|
||||
DuskLog.error("ModLoader: failed to open {}", modPath.filename().string());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string dllEntry;
|
||||
for (mz_uint i = 0, n = mz_zip_reader_get_num_files(&zip); i < n; ++i) {
|
||||
mz_zip_archive_file_stat stat{};
|
||||
if (!mz_zip_reader_file_stat(&zip, i, &stat))
|
||||
continue;
|
||||
if (mz_zip_reader_is_file_a_directory(&zip, i))
|
||||
continue;
|
||||
if (fs::path(stat.m_filename).extension() == k_libExt) {
|
||||
dllEntry = stat.m_filename;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dllEntry.empty()) {
|
||||
mz_zip_reader_end(&zip);
|
||||
DuskLog.warn("ModLoader: no *{} found in {} — skipping", k_libExt,
|
||||
modPath.filename().string());
|
||||
return;
|
||||
}
|
||||
|
||||
const fs::path cacheDir = fs::path("mods") / ".cache" / modPath.stem();
|
||||
std::error_code ec;
|
||||
fs::create_directories(cacheDir, ec);
|
||||
|
||||
const fs::path dllCachePath = cacheDir / fs::path(dllEntry).filename();
|
||||
if (!mz_zip_reader_extract_file_to_file(&zip, dllEntry.c_str(), dllCachePath.string().c_str(),
|
||||
0))
|
||||
{
|
||||
mz_zip_reader_end(&zip);
|
||||
DuskLog.error("ModLoader: failed to extract {} from {}", dllEntry,
|
||||
modPath.filename().string());
|
||||
return;
|
||||
}
|
||||
mz_zip_reader_end(&zip);
|
||||
|
||||
void* handle = pl_dlopen(dllCachePath);
|
||||
if (!handle) {
|
||||
DuskLog.error("ModLoader: failed to open {}: {}", dllCachePath.string(), pl_dlerror());
|
||||
return;
|
||||
}
|
||||
|
||||
LoadedMod mod;
|
||||
mod.mod_path = fs::absolute(modPath).string();
|
||||
mod.dir = fs::absolute(cacheDir).string();
|
||||
mod.handle = handle;
|
||||
mod.fn_init = reinterpret_cast<LoadedMod::FnInit>(pl_dlsym(handle, "mod_init"));
|
||||
mod.fn_tick = reinterpret_cast<LoadedMod::FnTick>(pl_dlsym(handle, "mod_tick"));
|
||||
mod.fn_cleanup = reinterpret_cast<LoadedMod::FnCleanup>(pl_dlsym(handle, "mod_cleanup"));
|
||||
mod.fn_set_imgui_ctx =
|
||||
reinterpret_cast<LoadedMod::FnSetImguiCtx>(pl_dlsym(handle, "dusk_mod_set_imgui_ctx"));
|
||||
|
||||
if (!mod.fn_init || !mod.fn_tick) {
|
||||
DuskLog.error("ModLoader: {} missing mod_init or mod_tick — skipping",
|
||||
fs::path(dllEntry).filename().string());
|
||||
pl_dlclose(handle);
|
||||
return;
|
||||
}
|
||||
|
||||
mod.name = metaName.empty() ? modPath.stem().string() : metaName;
|
||||
mod.version = metaVersion.empty() ? "?" : metaVersion;
|
||||
mod.author = metaAuthor.empty() ? "unknown" : metaAuthor;
|
||||
mod.description = metaDescription;
|
||||
|
||||
m_mods.push_back(std::move(mod));
|
||||
DuskLog.info("ModLoader: found '{}' v{} by {} ({})", m_mods.back().name, m_mods.back().version,
|
||||
m_mods.back().author, modPath.filename().string());
|
||||
}
|
||||
|
||||
void ModLoader::init() {
|
||||
if (m_initialized)
|
||||
return;
|
||||
m_initialized = true;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
if (!fs::exists(m_modsDir)) {
|
||||
DuskLog.info("ModLoader: mods directory '{}' not found — mod loading skipped",
|
||||
m_modsDir.string());
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::vector<fs::directory_entry> entries;
|
||||
for (auto& e : fs::directory_iterator(m_modsDir, ec))
|
||||
if (e.is_regular_file() && e.path().extension() == ".dusk")
|
||||
entries.push_back(e);
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const fs::directory_entry& a, const fs::directory_entry& b) {
|
||||
return a.path().filename() < b.path().filename();
|
||||
});
|
||||
|
||||
for (auto& entry : entries)
|
||||
tryLoadDusk(entry.path());
|
||||
|
||||
if (m_mods.empty()) {
|
||||
DuskLog.info("ModLoader: no mods found");
|
||||
return;
|
||||
}
|
||||
|
||||
DuskLog.info("ModLoader: initializing {} mod(s)...", m_mods.size());
|
||||
for (auto& mod : m_mods)
|
||||
buildAPI(mod);
|
||||
|
||||
for (auto& mod : m_mods) {
|
||||
ModGuard guard(&mod);
|
||||
try {
|
||||
mod.fn_init(&mod.api);
|
||||
mod.active = true;
|
||||
DuskLog.info("ModLoader: '{}' initialized", mod.name);
|
||||
} catch (const std::exception& e) {
|
||||
DuskLog.error("ModLoader: exception in {}.mod_init(): {}", mod.name, e.what());
|
||||
} catch (...) {
|
||||
DuskLog.error("ModLoader: unknown exception in {}.mod_init()", mod.name);
|
||||
}
|
||||
}
|
||||
|
||||
auto active =
|
||||
std::count_if(m_mods.begin(), m_mods.end(), [](const LoadedMod& m) { return m.active; });
|
||||
DuskLog.info("ModLoader: {}/{} mod(s) active", active, m_mods.size());
|
||||
}
|
||||
|
||||
void ModLoader::tick() {
|
||||
for (auto& mod : m_mods) {
|
||||
if (!mod.active)
|
||||
continue;
|
||||
ModGuard guard(&mod);
|
||||
try {
|
||||
mod.fn_tick(&mod.api);
|
||||
} catch (const std::exception& e) {
|
||||
DuskLog.error("ModLoader: exception in {}.mod_tick(): {} — disabling", mod.name,
|
||||
e.what());
|
||||
mod.active = false;
|
||||
} catch (...) {
|
||||
DuskLog.error("ModLoader: unknown exception in {}.mod_tick() — disabling", mod.name);
|
||||
mod.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModLoader::shutdown() {
|
||||
for (auto& mod : m_mods) {
|
||||
hookClearMod(&mod);
|
||||
if (mod.fn_cleanup) {
|
||||
ModGuard guard(&mod);
|
||||
try {
|
||||
mod.fn_cleanup(&mod.api);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
if (mod.handle) {
|
||||
pl_dlclose(mod.handle);
|
||||
mod.handle = nullptr;
|
||||
}
|
||||
}
|
||||
m_mods.clear();
|
||||
DuskLog.info("ModLoader: all mods unloaded");
|
||||
}
|
||||
|
||||
void ModLoader::callDrawCallback(const LoadedMod& mod, const ModDrawCallback& cb) {
|
||||
if (mod.fn_set_imgui_ctx)
|
||||
mod.fn_set_imgui_ctx(ImGui::GetCurrentContext());
|
||||
cb.draw_fn(cb.userdata);
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "d/d_tresure.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "f_op/f_op_camera_mng.h"
|
||||
#include "f_op/f_op_draw_tag.h"
|
||||
#include "f_op/f_op_overlap_mng.h"
|
||||
@@ -812,6 +813,7 @@ void fapGm_Execute() {
|
||||
fpcM_ManagementFunc(NULL, fapGm_After);
|
||||
#endif
|
||||
cCt_Counter(0);
|
||||
dusk::ModLoader::instance().tick();
|
||||
}
|
||||
|
||||
fapGm_HIO_c g_HIO;
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
#include "dusk/game_clock.h"
|
||||
#include "dusk/gyro.h"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
@@ -190,6 +191,8 @@ void main01(void) {
|
||||
OSReport("Calling cDyl_InitAsync()...\n");
|
||||
cDyl_InitAsync();
|
||||
|
||||
dusk::ModLoader::instance().init();
|
||||
|
||||
g_mDoAud_audioHeap = JKRCreateSolidHeap(audioHeapSize, JKRGetCurrentHeap(), false);
|
||||
JKRHEAP_NAME(g_mDoAud_audioHeap, "g_mDoAud_audioHeap");
|
||||
|
||||
@@ -292,6 +295,7 @@ void main01(void) {
|
||||
} while (dusk::IsRunning);
|
||||
|
||||
exit:;
|
||||
dusk::ModLoader::instance().shutdown();
|
||||
}
|
||||
|
||||
static bool IsBackendAvailable(AuroraBackend backend) {
|
||||
@@ -479,6 +483,7 @@ int game_main(int argc, char* argv[]) {
|
||||
("h,help", "Print usage")
|
||||
("console", "Show the Windows console window for logs", cxxopts::value<bool>()->default_value("false")->implicit_value("true"))
|
||||
("dvd", "Path to DVD image file", cxxopts::value<std::string>())
|
||||
("mods", "Path to mods directory", cxxopts::value<std::string>()->default_value("mods"))
|
||||
("backend", "Graphics API backend to use (auto, d3d12, metal, vulkan, null)", cxxopts::value<std::string>())
|
||||
("cvar", "Override configuration variables without modifying config", cxxopts::value<std::vector<std::string>>());
|
||||
|
||||
@@ -596,9 +601,8 @@ int game_main(int argc, char* argv[]) {
|
||||
mDoMain::developmentMode = 1; // Force Dev Mode for Debugging
|
||||
mDoDvdThd::SyncWidthSound = false;
|
||||
|
||||
dusk::ModLoader::instance().setModsDir(parsed_arg_options["mods"].as<std::string>());
|
||||
OSReport("Starting main01 (Game Loop)...\n");
|
||||
|
||||
|
||||
main01();
|
||||
|
||||
dusk::ShutdownCrashReporting();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(my_mod CXX)
|
||||
|
||||
# Path to the dusk source root.
|
||||
# Set this to your dusk submodule:
|
||||
# set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusk")
|
||||
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
|
||||
|
||||
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
|
||||
|
||||
# Output .dusk packages next to the build directory by default.
|
||||
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into")
|
||||
|
||||
add_dusk_mod(template_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Template Mod",
|
||||
"version": "1.0.0",
|
||||
"author": "Maddie",
|
||||
"description": "An example Dusk mod"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
This text has been loaded from the mods resources!
|
||||
Press R to rotate Link!
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "dusk/hook.hpp"
|
||||
#include "dusk/mod_api.h"
|
||||
#include "imgui.h"
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
static int TickCount = 0;
|
||||
static std::string TextContents;
|
||||
|
||||
static int32_t on_posMove_pre(void* args) {
|
||||
if (!mDoCPd_c::getHoldR(PAD_1))
|
||||
return 0;
|
||||
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
|
||||
link->shape_angle.y -= 2048;
|
||||
dusk::g_api->log_info("ROTATING %d", link->shape_angle.y);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void DrawTabContent(void*) {
|
||||
daAlink_c* link = daAlink_getAlinkActorClass();
|
||||
if (link) {
|
||||
ImGui::Text("Y angle: %d", (int)link->shape_angle.y);
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button("Reset rotation")) {
|
||||
link->shape_angle.y = 0;
|
||||
}
|
||||
}
|
||||
if (!TextContents.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted(TextContents.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawMenuItem(void*) {
|
||||
if (ImGui::MenuItem("Reset rotation")) {
|
||||
daAlink_c* link = daAlink_getAlinkActorClass();
|
||||
if (link) {
|
||||
link->shape_angle.y = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
void dusk_mod_set_imgui_ctx(void* ctx) {
|
||||
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(ctx));
|
||||
}
|
||||
|
||||
void mod_init(DuskModAPI* api) {
|
||||
api->log_info("Test Mod initializing...");
|
||||
|
||||
dusk::init(api);
|
||||
dusk::hookAddPre<&daAlink_c::posMove>(on_posMove_pre);
|
||||
|
||||
size_t size = 0;
|
||||
void* data = api->load_resource("text.txt", &size);
|
||||
if (data) {
|
||||
TextContents.assign(static_cast<char*>(data), size);
|
||||
api->free_resource(data);
|
||||
api->log_info("Loaded text.txt (%zu bytes)", size);
|
||||
} else {
|
||||
api->log_warn("Failed to load text.txt");
|
||||
}
|
||||
|
||||
api->register_tab_content(DrawTabContent, nullptr);
|
||||
api->register_menu_item(DrawMenuItem, nullptr);
|
||||
api->log_info("Test Mod ready. Mod folder: %s", api->mod_dir);
|
||||
}
|
||||
|
||||
void mod_tick(DuskModAPI* api) {
|
||||
++TickCount;
|
||||
}
|
||||
|
||||
void mod_cleanup(DuskModAPI* api) {
|
||||
api->log_info("Test Mod unloading after %d ticks.", TickCount);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user