Merge with origin/main

This commit is contained in:
jdflyer
2026-08-07 13:57:48 -07:00
204 changed files with 4811 additions and 14798 deletions
+17 -1
View File
@@ -20,7 +20,23 @@ extern "C" {
#ifdef __cplusplus
#define MOD_EXTERN_C extern "C"
#else
#define MOD_EXTERN_C
#define MOD_EXTERN_C extern
#endif
#ifdef __cplusplus
#define MOD_DECLARE_SERVICE( \
service_type, variable, service_id_value, major_value, minor_value) \
MOD_EXTERN_C const service_type* variable; \
template <> \
struct mods::ServiceTraits<service_type> { \
static constexpr const char* id = service_id_value; \
static constexpr uint16_t major_version = major_value; \
static constexpr uint16_t minor_version = minor_value; \
}
#else
#define MOD_DECLARE_SERVICE( \
service_type, variable, service_id_value, major_value, minor_value) \
MOD_EXTERN_C const service_type* variable
#endif
#define MOD_ABI_VERSION 1u
+35 -179
View File
@@ -1,210 +1,66 @@
#pragma once
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_GAME)
#error "DEFINE_HOOK requires add_mod(... FEATURES game)"
#if defined(_MSC_VER)
#pragma message("warning: <mods/hook.hpp> is deprecated; include <mods/svc/hook.hpp> instead")
#else
#warning "<mods/hook.hpp> is deprecated; include <mods/svc/hook.hpp> instead"
#endif
#include <mods/svc/hook.h>
#include <memory>
#include <type_traits>
#include <mods/svc/hook.hpp>
namespace mods {
template <class T>
T arg(void* argsRaw, int n) noexcept {
void** args = static_cast<void**>(argsRaw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
}
template <class T>
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
void** args = static_cast<void**>(argsRaw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
}
/*
* Trampoline generator + per-target state. Tag makes each hooked target's statics distinct; the
* target address comes from the declaration's metadata record, resolved by the host at mod
* initialization.
*/
template <class Tag, class R, class... A>
struct HookImpl {
static inline R (*g_orig)(A...) = nullptr;
static inline const HookService* hooks = nullptr;
static inline void* target = nullptr;
static bool dispatch_pre(void* args, void* retval) {
if (hooks == nullptr) {
return false;
}
int skipOriginal = 0;
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
return result == MOD_OK && skipOriginal != 0;
}
static void dispatch_post(void* args, void* retval) {
if (hooks != nullptr) {
hooks->dispatch_post(mod_ctx, target, args, retval);
}
}
static R trampoline(A... args) {
if constexpr (sizeof...(A) == 0) {
if constexpr (std::is_void_v<R>) {
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
if (!skipOriginal) {
g_orig(args...);
}
dispatch_post(nullptr, nullptr);
} else {
R result{};
const bool skipOriginal =
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
if (!skipOriginal) {
result = g_orig(args...);
}
dispatch_post(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 skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
if (!skipOriginal) {
g_orig(args...);
}
dispatch_post(static_cast<void*>(ptrs), nullptr);
} else {
R result{};
const bool skipOriginal = dispatch_pre(
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
if (!skipOriginal) {
result = g_orig(args...);
}
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
return result;
}
}
}
};
namespace detail {
template <auto Target>
using TargetTag = std::integral_constant<decltype(Target), Target>;
template <FixedString Name>
struct NameTag {};
} // namespace detail
/*
* Typed base for a hook on a function named at compile time (&daAlink_c::execute, &free_fn).
* Instantiate through DEFINE_HOOK, which pairs it with the metadata record the host resolves.
*/
template <auto Target>
struct Hook;
template <class C, class R, class... A, R (C::*Target)(A...)>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {};
template <class C, class R, class... A, R (C::*Target)(A...) const>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {};
template <class R, class... A, R (*Target)(A...)>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {};
/*
* Typed base for a hook on a function by its symbol name, for targets you can't name in C++:
* file-local statics, private members, or symbols without a header. The signature is written
* free-style with the receiver first and is *not* compiler-checked. Instantiate through
* DEFINE_HOOK_SYMBOL.
*/
template <FixedString Name, class Sig>
struct NamedHook;
template <FixedString Name, class R, class... A>
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, R, A...> {};
/*
* Declare a hook target. The declaration emits a metadata record that the host resolves at mod
* initialization. Every hook target must be declared.
*
* DEFINE_HOOK(&daAlink_c::execute, LinkExecute);
* DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
*
* mods::hook_add_pre<LinkExecute>(svc_hook, on_link_execute);
*
* DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O
* leading underscore) or the demangled qualified display name; overloaded display names are
* ambiguous and need the mangled form.
*/
#define DEFINE_HOOK(target, alias) \
[[maybe_unused]] static const void* const mod_meta_hook_##alias = \
&::mods::detail::HookRecordFor<(target), ::mods::FixedString{#target}>::Holder::record; \
struct alias : ::mods::Hook<(target)> { \
static void* resolved_target() { \
return ::mods::detail::HookRecordFor<(target), \
::mods::FixedString{#target}>::Holder::record.resolved; \
} \
}
#define DEFINE_HOOK_SYMBOL(name, sig, alias) \
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
::mods::detail::make_hook_name_record<::mods::FixedString{name}>(); \
struct alias : ::mods::NamedHook<::mods::FixedString{name}, sig> { \
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
}
template <class Entry>
ModResult hook_install(const HookService* hooks) {
if (hooks == nullptr) {
return MOD_UNAVAILABLE;
}
return hook::install<Entry>(hooks);
}
Entry::hooks = hooks;
if (Entry::target == nullptr) {
void* resolved = Entry::resolved_target();
if (resolved == nullptr) {
return MOD_UNAVAILABLE;
}
Entry::target = resolved;
}
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
reinterpret_cast<void**>(&Entry::g_orig));
template <class Entry>
ModResult hook_install() {
return hook::install<Entry>();
}
template <class Entry>
ModResult hook_add_pre(
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
const ModResult installed = hook_install<Entry>(hooks);
if (installed != MOD_OK) {
return installed;
}
return hook::add_pre<Entry>(hooks, callback, options);
}
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
template <class Entry>
ModResult hook_add_pre(HookPreFn callback, const HookOptions* options = nullptr) {
return hook::add_pre<Entry>(callback, options);
}
template <class Entry>
ModResult hook_add_post(
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
const ModResult installed = hook_install<Entry>(hooks);
if (installed != MOD_OK) {
return installed;
}
return hook::add_post<Entry>(hooks, callback, options);
}
return hooks->add_post(mod_ctx, Entry::target, callback, options);
template <class Entry>
ModResult hook_add_post(HookPostFn callback, const HookOptions* options = nullptr) {
return hook::add_post<Entry>(callback, options);
}
template <class Entry>
ModResult hook_replace(
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
const ModResult installed = hook_install<Entry>(hooks);
if (installed != MOD_OK) {
return installed;
}
return hook::replace<Entry>(hooks, callback, options);
}
return hooks->replace(mod_ctx, Entry::target, callback, options);
template <class Entry>
ModResult hook_replace(HookReplaceFn callback, const HookOptions* options = nullptr) {
return hook::replace<Entry>(callback, options);
}
template <class Entry>
ModResult hook_uninstall(const HookService* hooks) {
return hook::uninstall<Entry>(hooks);
}
template <class Entry>
ModResult hook_uninstall() {
return hook::uninstall<Entry>();
}
} // namespace mods
+42
View File
@@ -241,6 +241,48 @@ consteval auto make_hook_mem_names() {
return r;
}
#if defined(__GNUC__) && !defined(__clang__) && defined(__ELF__)
/* https://gcc.gnu.org/bugzilla/show_bug.cgi?id=41091 prevents inline static template members from
* sharing an explicit ELF section with ordinary variables. GCC can instead constant-evaluate a
* file-local record at each DEFINE_HOOK. */
template <auto Target>
void materialize_hook_mem(unsigned char* outPmf) {
const auto target = Target;
std::memcpy(outPmf, &target, sizeof(target));
}
template <auto Target, FixedString Disp>
consteval auto make_local_hook_record() {
using F = decltype(Target);
if constexpr (std::is_member_function_pointer_v<F>) {
constexpr auto names = make_hook_mem_names<Target, Disp>();
static_assert(sizeof(F) <= MOD_META_HOOK_MEM_EXT_CAPACITY,
"unsupported pointer-to-member representation");
if constexpr (sizeof(F) > MOD_META_HOOK_MEM_CAPACITY) {
HookMemExtRecord<names.len> record = {
{sizeof(HookMemExtRecord<names.len>), MOD_META_HOOK_MEM_EXT, 0}, sizeof(F),
materialize_hook_mem<Target>, nullptr, {}};
for (size_t i = 0; i < names.len; ++i) {
record.names[i] = names.chars[i];
}
return record;
} else {
HookMemRecord<F, names.len> record = {
{sizeof(HookMemRecord<F, names.len>), MOD_META_HOOK_MEM, 0}, 0, {Target}, nullptr,
{}};
for (size_t i = 0; i < names.len; ++i) {
record.names[i] = names.chars[i];
}
return record;
}
} else {
static_assert(std::is_pointer_v<F> && std::is_function_v<std::remove_pointer_t<F>>,
"hook target must be a function or member function");
return HookFnRecord<F>{{sizeof(HookFnRecord<F>), MOD_META_HOOK_FN, 0}, 0, Target, nullptr};
}
}
#endif
/*
* MSVC constant-evaluates a compact pointer-to-member only when every other operand in the
* initializer is a literal: no consteval calls, constexpr-object copies, or default member
+2 -2
View File
@@ -40,13 +40,13 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa
}; \
}
// Declares `static const service_type* variable`, filled in by the host before mod_initialize.
// Defines `const service_type* variable`, filled in by the host before mod_initialize.
// Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports
// must be checked against nullptr before use. The unversioned macros use the latest minor version;
// set an explicit version to target an older minor version for backwards compatibility.
#define IMPORT_SERVICE_EX( \
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
static const service_type* variable = nullptr; \
const service_type* variable = nullptr; \
MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = { \
{sizeof(ModMetaImport), MOD_META_IMPORT, static_cast<uint8_t>(flags_value)}, \
static_cast<uint16_t>(major_value), \
+49 -11
View File
@@ -2,9 +2,13 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera"
#define CAMERA_SERVICE_MAJOR 1u
#define CAMERA_SERVICE_MINOR 0u
#define CAMERA_SERVICE_MINOR 1u
/*
* Snapshot of a game camera for the frame currently being recorded.
@@ -42,6 +46,37 @@ typedef struct CameraInfo {
#define CAMERA_INFO_INIT {sizeof(CameraInfo)}
/* 0 is never a valid handle. */
typedef uint64_t CameraOperatorHandle;
typedef struct CameraOperatorState {
uint32_t struct_size;
/* Host inputs. */
uint64_t frame_counter;
uint64_t ticks;
float aspect;
/* Initial camera state and callback output. */
float eye[3];
float center[3];
float fovy;
float bank_degrees;
} CameraOperatorState;
/* Return true to use state for the current frame. Game thread only. */
typedef bool (*CameraOperateFn)(ModContext* ctx, CameraOperatorState* state, void* user_data);
typedef struct CameraOperatorDesc {
uint32_t struct_size;
const char* debug_name;
int32_t priority;
CameraOperateFn operate;
void* user_data;
} CameraOperatorDesc;
#define CAMERA_OPERATOR_DESC_INIT {sizeof(CameraOperatorDesc), NULL, 0, NULL, NULL}
typedef struct CameraService {
ServiceHeader header;
@@ -51,15 +86,18 @@ typedef struct CameraService {
* perspective camera.
*/
ModResult (*get_camera)(ModContext* ctx, const void* game_view, CameraInfo* out_info);
/* Minor version 1 */
/*
* Register an operator for the main camera. Operators run by descending priority, then
* registration order, until one returns true. debug_name and operate must be set; debug_name
* is copied. out_handle must not be NULL.
*/
ModResult (*register_camera_operator)(
ModContext* ctx, const CameraOperatorDesc* desc, CameraOperatorHandle* out_handle);
ModResult (*unregister_camera_operator)(ModContext* ctx, CameraOperatorHandle handle);
} CameraService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<CameraService> {
static constexpr const char* id = CAMERA_SERVICE_ID;
static constexpr uint16_t major_version = CAMERA_SERVICE_MAJOR;
static constexpr uint16_t minor_version = CAMERA_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(
CameraService, svc_camera, CAMERA_SERVICE_ID, CAMERA_SERVICE_MAJOR, CAMERA_SERVICE_MINOR);
+6 -10
View File
@@ -2,6 +2,10 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config"
#define CONFIG_SERVICE_MAJOR 1u
#define CONFIG_SERVICE_MINOR 0u
@@ -96,13 +100,5 @@ typedef struct ConfigService {
ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle);
} ConfigService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<ConfigService> {
static constexpr const char* id = CONFIG_SERVICE_ID;
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
static constexpr uint16_t minor_version = CONFIG_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(
ConfigService, svc_config, CONFIG_SERVICE_ID, CONFIG_SERVICE_MAJOR, CONFIG_SERVICE_MINOR);
+5 -10
View File
@@ -2,6 +2,10 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
/*
* The mod SDK imports this service automatically for mods built with FEATURES game; service-only
* and asset-only mods do not require it.
@@ -19,13 +23,4 @@ typedef struct GameService {
ServiceHeader header;
} GameService;
#ifdef __cplusplus
#include <mods/service.hpp>
template <>
struct mods::ServiceTraits<GameService> {
static constexpr const char* id = GAME_SERVICE_ID;
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
static constexpr uint16_t minor_version = GAME_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(GameService, svc_game, GAME_SERVICE_ID, GAME_SERVICE_MAJOR, GAME_SERVICE_MINOR);
+72 -12
View File
@@ -1,6 +1,11 @@
#pragma once
#include <mods/api.h>
#include <mods/svc/window.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_WEBGPU)
#error "mods/svc/gfx.h requires add_mod(... FEATURES webgpu)"
@@ -28,7 +33,7 @@
#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx"
#define GFX_SERVICE_MAJOR 1u
#define GFX_SERVICE_MINOR 0u
#define GFX_SERVICE_MINOR 1u
/* Maximum size for push_draw payload */
#define GFX_INLINE_DRAW_PAYLOAD_SIZE 128u
@@ -37,6 +42,7 @@
typedef uint64_t GfxDrawTypeHandle;
typedef uint64_t GfxStageHookHandle;
typedef uint64_t GfxComputeTypeHandle;
typedef uint64_t GfxPresentTargetHandle;
/* A suballocation in one of the shared per-frame streaming buffers. */
typedef struct GfxRange {
@@ -56,11 +62,13 @@ typedef struct GfxDeviceInfo {
WGPUTextureFormat depth_format; /* scene depth target format */
uint32_t sample_count; /* scene pass MSAA sample count */
bool uses_reversed_z; /* true means depth 1.0 is near */
WGPUInstance instance; /* borrowed; added in GfxService 1.1 */
WGPUAdapter adapter; /* borrowed; added in GfxService 1.1 */
} GfxDeviceInfo;
#define GFX_DEVICE_INFO_INIT \
{sizeof(GfxDeviceInfo), NULL, NULL, WGPUTextureFormat_Undefined, WGPUTextureFormat_Undefined, \
1u, false}
1u, false, NULL, NULL}
/*
* Passed to GfxDrawFn on the render worker thread; valid only during the call. The pass pipeline,
@@ -168,6 +176,48 @@ typedef struct GfxComputeTypeDesc {
#define GFX_COMPUTE_TYPE_DESC_INIT {sizeof(GfxComputeTypeDesc), NULL, NULL, NULL}
/*
* Invoked on the render worker while the frame encoder is open. The target texture and view have
* been acquired by the host and are borrowed for the callback. Record all target work on encoder,
* leave no pass open, and do not finish, submit, or present it. The host submits the shared command
* buffer and presents the target after submission. The streaming buffers contain data appended on
* the game thread before push_present.
*/
typedef struct GfxPresentContext {
uint32_t struct_size;
WGPUDevice device;
WGPUQueue queue;
WGPUCommandEncoder encoder;
WGPUTexture target_texture;
WGPUTextureView target_view;
WGPUTextureFormat target_format;
uint32_t target_width;
uint32_t target_height;
WGPUBuffer vertex_buffer;
WGPUBuffer index_buffer;
WGPUBuffer uniform_buffer;
WGPUBuffer storage_buffer;
} GfxPresentContext;
typedef void (*GfxPresentFn)(ModContext* ctx, const GfxPresentContext* present_ctx,
const void* payload, size_t payload_size, void* user_data);
typedef struct GfxPresentTargetDesc {
uint32_t struct_size;
const char* label; /* optional debug label */
uint32_t width; /* required for raw surfaces; ignored for WindowService windows */
uint32_t height;
WGPUTextureUsage usage; /* 0 defaults to RenderAttachment */
WGPUTextureFormat preferred_format;
WGPUCompositeAlphaMode preferred_alpha_mode;
GfxPresentFn render;
void* user_data;
} GfxPresentTargetDesc;
#define GFX_PRESENT_TARGET_DESC_INIT \
{sizeof(GfxPresentTargetDesc), NULL, 0u, 0u, WGPUTextureUsage_None, \
WGPUTextureFormat_Undefined, WGPUCompositeAlphaMode_Auto, NULL, NULL}
typedef struct GfxService {
ServiceHeader header;
@@ -200,15 +250,25 @@ typedef struct GfxService {
ModResult (*resolve_pass)(
ModContext* ctx, const GfxResolveDesc* desc, GfxResolvedTargets* out_targets);
ModResult (*create_pass)(ModContext* ctx, uint32_t width, uint32_t height);
/* Minor version 1 */
ModResult (*register_present_target)(ModContext* ctx, WGPUSurface surface,
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* out_handle);
ModResult (*register_window_present_target)(ModContext* ctx, WindowHandle window,
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* out_handle);
/* Raw-surface targets only; WindowService target resizes are managed automatically. */
ModResult (*resize_present_target)(
ModContext* ctx, GfxPresentTargetHandle handle, uint32_t width, uint32_t height);
ModResult (*unregister_present_target)(ModContext* ctx, GfxPresentTargetHandle handle);
/*
* MOD_OK means the task was queued.
* MOD_UNAVAILABLE means no task could be queued now (for example, a window has no pixel size).
* MOD_ERROR means an earlier task found the surface lost or deterministically invalid;
* unregister and recreate the target before pushing again.
*/
ModResult (*push_present)(
ModContext* ctx, GfxPresentTargetHandle handle, const void* payload, size_t payload_size);
} GfxService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<GfxService> {
static constexpr const char* id = GFX_SERVICE_ID;
static constexpr uint16_t major_version = GFX_SERVICE_MAJOR;
static constexpr uint16_t minor_version = GFX_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(GfxService, svc_gfx, GFX_SERVICE_ID, GFX_SERVICE_MAJOR, GFX_SERVICE_MINOR);
+39 -25
View File
@@ -2,21 +2,27 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
/*
* Intercept game functions by address. Prefer the typed helpers in mods/hook.hpp
* (hook_add_pre/hook_add_post/hook_replace over a &Class::method): they generate the
* trampoline and hide install/dispatch, which are the low-level primitives those helpers
* build. resolve() maps a symbol name to an address for targets you can't name at compile time
* (file-local statics included).
* Hooks allow intercepting calls to game functions, allowing you to:
* - Modify arguments
* - Perform your own work before (pre), after (post) or instead of (replace) the original call
* - From a pre hook, conditionally skip the original call and return your own value
*
* Every call is game-thread-only. Install and removal must run with no hooked function on the
* stack; the loader guarantees this by applying mod lifecycle changes between frames, which is
* why hooking a function that never returns (the outermost loop) makes a mod un-unloadable.
* In most cases, you'll want to instead use the C++ helpers in mods/svc/hook.hpp
* (mods::hook::add_pre/add_post/replace). They generate the trampoline passed to
* install and provide compile-time type checking.
*
* resolve() resolves an address by symbol name for targets you can't name at compile time
* (file-local statics included).
*/
#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook"
#define HOOK_SERVICE_MAJOR 1u
#define HOOK_SERVICE_MINOR 0u
#define HOOK_SERVICE_MINOR 1u
/* Symbol flags reported by resolve() */
typedef enum HookSymbolFlags {
@@ -46,7 +52,7 @@ typedef enum HookReplacePolicy {
/*
* Hook callbacks. `args` is an array of pointers to the call's arguments (index 0 is `this`
* for member functions); `retval` points at the return slot (NULL for void). Read and write
* them through mods::arg<T> / arg_ref<T> from mods/hook.hpp. `userdata` is the pointer
* them through mods::arg<T> / arg_ref<T> from mods/svc/hook.hpp. `userdata` is the pointer
* from HookOptions. All run on the game thread, in the hooked call's own stack frame.
*/
typedef HookAction (*HookPreFn)(ModContext* ctx, void* args, void* retval, void* userdata);
@@ -68,11 +74,18 @@ typedef struct HookService {
ServiceHeader header;
/*
* Install a trampoline detour on fn_addr and return the address to call the original through in
* *out_original_fn. The typed helpers generate the trampoline and call this; mods normally
* don't. The first mod to install a given target owns the live detour; later mods register as
* candidates so a hook survives the owner unloading (the detour is handed off and every
* original pointer is rewritten). Idempotent per (mod, out slot).
* Install a hook on fn_addr.
*
* trampoline_fn must point to a function that matches the original function's signature and
* dispatches pre- and post- hooks. This dispatch trampoline is normally generated at compile
* time using C++ template instantiation (see mods/svc/hook.hpp).
*
* The first hook install on a target will implicitly install a detour (patched instructions
* on the target that jump to the dispatch trampoline). When all hooks are uninstalled from a
* target, the detour is completely uninstalled.
*
* The address that the dispatch trampoline should call the original function through is written
* to out_original_fn.
*/
ModResult (*install)(
ModContext* ctx, void* fn_addr, void* trampoline_fn, void** out_original_fn);
@@ -112,15 +125,16 @@ typedef struct HookService {
*/
ModResult (*resolve)(
ModContext* ctx, const char* symbol, void** out_addr, HookSymbolFlags* out_flags);
/* Minor version 1 */
/*
* Uninstall the current mod's hook on fn_addr and unregister all callbacks.
* If no other mods have a hook installed on the target, the detour is uninstalled entirely.
*
* original_fn_slot must match the out_original_fn passed to install.
*/
ModResult (*uninstall)(ModContext* ctx, void* fn_addr, void** original_fn_slot);
} HookService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<HookService> {
static constexpr const char* id = HOOK_SERVICE_ID;
static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR;
static constexpr uint16_t minor_version = HOOK_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(HookService, svc_hook, HOOK_SERVICE_ID, HOOK_SERVICE_MAJOR, HOOK_SERVICE_MINOR);
+264
View File
@@ -0,0 +1,264 @@
#pragma once
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_GAME)
#error "DEFINE_HOOK requires add_mod(... FEATURES game)"
#endif
#include <mods/svc/hook.h>
#include <memory>
#include <type_traits>
namespace mods {
template <class T>
T arg(void* argsRaw, int n) noexcept {
void** args = static_cast<void**>(argsRaw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
}
template <class T>
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
void** args = static_cast<void**>(argsRaw);
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
}
/*
* Trampoline generator + per-target state. Tag makes each hooked target's statics distinct; the
* target address comes from the declaration's metadata record, resolved by the host at mod
* initialization.
*/
template <class Tag, class R, class... A>
struct HookImpl {
static inline R (*g_orig)(A...) = nullptr;
static inline const HookService* hooks = nullptr;
static inline void* target = nullptr;
static bool dispatch_pre(void* args, void* retval) {
if (hooks == nullptr) {
return false;
}
int skipOriginal = 0;
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
return result == MOD_OK && skipOriginal != 0;
}
static void dispatch_post(void* args, void* retval) {
if (hooks != nullptr) {
hooks->dispatch_post(mod_ctx, target, args, retval);
}
}
static R trampoline(A... args) {
if constexpr (sizeof...(A) == 0) {
if constexpr (std::is_void_v<R>) {
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
if (!skipOriginal) {
g_orig(args...);
}
dispatch_post(nullptr, nullptr);
} else {
R result{};
const bool skipOriginal =
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
if (!skipOriginal) {
result = g_orig(args...);
}
dispatch_post(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 skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
if (!skipOriginal) {
g_orig(args...);
}
dispatch_post(static_cast<void*>(ptrs), nullptr);
} else {
R result{};
const bool skipOriginal = dispatch_pre(
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
if (!skipOriginal) {
result = g_orig(args...);
}
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
return result;
}
}
}
};
namespace detail {
template <auto Target>
using TargetTag = std::integral_constant<decltype(Target), Target>;
template <FixedString Name>
struct NameTag {};
} // namespace detail
/*
* Typed base for a hook on a function named at compile time (&daAlink_c::execute, &free_fn).
* Instantiate through DEFINE_HOOK, which pairs it with the metadata record the host resolves.
*/
template <auto Target>
struct Hook;
template <class C, class R, class... A, R (C::*Target)(A...)>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {};
template <class C, class R, class... A, R (C::*Target)(A...) const>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {};
template <class R, class... A, R (*Target)(A...)>
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {};
/*
* Typed base for a hook on a function by its symbol name, for targets you can't name in C++:
* file-local statics, private members, or symbols without a header. The signature is written
* free-style with the receiver first and is *not* compiler-checked. Instantiate through
* DEFINE_HOOK_SYMBOL.
*/
template <FixedString Name, class Sig>
struct NamedHook;
template <FixedString Name, class R, class... A>
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, R, A...> {};
/*
* Declare a hook target. The declaration emits a metadata record that the host resolves at mod
* initialization. Every hook target must be declared.
*
* DEFINE_HOOK(&daAlink_c::execute, LinkExecute);
* DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
*
* mods::hook::add_pre<LinkExecute>(on_link_execute);
*
* DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O
* leading underscore) or the demangled qualified display name; overloaded display names are
* ambiguous and need the mangled form.
*/
#if defined(__GNUC__) && !defined(__clang__) && defined(__ELF__)
#define DEFINE_HOOK(target, alias) \
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
::mods::detail::make_local_hook_record<(target), ::mods::FixedString{#target}>(); \
struct alias : ::mods::Hook<(target)> { \
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
}
#else
#define DEFINE_HOOK(target, alias) \
[[maybe_unused]] static const void* const mod_meta_hook_##alias = \
&::mods::detail::HookRecordFor<(target), ::mods::FixedString{#target}>::Holder::record; \
struct alias : ::mods::Hook<(target)> { \
static void* resolved_target() { \
return ::mods::detail::HookRecordFor<(target), \
::mods::FixedString{#target}>::Holder::record.resolved; \
} \
}
#endif
#define DEFINE_HOOK_SYMBOL(name, sig, alias) \
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
::mods::detail::make_hook_name_record<::mods::FixedString{name}>(); \
struct alias : ::mods::NamedHook<::mods::FixedString{name}, sig> { \
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
}
namespace hook {
template <class Entry>
ModResult install(const HookService* hooks) {
if (hooks == nullptr) {
return MOD_UNAVAILABLE;
}
Entry::hooks = hooks;
if (Entry::target == nullptr) {
void* resolved = Entry::resolved_target();
if (resolved == nullptr) {
return MOD_UNAVAILABLE;
}
Entry::target = resolved;
}
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
reinterpret_cast<void**>(&Entry::g_orig));
}
template <class Entry>
ModResult install() {
return install<Entry>(svc_hook);
}
template <class Entry>
ModResult add_pre(
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
const ModResult installed = install<Entry>(hooks);
if (installed != MOD_OK) {
return installed;
}
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
}
template <class Entry>
ModResult add_pre(HookPreFn callback, const HookOptions* options = nullptr) {
return add_pre<Entry>(svc_hook, callback, options);
}
template <class Entry>
ModResult add_post(
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
const ModResult installed = install<Entry>(hooks);
if (installed != MOD_OK) {
return installed;
}
return hooks->add_post(mod_ctx, Entry::target, callback, options);
}
template <class Entry>
ModResult add_post(HookPostFn callback, const HookOptions* options = nullptr) {
return add_post<Entry>(svc_hook, callback, options);
}
template <class Entry>
ModResult replace(
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
const ModResult installed = install<Entry>(hooks);
if (installed != MOD_OK) {
return installed;
}
return hooks->replace(mod_ctx, Entry::target, callback, options);
}
template <class Entry>
ModResult replace(HookReplaceFn callback, const HookOptions* options = nullptr) {
return replace<Entry>(svc_hook, callback, options);
}
template <class Entry>
ModResult uninstall(const HookService* hooks) {
if (hooks == nullptr || !SERVICE_HAS(hooks, HookService, uninstall) ||
hooks->uninstall == nullptr || Entry::target == nullptr)
{
return MOD_UNAVAILABLE;
}
const ModResult result =
hooks->uninstall(mod_ctx, Entry::target, reinterpret_cast<void**>(&Entry::g_orig));
if (result == MOD_OK) {
Entry::hooks = nullptr;
Entry::g_orig = nullptr;
}
return result;
}
template <class Entry>
ModResult uninstall() {
return uninstall<Entry>(svc_hook);
}
} // namespace hook
} // namespace mods
+17 -13
View File
@@ -2,6 +2,10 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
/*
* The host service: the calling mod's identity and its runtime interface to the loader.
* Always available; every other service can be reached from it.
@@ -9,7 +13,7 @@
#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host"
#define HOST_SERVICE_MAJOR 2u
#define HOST_SERVICE_MINOR 1u
#define HOST_SERVICE_MINOR 2u
/*
* Ignore unknown values: later service minors may add events.
@@ -92,24 +96,24 @@ typedef struct HostService {
ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle);
ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle);
/* Minor version 1 */
/*
* Read-only directory containing this platform's packaged native runtime: the mod module
* and any RUNTIME_LIBRARIES. The path is absolute and remains valid until mod_shutdown
* returns. Libraries loaded dynamically from here are owned by the mod and must be unloaded
* during mod_shutdown.
*
* Added in minor version 1.
*/
const char* (*native_dir)(ModContext* ctx);
/* Minor version 2 */
/*
* A persistent writable directory reserved for the calling mod, created on first use.
*
* The returned path remains valid until mod_shutdown returns. *out_path is null on failure.
*/
ModResult (*data_dir)(ModContext* ctx, const char** out_path);
} HostService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<HostService> {
static constexpr const char* id = HOST_SERVICE_ID;
static constexpr uint16_t major_version = HOST_SERVICE_MAJOR;
static constexpr uint16_t minor_version = HOST_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(HostService, svc_host, HOST_SERVICE_ID, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR);
+5 -10
View File
@@ -2,6 +2,10 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
/*
* Logging into the game's console and log files. Messages are attributed to the calling mod
* (prefixed with its ID).
@@ -36,13 +40,4 @@ typedef struct LogService {
void (*error)(ModContext* ctx, const char* message);
} LogService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<LogService> {
static constexpr const char* id = LOG_SERVICE_ID;
static constexpr uint16_t major_version = LOG_SERVICE_MAJOR;
static constexpr uint16_t minor_version = LOG_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(LogService, svc_log, LOG_SERVICE_ID, LOG_SERVICE_MAJOR, LOG_SERVICE_MINOR);
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_FMT)
#error "mods/svc/log.hpp requires add_mod(... FEATURES fmt)"
#endif
#include <mods/svc/log.h>
#include <fmt/format.h>
#include <utility>
namespace mods::log {
template <typename... Args>
void write(LogLevel level, fmt::format_string<Args...> formatString, Args&&... args) {
const auto message = fmt::format(formatString, std::forward<Args>(args)...);
svc_log->write(mod_ctx, level, message.c_str());
}
template <typename... Args>
void trace(fmt::format_string<Args...> formatString, Args&&... args) {
write(LOG_LEVEL_TRACE, formatString, std::forward<Args>(args)...);
}
template <typename... Args>
void debug(fmt::format_string<Args...> formatString, Args&&... args) {
write(LOG_LEVEL_DEBUG, formatString, std::forward<Args>(args)...);
}
template <typename... Args>
void info(fmt::format_string<Args...> formatString, Args&&... args) {
write(LOG_LEVEL_INFO, formatString, std::forward<Args>(args)...);
}
template <typename... Args>
void warn(fmt::format_string<Args...> formatString, Args&&... args) {
write(LOG_LEVEL_WARN, formatString, std::forward<Args>(args)...);
}
template <typename... Args>
void error(fmt::format_string<Args...> formatString, Args&&... args) {
write(LOG_LEVEL_ERROR, formatString, std::forward<Args>(args)...);
}
} // namespace mods::log
+6 -10
View File
@@ -2,6 +2,10 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay"
#define OVERLAY_SERVICE_MAJOR 1u
#define OVERLAY_SERVICE_MINOR 0u
@@ -46,13 +50,5 @@ typedef struct OverlayService {
ModResult (*remove)(ModContext* ctx, OverlayHandle handle);
} OverlayService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<OverlayService> {
static constexpr const char* id = OVERLAY_SERVICE_ID;
static constexpr uint16_t major_version = OVERLAY_SERVICE_MAJOR;
static constexpr uint16_t minor_version = OVERLAY_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(
OverlayService, svc_overlay, OVERLAY_SERVICE_ID, OVERLAY_SERVICE_MAJOR, OVERLAY_SERVICE_MINOR);
+8 -11
View File
@@ -2,9 +2,14 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
/*
* Read-only access to the res/ tree of the calling mod's own bundle. Reload serves the new
* bundle's contents. For writable storage, use HostService::mod_dir.
* bundle's contents. Use HostService::data_dir for persistent storage or HostService::mod_dir
* for temporary storage.
*/
#define RESOURCE_SERVICE_ID "dev.twilitrealm.dusklight.resource"
@@ -41,13 +46,5 @@ typedef struct ResourceService {
void (*free)(ModContext* ctx, ResourceBuffer* buffer);
} ResourceService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<ResourceService> {
static constexpr const char* id = RESOURCE_SERVICE_ID;
static constexpr uint16_t major_version = RESOURCE_SERVICE_MAJOR;
static constexpr uint16_t minor_version = RESOURCE_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(ResourceService, svc_resource, RESOURCE_SERVICE_ID, RESOURCE_SERVICE_MAJOR,
RESOURCE_SERVICE_MINOR);
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define SAVE_SERVICE_ID "dev.twilitrealm.dusklight.save"
#define SAVE_SERVICE_MAJOR 1u
#define SAVE_SERVICE_MINOR 0u
/* 0 is never a valid handle. */
typedef uint64_t SaveObserverHandle;
/* Maximum combined blob size per mod and save slot. */
#define SAVE_BLOB_BUDGET_BYTES 65536u
/*
* Per-slot mod storage.
*
* Blobs are scoped to the calling mod and saved alongside each slot. Current-slot calls return
* MOD_UNAVAILABLE when no slot is active.
*
* Callbacks run on the game thread. Observer registrations are removed when the calling mod is
* detached.
*/
/* slot is the save-file index (0..2). */
typedef void (*SaveEventFn)(ModContext* ctx, uint32_t slot, void* user_data);
typedef struct SaveService {
ServiceHeader header;
/* Store a copy in the current slot. Returns MOD_UNAVAILABLE if the limit would be exceeded. */
ModResult (*set_blob)(ModContext* ctx, const char* name, const void* data, size_t size);
/*
* Read a blob from the current slot. Pass NULL for buf to query its size. Otherwise,
* inout_size is the buffer capacity on input and the blob size on success. Returns
* MOD_UNAVAILABLE if the blob does not exist.
*/
ModResult (*get_blob)(ModContext* ctx, const char* name, void* buf, size_t* inout_size);
ModResult (*delete_blob)(ModContext* ctx, const char* name);
/*
* Register save lifecycle callbacks. At least one callback is required. on_new_save runs
* after clearing the slot's blobs, on_save_loaded after activating the slot, and
* on_save_written after a successful game save. out_handle may be NULL.
*/
ModResult (*observe_saves)(ModContext* ctx, SaveEventFn on_new_save, SaveEventFn on_save_loaded,
SaveEventFn on_save_written, void* user_data, SaveObserverHandle* out_handle);
ModResult (*unobserve_saves)(ModContext* ctx, SaveObserverHandle handle);
/* Read the calling mod's blob from any slot. Uses the get_blob buffer contract. */
ModResult (*peek_blob)(
ModContext* ctx, uint32_t slot, const char* name, void* buf, size_t* inout_size);
} SaveService;
MOD_DECLARE_SERVICE(SaveService, svc_save, SAVE_SERVICE_ID, SAVE_SERVICE_MAJOR, SAVE_SERVICE_MINOR);
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define STAGE_SERVICE_ID "dev.twilitrealm.dusklight.stage"
#define STAGE_SERVICE_MAJOR 1u
#define STAGE_SERVICE_MINOR 0u
/* 0 is never a valid handle. */
typedef uint64_t StageActorHandle;
/*
* Runtime edits to Stage Info (.dzs/.dzr) data.
*/
typedef struct StageService {
ServiceHeader header;
/*
* Actor Node (ACTR/TGSC/SCOB/Door) Editing:
* stage must be a non-empty name of at most 8 characters. room 0xff and layer -1 match any room
* or layer for patch and delete operations; add_actor requires a specific room. Later-loaded mods
* win conflicts. record_crc is the CRC-32 of the unmodified record. Registrations are removed when
* the calling mod is detached. out_handle may be NULL.
*/
ModResult (*patch_actor)(ModContext* ctx, const char* stage, uint8_t room, int8_t layer,
uint32_t record_crc, const void* record, size_t record_size, StageActorHandle* out_handle);
ModResult (*delete_actor)(ModContext* ctx, const char* stage, uint8_t room, int8_t layer,
uint32_t record_crc, StageActorHandle* out_handle);
ModResult (*add_actor)(ModContext* ctx, const char* stage, uint8_t room, int8_t layer,
const void* record, size_t record_size, StageActorHandle* out_handle);
ModResult (*remove_actor_edit)(ModContext* ctx, StageActorHandle handle);
} StageService;
MOD_DECLARE_SERVICE(
StageService, svc_stage, STAGE_SERVICE_ID, STAGE_SERVICE_MAJOR, STAGE_SERVICE_MINOR);
+8 -12
View File
@@ -2,6 +2,10 @@
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture"
#define TEXTURE_SERVICE_MAJOR 1u
#define TEXTURE_SERVICE_MINOR 0u
@@ -70,20 +74,12 @@ typedef struct TextureService {
* "tex1_{w}x{h}_{hash}_{fmt}.dds"); "_mipN" sidecars next to it are picked up automatically.
* The file is decoded lazily on first use by the renderer.
*/
ModResult (*register_file)(ModContext* ctx, const char* bundle_path,
TextureReplacementHandle* out_handle);
ModResult (*register_file)(
ModContext* ctx, const char* bundle_path, TextureReplacementHandle* out_handle);
/* Remove a replacement previously registered by the calling mod. */
ModResult (*unregister)(ModContext* ctx, TextureReplacementHandle handle);
} TextureService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<TextureService> {
static constexpr const char* id = TEXTURE_SERVICE_ID;
static constexpr uint16_t major_version = TEXTURE_SERVICE_MAJOR;
static constexpr uint16_t minor_version = TEXTURE_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(
TextureService, svc_texture, TEXTURE_SERVICE_ID, TEXTURE_SERVICE_MAJOR, TEXTURE_SERVICE_MINOR);
+24 -13
View File
@@ -3,13 +3,17 @@
#include <mods/api.h>
#include <mods/svc/config.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
#define UI_SERVICE_MAJOR 1u
#define UI_SERVICE_MINOR 0u
#define UI_SERVICE_MINOR 1u
/*
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, scoped
* RCSS stylesheets and menu bar tabs.
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts,
* scoped RCSS stylesheets and menu bar tabs.
*
* All calls must be made on the game thread from mod callbacks (initialize, update, hooks, or UI
* callbacks). Handles are opaque, generation-checked ids; a stale or unknown handle fails with
@@ -208,6 +212,19 @@ typedef struct UiMenuTabDesc {
#define UI_MENU_TAB_DESC_INIT {sizeof(UiMenuTabDesc), NULL, NULL, NULL}
typedef struct UiToastDesc {
uint32_t struct_size;
/* Optional RCSS class, such as "warning" or a custom mod-defined type. */
const char* type;
/* Optional RML. At least one of title_rml or body_rml must be non-empty. */
const char* title_rml;
const char* body_rml;
/* How long the toast remains open; 0 uses the default of 5000 ms. */
uint32_t duration_ms;
} UiToastDesc;
#define UI_TOAST_DESC_INIT {sizeof(UiToastDesc), NULL, NULL, NULL, 0u}
typedef struct UiService {
ServiceHeader header;
@@ -271,15 +288,9 @@ typedef struct UiService {
ModResult (*register_menu_tab)(
ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab);
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
/* Enqueue a toast notification. */
ModResult (*push_toast)(ModContext* ctx, const UiToastDesc* desc);
} UiService;
#ifdef __cplusplus
#include "mods/service.hpp"
template <>
struct mods::ServiceTraits<UiService> {
static constexpr const char* id = UI_SERVICE_ID;
static constexpr uint16_t major_version = UI_SERVICE_MAJOR;
static constexpr uint16_t minor_version = UI_SERVICE_MINOR;
};
#endif
MOD_DECLARE_SERVICE(UiService, svc_ui, UI_SERVICE_ID, UI_SERVICE_MAJOR, UI_SERVICE_MINOR);
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#include <limits.h>
#define WINDOW_SERVICE_ID "dev.twilitrealm.dusklight.window"
#define WINDOW_SERVICE_MAJOR 1u
#define WINDOW_SERVICE_MINOR 0u
#define WINDOW_POSITION_UNDEFINED INT32_MIN
typedef uint64_t WindowHandle;
typedef enum WindowFlags {
WINDOW_FLAG_NONE = 0u,
WINDOW_FLAG_RESIZABLE = 1u << 0u,
WINDOW_FLAG_HIDDEN = 1u << 1u,
WINDOW_FLAG_BORDERLESS = 1u << 2u,
WINDOW_FLAG_ALWAYS_ON_TOP = 1u << 3u,
WINDOW_FLAG_TRANSPARENT = 1u << 4u,
} WindowFlags;
typedef enum WindowEventType {
WINDOW_EVENT_CLOSE_REQUESTED = 0,
WINDOW_EVENT_RESIZED = 1,
WINDOW_EVENT_MOVED = 2,
WINDOW_EVENT_FOCUS_GAINED = 3,
WINDOW_EVENT_FOCUS_LOST = 4,
WINDOW_EVENT_SHOWN = 5,
WINDOW_EVENT_HIDDEN = 6,
} WindowEventType;
typedef struct WindowEvent {
uint32_t struct_size;
WindowEventType type;
int32_t x;
int32_t y;
uint32_t width;
uint32_t height;
uint32_t pixel_width;
uint32_t pixel_height;
float display_scale;
} WindowEvent;
typedef void (*WindowEventFn)(
ModContext* ctx, WindowHandle window, const WindowEvent* event, void* user_data);
typedef struct WindowDesc {
uint32_t struct_size;
const char* title;
uint32_t width;
uint32_t height;
int32_t x;
int32_t y;
uint32_t flags;
WindowEventFn on_event;
void* user_data;
} WindowDesc;
#define WINDOW_DESC_INIT \
{sizeof(WindowDesc), NULL, 640u, 480u, WINDOW_POSITION_UNDEFINED, WINDOW_POSITION_UNDEFINED, \
WINDOW_FLAG_RESIZABLE | WINDOW_FLAG_HIDDEN, NULL, NULL}
typedef struct WindowInfo {
uint32_t struct_size;
int32_t x;
int32_t y;
uint32_t width;
uint32_t height;
uint32_t pixel_width;
uint32_t pixel_height;
float display_scale;
bool visible;
bool focused;
} WindowInfo;
#define WINDOW_INFO_INIT {sizeof(WindowInfo), 0, 0, 0u, 0u, 0u, 0u, 1.0f, false, false}
/*
* Auxiliary native windows. All functions and callbacks run on the game thread. Window close
* events are requests; the window remains alive until destroy_window is called. Any graphics
* present target attached to a window must be unregistered before the window can be destroyed;
* at most one present target may be attached to a window at a time.
*/
typedef struct WindowService {
ServiceHeader header;
ModResult (*create_window)(ModContext* ctx, const WindowDesc* desc, WindowHandle* out_window);
ModResult (*destroy_window)(ModContext* ctx, WindowHandle window);
ModResult (*show_window)(ModContext* ctx, WindowHandle window);
ModResult (*hide_window)(ModContext* ctx, WindowHandle window);
ModResult (*set_title)(ModContext* ctx, WindowHandle window, const char* title);
ModResult (*set_size)(ModContext* ctx, WindowHandle window, uint32_t width, uint32_t height);
ModResult (*get_info)(ModContext* ctx, WindowHandle window, WindowInfo* out_info);
} WindowService;
MOD_DECLARE_SERVICE(
WindowService, svc_window, WINDOW_SERVICE_ID, WINDOW_SERVICE_MAJOR, WINDOW_SERVICE_MINOR);