modding.md, test mod, template mod, better imgui context

This commit is contained in:
madeline
2026-04-20 07:52:35 -07:00
parent 8356eff4ce
commit e7081f770a
11 changed files with 493 additions and 68 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"type": "cppvsdbg",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console", "--mods", "${workspaceRoot}/tools/mod_template/mods"],
"args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console", "--mods", "${workspaceRoot}/mods"],
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"symbolSearchPath": "${command:cmake.launchTargetPath}",
+2 -1
View File
@@ -7,7 +7,8 @@ function(add_dusk_mod target_name)
message(FATAL_ERROR "add_dusk_mod: MOD_JSON is required")
endif()
add_library(${target_name} SHARED ${ARG_SOURCES})
add_library(${target_name} SHARED ${ARG_SOURCES}
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/dusk_imgui_ctx.cpp")
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)
+5
View File
@@ -0,0 +1,5 @@
#include "imgui.h"
extern "C" void dusk_mod_set_imgui_ctx(void* ctx) {
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(ctx));
}
+309
View File
@@ -0,0 +1,309 @@
# Dusk Mod API
Mods are shared libraries packaged into a `.dusk` zip archive. The loader scans the `mods/` directory at startup, extracts each library, and calls your exports each frame.
## Table of Contents
1. [Getting Started](#getting-started)
2. [mod.json](#modjson)
3. [Required Exports](#required-exports)
4. [DuskModAPI Reference](#duskmodapi-reference)
5. [Logging](#logging)
6. [Loading Resources](#loading-resources)
7. [ImGui Integration](#imgui-integration)
8. [Hooking Game Functions](#hooking-game-functions)
- [Pre-hooks](#pre-hooks)
- [Post-hooks](#post-hooks)
- [Replace hooks](#replace-hooks)
- [Reading and writing arguments](#reading-and-writing-arguments)
- [Raw C hook API](#raw-c-hook-api)
9. [Full Example](#full-example)
---
## Getting Started
Fork the [mod template](../tools/mod_template/), it is a self-contained CMake project that references dusk as a subdirectory.
```
my_mod/
├── CMakeLists.txt
├── mod.json
├── src/mod.cpp
└── res/ (optional bundled resources)
```
**CMakeLists.txt:**
```cmake
cmake_minimum_required(VERSION 3.25)
project(my_mod CXX)
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusk" CACHE PATH "Path to dusk source root")
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
add_dusk_mod(my_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res # optional
)
```
After building, `my_mod.dusk` is placed in `mods/` next to the project root (`DUSK_MODS_OUTPUT_DIR` cache variable). Copy it to the game's `mods/` folder and launch.
The `.dusk` archive is a standard zip containing `mod.json`, the compiled library, and an optional `res/` tree. `add_dusk_mod()` creates it automatically.
---
## mod.json
```json
{
"name": "My Mod",
"version": "1.0.0",
"author": "Your Name",
"description": "A short description shown in the mod manager."
}
```
All fields are optional but recommended. `name` falls back to the filename, `version` to `"?"`.
---
## Required Exports
```cpp
#include "dusk/mod_api.h"
extern "C" {
void mod_init (DuskModAPI* api); // required — called once at startup
void mod_tick (DuskModAPI* api); // required — called every frame
void mod_cleanup(DuskModAPI* api); // optional — called on shutdown
}
```
---
## DuskModAPI Reference
The `api` pointer is valid for the lifetime of the mod. When using `hook.hpp`, call `dusk::init(api)` once and `dusk::g_api` is set for you.
| Field | Description |
|-------|-------------|
| `api_version` | ABI version, check against `DUSK_MOD_API_VERSION` if needed |
| `mod_dir` | Absolute path to the extracted mod cache directory |
| `log_info` / `log_warn` / `log_error` | `printf`-style logging, prefixed with the mod name |
| `load_resource` / `free_resource` | Load files from the `res/` tree in the `.dusk` archive |
| `register_tab_content` | Add a panel to the mod manager's per-mod tab |
| `register_menu_item` | Add an item to the quick-access menu |
| `hook_install` / `hook_pre` / `hook_post` / `hook_replace` | Low-level hook registration (see [Raw C hook API](#raw-c-hook-api)) |
| `hook_dispatch_pre` / `hook_dispatch_post` | Called by the trampoline, do not call directly |
---
## Logging
```cpp
api->log_info("Player health: %d", hp);
api->log_warn("Something looks wrong");
api->log_error("Fatal: %s", msg);
```
Output appears in the dusk console as `[My Mod] ...`
The format string is `printf`-compatible.
---
## Loading Resources
```cpp
size_t size = 0;
void* data = api->load_resource("config.txt", &size);
if (data) {
std::string text(static_cast<char*>(data), size);
api->free_resource(data);
}
```
- Path is relative to `res/`, pass `"config.txt"` not `"res/config.txt"`
- Always call `free_resource`, the buffer is owned by miniz
- For writable storage, write files under `api->mod_dir`
---
## ImGui Integration
**Tab content:** shown in the mod's panel in the Mods window, called every frame while visible:
```cpp
static void DrawPanel(void* userdata) {
ImGui::Text("Hello!");
}
api->register_tab_content(DrawPanel, nullptr);
```
Pass a pointer through `userdata` if your callback needs state:
```cpp
api->register_tab_content(DrawPanel, &g_state);
```
**Menu items:** added to the quick-access menu. Use `ImGui::MenuItem`, `ImGui::Separator`, etc.:
```cpp
static void DrawMenuEntry(void*) {
if (ImGui::MenuItem("Reset rotation")) { ... }
}
api->register_menu_item(DrawMenuEntry, nullptr);
```
---
## Hooking Game Functions
Call `dusk::init(api)` first.
```cpp
#include "dusk/hook.hpp"
extern "C" void mod_init(DuskModAPI* api) {
dusk::init(api);
dusk::hookAddPre<&ClassName::Method>(callback);
}
```
The trampoline is installed once per address. Multiple mods can register pre/post callbacks for the same function independently.
### Pre-hooks
Run before the original. Return `0` to let it proceed, non-zero to cancel it. Post-hooks still run either way.
```cpp
static int32_t on_posMove_pre(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0); // this
if (link->shape_angle.y > 10000)
return 1; // cancel
return 0;
}
dusk::hookAddPre<&daAlink_c::posMove>(on_posMove_pre);
```
### Post-hooks
Run after the original (or replace-hook).
```cpp
static void on_posMove_post(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
dusk::g_api->log_info("New Y angle: %d", (int)link->shape_angle.y);
}
dusk::hookAddPost<&daAlink_c::posMove>(on_posMove_post);
```
### Replace hooks
Completely substitutes the original. Only one replace-hook per function, a second install overwrites with a warning.
```cpp
static void on_posMove_replace(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
link->shape_angle.y += 100;
}
dusk::hookSetReplace<&daAlink_c::posMove>(on_posMove_replace);
```
To call the original from inside a replace-hook:
```cpp
using Entry = dusk::HookEntry<&daAlink_c::posMove>;
static void on_posMove_replace(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
link->shape_angle.y = 0;
Entry::g_orig(link);
}
```
### Reading and writing arguments
`args` is a `void*[N]` array. Index `0` is `this`, subsequent indices are parameters in declaration order.
```cpp
T value = dusk::arg <T>(args, n); // copy
T& ref = dusk::argRef<T>(args, n); // reference (read/write)
```
**Example:** halve incoming damage
```cpp
// void daEnemy_c::takeDamage(int amount, daActor_c* source)
static int32_t on_takeDamage_pre(void* args) {
dusk::argRef<int>(args, 1) /= 2;
return 0;
}
dusk::hookAddPre<&daEnemy_c::takeDamage>(on_takeDamage_pre);
```
For reference parameters (e.g. `const cXyz& pos`), use `argRef<cXyz>` to get a direct reference.
---
## Full Example
```cpp
#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"
static int g_ticks = 0;
static int32_t on_posMove_pre(void* args) {
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
if (mDoCPd_c::getHoldR(PAD_1)) {
link->shape_angle.y -= 2048;
}
return 0;
}
static void DrawPanel(void*) {
daAlink_c* link = daAlink_getAlinkActorClass();
ImGui::Text("Ticks: %d", g_ticks);
if (link) {
ImGui::Text("Y angle: %d", (int)link->shape_angle.y);
if (ImGui::Button("Reset rotation")) {
link->shape_angle.y = 0;
}
}
}
static void DrawMenuEntry(void*) {
daAlink_c* link = daAlink_getAlinkActorClass();
if (ImGui::MenuItem("Reset rotation", nullptr, false, link != nullptr)) {
link->shape_angle.y = 0;
}
}
extern "C" {
void mod_init(DuskModAPI* api) {
dusk::init(api);
dusk::hookAddPre<&daAlink_c::posMove>(on_posMove_pre);
api->register_tab_content(DrawPanel, nullptr);
api->register_menu_item(DrawMenuEntry, nullptr);
}
void mod_tick(DuskModAPI* api) {
++g_ticks;
}
void mod_cleanup(DuskModAPI* api) {
api->log_info("Unloaded after %d ticks.", g_ticks);
}
}
```
View File
-2
View File
@@ -1,2 +0,0 @@
This text has been loaded from the mods resources!
Press R to rotate Link!
+2 -64
View File
@@ -1,80 +1,18 @@
#include "d/actor/d_a_alink.h"
#include "dusk/hook.hpp"
#include "dusk/mod_api.h"
#include "imgui.h"
#include "m_Do/m_Do_controller_pad.h"
#include <string>
static int TickCount = 0;
static std::string TextContents;
static int32_t on_posMove_pre(void* args) {
if (!mDoCPd_c::getHoldR(PAD_1))
return 0;
daAlink_c* link = dusk::arg<daAlink_c*>(args, 0);
link->shape_angle.y -= 2048;
dusk::g_api->log_info("ROTATING %d", link->shape_angle.y);
return 0;
}
static void DrawTabContent(void*) {
daAlink_c* link = daAlink_getAlinkActorClass();
if (link) {
ImGui::Text("Y angle: %d", (int)link->shape_angle.y);
ImGui::Spacing();
if (ImGui::Button("Reset rotation")) {
link->shape_angle.y = 0;
}
}
if (!TextContents.empty()) {
ImGui::Separator();
ImGui::TextUnformatted(TextContents.c_str());
}
}
static void DrawMenuItem(void*) {
if (ImGui::MenuItem("Reset rotation")) {
daAlink_c* link = daAlink_getAlinkActorClass();
if (link) {
link->shape_angle.y = 0;
}
}
}
extern "C" {
void dusk_mod_set_imgui_ctx(void* ctx) {
ImGui::SetCurrentContext(static_cast<ImGuiContext*>(ctx));
}
void mod_init(DuskModAPI* api) {
api->log_info("Test Mod initializing...");
dusk::init(api);
dusk::hookAddPre<&daAlink_c::posMove>(on_posMove_pre);
size_t size = 0;
void* data = api->load_resource("text.txt", &size);
if (data) {
TextContents.assign(static_cast<char*>(data), size);
api->free_resource(data);
api->log_info("Loaded text.txt (%zu bytes)", size);
} else {
api->log_warn("Failed to load text.txt");
}
api->register_tab_content(DrawTabContent, nullptr);
api->register_menu_item(DrawMenuItem, nullptr);
api->log_info("Test Mod ready. Mod folder: %s", api->mod_dir);
}
void mod_tick(DuskModAPI* api) {
++TickCount;
(void)api;
}
void mod_cleanup(DuskModAPI* api) {
api->log_info("Test Mod unloading after %d ticks.", TickCount);
(void)api;
}
}
+13
View File
@@ -0,0 +1,13 @@
cmake_minimum_required(VERSION 3.25)
project(mod_test 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(mod_test
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
)
+6
View File
@@ -0,0 +1,6 @@
{
"name": "API Test Mod",
"version": "1.0.0",
"author": "dusk",
"description": "Exercises every feature of the Dusk mod API."
}
+1
View File
@@ -0,0 +1 @@
Hello from the mod archive!
+154
View File
@@ -0,0 +1,154 @@
// Tests every feature of the Dusk mod API. Results shown in the mod tab.
#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>
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 char g_mod_dir_snippet[64] = {};
// Pre-hook on posMove. Hold L to test argRef write and cancellation.
static int32_t on_posMove_pre(void* args) {
g_pre_fired = true;
if (mDoCPd_c::getHoldL(PAD_1)) {
dusk::argRef<daAlink_c*>(args, 0)->shape_angle.y = 0;
g_arg_write_ok = true;
++g_pre_cancel_count;
return 1; // cancel
}
return 0;
}
// Post-hook on posMove. Fires even when the pre-hook cancelled.
static void on_posMove_post(void* args) {
g_post_fired = true;
++g_post_count;
(void)args;
}
// 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) {
g_replace_fired = true;
ExecuteEntry::g_orig(dusk::arg<daAlink_c*>(args, 0));
}
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);
};
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);
ImGui::SeparatorText("Resources");
status("load_resource (hello.txt)", g_resource_ok);
if (!g_resource_text.empty())
ImGui::TextWrapped(" \"%s\"", g_resource_text.c_str());
ImGui::SeparatorText("API Fields");
status("mod_dir non-empty", g_mod_dir_snippet[0] != '\0');
ImGui::TextWrapped(" %s", g_mod_dir_snippet);
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;
}
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;
}
}
extern "C" {
void mod_init(DuskModAPI* api) {
dusk::init(api);
api->log_info("mod_test initializing");
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);
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();
api->free_resource(data);
g_resource_ok = true;
api->log_info("load_resource OK: \"%s\"", g_resource_text.c_str());
} 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)
api->log_info("load_resource missing-file: correctly returned nullptr");
else {
api->log_error("load_resource missing-file: unexpectedly returned data");
api->free_resource(missing);
}
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->log_info("mod_test ready");
}
void mod_tick(DuskModAPI* api) {
++g_ticks;
(void)api;
}
void mod_cleanup(DuskModAPI* api) {
api->log_info("mod_test unloaded after %d ticks", g_ticks);
}
}