mod loader

This commit is contained in:
madeline
2026-04-20 04:17:42 -07:00
parent c43f33a5bc
commit 22d906a248
30 changed files with 1170 additions and 51 deletions
+2
View File
@@ -49,3 +49,5 @@ compile_commands.json
pipeline_cache.bin
extract
*.dusk
+2 -2
View File
@@ -6,12 +6,12 @@
"type": "cppvsdbg",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console"],
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console", "--mods", "${workspaceRoot}/tools/mod_template/mods"],
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"symbolSearchPath": "${command:cmake.launchTargetPath}",
"console": "integratedTerminal",
"cwd":"${workspaceRoot}"
"cwd":"${workspaceRoot}",
}
]
}
+88 -29
View File
@@ -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 "")
+59
View File
@@ -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()
+4
View File
@@ -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
)
+5 -5
View File
@@ -1049,11 +1049,11 @@ public:
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
extern dComIfG_inf_c g_dComIfG_gameInfo;
extern GXColor g_blackColor;
extern GXColor g_clearColor;
extern GXColor g_whiteColor;
extern GXColor g_saftyWhiteColor;
DUSK_GAME_EXTERN dComIfG_inf_c g_dComIfG_gameInfo;
DUSK_GAME_EXTERN GXColor g_blackColor;
DUSK_GAME_EXTERN GXColor g_clearColor;
DUSK_GAME_EXTERN GXColor g_whiteColor;
DUSK_GAME_EXTERN GXColor g_saftyWhiteColor;
int dComLbG_PhaseHandler(request_of_phase_process_class*, request_of_phase_process_fn*,
void*);
+4 -9
View File
@@ -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);
+84
View File
@@ -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
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
namespace dusk {
void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store);
void hookRegisterPre (void* fn_addr, void* mod, int32_t (*fn)(void* args));
void hookRegisterPost(void* fn_addr, void* mod, 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
+50
View File
@@ -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
+64
View File
@@ -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
+13
View File
@@ -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))
+2 -1
View File
@@ -4,6 +4,7 @@
#include "JSystem/JUtility/JUTGamePad.h"
#include "SSystem/SComponent/c_API_controller_pad.h"
#include "dusk/settings.h"
#include "global.h"
// Controller Ports 1 - 4
enum { PAD_1, PAD_2, PAD_3, PAD_4 };
@@ -94,7 +95,7 @@ public:
static void stopMotorWaveHard(u32 pad) { return m_gamePad[pad]->stopMotorWaveHard(); }
static JUTGamePad* m_gamePad[4];
static interface_of_controller_pad m_cpadInfo[4];
static DUSK_GAME_DATA interface_of_controller_pad m_cpadInfo[4];
static interface_of_controller_pad m_debugCpadInfo[4];
};
+1 -1
View File
@@ -371,6 +371,7 @@ public:
static int m_height;
static f32 m_heightF;
static f32 m_widthF;
#endif
#if TARGET_PC
static f32 m_safeMinXF;
@@ -380,7 +381,6 @@ public:
static f32 m_safeWidthF;
static f32 m_safeHeightF;
#endif
#endif
};
#endif /* M_DO_M_DO_GRAPHIC_H */
+6
View File
@@ -142,6 +142,12 @@ DynamicModuleControl::DynamicModuleControl(char const* name) {
}
#endif
#if TARGET_PC
// dump() is declared but its definition is inside #if !TARGET_PC above; stub it out.
void DynamicModuleControlBase::dump() {}
void DynamicModuleControlBase::dump(char*) {}
#endif
u32 DynamicModuleControl::sAllocBytes;
JKRArchive* DynamicModuleControl::sArchive;
+13
View File
@@ -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();
}
+149
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+78
View File
@@ -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
+15
View File
@@ -0,0 +1,15 @@
#pragma once
namespace dusk {
class ImGuiMenuMods {
public:
void draw();
void showModsWindow();
private:
bool m_showWindow = false;
};
} // namespace dusk
+15
View File
@@ -0,0 +1,15 @@
/**
* Thin Windows launcher EXE. The game lives in dusk.dll, this just forwards
* the Windows entry point to it. Keeping the game as a DLL lets mod .dll
* files link against dusk.lib and resolve all game symbols at load time.
*/
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
// see src/dusk/main.cpp
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
View File
@@ -1,5 +1,5 @@
#if _WIN32
#define WINDOWS_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <shellapi.h>
#endif
@@ -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
+377
View File
@@ -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
+2
View File
@@ -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;
+6 -2
View File
@@ -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();
+18
View File
@@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.25)
project(my_mod CXX)
# Path to the dusk source root.
# Set this to your dusk submodule:
# set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusk")
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
# Output .dusk packages next to the build directory by default.
set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into")
add_dusk_mod(template_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
)
+6
View File
@@ -0,0 +1,6 @@
{
"name": "Template Mod",
"version": "1.0.0",
"author": "Maddie",
"description": "An example Dusk mod"
}
+2
View File
@@ -0,0 +1,2 @@
This text has been loaded from the mods resources!
Press R to rotate Link!
+80
View File
@@ -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);
}
}