From b2871054a66cd80aaee0a8f8012efa62b5a1bb21 Mon Sep 17 00:00:00 2001 From: madeline Date: Mon, 11 May 2026 02:55:11 -0700 Subject: [PATCH] address review, rmlui, better api, catmod --- CMakeLists.txt | 12 +- cmake/DuskModSDK.cmake | 15 +- cmake/dusk_imgui_ctx.cpp | 5 - files.cmake | 3 +- include/Z2AudioLib/Z2AudioMgr.h | 2 +- include/d/d_meter2_info.h | 5 +- include/dusk/hook.hpp | 48 ++- include/dusk/hook_system.hpp | 8 +- include/dusk/mod_api.h | 75 ++-- include/dusk/mod_loader.hpp | 45 +- include/dusk/mod_utils.h | 28 ++ include/global.h | 2 +- include/m_Do/m_Do_audio.h | 2 +- .../include/JSystem/JMath/JMATrigonometric.h | 7 +- libs/JSystem/src/JMath/JMATrigonometric.cpp | 6 +- res/rml/window.rcss | 29 ++ src/Z2AudioLib/Z2AudioMgr.cpp | 2 +- src/d/d_meter2_info.cpp | 2 +- src/dusk/gx_helper.cpp | 2 - src/dusk/hook_system.cpp | 67 ++- src/dusk/imgui/ImGuiConsole.cpp | 2 - src/dusk/imgui/ImGuiConsole.hpp | 2 - src/dusk/imgui/ImGuiMenuMods.cpp | 86 ---- src/dusk/imgui/ImGuiMenuMods.hpp | 15 - src/dusk/mod_loader.cpp | 401 ++++++++++++++---- src/dusk/ui/menu_bar.cpp | 2 + src/dusk/ui/mods_window.cpp | 125 ++++++ src/dusk/ui/mods_window.hpp | 23 + src/f_ap/f_ap_game.cpp | 2 +- src/m_Do/m_Do_audio.cpp | 2 +- tools/cat_mod/CMakeLists.txt | 13 + tools/cat_mod/mod.json | 6 + tools/cat_mod/res/.gitkeep | 0 tools/cat_mod/src/mod.cpp | 250 +++++++++++ tools/mod_test/src/mod.cpp | 166 ++++---- 35 files changed, 1065 insertions(+), 395 deletions(-) delete mode 100644 cmake/dusk_imgui_ctx.cpp create mode 100644 include/dusk/mod_utils.h delete mode 100644 src/dusk/imgui/ImGuiMenuMods.cpp delete mode 100644 src/dusk/imgui/ImGuiMenuMods.hpp create mode 100644 src/dusk/ui/mods_window.cpp create mode 100644 src/dusk/ui/mods_window.hpp create mode 100644 tools/cat_mod/CMakeLists.txt create mode 100644 tools/cat_mod/mod.json create mode 100644 tools/cat_mod/res/.gitkeep create mode 100644 tools/cat_mod/src/mod.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f37edece8c..7f05b4ca21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,7 @@ if (MSVC AND TARGET rmlui_core) set_target_properties(rmlui_core PROPERTIES DISABLE_PRECOMPILE_HEADERS ON) endif () + add_subdirectory(libs/freeverb) option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)") @@ -457,6 +458,7 @@ set_source_files_properties( foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES) target_compile_definitions(${jsystem_lib} PRIVATE ${GAME_COMPILE_DEFS} + DUSK_BUILDING_GAME=1 $<$:DEBUG=1> $<$:PARTIAL_DEBUG=1> ) @@ -481,7 +483,8 @@ elseif(WIN32) add_library(dusk_game SHARED ${DUSK_FILES}) set_target_properties(dusk_game PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON - OUTPUT_NAME dusk) + OUTPUT_NAME dusk + PDB_NAME dusk_game) add_executable(dusk WIN32 src/dusk/launcher_win32.cpp) target_link_libraries(dusk PRIVATE dusk_game) @@ -492,10 +495,15 @@ else () set(DUSK_MAIN_TARGET dusk) endif () +if (WIN32 AND TARGET imgui) + target_compile_definitions(imgui PRIVATE "IMGUI_API=__declspec(dllexport)") + target_sources(${DUSK_MAIN_TARGET} PRIVATE $) +endif () + target_compile_definitions(${DUSK_MAIN_TARGET} PRIVATE ${GAME_COMPILE_DEFS} DUSK_BUILDING_GAME=1) target_include_directories(${DUSK_MAIN_TARGET} PRIVATE ${GAME_INCLUDE_DIRS}) target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES}) -target_precompile_headers(${DUSK_MAIN_TARGET} PRIVATE "$<$:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>") +target_precompile_headers(${DUSK_MAIN_TARGET} PRIVATE "$<$:${CMAKE_CURRENT_LIST_DIR}/include/dusk_pch.hpp>") if(WIN32) target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE Psapi) diff --git a/cmake/DuskModSDK.cmake b/cmake/DuskModSDK.cmake index 9ae8e92b93..286725ae01 100644 --- a/cmake/DuskModSDK.cmake +++ b/cmake/DuskModSDK.cmake @@ -7,8 +7,7 @@ function(add_dusk_mod target_name) message(FATAL_ERROR "add_dusk_mod: MOD_JSON is required") endif() - add_library(${target_name} SHARED ${ARG_SOURCES} - "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/dusk_imgui_ctx.cpp") + 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) @@ -21,20 +20,10 @@ function(add_dusk_mod target_name) 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") + set_target_properties(${target_name} PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") 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") diff --git a/cmake/dusk_imgui_ctx.cpp b/cmake/dusk_imgui_ctx.cpp deleted file mode 100644 index 48848ffcee..0000000000 --- a/cmake/dusk_imgui_ctx.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "imgui.h" - -extern "C" void dusk_mod_set_imgui_ctx(void* ctx) { - ImGui::SetCurrentContext(static_cast(ctx)); -} diff --git a/files.cmake b/files.cmake index b141291e7b..45e6518d0c 100644 --- a/files.cmake +++ b/files.cmake @@ -1491,6 +1491,8 @@ set(DUSK_FILES src/dusk/ui/pane.hpp src/dusk/ui/menu_bar.cpp src/dusk/ui/menu_bar.hpp + src/dusk/ui/mods_window.cpp + src/dusk/ui/mods_window.hpp src/dusk/ui/prelaunch.cpp src/dusk/ui/prelaunch.hpp src/dusk/ui/preset.cpp @@ -1517,7 +1519,6 @@ set(DUSK_FILES src/dusk/OSMutex.cpp src/dusk/hook_system.cpp src/dusk/mod_loader.cpp - src/dusk/imgui/ImGuiMenuMods.cpp src/dusk/gx_helper.cpp src/dusk/discord.cpp src/dusk/discord.hpp diff --git a/include/Z2AudioLib/Z2AudioMgr.h b/include/Z2AudioLib/Z2AudioMgr.h index e543bf6061..020eb7681a 100644 --- a/include/Z2AudioLib/Z2AudioMgr.h +++ b/include/Z2AudioLib/Z2AudioMgr.h @@ -34,7 +34,7 @@ public: bool isResetting() { return mResettingFlag; } static Z2AudioMgr* getInterface() { return mAudioMgrPtr; } - static Z2AudioMgr* mAudioMgrPtr; + static DUSK_GAME_DATA Z2AudioMgr* mAudioMgrPtr; /* 0x0514 */ virtual bool startSound(JAISoundID soundID, JAISoundHandle* handle, const JGeometry::TVec3* posPtr); /* 0x0518 */ bool mResettingFlag; diff --git a/include/d/d_meter2_info.h b/include/d/d_meter2_info.h index 51f07f4dc3..a941d68cb9 100644 --- a/include/d/d_meter2_info.h +++ b/include/d/d_meter2_info.h @@ -2,6 +2,7 @@ #define D_METER_D_METER2_INFO_H #include "SSystem/SComponent/c_xyz.h" +#include "global.h" class CPaneMgr; class J2DTextBox; @@ -301,7 +302,7 @@ public: /* 0xF3 */ u8 unk_0xf3[5]; }; -extern dMeter2Info_c g_meter2_info; +DUSK_GAME_EXTERN dMeter2Info_c g_meter2_info; void dMeter2Info_setSword(u8 i_itemId, bool i_offItemBit); void dMeter2Info_setCloth(u8 i_clothId, bool i_offItemBit); @@ -849,6 +850,8 @@ inline void dMeter2Info_setFloatingMessage(u16 i_msgID, s16 i_msgTimer, bool i_w g_meter2_info.setFloatingMessage(i_msgID, i_msgTimer, i_wakuVisible); } +// Show a custom text notification using the floating-message HUD display. + inline void dMeter2Info_setMiniGameCount(s8 i_count) { g_meter2_info.setMiniGameCount(i_count); } diff --git a/include/dusk/hook.hpp b/include/dusk/hook.hpp index eac28a7c4e..5725e07e29 100644 --- a/include/dusk/hook.hpp +++ b/include/dusk/hook.hpp @@ -35,19 +35,54 @@ struct HookEntryBase { static R trampoline(Self self, A... args) { void* ptrs[] = {static_cast(std::addressof(self)), static_cast(std::addressof(args))...}; - const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast(ptrs)); if constexpr (std::is_void_v) { + const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast(ptrs), nullptr); if (!cancel) g_orig(self, args...); - g_api->hook_dispatch_post(mfpAddr(MFP), static_cast(ptrs)); + g_api->hook_dispatch_post(mfpAddr(MFP), static_cast(ptrs), nullptr); } else { R result{}; + const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast(ptrs), static_cast(std::addressof(result))); if (!cancel) result = g_orig(self, args...); - g_api->hook_dispatch_post(mfpAddr(MFP), static_cast(ptrs)); + g_api->hook_dispatch_post(mfpAddr(MFP), static_cast(ptrs), static_cast(std::addressof(result))); return result; } } }; +template +struct HookEntryFreeBase { + static inline Orig g_orig = nullptr; + + static R trampoline(A... args) { + if constexpr (sizeof...(A) == 0) { + if constexpr (std::is_void_v) { + const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), nullptr, nullptr); + if (!cancel) g_orig(args...); + g_api->hook_dispatch_post(mfpAddr(FP), nullptr, nullptr); + } else { + R result{}; + const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), nullptr, static_cast(std::addressof(result))); + if (!cancel) result = g_orig(args...); + g_api->hook_dispatch_post(mfpAddr(FP), nullptr, static_cast(std::addressof(result))); + return result; + } + } else { + void* ptrs[] = {static_cast(std::addressof(args))...}; + if constexpr (std::is_void_v) { + const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), static_cast(ptrs), nullptr); + if (!cancel) g_orig(args...); + g_api->hook_dispatch_post(mfpAddr(FP), static_cast(ptrs), nullptr); + } else { + R result{}; + const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), static_cast(ptrs), static_cast(std::addressof(result))); + if (!cancel) result = g_orig(args...); + g_api->hook_dispatch_post(mfpAddr(FP), static_cast(ptrs), static_cast(std::addressof(result))); + return result; + } + } + } +}; + template struct HookEntry; @@ -57,6 +92,9 @@ struct HookEntry : HookEntryBase {}; template struct HookEntry : HookEntryBase {}; +template +struct HookEntry : HookEntryFreeBase {}; + template void hookAddPre(int32_t (*fn)(void* args)) { using E = HookEntry; @@ -66,7 +104,7 @@ void hookAddPre(int32_t (*fn)(void* args)) { } template -void hookAddPost(void (*fn)(void* args)) { +void hookAddPost(void (*fn)(void* args, void* retval)) { using E = HookEntry; g_api->hook_install(mfpAddr(MFP), reinterpret_cast(E::trampoline), reinterpret_cast(&E::g_orig)); @@ -74,7 +112,7 @@ void hookAddPost(void (*fn)(void* args)) { } template -void hookSetReplace(void (*fn)(void* args)) { +void hookSetReplace(void (*fn)(void* args, void* retval)) { using E = HookEntry; g_api->hook_install(mfpAddr(MFP), reinterpret_cast(E::trampoline), reinterpret_cast(&E::g_orig)); diff --git a/include/dusk/hook_system.hpp b/include/dusk/hook_system.hpp index c173a3c44b..ffd32c318e 100644 --- a/include/dusk/hook_system.hpp +++ b/include/dusk/hook_system.hpp @@ -7,11 +7,11 @@ namespace dusk { void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store); void hookRegisterPre (void* fn_addr, void* mod, int32_t (*fn)(void* args)); -void hookRegisterPost(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args)); -bool hookSetReplace (void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args)); +void hookRegisterPost(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval)); +bool hookSetReplace (void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval)); -bool hookDispatchPre (void* fn_addr, void* args); -void hookDispatchPost(void* fn_addr, void* args); +bool hookDispatchPre (void* fn_addr, void* args, void* retval); +void hookDispatchPost(void* fn_addr, void* args, void* retval); void hookClearMod(void* mod); diff --git a/include/dusk/mod_api.h b/include/dusk/mod_api.h index 8fa2f557f7..af3b9571c9 100644 --- a/include/dusk/mod_api.h +++ b/include/dusk/mod_api.h @@ -1,58 +1,65 @@ -#ifndef DUSK_MOD_API_H -#define DUSK_MOD_API_H +#pragma once -#include -#include -#include +#include +#include -#ifdef __cplusplus -extern "C" { +#if defined(_WIN32) +#define DUSK_MOD_EXPORT __declspec(dllexport) +#else +#define DUSK_MOD_EXPORT __attribute__((visibility("default"))) #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 void* DuskPanelHandle; +typedef void* DuskElemHandle; // Place this once at file scope in your mod to declare the minimum API version required. // The loader will refuse to initialize the mod if the engine's API version is older. -#define DUSK_REQUIRE_API_VERSION \ - DUSK_MOD_EXPORT uint32_t mod_api_version = DUSK_MOD_API_VERSION; +#define DUSK_REQUIRE_API_VERSION \ + extern "C" DUSK_MOD_EXPORT uint32_t mod_api_version = DUSK_MOD_API_VERSION; -typedef struct DuskModAPI { - uint32_t api_version; +struct DuskModAPIv1 { + uint32_t api_version; const char* mod_dir; - void (*log_info) (const char* fmt, ...); - void (*log_warn) (const char* fmt, ...); + void (*log_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 (*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 (*register_tab_content)( + void (*build_fn)(DuskPanelHandle panel, void* userdata), void* userdata); + void (*register_tab_update)(void (*update_fn)(void* userdata), void* userdata); + + void (*panel_add_section)(DuskPanelHandle panel, const char* text); + void (*panel_add_button)( + DuskPanelHandle panel, const char* label, void (*cb)(void* userdata), void* userdata); + DuskElemHandle (*panel_add_badge_row)(DuskPanelHandle panel, const char* label, int ok); + DuskElemHandle (*panel_add_dyn_text)(DuskPanelHandle panel, const char* text); + DuskElemHandle (*panel_add_progress)(DuskPanelHandle panel, float value); + + void (*elem_set_badge)(DuskElemHandle elem, int ok); + void (*elem_set_text)(DuskElemHandle elem, const char* text); + void (*elem_set_progress)(DuskElemHandle elem, float value); void (*hook_install)(void* fn_addr, void* tramp_fn, void** orig_store); - void (*hook_pre) (void* fn_addr, int32_t (*fn)(void* args)); - void (*hook_post) (void* fn_addr, void (*fn)(void* args)); - void (*hook_replace)(void* fn_addr, void (*fn)(void* args)); + void (*hook_pre)(void* fn_addr, int32_t (*fn)(void* args)); + void (*hook_post)(void* fn_addr, void (*fn)(void* args, void* retval)); + void (*hook_replace)(void* fn_addr, void (*fn)(void* args, void* retval)); - bool (*hook_dispatch_pre) (void* fn_addr, void* args); - void (*hook_dispatch_post)(void* fn_addr, void* args); + bool (*hook_dispatch_pre)(void* fn_addr, void* args, void* retval); + void (*hook_dispatch_post)(void* fn_addr, void* args, void* retval); - void (*service_publish)(const char* name, void* ptr); - void* (*service_get) (const char* name); -} DuskModAPI; + void (*service_publish)(const char* name, void* ptr); + void* (*service_get)(const char* name); +}; +using DuskModAPI = DuskModAPIv1; + +extern "C" { void mod_init(DuskModAPI* api); void mod_tick(DuskModAPI* api); - -#ifdef __cplusplus } -#endif - -#endif diff --git a/include/dusk/mod_loader.hpp b/include/dusk/mod_loader.hpp index 5500facd72..f9c488ce01 100644 --- a/include/dusk/mod_loader.hpp +++ b/include/dusk/mod_loader.hpp @@ -5,11 +5,17 @@ #include #include "dusk/mod_api.h" +#include "miniz.h" namespace dusk { -struct ModDrawCallback { - void (*draw_fn)(void* userdata); +struct RmlTabContentCallback { + void (*build_fn)(void* panel, void* userdata); + void* userdata; +}; + +struct RmlTabUpdateCallback { + void (*update_fn)(void* userdata); void* userdata; }; @@ -21,24 +27,26 @@ struct LoadedMod { std::string mod_path; std::string dir; - void* handle = nullptr; - bool active = false; - bool load_failed = false; + void* handle = nullptr; + bool active = false; + bool load_failed = false; - using FnInit = void (*)(DuskModAPI*); - using FnTick = void (*)(DuskModAPI*); - using FnCleanup = void (*)(DuskModAPI*); - using FnSetImguiCtx = void (*)(void*); + using FnInit = void (*)(DuskModAPI*); + using FnTick = void (*)(DuskModAPI*); + using FnCleanup = void (*)(DuskModAPI*); - FnInit fn_init = nullptr; - FnTick fn_tick = nullptr; - FnCleanup fn_cleanup = nullptr; - FnSetImguiCtx fn_set_imgui_ctx = nullptr; + FnInit fn_init = nullptr; + FnTick fn_tick = nullptr; + FnCleanup fn_cleanup = nullptr; DuskModAPI api{}; - std::vector tab_content; - std::vector menu_items; + std::vector zip_data; + mz_zip_archive res_zip{}; + bool res_zip_open = false; + + std::vector tab_content; + std::vector tab_updates; }; class ModLoader { @@ -51,15 +59,14 @@ public: void shutdown(); const std::vector& mods() const { return m_mods; } - static void callDrawCallback(const LoadedMod& mod, const ModDrawCallback& cb); private: std::vector m_mods; - std::filesystem::path m_modsDir; - bool m_initialized = false; + std::filesystem::path m_modsDir; + bool m_initialized = false; void tryLoadDusk(const std::filesystem::path& modPath); void buildAPI(LoadedMod& mod); }; -} // namespace dusk +} // namespace dusk diff --git a/include/dusk/mod_utils.h b/include/dusk/mod_utils.h new file mode 100644 index 0000000000..9dc7efa533 --- /dev/null +++ b/include/dusk/mod_utils.h @@ -0,0 +1,28 @@ +#pragma once + +#include "f_op/f_op_actor_mng.h" +#include "f_pc/f_pc_layer.h" +#include "f_pc/f_pc_manager.h" +#include "f_pc/f_pc_node.h" +#include "m_Do/m_Do_controller_pad.h" + +// Remove a button from this frame's trigger state so the game won't see it +// Call after detecting a combo in mod_tick to prevent double-processing +inline void consumeInput(u32 pad, u32 buttonMask) { + mDoCPd_c::getCpadInfo(pad).mPressedButtonFlags &= ~buttonMask; +} + +// Spawn an actor in the play scene layer +// calling fopAcM_create directly outside game simulation context creates the actor in the wrong +// layer, corrupting its first-frame rendering setup +inline fpc_ProcID fopAcM_createInPlayScene(s16 proc_name, u32 params, const cXyz* pos, int room_no, + const csXyz* angle, const cXyz* scale, s8 argument) { + layer_class* savedLayer = fpcLy_CurrentLayer(); + base_process_class* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e); + if (playScene != nullptr) { + fpcLy_SetCurrentLayer(&((process_node_class*)playScene)->layer); + } + fpc_ProcID result = fopAcM_create(proc_name, params, pos, room_no, angle, scale, argument); + fpcLy_SetCurrentLayer(savedLayer); + return result; +} diff --git a/include/global.h b/include/global.h index 11415b0737..4cc57e2a2c 100644 --- a/include/global.h +++ b/include/global.h @@ -120,7 +120,7 @@ inline int __builtin_clz(unsigned int v) { # 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_EXTERN extern __declspec(dllexport) # define DUSK_GAME_DATA __declspec(dllexport) #else # define DUSK_GAME_EXTERN extern diff --git a/include/m_Do/m_Do_audio.h b/include/m_Do/m_Do_audio.h index 4bc2f20c94..db341f3327 100644 --- a/include/m_Do/m_Do_audio.h +++ b/include/m_Do/m_Do_audio.h @@ -33,7 +33,7 @@ public: static void onBgmSet() { mBgmSet = true; } static void offBgmSet() { mBgmSet = false; } - static u8 mInitFlag; + static DUSK_GAME_DATA u8 mInitFlag; static u8 mResetFlag; static u8 mBgmSet; }; diff --git a/libs/JSystem/include/JSystem/JMath/JMATrigonometric.h b/libs/JSystem/include/JSystem/JMath/JMATrigonometric.h index e51296ba3b..b53cd97326 100644 --- a/libs/JSystem/include/JSystem/JMath/JMATrigonometric.h +++ b/libs/JSystem/include/JSystem/JMath/JMATrigonometric.h @@ -4,6 +4,7 @@ #include #include #include +#include "global.h" #ifdef __cplusplus extern "C" { @@ -141,9 +142,9 @@ struct TAsinAcosTable { } }; -extern TSinCosTable<13, f32> sincosTable_; -extern TAtanTable<1024, f32> atanTable_; -extern TAsinAcosTable<1024, f32> asinAcosTable_; +DUSK_GAME_EXTERN TSinCosTable<13, f32> sincosTable_; +DUSK_GAME_EXTERN TAtanTable<1024, f32> atanTable_; +DUSK_GAME_EXTERN TAsinAcosTable<1024, f32> asinAcosTable_; inline f32 acosDegree(f32 x) { return asinAcosTable_.acosDegree(x); diff --git a/libs/JSystem/src/JMath/JMATrigonometric.cpp b/libs/JSystem/src/JMath/JMATrigonometric.cpp index 5d394ac9c2..8ebdb57ebc 100644 --- a/libs/JSystem/src/JMath/JMATrigonometric.cpp +++ b/libs/JSystem/src/JMath/JMATrigonometric.cpp @@ -16,10 +16,10 @@ inline f64 getConst2() { return 9.765625E-4; } -TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32); +DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32); -TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32); +DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32); -TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32); +DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32); } // namespace JMath diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 45473f312f..286301fd89 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -380,6 +380,11 @@ progress.progress-ongoing fill { border-radius: 3dp; } +progress.progress-health fill { + background-color: #cc3322; + border-radius: 3dp; +} + button.achievement-clear { flex: 0 0 auto; align-self: center; @@ -494,6 +499,30 @@ progress.verification-progress-bar { color: rgba(224, 219, 200, 65%); } +.mod-info-row { + display: flex; + align-items: center; + gap: 12dp; + padding: 4dp 0; +} + +.mod-info-label { + font-family: "Fira Sans Condensed"; + font-weight: bold; + opacity: 0.55; + flex: 0 0 80dp; +} + +.mod-info-value { + flex: 1 1 0; +} + +.mod-path { + font-size: 14dp; + word-break: break-all; + opacity: 0.7; +} + .modal-actions { display: flex; flex-direction: row; diff --git a/src/Z2AudioLib/Z2AudioMgr.cpp b/src/Z2AudioLib/Z2AudioMgr.cpp index d6d78fb4b7..2af3d39662 100644 --- a/src/Z2AudioLib/Z2AudioMgr.cpp +++ b/src/Z2AudioLib/Z2AudioMgr.cpp @@ -19,7 +19,7 @@ #include "Z2AudioCS/Z2AudioCS.h" #endif -Z2AudioMgr* Z2AudioMgr::mAudioMgrPtr; +DUSK_GAME_DATA Z2AudioMgr* Z2AudioMgr::mAudioMgrPtr; u8 gMuffleOutOfRangeMic = false; Z2AudioMgr::Z2AudioMgr() : mSoundStarter(true) { diff --git a/src/d/d_meter2_info.cpp b/src/d/d_meter2_info.cpp index bb533e6370..ba527c7685 100644 --- a/src/d/d_meter2_info.cpp +++ b/src/d/d_meter2_info.cpp @@ -592,7 +592,7 @@ BOOL dMeter2Info_c::isDirectUseItem(int param_0) { return (mDirectUseItem & (u8)(1 << param_0)) ? TRUE : FALSE; } -dMeter2Info_c g_meter2_info; +DUSK_GAME_DATA dMeter2Info_c g_meter2_info; int dMeter2Info_c::setMeterString(s32 i_string) { if (mMeterString != 0) { diff --git a/src/dusk/gx_helper.cpp b/src/dusk/gx_helper.cpp index 9f9e321d15..9c7c91b69a 100644 --- a/src/dusk/gx_helper.cpp +++ b/src/dusk/gx_helper.cpp @@ -1,9 +1,7 @@ #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); diff --git a/src/dusk/hook_system.cpp b/src/dusk/hook_system.cpp index 54189dfcd5..10c2ddf17d 100644 --- a/src/dusk/hook_system.cpp +++ b/src/dusk/hook_system.cpp @@ -10,7 +10,7 @@ namespace dusk { -extern void* g_dusk_hook_current_mod; +extern thread_local void* g_dusk_hook_current_mod; struct PreHookFn { void* mod; @@ -19,7 +19,7 @@ struct PreHookFn { struct VoidHookFn { void* mod; const char* mod_name; - void (*fn)(void* args); + void (*fn)(void* args, void* retval); }; struct HookSlot { @@ -28,18 +28,12 @@ struct HookSlot { std::vector post; }; -static std::unordered_map& registry() { - static std::unordered_map s; - return s; -} -static std::unordered_map& installed() { - static std::unordered_map s; - return s; -} +static std::unordered_map s_registry; +static std::unordered_map s_installed; // Follow E9/FF25 chains to skip MSVC incremental-link and import stubs static void* resolveImportThunk(void* addr) { -#if _WIN32 +#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__)) for (int i = 0; i < 8; ++i) { const auto* p = static_cast(addr); if (p[0] == 0xFF && p[1] == 0x25) { @@ -51,8 +45,9 @@ static void* resolveImportThunk(void* addr) { int32_t offset; std::memcpy(&offset, p + 1, 4); addr = const_cast(p) + 5 + offset; - } else + } else { break; + } } #endif return addr; @@ -67,8 +62,8 @@ struct ModGuard { void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store) { fn_addr = resolveImportThunk(fn_addr); auto key = reinterpret_cast(fn_addr); - auto it = installed().find(key); - if (it != installed().end()) { + auto it = s_installed.find(key); + if (it != s_installed.end()) { *orig_store = it->second; return; } @@ -78,60 +73,63 @@ void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store) { 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); + DuskLog.warn( + "HookSystem: funchook failed for {:p} (prepare={} install={})", fn_addr, prep, inst); funchook_destroy(fh); return; } funchook_destroy(fh); - installed()[key] = fn; + s_installed[key] = fn; *orig_store = fn; } -bool hookDispatchPre(void* fn_addr, void* args) { - auto it = registry().find(reinterpret_cast(fn_addr)); - if (it == registry().end()) +bool hookDispatchPre(void* fn_addr, void* args, void* retval) { + auto it = s_registry.find(reinterpret_cast(fn_addr)); + if (it == s_registry.end()) { return false; + } auto& slot = it->second; for (auto& h : slot.pre) { ModGuard g(h.mod); - if (h.fn(args) != 0) + if (h.fn(args) != 0) { return true; + } } if (slot.replace.fn) { ModGuard g(slot.replace.mod); - slot.replace.fn(args); + slot.replace.fn(args, retval); return true; } return false; } -void hookDispatchPost(void* fn_addr, void* args) { - auto it = registry().find(reinterpret_cast(fn_addr)); - if (it == registry().end()) +void hookDispatchPost(void* fn_addr, void* args, void* retval) { + auto it = s_registry.find(reinterpret_cast(fn_addr)); + if (it == s_registry.end()) { return; + } for (auto& h : it->second.post) { if (h.fn) { ModGuard g(h.mod); - h.fn(args); + h.fn(args, retval); } } } void hookRegisterPre(void* fn_addr, void* mod, int32_t (*fn)(void* args)) { - registry()[reinterpret_cast(fn_addr)].pre.push_back({mod, fn}); + s_registry[reinterpret_cast(fn_addr)].pre.push_back({mod, fn}); } -void hookRegisterPost(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args)) { - registry()[reinterpret_cast(fn_addr)].post.push_back({mod, mod_name, fn}); +void hookRegisterPost(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval)) { + s_registry[reinterpret_cast(fn_addr)].post.push_back({mod, mod_name, fn}); } -bool hookSetReplace(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args)) { - auto& slot = registry()[reinterpret_cast(fn_addr)]; +bool hookSetReplace(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args, void* retval)) { + auto& slot = s_registry[reinterpret_cast(fn_addr)]; if (slot.replace.fn) { DuskLog.error("HookSystem: '{}' conflicts with '{}', both replace the same function", - mod_name, slot.replace.mod_name); + mod_name, slot.replace.mod_name); return false; } slot.replace = {mod, mod_name, fn}; @@ -139,7 +137,7 @@ bool hookSetReplace(void* fn_addr, void* mod, const char* mod_name, void (*fn)(v } void hookClearMod(void* mod) { - for (auto& [addr, slot] : registry()) { + for (auto& [addr, slot] : s_registry) { auto erase = [&](auto& v) { v.erase( std::remove_if(v.begin(), v.end(), [mod](const auto& h) { return h.mod == mod; }), @@ -147,8 +145,9 @@ void hookClearMod(void* mod) { }; erase(slot.pre); erase(slot.post); - if (slot.replace.mod == mod) + if (slot.replace.mod == mod) { slot.replace = {}; + } } } diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index f1246bbcb8..94ba944899 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -276,7 +276,6 @@ namespace dusk { if (showMenu && ImGui::BeginMainMenuBar()) { m_menuGame.draw(); m_menuTools.draw(); - m_menuMods.draw(); ImGui::EndMainMenuBar(); } @@ -376,7 +375,6 @@ namespace dusk { m_menuTools.ShowPlayerInfo(); m_menuTools.ShowAudioDebug(); m_menuTools.ShowSaveEditor(); - m_menuMods.showModsWindow(); m_menuTools.ShowStateShare(); m_menuTools.ShowActorSpawner(); } diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 8c9d6b1929..6362a146b6 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -8,7 +8,6 @@ #include #include "ImGuiMenuGame.hpp" -#include "ImGuiMenuMods.hpp" #include "ImGuiMenuTools.hpp" #include "dusk/main.h" #include "imgui.h" @@ -46,7 +45,6 @@ private: std::deque m_toasts; ImGuiMenuGame m_menuGame; - ImGuiMenuMods m_menuMods; // Keep always last ImGuiMenuTools m_menuTools; diff --git a/src/dusk/imgui/ImGuiMenuMods.cpp b/src/dusk/imgui/ImGuiMenuMods.cpp deleted file mode 100644 index 07d362696f..0000000000 --- a/src/dusk/imgui/ImGuiMenuMods.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#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.load_failed ? " [failed]" : 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()); - - if (mod.load_failed) { - ImGui::TextColored(ImVec4(1.f, 0.3f, 0.3f, 1.f), "Status: Failed to load"); - } else { - 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()); - } - - if (!mod.load_failed) { - for (const auto& cb : mod.tab_content) { - ImGui::Separator(); - ModLoader::callDrawCallback(mod, cb); - } - } - - ImGui::EndTabItem(); - } - } - ImGui::EndTabBar(); - } - - ImGui::End(); -} - -} // namespace dusk diff --git a/src/dusk/imgui/ImGuiMenuMods.hpp b/src/dusk/imgui/ImGuiMenuMods.hpp deleted file mode 100644 index 09a635fe7f..0000000000 --- a/src/dusk/imgui/ImGuiMenuMods.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -namespace dusk { - -class ImGuiMenuMods { -public: - void draw(); - - void showModsWindow(); - -private: - bool m_showWindow = false; -}; - -} // namespace dusk diff --git a/src/dusk/mod_loader.cpp b/src/dusk/mod_loader.cpp index 8fcd6b6635..34384d6366 100644 --- a/src/dusk/mod_loader.cpp +++ b/src/dusk/mod_loader.cpp @@ -2,13 +2,16 @@ #include "dusk/hook_system.hpp" #include "dusk/logging.h" +#include + + #include #include +#include #include #include #include -#include "imgui.h" #include "miniz.h" #include "nlohmann/json.hpp" @@ -29,10 +32,11 @@ static void pl_dlclose(void* 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); + GetLastError(), 0, buf, sizeof(buf), nullptr); std::string s = buf; - while (!s.empty() && (s.back() == '\r' || s.back() == '\n')) + while (!s.empty() && (s.back() == '\r' || s.back() == '\n')) { s.pop_back(); + } return s; } static constexpr const char* k_libExt = ".dll"; @@ -40,7 +44,11 @@ static constexpr const char* k_libExt = ".dll"; #else #include static void* pl_dlopen(const std::filesystem::path& p) { +#if defined(__linux__) + return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL | RTLD_DEEPBIND); +#else return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL); +#endif } static void* pl_dlsym(void* h, const char* name) { return dlsym(h, name); @@ -59,11 +67,30 @@ static constexpr const char* k_libExt = ".so"; #endif #endif -static dusk::LoadedMod* g_currentMod = nullptr; +#if defined(_M_ARM64) || defined(__aarch64__) +static constexpr std::string_view k_archSuffix = "_arm64"; +#elif defined(_M_X64) || defined(__x86_64__) +static constexpr std::string_view k_archSuffix = "_x64"; +#elif defined(_M_IX86) || defined(__i386__) +static constexpr std::string_view k_archSuffix = "_x86"; +#else +static constexpr std::string_view k_archSuffix = ""; +#endif + +static FILE* fs_fopen(const std::filesystem::path& p, const char* mode) { +#if defined(_WIN32) + std::wstring wmode(mode, mode + strlen(mode)); + return _wfopen(p.wstring().c_str(), wmode.c_str()); +#else + return fopen(p.c_str(), mode); +#endif +} + +static thread_local dusk::LoadedMod* g_currentMod = nullptr; static std::unordered_map g_services; namespace dusk { -void* g_dusk_hook_current_mod = nullptr; +thread_local void* g_dusk_hook_current_mod = nullptr; } struct ModGuard { @@ -82,48 +109,61 @@ static const char* modName() { } 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); + 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); + 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); + 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) + 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); + } + if (!g_currentMod || !relative_path) { + DuskLog.error("load_resource: called outside mod context or with null path"); return nullptr; } + if (!g_currentMod->res_zip_open) { + DuskLog.error("[{}] load_resource: zip not available", g_currentMod->name); + 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); + void* data = mz_zip_reader_extract_file_to_heap(&g_currentMod->res_zip, entry.c_str(), &sz, 0); if (!data) { - DuskLog.warn("[{}] load_resource: '{}' not found in zip", g_currentMod->name, entry); + DuskLog.error("[{}] load_resource: '{}' not found in zip", g_currentMod->name, entry); return nullptr; } - if (out_size) + if (out_size) { *out_size = sz; + } return data; } @@ -131,15 +171,151 @@ 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}); +namespace { + +class ModClickListener : public Rml::EventListener { +public: + ModClickListener(void (*cb)(void*), void* ud) : m_cb(cb), m_ud(ud) {} + void ProcessEvent(Rml::Event&) override { m_cb(m_ud); } + void OnDetach(Rml::Element*) override { delete this; } +private: + void (*m_cb)(void*); + void* m_ud; +}; + +static std::string escape_rml(const char* text) { + std::string out; + for (const char* p = text; *p; ++p) { + switch (*p) { + case '&': out += "&"; break; + case '<': out += "<"; break; + case '>': out += ">"; break; + default: out += *p; break; + } + } + return out; +} + +} + +static void cb_panel_add_section(DuskPanelHandle panel, const char* text) { + auto* pane = static_cast(panel); + if (!pane || !text) { + return; + } + auto el = pane->GetOwnerDocument()->CreateElement("div"); + el->SetClass("section-heading", true); + el->SetInnerRML(escape_rml(text)); + pane->AppendChild(std::move(el)); +} + +static void cb_panel_add_button(DuskPanelHandle panel, const char* label, + void (*cb)(void*), void* userdata) { + auto* pane = static_cast(panel); + if (!pane || !label || !cb) { + return; + } + auto btn = pane->GetOwnerDocument()->CreateElement("button"); + btn->SetInnerRML(escape_rml(label)); + btn->AddEventListener(Rml::EventId::Click, new ModClickListener(cb, userdata)); + pane->AppendChild(std::move(btn)); +} + +static DuskElemHandle cb_panel_add_badge_row(DuskPanelHandle panel, const char* label, int ok) { + auto* pane = static_cast(panel); + if (!pane || !label) { + return nullptr; + } + auto* doc = pane->GetOwnerDocument(); + + auto row = doc->CreateElement("div"); + row->SetClass("mod-info-row", true); + + auto badge = doc->CreateElement("span"); + badge->SetClass("achievement-badge", true); + badge->SetClass(ok ? "unlocked" : "locked", true); + badge->SetInnerRML(ok ? "PASS" : "WAIT"); + Rml::Element* badgePtr = row->AppendChild(std::move(badge)); + + auto lbl = doc->CreateElement("span"); + lbl->SetClass("mod-info-value", true); + lbl->SetInnerRML(escape_rml(label)); + row->AppendChild(std::move(lbl)); + + pane->AppendChild(std::move(row)); + return static_cast(badgePtr); +} + +static DuskElemHandle cb_panel_add_dyn_text(DuskPanelHandle panel, const char* text) { + auto* pane = static_cast(panel); + if (!pane) { + return nullptr; + } + auto el = pane->GetOwnerDocument()->CreateElement("div"); + el->SetInnerRML(text ? escape_rml(text) : std::string{}); + Rml::Element* ptr = pane->AppendChild(std::move(el)); + return static_cast(ptr); +} + +static void cb_elem_set_badge(DuskElemHandle elem, int ok) { + auto* el = static_cast(elem); + if (!el) { + return; + } + el->SetClass("unlocked", ok != 0); + el->SetClass("locked", ok == 0); + el->SetInnerRML(ok ? "PASS" : "WAIT"); +} + +static void cb_elem_set_text(DuskElemHandle elem, const char* text) { + auto* el = static_cast(elem); + if (!el || !text) { + return; + } + el->SetInnerRML(escape_rml(text)); +} + +static DuskElemHandle cb_panel_add_progress(DuskPanelHandle panel, float value) { + auto* pane = static_cast(panel); + if (!pane) { + return nullptr; + } + auto el = pane->GetOwnerDocument()->CreateElement("progress"); + el->SetClass("progress-health", true); + el->SetAttribute("value", value); + Rml::Element* ptr = pane->AppendChild(std::move(el)); + return static_cast(ptr); +} + +static void cb_elem_set_progress(DuskElemHandle elem, float value) { + auto* el = static_cast(elem); + if (!el) { + return; + } + el->SetAttribute("value", value); +} + +static void cb_register_tab_content(void (*build_fn)(void*, void*), void* userdata) { + if (g_currentMod && build_fn) { + g_currentMod->tab_content.push_back({build_fn, userdata}); + } +} + +static void cb_register_tab_update(void (*update_fn)(void*), void* userdata) { + if (g_currentMod && update_fn) { + g_currentMod->tab_updates.push_back({update_fn, userdata}); + } } static void cb_service_publish(const char* name, void* ptr) { - if (name) { - g_services[name] = ptr; + if (!name) { + return; } + if (g_services.count(name)) { + DuskLog.error( + "[{}] service_publish: '{}' already published by another mod", modName(), name); + } + g_services[name] = ptr; } static void* cb_service_get(const char* name) { @@ -150,20 +326,15 @@ static void* cb_service_get(const char* name) { return it != g_services.end() ? it->second : nullptr; } -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)) { +static void api_hook_post(void* addr, void (*fn)(void* args, void* retval)) { dusk::hookRegisterPost(addr, g_currentMod, modName(), fn); } -static void api_hook_replace(void* addr, void (*fn)(void* args)) { +static void api_hook_replace(void* addr, void (*fn)(void* args, void* retval)) { if (!dusk::hookSetReplace(addr, g_currentMod, modName(), fn)) { if (g_currentMod) { g_currentMod->load_failed = true; @@ -171,11 +342,12 @@ static void api_hook_replace(void* addr, void (*fn)(void* args)) { } } +static dusk::ModLoader g_modLoader; + namespace dusk { ModLoader& ModLoader::instance() { - static ModLoader inst; - return inst; + return g_modLoader; } void ModLoader::buildAPI(LoadedMod& mod) { @@ -187,7 +359,15 @@ void ModLoader::buildAPI(LoadedMod& mod) { 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.register_tab_update = cb_register_tab_update; + mod.api.panel_add_section = cb_panel_add_section; + mod.api.panel_add_button = cb_panel_add_button; + mod.api.panel_add_badge_row = cb_panel_add_badge_row; + mod.api.panel_add_dyn_text = cb_panel_add_dyn_text; + mod.api.elem_set_badge = cb_elem_set_badge; + mod.api.elem_set_text = cb_elem_set_text; + mod.api.panel_add_progress = cb_panel_add_progress; + mod.api.elem_set_progress = cb_elem_set_progress; mod.api.hook_install = hookInstallByAddr; mod.api.hook_pre = api_hook_pre; mod.api.hook_post = api_hook_post; @@ -195,16 +375,31 @@ void ModLoader::buildAPI(LoadedMod& mod) { mod.api.hook_dispatch_pre = hookDispatchPre; mod.api.hook_dispatch_post = hookDispatchPost; mod.api.service_publish = cb_service_publish; - mod.api.service_get = cb_service_get; + mod.api.service_get = cb_service_get; } void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { namespace fs = std::filesystem; + std::vector zipBytes; + { + FILE* f = fs_fopen(modPath, "rb"); + if (!f) { + DuskLog.error("ModLoader: failed to open {}", modPath.filename().string()); + return; + } + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); + zipBytes.resize(static_cast(fsize)); + fread(zipBytes.data(), 1, zipBytes.size(), f); + fclose(f); + } + std::string metaName, metaVersion, metaAuthor, metaDescription; { mz_zip_archive zip{}; - if (mz_zip_reader_init_file(&zip, modPath.string().c_str(), 0)) { + if (mz_zip_reader_init_mem(&zip, zipBytes.data(), zipBytes.size(), 0)) { size_t jsonSize = 0; void* jsonData = mz_zip_reader_extract_file_to_heap(&zip, "mod.json", &jsonSize, 0); mz_zip_reader_end(&zip); @@ -220,36 +415,46 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { metaDescription = j.value("description", ""); } catch (const std::exception& e) { mz_free(jsonData); - DuskLog.warn("ModLoader: bad mod.json in {}: {}", modPath.filename().string(), - e.what()); + 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)) { + if (!mz_zip_reader_init_mem(&zip, zipBytes.data(), zipBytes.size(), 0)) { DuskLog.error("ModLoader: failed to open {}", modPath.filename().string()); return; } - std::string dllEntry; + std::string dllEntry, dllFallback; 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)) + 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 (mz_zip_reader_is_file_a_directory(&zip, i)) { + continue; + } + fs::path fname(stat.m_filename); + if (fname.extension() == k_libExt) { + if (!k_archSuffix.empty() && fname.stem().string().ends_with(k_archSuffix)) { + dllEntry = stat.m_filename; + break; + } else if (dllFallback.empty()) { + dllFallback = stat.m_filename; + } + } + } + if (dllEntry.empty()) { + dllEntry = dllFallback; } if (dllEntry.empty()) { mz_zip_reader_end(&zip); - DuskLog.warn("ModLoader: no *{} found in {} — skipping", k_libExt, - modPath.filename().string()); + DuskLog.warn( + "ModLoader: no *{} found in {} — skipping", k_libExt, modPath.filename().string()); return; } @@ -258,15 +463,28 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { 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()); + + size_t dllSize = 0; + void* dllData = mz_zip_reader_extract_file_to_heap(&zip, dllEntry.c_str(), &dllSize, 0); + mz_zip_reader_end(&zip); + + if (!dllData) { + DuskLog.error( + "ModLoader: failed to extract {} from {}", dllEntry, modPath.filename().string()); return; } - mz_zip_reader_end(&zip); + { + FILE* out = fs_fopen(dllCachePath, "wb"); + if (out) { + fwrite(dllData, 1, dllSize, out); + fclose(out); + } else { + mz_free(dllData); + DuskLog.error("ModLoader: failed to write {}", dllCachePath.string()); + return; + } + } + mz_free(dllData); void* handle = pl_dlopen(dllCachePath); if (!handle) { @@ -281,7 +499,7 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { auto* mod_api_ver = reinterpret_cast(pl_dlsym(handle, "mod_api_version")); if (mod_api_ver && *mod_api_ver != DUSK_MOD_API_VERSION) { DuskLog.error("ModLoader: {} expects API v{} but engine is v{}, skipping", - fs::path(dllEntry).filename().string(), *mod_api_ver, DUSK_MOD_API_VERSION); + fs::path(dllEntry).filename().string(), *mod_api_ver, DUSK_MOD_API_VERSION); pl_dlclose(handle); return; } @@ -289,12 +507,10 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { mod.fn_init = reinterpret_cast(pl_dlsym(handle, "mod_init")); mod.fn_tick = reinterpret_cast(pl_dlsym(handle, "mod_tick")); mod.fn_cleanup = reinterpret_cast(pl_dlsym(handle, "mod_cleanup")); - mod.fn_set_imgui_ctx = - reinterpret_cast(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()); + fs::path(dllEntry).filename().string()); pl_dlclose(handle); return; } @@ -304,35 +520,47 @@ void ModLoader::tryLoadDusk(const std::filesystem::path& modPath) { mod.author = metaAuthor.empty() ? "unknown" : metaAuthor; mod.description = metaDescription; + mod.zip_data = std::move(zipBytes); m_mods.push_back(std::move(mod)); + { + LoadedMod& stored = m_mods.back(); + if (mz_zip_reader_init_mem(&stored.res_zip, stored.zip_data.data(), stored.zip_data.size(), 0)) { + stored.res_zip_open = true; + } + } DuskLog.info("ModLoader: found '{}' v{} by {} ({})", m_mods.back().name, m_mods.back().version, - m_mods.back().author, modPath.filename().string()); + m_mods.back().author, modPath.filename().string()); } void ModLoader::init() { - if (m_initialized) + if (m_initialized) { return; + } m_initialized = true; namespace fs = std::filesystem; if (!fs::is_directory(m_modsDir)) { - DuskLog.info("ModLoader: mods directory '{}' not found — mod loading skipped", - m_modsDir.string()); + DuskLog.info( + "ModLoader: mods directory '{}' not found — mod loading skipped", m_modsDir.string()); return; } std::error_code ec; std::vector entries; - for (auto& e : fs::directory_iterator(m_modsDir, ec)) - if (e.is_regular_file() && e.path().extension() == ".dusk") + 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(); - }); + [](const fs::directory_entry& a, const fs::directory_entry& b) { + return a.path().filename() < b.path().filename(); + }); - for (auto& entry : entries) + m_mods.reserve(entries.size()); + for (auto& entry : entries) { tryLoadDusk(entry.path()); + } if (m_mods.empty()) { DuskLog.info("ModLoader: no mods found"); @@ -340,8 +568,9 @@ void ModLoader::init() { } DuskLog.info("ModLoader: initializing {} mod(s)...", m_mods.size()); - for (auto& mod : m_mods) + for (auto& mod : m_mods) { buildAPI(mod); + } for (auto& mod : m_mods) { ModGuard guard(&mod); @@ -367,14 +596,15 @@ void ModLoader::init() { void ModLoader::tick() { for (auto& mod : m_mods) { - if (!mod.active) + 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()); + 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); @@ -393,6 +623,11 @@ void ModLoader::shutdown() { } catch (...) { } } + if (mod.res_zip_open) { + mz_zip_reader_end(&mod.res_zip); + mod.res_zip_open = false; + } + mod.zip_data.clear(); if (mod.handle) { pl_dlclose(mod.handle); mod.handle = nullptr; @@ -403,10 +638,4 @@ void ModLoader::shutdown() { 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 diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index ea791d745b..e3ecb0ba3f 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -14,6 +14,7 @@ #include "f_pc/f_pc_name.h" #include "imgui.h" #include "modal.hpp" +#include "mods_window.hpp" #include "settings.hpp" #include "ui.hpp" #include "window.hpp" @@ -58,6 +59,7 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById( } mTabBar->add_tab("Achievements", [this] { push(std::make_unique()); }); + mTabBar->add_tab("Mods", [this] { push(std::make_unique()); }); mTabBar->add_tab("Reset", [this] { mTabBar->set_active_tab(-1); const auto dismiss = [](Modal& modal) { modal.pop(); }; diff --git a/src/dusk/ui/mods_window.cpp b/src/dusk/ui/mods_window.cpp new file mode 100644 index 0000000000..6f4c1b70ab --- /dev/null +++ b/src/dusk/ui/mods_window.cpp @@ -0,0 +1,125 @@ +#include "mods_window.hpp" + +#include "dusk/mod_loader.hpp" +#include "fmt/format.h" +#include "pane.hpp" + +namespace dusk::ui { +namespace { + +Rml::String build_mod_detail_rml(const dusk::LoadedMod& mod) { + const char* statusClass; + const char* statusText; + if (mod.load_failed) { + statusClass = "locked"; + statusText = "Failed"; + } else if (mod.active) { + statusClass = "unlocked"; + statusText = "Active"; + } else { + statusClass = ""; + statusText = "Disabled"; + } + + return fmt::format( + R"(
)" + R"(Version)" + R"({})" + R"(
)" + R"(
)" + R"(Author)" + R"({})" + R"(
)" + R"(
)" + R"(Status)" + R"({})" + R"(
)" + R"(
)" + R"(Path)" + R"({})" + R"(
)", + mod.version, + mod.author, + statusClass, statusText, + mod.mod_path + ); +} + +} // namespace + +ModsWindow::ModsWindow() { + const auto& mods = dusk::ModLoader::instance().mods(); + + if (mods.empty()) { + add_tab("Mods", [this](Rml::Element* content) { + auto& pane = add_child(content, Pane::Type::Uncontrolled); + pane.add_text("No mods installed."); + pane.finalize(); + }); + return; + } + + for (size_t i = 0; i < mods.size(); ++i) { + mSnapshot.push_back({mods[i].active, mods[i].load_failed}); + + add_tab(mods[i].name, [this, i](Rml::Element* content) { + mActiveModIndex = static_cast(i); + + const auto& curMods = dusk::ModLoader::instance().mods(); + if (i >= curMods.size()) { + return; + } + const auto& mod = curMods[i]; + + auto& pane = add_child(content, Pane::Type::Uncontrolled); + + pane.add_section("Details"); + pane.add_rml(build_mod_detail_rml(mod)); + + if (!mod.description.empty()) { + pane.add_section("Description"); + pane.add_text(mod.description); + } + + for (const auto& cb : mod.tab_content) { + cb.build_fn(static_cast(pane.root()), cb.userdata); + } + + pane.finalize(); + }); + } +} + +void ModsWindow::update() { + const auto& mods = dusk::ModLoader::instance().mods(); + + bool dirty = mods.size() != mSnapshot.size(); + if (!dirty) { + for (size_t i = 0; i < mods.size(); ++i) { + if (mods[i].active != mSnapshot[i].active || + mods[i].load_failed != mSnapshot[i].load_failed) + { + dirty = true; + break; + } + } + } + + if (dirty) { + mSnapshot.clear(); + for (const auto& mod : mods) { + mSnapshot.push_back({mod.active, mod.load_failed}); + } + refresh_active_tab(); + } + + if (mActiveModIndex >= 0 && static_cast(mActiveModIndex) < mods.size()) { + for (const auto& cb : mods[mActiveModIndex].tab_updates) { + cb.update_fn(cb.userdata); + } + } + + Window::update(); +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/mods_window.hpp b/src/dusk/ui/mods_window.hpp new file mode 100644 index 0000000000..5f10bc8087 --- /dev/null +++ b/src/dusk/ui/mods_window.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "window.hpp" + +#include + +namespace dusk::ui { + +class ModsWindow : public Window { +public: + ModsWindow(); + void update() override; + +private: + struct ModSnapshot { + bool active; + bool load_failed; + }; + std::vector mSnapshot; + int mActiveModIndex = 0; +}; + +} // namespace dusk::ui diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index b44de52db4..92b48a74c7 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -827,6 +827,7 @@ void fapGm_Execute() { #if TARGET_PC duskExecute(); + dusk::ModLoader::instance().tick(); #endif #ifdef TARGET_PC @@ -836,7 +837,6 @@ void fapGm_Execute() { #endif cCt_Counter(0); - dusk::ModLoader::instance().tick(); #ifdef TARGET_PC dusk::speedrun::onGameFrame(); dusk::AchievementSystem::get().tick(); diff --git a/src/m_Do/m_Do_audio.cpp b/src/m_Do/m_Do_audio.cpp index fc44eb7fb6..cb4af26198 100644 --- a/src/m_Do/m_Do_audio.cpp +++ b/src/m_Do/m_Do_audio.cpp @@ -19,7 +19,7 @@ #include #endif -u8 mDoAud_zelAudio_c::mInitFlag; +DUSK_GAME_DATA u8 mDoAud_zelAudio_c::mInitFlag; u8 mDoAud_zelAudio_c::mResetFlag; diff --git a/tools/cat_mod/CMakeLists.txt b/tools/cat_mod/CMakeLists.txt new file mode 100644 index 0000000000..71d5413d55 --- /dev/null +++ b/tools/cat_mod/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.25) +project(cat_carry_mod CXX) + +set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root") +add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL) + +set(DUSK_MODS_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/mods" CACHE PATH "Directory to write .dusk packages into") + +add_dusk_mod(cat_carry_mod + SOURCES src/mod.cpp + MOD_JSON mod.json + RES_DIR res +) diff --git a/tools/cat_mod/mod.json b/tools/cat_mod/mod.json new file mode 100644 index 0000000000..a65c6e6e7f --- /dev/null +++ b/tools/cat_mod/mod.json @@ -0,0 +1,6 @@ +{ + "name": "Chloe the Cat", + "version": "1.0.0", + "author": "Maddie", + "description": "Carry your new kitty companion Chloe throughout your adventure, but don't let her die!" +} diff --git a/tools/cat_mod/res/.gitkeep b/tools/cat_mod/res/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/cat_mod/src/mod.cpp b/tools/cat_mod/src/mod.cpp new file mode 100644 index 0000000000..7ef0f5fcee --- /dev/null +++ b/tools/cat_mod/src/mod.cpp @@ -0,0 +1,250 @@ +#include "d/actor/d_a_alink.h" +#include "d/actor/d_a_npc_ne.h" +#include "d/d_com_inf_game.h" +#include "d/d_meter2_info.h" +#include "d/d_msg_object.h" +#include "dusk/hook.hpp" +#include "dusk/mod_api.h" +#include "dusk/mod_utils.h" +#include "f_op/f_op_actor.h" +#include "f_op/f_op_actor_mng.h" +#include "f_op/f_op_overlap_mng.h" +#include "m_Do/m_Do_audio.h" +#include "m_Do/m_Do_controller_pad.h" + +#include +#include + +static constexpr s16 ACTOR_NPC_NE = 269; +static constexpr u16 NOTIFY_MSG_ID = 0xFFFE; +static const char* DEATH_MSG_TEXT = "It seems Chloe has died..."; + +using GetStringEntry = dusk::HookEntry<&dMsgObject_c::getString>; + +static void on_getString_post(void* args, void* retval) { + if (dusk::arg(args, 0) != NOTIFY_MSG_ID) { + return; + } + + strcpy(dusk::arg(args, 5), DEATH_MSG_TEXT); + strcpy(dusk::arg(args, 7), DEATH_MSG_TEXT); + + if (retval) { + *static_cast(retval) = true; + } +} +static constexpr int CAT_MAX_HP = 1; + +static fpc_ProcID s_cat_id = fpcM_ERROR_PROCESS_ID_e; +static int s_cat_hp = CAT_MAX_HP; +static bool s_cat_dead = false; + +static bool s_summon_carry = false; + +static bool s_has_spawn = false; +static cXyz s_spawn_pos = {}; +static s8 s_spawn_room = -1; +static char s_spawn_stage[8] = {}; + +static DuskElemHandle s_el_hp = nullptr; +static DuskElemHandle s_el_hp_bar = nullptr; +static DuskElemHandle s_el_status = nullptr; + +static fopAc_ac_c* getCat() { + if (s_cat_id == fpcM_ERROR_PROCESS_ID_e) { + return nullptr; + } + fopAc_ac_c* cat = fopAcM_SearchByID(s_cat_id); + if (!cat) { + s_cat_id = fpcM_ERROR_PROCESS_ID_e; + } + return cat; +} + +static void killCat() { + fopAc_ac_c* cat = getCat(); + if (cat) { + fopAcM_delete(cat); + s_cat_id = fpcM_ERROR_PROCESS_ID_e; + } + mDoAud_seStartMenu(Z2SE_CAT_CRY_ANNOY); + s_cat_dead = true; + dMeter2Info_setFloatingMessage(NOTIFY_MSG_ID, 150, false); + dusk::g_api->log_info("cat_mod: the cat has died"); +} + +static bool inSpawnStage() { + return strncmp(dComIfGp_getStartStageName(), s_spawn_stage, sizeof(s_spawn_stage)) == 0; +} + +static void spawnCat(bool carry = false) { + if (s_cat_dead || dComIfGp_event_runCheck()) { + return; + } + daAlink_c* link = daAlink_getAlinkActorClass(); + if (!link) { + return; + } + + cXyz pos; + s8 roomNo; + csXyz angle = {}; + if (s_has_spawn && inSpawnStage()) { + pos = s_spawn_pos; + roomNo = s_spawn_room; + } else { + f32 yaw = link->shape_angle.y; + pos = link->current.pos; + pos.x += cM_ssin(yaw) * 30.0f; + pos.z += cM_scos(yaw) * 30.0f; + roomNo = link->current.roomNo; + angle.y = (s16)(link->shape_angle.y + (s16)0x8000); + } + cXyz scale = {1.0f, 1.0f, 1.0f}; + + s_cat_id = fopAcM_createInPlayScene( + ACTOR_NPC_NE, -1, &pos, roomNo, &angle, &scale, -1); + + if (s_cat_id != fpcM_ERROR_PROCESS_ID_e) { + dusk::g_api->log_info("cat_mod: cat spawned (hp %d/%d)", s_cat_hp, CAT_MAX_HP); + s_summon_carry = carry; + } +} + +static void on_setDamagePoint_post(void* args, void* /*retval*/) { + if (s_cat_dead) { + return; + } + int dmg = dusk::arg(args, 1); + if (dmg <= 0) { + return; + } + fopAc_ac_c* cat = getCat(); + bool cat_free = cat != nullptr && !fopAcM_checkCarryNow(cat); + if (cat_free) { + return; + } + s_cat_hp -= dmg; + dusk::g_api->log_info("cat_mod: cat took %d damage (hp %d/%d)", dmg, s_cat_hp, CAT_MAX_HP); + if (s_cat_hp <= 0) { + s_cat_hp = 0; + killCat(); + } + else { + mDoAud_seStartMenu(Z2SE_CAT_CRY_CARRY); + } +} + +static void BuildPanel(DuskPanelHandle panel, void*) { + DuskModAPI* api = dusk::g_api; + api->panel_add_section(panel, "Cat"); + s_el_status = api->panel_add_dyn_text(panel, s_cat_dead ? "Dead" : "Alive"); + + float fraction = static_cast(s_cat_hp) / CAT_MAX_HP; + s_el_hp_bar = api->panel_add_progress(panel, fraction); + + char buf[32]; + snprintf(buf, sizeof(buf), "%d / %d HP", s_cat_hp, CAT_MAX_HP); + s_el_hp = api->panel_add_dyn_text(panel, buf); +} + +static void UpdatePanel(void*) { + DuskModAPI* api = dusk::g_api; + api->elem_set_text(s_el_status, s_cat_dead ? "Dead" : "Alive"); + + float fraction = static_cast(s_cat_hp) / CAT_MAX_HP; + api->elem_set_progress(s_el_hp_bar, fraction); + + char buf[32]; + snprintf(buf, sizeof(buf), "%d / %d HP", s_cat_hp, CAT_MAX_HP); + api->elem_set_text(s_el_hp, buf); +} + +extern "C" { + +void mod_init(DuskModAPI* api) { + dusk::init(api); + dusk::hookAddPost<&dMsgObject_c::getString>(on_getString_post); + dusk::hookAddPost<&daAlink_c::setDamagePoint>(on_setDamagePoint_post); + api->register_tab_content(BuildPanel, nullptr); + api->register_tab_update(UpdatePanel, nullptr); + api->log_info("cat_mod: ready"); +} + +void mod_tick(DuskModAPI* api) { + (void)api; + + if (s_cat_dead) { + return; + } + + fopAc_ac_c* cat = getCat(); + + // Load zone detected: dismiss cat into inventory before the area unloads. + if (cat && fopAcM_checkCarryNow(cat) && fopOvlpM_IsDoingReq()) { + fopAcM_delete(cat); + s_cat_id = fpcM_ERROR_PROCESS_ID_e; + s_has_spawn = false; + daAlink_c* link = daAlink_getAlinkActorClass(); + if (link) { + link->procPreActionUnequipInit(0, nullptr); + } + return; + } + + if (!cat) { + if (s_has_spawn && inSpawnStage() && !dComIfGp_event_runCheck()) { + spawnCat(); + } else if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) { + consumeInput(PAD_1, PAD_TRIGGER_Z); + spawnCat(true); + } + return; + } + + if (s_summon_carry) { + s_summon_carry = false; + daAlink_c* link = daAlink_getAlinkActorClass(); + if (link) { + link->field_0x27f4 = cat; + link->procGrabReadyInit(); + } + } + + if (!fopAcM_checkCarryNow(cat)) { + memcpy(s_spawn_stage, dComIfGp_getStartStageName(), sizeof(s_spawn_stage)); + s_spawn_room = cat->current.roomNo; + s_spawn_pos = cat->current.pos; + s_has_spawn = true; + } + + npc_ne_class* ne = static_cast(cat); + ne->mBehavior = npc_ne_class::BHV_TAME; + ne->mNoFollow = 0; + ne->mTexture = 0; + ne->mBtkFrame = 0; + + if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigZ(PAD_1) && fopAcM_checkCarryNow(cat)) { + consumeInput(PAD_1, PAD_TRIGGER_Z); + fopAcM_delete(cat); + s_cat_id = fpcM_ERROR_PROCESS_ID_e; + s_has_spawn = false; + daAlink_c* link = daAlink_getAlinkActorClass(); + if (link) { + link->procPreActionUnequipInit(0, nullptr); + } + } +} + +void mod_cleanup(DuskModAPI* api) { + (void)api; + s_cat_id = fpcM_ERROR_PROCESS_ID_e; + s_cat_hp = CAT_MAX_HP; + s_cat_dead = false; + s_summon_carry = false; + s_el_hp = nullptr; + s_el_hp_bar = nullptr; + s_el_status = nullptr; +} + +} diff --git a/tools/mod_test/src/mod.cpp b/tools/mod_test/src/mod.cpp index 44de2f5020..83559c83e0 100644 --- a/tools/mod_test/src/mod.cpp +++ b/tools/mod_test/src/mod.cpp @@ -3,23 +3,30 @@ #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 -#include +#include -static int g_ticks = 0; -static bool g_pre_fired = false; -static bool g_post_fired = false; -static bool g_replace_fired = false; -static bool g_arg_write_ok = false; -static int g_pre_cancel_count = 0; -static int g_post_count = 0; -static bool g_resource_ok = false; -static std::string g_resource_text; +static int g_ticks = 0; +static bool g_pre_fired = false; +static bool g_post_fired = false; +static bool g_replace_fired = false; +static bool g_arg_write_ok = false; +static int g_pre_cancel_count = 0; +static int g_post_count = 0; +static bool g_resource_ok = false; +static char g_resource_text[256] = {}; static char g_mod_dir_snippet[64] = {}; +static DuskElemHandle g_el_pre_badge = nullptr; +static DuskElemHandle g_el_post_badge = nullptr; +static DuskElemHandle g_el_replace_badge = nullptr; +static DuskElemHandle g_el_argwrite_badge = nullptr; +static DuskElemHandle g_el_cancel_count = nullptr; +static DuskElemHandle g_el_post_count = nullptr; +static DuskElemHandle g_el_link_angle = nullptr; + // Pre-hook on posMove. Hold L to test argRef write and cancellation. static int32_t on_posMove_pre(void* args) { g_pre_fired = true; @@ -33,70 +40,83 @@ static int32_t on_posMove_pre(void* args) { } // Post-hook on posMove. Fires even when the pre-hook cancelled. -static void on_posMove_post(void* args) { +static void on_posMove_post(void* args, void* retval) { g_post_fired = true; ++g_post_count; (void)args; + (void)retval; } // Replace-hook on execute. Calls through to the original so gameplay is unaffected. using ExecuteEntry = dusk::HookEntry<&daAlink_c::execute>; -static void on_execute_replace(void* args) { +static void on_execute_replace(void* args, void* retval) { g_replace_fired = true; - ExecuteEntry::g_orig(dusk::arg(args, 0)); + int result = ExecuteEntry::g_orig(dusk::arg(args, 0)); + if (retval) { + *static_cast(retval) = result; + } } -static void DrawPanel(void*) { - auto status = [](const char* label, bool ok) { - ImGui::TextColored(ok ? ImVec4(0, 1, 0, 1) : ImVec4(1, 0.35f, 0.35f, 1), - ok ? "[PASS]" : "[WAIT]"); - ImGui::SameLine(); - ImGui::Text("%s", label); - }; +static void on_reset(void*) { + g_pre_fired = false; + g_post_fired = false; + g_replace_fired = false; + g_arg_write_ok = false; + g_pre_cancel_count = 0; + g_post_count = 0; +} - ImGui::SeparatorText("Hooks"); - status("pre-hook fired (posMove)", g_pre_fired); - status("post-hook fired (posMove)", g_post_fired); - status("replace-hook fired (execute)", g_replace_fired); - status("argRef write + pre cancel (hold L)", g_arg_write_ok); - ImGui::Text(" pre cancels: %d post calls: %d", g_pre_cancel_count, g_post_count); +static void BuildPanel(DuskPanelHandle panel, void*) { + DuskModAPI* api = dusk::g_api; - ImGui::SeparatorText("Resources"); - status("load_resource (hello.txt)", g_resource_ok); - if (!g_resource_text.empty()) - ImGui::TextWrapped(" \"%s\"", g_resource_text.c_str()); + api->panel_add_section(panel, "Hooks"); + g_el_pre_badge = api->panel_add_badge_row(panel, "pre-hook fired (posMove)", g_pre_fired); + g_el_post_badge = api->panel_add_badge_row(panel, "post-hook fired (posMove)", g_post_fired); + g_el_replace_badge = + api->panel_add_badge_row(panel, "replace-hook fired (execute)", g_replace_fired); + g_el_argwrite_badge = + api->panel_add_badge_row(panel, "argRef write + pre cancel (hold L)", g_arg_write_ok); - ImGui::SeparatorText("API Fields"); - status("mod_dir non-empty", g_mod_dir_snippet[0] != '\0'); - ImGui::TextWrapped(" %s", g_mod_dir_snippet); + char countBuf[64]; + snprintf(countBuf, sizeof(countBuf), "pre cancels: %d", g_pre_cancel_count); + g_el_cancel_count = api->panel_add_dyn_text(panel, countBuf); + snprintf(countBuf, sizeof(countBuf), "post calls: %d", g_post_count); + g_el_post_count = api->panel_add_dyn_text(panel, countBuf); - ImGui::Spacing(); - ImGui::Separator(); - if (ImGui::Button("Reset results")) { - g_pre_fired = false; - g_post_fired = false; - g_replace_fired = false; - g_arg_write_ok = false; - g_pre_cancel_count = 0; - g_post_count = 0; + api->panel_add_section(panel, "Resources"); + api->panel_add_badge_row(panel, "load_resource (hello.txt)", g_resource_ok); + if (g_resource_text[0] != '\0') { + api->panel_add_dyn_text(panel, g_resource_text); } + + api->panel_add_section(panel, "API Fields"); + api->panel_add_badge_row(panel, "mod_dir non-empty", g_mod_dir_snippet[0] != '\0'); + api->panel_add_dyn_text(panel, g_mod_dir_snippet); + + api->panel_add_section(panel, "Actions"); + api->panel_add_button(panel, "Reset Results", on_reset, nullptr); + + g_el_link_angle = api->panel_add_dyn_text(panel, ""); +} + +static void UpdatePanel(void*) { + DuskModAPI* api = dusk::g_api; + + api->elem_set_badge(g_el_pre_badge, g_pre_fired); + api->elem_set_badge(g_el_post_badge, g_post_fired); + api->elem_set_badge(g_el_replace_badge, g_replace_fired); + api->elem_set_badge(g_el_argwrite_badge, g_arg_write_ok); + + char buf[64]; + snprintf(buf, sizeof(buf), "pre cancels: %d", g_pre_cancel_count); + api->elem_set_text(g_el_cancel_count, buf); + + snprintf(buf, sizeof(buf), "post calls: %d", g_post_count); + api->elem_set_text(g_el_post_count, buf); + daAlink_c* link = daAlink_getAlinkActorClass(); - if (link) { - ImGui::SameLine(); - ImGui::Text("(Link y angle: %d)", (int)link->shape_angle.y); - } -} - -static void DrawMenuEntry(void*) { - if (ImGui::MenuItem("Test: log all levels")) { - dusk::g_api->log_info("log_info test"); - dusk::g_api->log_warn("log_warn test"); - dusk::g_api->log_error("log_error test"); - } - if (ImGui::MenuItem("Test: reset Link y angle")) { - daAlink_c* link = daAlink_getAlinkActorClass(); - if (link) link->shape_angle.y = 0; - } + snprintf(buf, sizeof(buf), "Link y angle: %d", link ? (int)link->shape_angle.y : 0); + api->elem_set_text(g_el_link_angle, buf); } extern "C" { @@ -108,36 +128,38 @@ void mod_init(DuskModAPI* api) { api->log_warn("log_warn smoke test"); api->log_error("log_error smoke test"); - std::snprintf(g_mod_dir_snippet, sizeof(g_mod_dir_snippet), "%.60s", api->mod_dir); + snprintf(g_mod_dir_snippet, sizeof(g_mod_dir_snippet), "%.60s", api->mod_dir); size_t size = 0; void* data = api->load_resource("hello.txt", &size); if (data) { - g_resource_text.assign(static_cast(data), size); - while (!g_resource_text.empty() && g_resource_text.back() == '\n') - g_resource_text.pop_back(); + size_t copy = size < sizeof(g_resource_text) - 1 ? size : sizeof(g_resource_text) - 1; + memcpy(g_resource_text, data, copy); + g_resource_text[copy] = '\0'; + while (copy > 0 && g_resource_text[copy - 1] == '\n') { + g_resource_text[--copy] = '\0'; + } api->free_resource(data); g_resource_ok = true; - api->log_info("load_resource OK: \"%s\"", g_resource_text.c_str()); + api->log_info("load_resource OK: \"%s\"", g_resource_text); } else { api->log_error("load_resource FAILED for hello.txt"); } - // Missing file should return nullptr gracefully. void* missing = api->load_resource("does_not_exist.bin", nullptr); - if (!missing) + if (!missing) { api->log_info("load_resource missing-file: correctly returned nullptr"); - else { + } else { api->log_error("load_resource missing-file: unexpectedly returned data"); api->free_resource(missing); } - dusk::hookAddPre <&daAlink_c::posMove>(on_posMove_pre); + dusk::hookAddPre<&daAlink_c::posMove>(on_posMove_pre); dusk::hookAddPost<&daAlink_c::posMove>(on_posMove_post); dusk::hookSetReplace<&daAlink_c::execute>(on_execute_replace); - api->register_tab_content(DrawPanel, nullptr); - api->register_menu_item(DrawMenuEntry, nullptr); + api->register_tab_content(BuildPanel, nullptr); + api->register_tab_update(UpdatePanel, nullptr); api->log_info("mod_test ready"); } @@ -149,6 +171,8 @@ void mod_tick(DuskModAPI* api) { void mod_cleanup(DuskModAPI* api) { api->log_info("mod_test unloaded after %d ticks", g_ticks); + g_el_pre_badge = g_el_post_badge = g_el_replace_badge = nullptr; + g_el_argwrite_badge = g_el_cancel_count = g_el_post_count = nullptr; + g_el_link_angle = nullptr; } - }