mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-08 12:16:17 -04:00
address review, rmlui, better api, catmod
This commit is contained in:
+10
-2
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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 = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
|
||||
class ImGuiMenuMods {
|
||||
public:
|
||||
void draw();
|
||||
|
||||
void showModsWindow();
|
||||
|
||||
private:
|
||||
bool m_showWindow = false;
|
||||
};
|
||||
|
||||
} // namespace dusk
|
||||
+315
-86
@@ -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 += "&"; 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<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
|
||||
|
||||
@@ -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(); };
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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!"
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user