address review, rmlui, better api, catmod

This commit is contained in:
madeline
2026-05-11 02:55:11 -07:00
parent 5bead49902
commit b2871054a6
35 changed files with 1065 additions and 395 deletions
+10 -2
View File
@@ -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
$<$<CONFIG:Debug>:DEBUG=1>
$<$<CONFIG:Debug>: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 $<TARGET_OBJECTS:imgui>)
endif ()
target_compile_definitions(${DUSK_MAIN_TARGET} PRIVATE ${GAME_COMPILE_DEFS} DUSK_BUILDING_GAME=1)
target_include_directories(${DUSK_MAIN_TARGET} PRIVATE ${GAME_INCLUDE_DIRS})
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_precompile_headers(${DUSK_MAIN_TARGET} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
target_precompile_headers(${DUSK_MAIN_TARGET} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_LIST_DIR}/include/dusk_pch.hpp>")
if(WIN32)
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE Psapi)
+2 -13
View File
@@ -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$<$<CONFIG:Debug>: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")
-5
View File
@@ -1,5 +0,0 @@
#include "imgui.h"
extern "C" void dusk_mod_set_imgui_ctx(void* ctx) {
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(ctx));
}
+2 -1
View File
@@ -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
+1 -1
View File
@@ -34,7 +34,7 @@ public:
bool isResetting() { return mResettingFlag; }
static Z2AudioMgr* getInterface() { return mAudioMgrPtr; }
static Z2AudioMgr* mAudioMgrPtr;
static DUSK_GAME_DATA Z2AudioMgr* mAudioMgrPtr;
/* 0x0514 */ virtual bool startSound(JAISoundID soundID, JAISoundHandle* handle, const JGeometry::TVec3<f32>* posPtr);
/* 0x0518 */ bool mResettingFlag;
+4 -1
View File
@@ -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);
}
+43 -5
View File
@@ -35,19 +35,54 @@ struct HookEntryBase {
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>) {
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast<void*>(ptrs), nullptr);
if (!cancel) g_orig(self, args...);
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs));
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs), nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(MFP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(self, args...);
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs));
g_api->hook_dispatch_post(mfpAddr(MFP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
return result;
}
}
};
template <auto FP, class R, class Orig, class... A>
struct HookEntryFreeBase {
static inline Orig g_orig = nullptr;
static R trampoline(A... args) {
if constexpr (sizeof...(A) == 0) {
if constexpr (std::is_void_v<R>) {
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), nullptr, nullptr);
if (!cancel) g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), nullptr, nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), nullptr, static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), nullptr, static_cast<void*>(std::addressof(result)));
return result;
}
} else {
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
if constexpr (std::is_void_v<R>) {
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), static_cast<void*>(ptrs), nullptr);
if (!cancel) g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), static_cast<void*>(ptrs), nullptr);
} else {
R result{};
const bool cancel = g_api->hook_dispatch_pre(mfpAddr(FP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
if (!cancel) result = g_orig(args...);
g_api->hook_dispatch_post(mfpAddr(FP), static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
return result;
}
}
}
};
template <auto MFP>
struct HookEntry;
@@ -57,6 +92,9 @@ struct HookEntry<MFP> : HookEntryBase<MFP, R, C*, R(*)(C*, A...), A...> {};
template <class C, class R, class... A, R (C::*MFP)(A...) const>
struct HookEntry<MFP> : HookEntryBase<MFP, R, const C*, R(*)(const C*, A...), A...> {};
template <class R, class... A, R (*FP)(A...)>
struct HookEntry<FP> : HookEntryFreeBase<FP, R, R(*)(A...), A...> {};
template <auto MFP>
void hookAddPre(int32_t (*fn)(void* args)) {
using E = HookEntry<MFP>;
@@ -66,7 +104,7 @@ void hookAddPre(int32_t (*fn)(void* args)) {
}
template <auto MFP>
void hookAddPost(void (*fn)(void* args)) {
void hookAddPost(void (*fn)(void* args, void* retval)) {
using E = HookEntry<MFP>;
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
reinterpret_cast<void**>(&E::g_orig));
@@ -74,7 +112,7 @@ void hookAddPost(void (*fn)(void* args)) {
}
template <auto MFP>
void hookSetReplace(void (*fn)(void* args)) {
void hookSetReplace(void (*fn)(void* args, void* retval)) {
using E = HookEntry<MFP>;
g_api->hook_install(mfpAddr(MFP), reinterpret_cast<void*>(E::trampoline),
reinterpret_cast<void**>(&E::g_orig));
+4 -4
View File
@@ -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);
+41 -34
View File
@@ -1,58 +1,65 @@
#ifndef DUSK_MOD_API_H
#define DUSK_MOD_API_H
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <cstddef>
#include <cstdint>
#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
+26 -19
View File
@@ -5,11 +5,17 @@
#include <vector>
#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<ModDrawCallback> tab_content;
std::vector<ModDrawCallback> menu_items;
std::vector<uint8_t> zip_data;
mz_zip_archive res_zip{};
bool res_zip_open = false;
std::vector<RmlTabContentCallback> tab_content;
std::vector<RmlTabUpdateCallback> tab_updates;
};
class ModLoader {
@@ -51,15 +59,14 @@ public:
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;
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
+28
View File
@@ -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;
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -33,7 +33,7 @@ public:
static void onBgmSet() { mBgmSet = true; }
static void offBgmSet() { mBgmSet = false; }
static u8 mInitFlag;
static DUSK_GAME_DATA u8 mInitFlag;
static u8 mResetFlag;
static u8 mBgmSet;
};
@@ -4,6 +4,7 @@
#include <types.h>
#include <cmath>
#include <utility>
#include "global.h"
#ifdef __cplusplus
extern "C" {
@@ -141,9 +142,9 @@ struct TAsinAcosTable {
}
};
extern TSinCosTable<13, f32> sincosTable_;
extern TAtanTable<1024, f32> atanTable_;
extern TAsinAcosTable<1024, f32> asinAcosTable_;
DUSK_GAME_EXTERN TSinCosTable<13, f32> sincosTable_;
DUSK_GAME_EXTERN TAtanTable<1024, f32> atanTable_;
DUSK_GAME_EXTERN TAsinAcosTable<1024, f32> asinAcosTable_;
inline f32 acosDegree(f32 x) {
return asinAcosTable_.acosDegree(x);
+3 -3
View File
@@ -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
+29
View File
@@ -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;
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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) {
-2
View File
@@ -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);
+33 -34
View File
@@ -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<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;
}
static std::unordered_map<uintptr_t, HookSlot> s_registry;
static std::unordered_map<uintptr_t, void*> s_installed;
// Follow E9/FF25 chains to skip MSVC incremental-link and import stubs
static void* resolveImportThunk(void* addr) {
#if _WIN32
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
for (int i = 0; i < 8; ++i) {
const auto* p = static_cast<const uint8_t*>(addr);
if (p[0] == 0xFF && p[1] == 0x25) {
@@ -51,8 +45,9 @@ static void* resolveImportThunk(void* addr) {
int32_t offset;
std::memcpy(&offset, p + 1, 4);
addr = const_cast<uint8_t*>(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<uintptr_t>(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<uintptr_t>(fn_addr));
if (it == registry().end())
bool hookDispatchPre(void* fn_addr, void* args, void* retval) {
auto it = s_registry.find(reinterpret_cast<uintptr_t>(fn_addr));
if (it == s_registry.end()) {
return false;
}
auto& slot = it->second;
for (auto& h : slot.pre) {
ModGuard g(h.mod);
if (h.fn(args) != 0)
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<uintptr_t>(fn_addr));
if (it == registry().end())
void hookDispatchPost(void* fn_addr, void* args, void* retval) {
auto it = s_registry.find(reinterpret_cast<uintptr_t>(fn_addr));
if (it == s_registry.end()) {
return;
}
for (auto& h : it->second.post) {
if (h.fn) {
ModGuard g(h.mod);
h.fn(args);
h.fn(args, retval);
}
}
}
void hookRegisterPre(void* fn_addr, void* mod, int32_t (*fn)(void* args)) {
registry()[reinterpret_cast<uintptr_t>(fn_addr)].pre.push_back({mod, fn});
s_registry[reinterpret_cast<uintptr_t>(fn_addr)].pre.push_back({mod, fn});
}
void hookRegisterPost(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args)) {
registry()[reinterpret_cast<uintptr_t>(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<uintptr_t>(fn_addr)].post.push_back({mod, mod_name, fn});
}
bool hookSetReplace(void* fn_addr, void* mod, const char* mod_name, void (*fn)(void* args)) {
auto& slot = registry()[reinterpret_cast<uintptr_t>(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<uintptr_t>(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 = {};
}
}
}
-2
View File
@@ -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();
}
-2
View File
@@ -8,7 +8,6 @@
#include <aurora/aurora.h>
#include "ImGuiMenuGame.hpp"
#include "ImGuiMenuMods.hpp"
#include "ImGuiMenuTools.hpp"
#include "dusk/main.h"
#include "imgui.h"
@@ -46,7 +45,6 @@ private:
std::deque<Toast> m_toasts;
ImGuiMenuGame m_menuGame;
ImGuiMenuMods m_menuMods;
// Keep always last
ImGuiMenuTools m_menuTools;
-86
View File
@@ -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
-15
View File
@@ -1,15 +0,0 @@
#pragma once
namespace dusk {
class ImGuiMenuMods {
public:
void draw();
void showModsWindow();
private:
bool m_showWindow = false;
};
} // namespace dusk
+315 -86
View File
@@ -2,13 +2,16 @@
#include "dusk/hook_system.hpp"
#include "dusk/logging.h"
#include <RmlUi/Core.h>
#include <algorithm>
#include <cstdarg>
#include <cstdio>
#include <filesystem>
#include <string>
#include <unordered_map>
#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 <dlfcn.h>
static void* pl_dlopen(const std::filesystem::path& p) {
#if defined(__linux__)
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL | RTLD_DEEPBIND);
#else
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL);
#endif
}
static void* pl_dlsym(void* h, const char* name) {
return dlsym(h, name);
@@ -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<std::string, void*> 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 += "&amp;"; break;
case '<': out += "&lt;"; break;
case '>': out += "&gt;"; break;
default: out += *p; break;
}
}
return out;
}
}
static void cb_panel_add_section(DuskPanelHandle panel, const char* text) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane || !text) {
return;
}
auto el = pane->GetOwnerDocument()->CreateElement("div");
el->SetClass("section-heading", true);
el->SetInnerRML(escape_rml(text));
pane->AppendChild(std::move(el));
}
static void cb_panel_add_button(DuskPanelHandle panel, const char* label,
void (*cb)(void*), void* userdata) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane || !label || !cb) {
return;
}
auto btn = pane->GetOwnerDocument()->CreateElement("button");
btn->SetInnerRML(escape_rml(label));
btn->AddEventListener(Rml::EventId::Click, new ModClickListener(cb, userdata));
pane->AppendChild(std::move(btn));
}
static DuskElemHandle cb_panel_add_badge_row(DuskPanelHandle panel, const char* label, int ok) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane || !label) {
return nullptr;
}
auto* doc = pane->GetOwnerDocument();
auto row = doc->CreateElement("div");
row->SetClass("mod-info-row", true);
auto badge = doc->CreateElement("span");
badge->SetClass("achievement-badge", true);
badge->SetClass(ok ? "unlocked" : "locked", true);
badge->SetInnerRML(ok ? "PASS" : "WAIT");
Rml::Element* badgePtr = row->AppendChild(std::move(badge));
auto lbl = doc->CreateElement("span");
lbl->SetClass("mod-info-value", true);
lbl->SetInnerRML(escape_rml(label));
row->AppendChild(std::move(lbl));
pane->AppendChild(std::move(row));
return static_cast<DuskElemHandle>(badgePtr);
}
static DuskElemHandle cb_panel_add_dyn_text(DuskPanelHandle panel, const char* text) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane) {
return nullptr;
}
auto el = pane->GetOwnerDocument()->CreateElement("div");
el->SetInnerRML(text ? escape_rml(text) : std::string{});
Rml::Element* ptr = pane->AppendChild(std::move(el));
return static_cast<DuskElemHandle>(ptr);
}
static void cb_elem_set_badge(DuskElemHandle elem, int ok) {
auto* el = static_cast<Rml::Element*>(elem);
if (!el) {
return;
}
el->SetClass("unlocked", ok != 0);
el->SetClass("locked", ok == 0);
el->SetInnerRML(ok ? "PASS" : "WAIT");
}
static void cb_elem_set_text(DuskElemHandle elem, const char* text) {
auto* el = static_cast<Rml::Element*>(elem);
if (!el || !text) {
return;
}
el->SetInnerRML(escape_rml(text));
}
static DuskElemHandle cb_panel_add_progress(DuskPanelHandle panel, float value) {
auto* pane = static_cast<Rml::Element*>(panel);
if (!pane) {
return nullptr;
}
auto el = pane->GetOwnerDocument()->CreateElement("progress");
el->SetClass("progress-health", true);
el->SetAttribute("value", value);
Rml::Element* ptr = pane->AppendChild(std::move(el));
return static_cast<DuskElemHandle>(ptr);
}
static void cb_elem_set_progress(DuskElemHandle elem, float value) {
auto* el = static_cast<Rml::Element*>(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<uint8_t> 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<size_t>(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<uint32_t*>(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<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());
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<fs::directory_entry> 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
+2
View File
@@ -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<AchievementsWindow>()); });
mTabBar->add_tab("Mods", [this] { push(std::make_unique<ModsWindow>()); });
mTabBar->add_tab("Reset", [this] {
mTabBar->set_active_tab(-1);
const auto dismiss = [](Modal& modal) { modal.pop(); };
+125
View File
@@ -0,0 +1,125 @@
#include "mods_window.hpp"
#include "dusk/mod_loader.hpp"
#include "fmt/format.h"
#include "pane.hpp"
namespace dusk::ui {
namespace {
Rml::String build_mod_detail_rml(const dusk::LoadedMod& mod) {
const char* statusClass;
const char* statusText;
if (mod.load_failed) {
statusClass = "locked";
statusText = "Failed";
} else if (mod.active) {
statusClass = "unlocked";
statusText = "Active";
} else {
statusClass = "";
statusText = "Disabled";
}
return fmt::format(
R"(<div class="mod-info-row">)"
R"(<span class="mod-info-label">Version</span>)"
R"(<span class="mod-info-value">{}</span>)"
R"(</div>)"
R"(<div class="mod-info-row">)"
R"(<span class="mod-info-label">Author</span>)"
R"(<span class="mod-info-value">{}</span>)"
R"(</div>)"
R"(<div class="mod-info-row">)"
R"(<span class="mod-info-label">Status</span>)"
R"(<span class="achievement-badge {}">{}</span>)"
R"(</div>)"
R"(<div class="mod-info-row">)"
R"(<span class="mod-info-label">Path</span>)"
R"(<span class="mod-info-value mod-path">{}</span>)"
R"(</div>)",
mod.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<Pane>(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<int>(i);
const auto& curMods = dusk::ModLoader::instance().mods();
if (i >= curMods.size()) {
return;
}
const auto& mod = curMods[i];
auto& pane = add_child<Pane>(content, Pane::Type::Uncontrolled);
pane.add_section("Details");
pane.add_rml(build_mod_detail_rml(mod));
if (!mod.description.empty()) {
pane.add_section("Description");
pane.add_text(mod.description);
}
for (const auto& cb : mod.tab_content) {
cb.build_fn(static_cast<void*>(pane.root()), cb.userdata);
}
pane.finalize();
});
}
}
void ModsWindow::update() {
const auto& mods = dusk::ModLoader::instance().mods();
bool dirty = mods.size() != mSnapshot.size();
if (!dirty) {
for (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<size_t>(mActiveModIndex) < mods.size()) {
for (const auto& cb : mods[mActiveModIndex].tab_updates) {
cb.update_fn(cb.userdata);
}
}
Window::update();
}
} // namespace dusk::ui
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "window.hpp"
#include <vector>
namespace dusk::ui {
class ModsWindow : public Window {
public:
ModsWindow();
void update() override;
private:
struct ModSnapshot {
bool active;
bool load_failed;
};
std::vector<ModSnapshot> mSnapshot;
int mActiveModIndex = 0;
};
} // namespace dusk::ui
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -19,7 +19,7 @@
#include <revolution/sc.h>
#endif
u8 mDoAud_zelAudio_c::mInitFlag;
DUSK_GAME_DATA u8 mDoAud_zelAudio_c::mInitFlag;
u8 mDoAud_zelAudio_c::mResetFlag;
+13
View File
@@ -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
)
+6
View File
@@ -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!"
}
View File
+250
View File
@@ -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 <cstdio>
#include <cstring>
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<u32>(args, 0) != NOTIFY_MSG_ID) {
return;
}
strcpy(dusk::arg<char*>(args, 5), DEATH_MSG_TEXT);
strcpy(dusk::arg<char*>(args, 7), DEATH_MSG_TEXT);
if (retval) {
*static_cast<bool*>(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<int>(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<float>(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<float>(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<npc_ne_class*>(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;
}
}
+95 -71
View File
@@ -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 <cstdio>
#include <string>
#include <cstring>
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<daAlink_c*>(args, 0));
int result = ExecuteEntry::g_orig(dusk::arg<daAlink_c*>(args, 0));
if (retval) {
*static_cast<int*>(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<char*>(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;
}
}