Doc updates

This commit is contained in:
Luke Street
2026-07-08 00:44:17 -06:00
parent e9e16a8ac1
commit 16c573a170
+62 -47
View File
@@ -1,11 +1,15 @@
# Dusklight Mod API
Mods are distributed as `.dusk` files: zip archives containing a `mod.json` manifest and, optionally, compiled code
libraries and resources.
Mods are `.dusk` bundles: zip archives that can contain code (in the form of native libraries), resources, DVD overlay
files, and texture replacements. Mods may be enabled, disabled and reloaded at runtime.
Most things a mod does goes through **services**: small, versioned C APIs. Dusklight provides built-in services, and
mods can define their own to talk to each other. Mods also link against the game itself: include game headers and call
game functions directly.
When code mods are loaded, they get dynamically linked by the operating system to the running game process. The mod
exports lifecycle functions that Dusklight calls into (`mod_initialize`, `mod_update`, `mod_shutdown`), and the mod
communicates with the host via **services**: plain C APIs, individually versioned. Dusklight exports several built-in
services, and mods may export services of their own, permitting framework mods and cross-mod integration.
Beyond services, mods have full access to the original game's code: include game headers, call directly into any public
function, read and write data fields, and hook the vast majority of game functions.
## Table of Contents
@@ -55,14 +59,16 @@ add_mod(my_mod
```
Building produces `my_mod.dusk` in `build/<preset>/mods/` (configurable via the `DUSK_MODS_OUTPUT_DIR` cache variable).
Copy it into the game's mods folder and launch:
Dusklight searches a `mods/` directory next to the app in addition to the user directory, so a dev build launched from
`build/<preset>/` picks these up automatically: rebuild, relaunch (or click Reload), done.
For a regular game install, copy the `.dusk` into the user mods folder:
- Windows: `%APPDATA%\TwilitRealm\Dusklight\mods`
- Linux: `~/.local/share/TwilitRealm/Dusklight/mods`
- macOS: `~/Library/Application Support/TwilitRealm/Dusklight/mods`
You can also pass `--mods <dir>` on the command line, which is handy during development. Mods will load from there
instead of the user directory above.
Passing `--mods <dir>` on the command line replaces the user directory with one of your choosing.
---
@@ -83,9 +89,8 @@ instead of the user directory above.
`id` is required: a unique, stable identifier (reverse-DNS style; periods, underscores, and alphanumerics). Everything
else is optional but recommended.
`icon` and `banner` are bundle-relative paths to PNG images for the in-game mod manager: the square icon (e.g. 512x512),
the banner (roughly 3.5:1).
Both keys are optional; if omitted, `res/icon.png` and `res/banner.png` are used automatically when present.
`icon` and `banner` are bundle-relative paths to PNG images for the in-game mod manager: the square icon (e.g.
512x512), the banner (~3.5:1). If omitted, `res/icon.png` and `res/banner.png` are used automatically when present.
---
@@ -131,7 +136,14 @@ IMPORT_SERVICE_VERSION(LogService, svc_log, 2); // required, minor versi
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
```
The rules (see `include/mods/api.h` for the full contract):
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major**
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
functions to the end of the struct without breaking existing callers and simply bumps the minor version. Mods that
want the newer functions may use `IMPORT_SERVICE_VERSION` to require that minor at **load time**, or `SERVICE_HAS` to
check at **runtime** whether a specific function is available.
The contract (see `include/mods/api.h` for the full version):
- **A required import is guaranteed valid.** If the service is missing or too old, the mod fails to load with a clear
error. No need to null check at call sites.
@@ -140,9 +152,6 @@ The rules (see `include/mods/api.h` for the full contract):
- Fields newer than your imported minor must be gated behind `SERVICE_HAS(service, ServiceType, field)` plus a null
check.
Service versions follow one rule: a **major** bump is a breaking change (treated as a different service entirely), a *
*minor** bump only appends functions.
---
## Built-in Services
@@ -158,8 +167,8 @@ svc_log->error(mod_ctx, "very bad");
svc_log->write(mod_ctx, LOG_LEVEL_DEBUG, "verbose details");
```
Messages appear in the console prefixed with your mod ID. Messages are plain strings: use `snprintf` or `fmt::format`
for formatting.
Messages appear in the console prefixed with your mod ID. Messages are plain UTF-8 strings and are copied before the
call returns; use `snprintf` or `fmt::format` for formatting.
### ResourceService (`mods/svc/resource.h`)
@@ -176,8 +185,8 @@ if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
}
```
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. For writable storage, use the directory from
`svc_host->mod_dir(mod_ctx)`.
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. Note that the bundle is read-only; for writable
storage, use the directory from `svc_host->mod_dir(mod_ctx)`.
### HostService (`mods/svc/host.h`)
@@ -215,14 +224,14 @@ every service dropped its state. For your own mod's teardown, use `mod_shutdown`
### HookService (`mods/svc/hook.h`)
Install hooks on game functions. You'll rarely call it directly; use the typed helpers in `mods/hook.hpp` described
below.
Installs hooks on game functions and resolves symbols by name. You'll rarely call it directly; use the typed helpers in
`mods/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
### OverlayService (`mods/svc/overlay.h`)
Registers DVD file overlays at runtime. The dynamic counterpart to the static `overlay/` directory (
see [Asset Overlays](#asset-overlays)). Overlay a disc path with a file from your bundle, or with a caller-owned
buffer (copied on registration):
Registers DVD file overlays at runtime: the dynamic counterpart to the static `overlay/` directory (see
[Asset Overlays](#asset-overlays)). Overlay a disc path with a file from your bundle, or with a caller-owned buffer
(copied on registration):
```cpp
IMPORT_SERVICE(OverlayService, svc_overlay);
@@ -233,16 +242,16 @@ svc_overlay->add_buffer(mod_ctx, "/generated.txt", data, size, nullptr);
svc_overlay->remove(mod_ctx, handle);
```
`disc_path` must be absolute (leading `/`) and is matched against the disc case-insensitively. Paths that don't exist on
the disc are added as new files. Changes are applied at the next frame boundary, and data the game already read stays in
memory until the file is re-read (sometimes a scene reload, sometimes never; a restart may be required).
`disc_path` must be absolute (leading `/`) and is matched against the disc case-insensitively. Paths that don't exist
on the disc are added as new files. Changes are applied at the next frame boundary, and data the game already read
stays in memory until the file is re-read: sometimes a scene reload, and in the worst case, a full restart.
See [Asset Overlays](#asset-overlays) for priority and conflict handling.
### TextureService (`mods/svc/texture.h`)
Registers texture replacements at runtime. The dynamic counterpart to the static `textures/` directory (
see [Asset Overlays](#asset-overlays)). Two forms: raw texel data with an explicit key, or an encoded `.dds`/`.png` from
Registers texture replacements at runtime: the dynamic counterpart to the static `textures/` directory (see
[Asset Overlays](#asset-overlays)). Two forms: raw texel data with an explicit key, or an encoded `.dds`/`.png` from
your bundle whose filename encodes the key:
```cpp
@@ -264,7 +273,7 @@ svc_texture->register_data(mod_ctx, &key, &data, nullptr);
svc_texture->unregister(mod_ctx, handle);
```
Filenames use the same convention as the user's `texture_replacements` directory:
Filenames use the same Dolphin-style convention as the user's `texture_replacements` directory:
`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, where hashes may be `$` (wildcard). `_mipN` sidecar files next to
a registered file are picked up automatically. Files are decoded lazily on first use by the renderer; raw data is copied
at registration. Registrations follow your mod's lifecycle.
@@ -311,6 +320,7 @@ Writes that store the same value are silent. Values applied from `config.json` o
## Hooking Game Functions
Mods may hook the vast majority of game functions, including file-local static, private and virtual functions.
`mods/hook.hpp` provides typed helpers over the hook service:
```cpp
@@ -338,8 +348,8 @@ dusk::mods::hook_add_pre<&daAlink_c::posMove>(svc_hook, on_pos_move_pre);
### Post-hooks
Run after the original (or after a replace-hook, or after a cancelled original). `retval` points to the return value, if
any.
Run after the original (or after a replace-hook, or after a cancelled original). `retval` points to the return value,
if any.
```cpp
void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... }
@@ -442,8 +452,9 @@ Two spellings work on every platform:
### Game code ABI contract
If your mod calls or hooks game code directly (anything beyond the service APIs), import `GameService` (
`mods/svc/game.h`):
A primary consideration when letting mods link against the game is maintaining ABI stability across Dusklight
versions. If your mod calls or hooks game code directly (anything beyond the service APIs), import `GameService`
(`mods/svc/game.h`):
```cpp
IMPORT_SERVICE(GameService, svc_game);
@@ -451,25 +462,28 @@ IMPORT_SERVICE(GameService, svc_game);
Its major version is the game code ABI epoch: it's bumped when game struct or vtable layouts change incompatibly, and
the ordinary service version check then rejects your mod with a clear error instead of letting it corrupt memory in a
version it wasn't built for. Service-only and asset-only mods should *not* import it; they stay compatible across game
ABI changes.
version it wasn't built for. Service-only and asset-only mods should *not* import it and will continue to work across
game ABI changes.
The more you can do through services, the better: a mod that avoids touching game code directly sidesteps future ABI
breaks entirely and plays nicer with other enabled mods.
---
## Asset Overlays
Files placed under `overlay/` in the `.dusk` archive override game files at the corresponding path. For example,
`overlay/res/Stage/...` shadows that file on the game disc image. This requires no code: an archive with just
`mod.json` and `overlay/` is a complete mod.
Files placed under `overlay/` in the `.dusk` archive override game files at the corresponding path, equivalent to
replacing files in the .iso. This requires no code: an archive with just `mod.json` and `overlay/` is a complete mod.
Files placed under `textures/` register as texture replacements the same way. Filenames follow the replacement naming
convention (`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, `$` as a hash wildcard). Subdirectories are scanned
recursively; only the filename needs to match.
Files placed under `textures/` register as texture replacements, and act just like the user's general
`texture_replacements/` directory: Dolphin-style naming, matched by texture hash
(`tex1_{w}x{h}_{texhash}[_{tluthash}]_{fmt}.dds|.png`, `$` as a hash wildcard). Subdirectories are scanned recursively;
only the filename needs to match.
Both follow the mod's lifecycle: disabling the mod removes its overrides (files revert to the disc contents on their
next open; added files stop existing), and reloading serves the new bundle's content. Game data the engine already read
stays as-is until it is loaded again, which may require a scene reload or a full restart. Texture replacements usually
take effect immediately.
Both mechanisms are tied to the mod's lifecycle: disabling the mod removes its overrides (files revert to the disc
contents on their next open; added files stop existing), and reloading serves the new bundle's content. However, game
data the engine already read stays as-is until it is loaded again, which may require a scene change or, in the worst
case, a full restart. Texture replacements usually take effect immediately.
If multiple sources replace the same file or texture, the last one wins: runtime registrations override static
`textures/` or `overlay/` files, and later-loaded mods override earlier ones. Cross-mod conflicts log warnings.
@@ -527,7 +541,8 @@ explicit results.
## Advanced: Exporting Services
Mods can provide services to other mods. Define the interface in a header both mods share:
Mods may export services of their own, permitting framework mods and cross-mod integration. Define the interface in a
header both mods share:
```cpp
// my_mod_api.h