From 5ea7f7ca9a057a028d786f7174c9ba15490808f9 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 15 Jul 2026 12:04:00 -0600 Subject: [PATCH 1/7] Mod SDK: Auto-download link stub for self-contained mods --- cmake/GameABIConfig.cmake | 9 ++++++++ cmake/ModSDK.cmake | 47 +++++++++++++++++++++++++++++++-------- sdk/CMakeLists.txt | 3 ++- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/cmake/GameABIConfig.cmake b/cmake/GameABIConfig.cmake index 8b95dcaf67..0b965aa082 100644 --- a/cmake/GameABIConfig.cmake +++ b/cmake/GameABIConfig.cmake @@ -47,6 +47,15 @@ target_link_libraries(dusklight_mod_feature_game INTERFACE dusklight_mod_api dusklight_game_abi_headers) target_compile_definitions(dusklight_mod_feature_game INTERFACE DUSK_MOD_FEATURE_GAME=1) +# Game headers assume global.h comes first in the translation unit (it defines DUSK_GAME_DATA +# and friends); force-include it so mods don't depend on include order. +if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + target_compile_options(dusklight_mod_feature_game INTERFACE + "$<$:/FIglobal.h>") +else () + target_compile_options(dusklight_mod_feature_game INTERFACE + "$<$:SHELL:-include global.h>") +endif () target_sources(dusklight_mod_feature_game INTERFACE ${_game_root}/sdk/src/game_feature.cpp) diff --git a/cmake/ModSDK.cmake b/cmake/ModSDK.cmake index 87125e12fd..0371a7d61b 100644 --- a/cmake/ModSDK.cmake +++ b/cmake/ModSDK.cmake @@ -2,6 +2,8 @@ # [RUNTIME_LIBRARIES ...] [RES_DIR ] [OVERLAY_DIR ] # [TEXTURES_DIR ] [OUTPUT_DIR ] [BUNDLE]) set(DUSK_MODS_OUTPUT_DIR "${CMAKE_BINARY_DIR}/mods" CACHE PATH "Directory to write mod packages into") +set(DUSKLIGHT_SDK_STUB_URL "https://github.com/encounter/dusklight/releases/download/sdk" + CACHE STRING "Base URL for game link stubs downloaded by out-of-tree mod builds") function(_mod_lib_info out_platform_var out_name_var) set(_arch "${CMAKE_SYSTEM_PROCESSOR}") @@ -29,6 +31,34 @@ function(_mod_lib_info out_platform_var out_name_var) set(${out_name_var} "mod${_ext}" PARENT_SCOPE) endfunction() +# For out-of-tree builds without a game binary: download the version-independent link stub +# (generated by symgen) for the target platform. +function(_mod_download_link_stub out_var) + _mod_lib_info(_platform _lib_name) + if (WIN32) + set(_asset "${_platform}.lib") + elseif (ANDROID) + set(_asset "stub-${_platform}.so") + else () + set(_asset "stub-${_platform}") + endif () + set(_stub "${CMAKE_BINARY_DIR}/dusklight-sdk-stubs/${_asset}") + if (NOT EXISTS "${_stub}") + message(STATUS "Mod SDK: downloading link stub ${_asset}") + file(DOWNLOAD "${DUSKLIGHT_SDK_STUB_URL}/${_asset}" "${_stub}.tmp" STATUS _status) + list(GET _status 0 _code) + if (NOT _code EQUAL 0) + list(GET _status 1 _error) + file(REMOVE "${_stub}.tmp") + message(FATAL_ERROR + "Mod SDK: failed to download ${DUSKLIGHT_SDK_STUB_URL}/${_asset}: ${_error}\n" + "Set DUSK_GAME_EXE to a game binary or link stub to skip the download.") + endif () + file(RENAME "${_stub}.tmp" "${_stub}") + endif () + set(${out_var} "${_stub}" PARENT_SCOPE) +endfunction() + function(_mod_resolve_source_path out_var path) if (IS_ABSOLUTE "${path}") set(_path "${path}") @@ -150,8 +180,7 @@ function(add_mod target_name) elseif (DUSK_GAME_EXE) _mod_resolve_source_path(_game_exe "${DUSK_GAME_EXE}") else () - message(FATAL_ERROR - "add_mod: FEATURES ${_features} requires DUSK_GAME_EXE (game executable)") + _mod_download_link_stub(_game_exe) endif () target_link_options(${target_name} PRIVATE -Xlinker -bundle_loader -Xlinker "${_game_exe}") @@ -164,12 +193,13 @@ function(add_mod target_name) if (_needs_host_link) if (TARGET dusklight) target_link_libraries(${target_name} PRIVATE dusklight) - elseif (DUSK_GAME_EXE) - _mod_resolve_source_path(_game_lib "${DUSK_GAME_EXE}") - target_link_libraries(${target_name} PRIVATE "${_game_lib}") else () - message(FATAL_ERROR - "add_mod: FEATURES ${_features} requires DUSK_GAME_EXE (libmain.so or stub)") + if (DUSK_GAME_EXE) + _mod_resolve_source_path(_game_lib "${DUSK_GAME_EXE}") + else () + _mod_download_link_stub(_game_lib) + endif () + target_link_libraries(${target_name} PRIVATE "${_game_lib}") endif () endif () set_target_properties(${target_name} PROPERTIES @@ -202,8 +232,7 @@ function(add_mod target_name) "(sdk/windows-.lib)") endif () else () - message(FATAL_ERROR - "add_mod: FEATURES ${_features} requires DUSK_GAME_EXE (import library)") + _mod_download_link_stub(_game_lib) endif () target_link_libraries(${target_name} PRIVATE "${_game_lib}") endif () diff --git a/sdk/CMakeLists.txt b/sdk/CMakeLists.txt index 2f25c28efe..cd80781eb6 100644 --- a/sdk/CMakeLists.txt +++ b/sdk/CMakeLists.txt @@ -7,7 +7,8 @@ # add_subdirectory(/sdk dusk-sdk EXCLUDE_FROM_ALL) # add_mod(my_mod FEATURES game webgpu SOURCES ... MOD_JSON mod.json) # -# TODO: auto-download link targets from tag +# On platforms where mods link against the game binary (Windows/Apple/Android), a +# version-independent link stub is downloaded automatically unless DUSK_GAME_EXE is set. cmake_minimum_required(VERSION 3.25) From 5db55c508be6e48c218515bcb23d1c0572ba48c1 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 15 Jul 2026 12:04:26 -0600 Subject: [PATCH 2/7] Mod API: Rename dusk::mods -> mods --- mods/ao_mod/src/mod.cpp | 20 +++++++++--------- mods/shadow_mod/src/mod.cpp | 36 ++++++++++++++++----------------- sdk/include/mods/hook.hpp | 19 +++++++++-------- sdk/include/mods/meta.hpp | 10 ++++----- sdk/include/mods/service.hpp | 27 ++++++++++++------------- sdk/include/mods/svc/camera.h | 2 +- sdk/include/mods/svc/config.h | 2 +- sdk/include/mods/svc/game.h | 2 +- sdk/include/mods/svc/gfx.h | 2 +- sdk/include/mods/svc/hook.h | 4 ++-- sdk/include/mods/svc/host.h | 2 +- sdk/include/mods/svc/log.h | 2 +- sdk/include/mods/svc/overlay.h | 2 +- sdk/include/mods/svc/resource.h | 2 +- sdk/include/mods/svc/texture.h | 2 +- sdk/include/mods/svc/ui.h | 2 +- 16 files changed, 66 insertions(+), 70 deletions(-) diff --git a/mods/ao_mod/src/mod.cpp b/mods/ao_mod/src/mod.cpp index a7eb4f4028..fb36ddb40c 100644 --- a/mods/ao_mod/src/mod.cpp +++ b/mods/ao_mod/src/mod.cpp @@ -752,7 +752,7 @@ ModResult register_bool_option( cvarDesc.type = CONFIG_VAR_BOOL; cvarDesc.default_bool = defaultValue; if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register AO option"); + return mods::set_error(error, MOD_ERROR, "failed to register AO option"); } return MOD_OK; } @@ -764,7 +764,7 @@ ModResult register_int_option( cvarDesc.type = CONFIG_VAR_INT; cvarDesc.default_int = defaultValue; if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register AO option"); + return mods::set_error(error, MOD_ERROR, "failed to register AO option"); } return MOD_OK; } @@ -785,7 +785,7 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { result = svc_resource->load(mod_ctx, "composite.wgsl", &g_compositeSource); } if (result != MOD_OK) { - return dusk::mods::set_error(error, result, "failed to load AO shaders"); + return mods::set_error(error, result, "failed to load AO shaders"); } result = register_bool_option("effectEnabled", false, g_cvarEnabled, error); @@ -814,7 +814,7 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { } if (svc_gfx->get_device_info(mod_ctx, &g_deviceInfo) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to query device info"); + return mods::set_error(error, MOD_ERROR, "failed to query device info"); } if (!build_compute_pipeline("AO preprocess depth", g_preprocessSource, "preprocess_depth", g_preprocessPipeline, g_preprocessLayout) || @@ -824,35 +824,35 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { !build_compute_pipeline( "AO denoise", g_denoiseSource, "spatial_denoise", g_denoisePipeline, g_denoiseLayout)) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO compute pipelines"); + return mods::set_error(error, MOD_ERROR, "failed to create AO compute pipelines"); } if (!build_composite_pipeline(true, g_compositePipeline, g_compositeLayout) || !build_composite_pipeline(false, g_compositeDebugPipeline, g_compositeDebugLayout)) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO composite pipeline"); + return mods::set_error(error, MOD_ERROR, "failed to create AO composite pipeline"); } if (!build_hilbert_lut()) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to create AO noise LUT"); + return mods::set_error(error, MOD_ERROR, "failed to create AO noise LUT"); } GfxComputeTypeDesc computeDesc = GFX_COMPUTE_TYPE_DESC_INIT; computeDesc.label = "AO chain"; computeDesc.callback = on_compute; if (svc_gfx->register_compute_type(mod_ctx, &computeDesc, &g_computeType) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register compute type"); + return mods::set_error(error, MOD_ERROR, "failed to register compute type"); } GfxDrawTypeDesc drawDesc = GFX_DRAW_TYPE_DESC_INIT; drawDesc.label = "AO composite"; drawDesc.draw = on_draw; if (svc_gfx->register_draw_type(mod_ctx, &drawDesc, &g_drawType) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register draw type"); + return mods::set_error(error, MOD_ERROR, "failed to register draw type"); } GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT; stageDesc.callback = on_scene_after_opaque; if (svc_gfx->register_stage_hook( mod_ctx, GFX_STAGE_SCENE_AFTER_OPAQUE, &stageDesc, &g_afterOpaqueHook) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register stage hook"); + return mods::set_error(error, MOD_ERROR, "failed to register stage hook"); } UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT; diff --git a/mods/shadow_mod/src/mod.cpp b/mods/shadow_mod/src/mod.cpp index 0319dab3dc..97439d7015 100644 --- a/mods/shadow_mod/src/mod.cpp +++ b/mods/shadow_mod/src/mod.cpp @@ -945,7 +945,7 @@ ModResult register_bool_option( cvarDesc.type = CONFIG_VAR_BOOL; cvarDesc.default_bool = defaultValue; if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register shadow option"); + return mods::set_error(error, MOD_ERROR, "failed to register shadow option"); } return MOD_OK; } @@ -957,7 +957,7 @@ ModResult register_int_option( cvarDesc.type = CONFIG_VAR_INT; cvarDesc.default_int = defaultValue; if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register shadow option"); + return mods::set_error(error, MOD_ERROR, "failed to register shadow option"); } return MOD_OK; } @@ -969,7 +969,7 @@ extern "C" { MOD_EXPORT ModResult mod_initialize(ModError* error) { ModResult result = svc_resource->load(mod_ctx, "shadow.wgsl", &g_shaderSource); if (result != MOD_OK || g_shaderSource.data == nullptr) { - return dusk::mods::set_error(error, result, "failed to load shadow.wgsl"); + return mods::set_error(error, result, "failed to load shadow.wgsl"); } result = register_bool_option("effectEnabled", false, g_cvarEnabled, error); @@ -1014,56 +1014,56 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) { } if (svc_gfx->get_device_info(mod_ctx, &g_deviceInfo) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to query device info"); + return mods::set_error(error, MOD_ERROR, "failed to query device info"); } if (!build_composite_pipeline(true, g_compositePipeline, g_compositeLayout) || !build_composite_pipeline(false, g_compositeDebugPipeline, g_compositeDebugLayout)) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to create composite pipeline"); + return mods::set_error(error, MOD_ERROR, "failed to create composite pipeline"); } GfxDrawTypeDesc drawDesc = GFX_DRAW_TYPE_DESC_INIT; drawDesc.label = "shadow composite"; drawDesc.draw = on_draw; if (svc_gfx->register_draw_type(mod_ctx, &drawDesc, &g_drawType) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register draw type"); + return mods::set_error(error, MOD_ERROR, "failed to register draw type"); } GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT; stageDesc.callback = on_scene_begin; if (svc_gfx->register_stage_hook( mod_ctx, GFX_STAGE_SCENE_BEGIN, &stageDesc, &g_sceneBeginHook) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register stage hook"); + return mods::set_error(error, MOD_ERROR, "failed to register stage hook"); } stageDesc.callback = on_scene_after_terrain; if (svc_gfx->register_stage_hook( mod_ctx, GFX_STAGE_SCENE_AFTER_TERRAIN, &stageDesc, &g_sceneAfterTerrainHook) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register stage hook"); + return mods::set_error(error, MOD_ERROR, "failed to register stage hook"); } stageDesc.callback = on_frame_before_hud; if (svc_gfx->register_stage_hook( mod_ctx, GFX_STAGE_FRAME_BEFORE_HUD, &stageDesc, &g_frameBeforeHudHook) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to register stage hook"); + return mods::set_error(error, MOD_ERROR, "failed to register stage hook"); } // Skip the game's own shadow rendering while the dynamic pass is active: the // shadowControl pair covers the actor real/blob shadows, drawCloudShadow the weather // cloud shadows. - if (dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || - dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || - dusk::mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK) + if (mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || + mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK || + mods::hook_add_pre(svc_hook, on_game_shadow_pre) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering"); + return mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering"); } - if (dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK || - dusk::mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK) + if (mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK || + mods::hook_add_pre(svc_hook, on_frustum_clip_pre) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping"); + return mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping"); } - if (dusk::mods::hook_add_pre(svc_hook, on_copy_tex_pre) != MOD_OK) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex"); + if (mods::hook_add_pre(svc_hook, on_copy_tex_pre) != MOD_OK) { + return mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex"); } UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT; panelDesc.build = build_panel; diff --git a/sdk/include/mods/hook.hpp b/sdk/include/mods/hook.hpp index d48e9bff9f..07999da84c 100644 --- a/sdk/include/mods/hook.hpp +++ b/sdk/include/mods/hook.hpp @@ -9,7 +9,7 @@ #include #include -namespace dusk::mods { +namespace mods { template T arg(void* argsRaw, int n) noexcept { @@ -133,7 +133,7 @@ struct NamedHook : HookImpl, R, A...> {}; * DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack", * void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit); * - * dusk::mods::hook_add_pre(svc_hook, on_link_execute); + * mods::hook_add_pre(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 @@ -141,19 +141,18 @@ struct NamedHook : HookImpl, R, A...> {}; */ #define DEFINE_HOOK(target, alias) \ [[maybe_unused]] static const void* const mod_meta_hook_##alias = \ - &::dusk::mods::detail::HookRecordFor<(target), \ - ::dusk::mods::FixedString{#target}>::Holder::record; \ - struct alias : ::dusk::mods::Hook<(target)> { \ + &::mods::detail::HookRecordFor<(target), ::mods::FixedString{#target}>::Holder::record; \ + struct alias : ::mods::Hook<(target)> { \ static void* resolved_target() { \ - return ::dusk::mods::detail::HookRecordFor<(target), \ - ::dusk::mods::FixedString{#target}>::Holder::record.resolved; \ + 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 = \ - ::dusk::mods::detail::make_hook_name_record<::dusk::mods::FixedString{name}>(); \ - struct alias : ::dusk::mods::NamedHook<::dusk::mods::FixedString{name}, sig> { \ + ::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; } \ } @@ -208,4 +207,4 @@ ModResult hook_replace( return hooks->replace(mod_ctx, Entry::target, callback, options); } -} // namespace dusk::mods +} // namespace mods diff --git a/sdk/include/mods/meta.hpp b/sdk/include/mods/meta.hpp index 7d5d1e5eec..023783cea1 100644 --- a/sdk/include/mods/meta.hpp +++ b/sdk/include/mods/meta.hpp @@ -52,7 +52,7 @@ extern "C" const unsigned char __stop_modmeta[]; #define MOD_META_BOUNDS_END (__stop_modmeta) #endif -namespace dusk::mods { +namespace mods { /* A string usable as a template argument: carries a symbol/target name into record builders * and makes each hook declaration's static state unique. */ @@ -254,8 +254,7 @@ struct HookFnHolder { {sizeof(HookFnRecord), MOD_META_HOOK_FN, 0}, 0, Target, nullptr}; }; -template > +template > struct HookRecordFor { using Holder = HookFnHolder; }; @@ -268,8 +267,7 @@ struct HookRecordFor { struct Bind> { using Type = HookMemHolder().chars[Is]...>; }; - using Holder = - Bind().len>>::Type; + using Holder = Bind().len>>::Type; }; template @@ -284,4 +282,4 @@ consteval auto make_hook_name_record() { } } // namespace detail -} // namespace dusk::mods +} // namespace mods diff --git a/sdk/include/mods/service.hpp b/sdk/include/mods/service.hpp index d97d8def31..a6010a375c 100644 --- a/sdk/include/mods/service.hpp +++ b/sdk/include/mods/service.hpp @@ -7,7 +7,7 @@ #include #include -namespace dusk::mods { +namespace mods { template struct ServiceTraits; @@ -23,14 +23,14 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa return code; } -} // namespace dusk::mods +} // namespace mods #define DEFINE_MOD() \ extern "C" { \ MOD_EXPORT ModContext* mod_ctx = nullptr; \ } \ MOD_META_RECORD static constinit ModMetaHeader mod_meta_header_record = \ - ::dusk::mods::detail::make_header(); \ + ::mods::detail::make_header(); \ MOD_META_BOUNDS_DEFN \ extern "C" { \ MOD_EXPORT constinit const ModMeta mod_meta = { \ @@ -52,26 +52,26 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa static_cast(major_value), \ static_cast(min_minor_value), \ &(variable), \ - ::dusk::mods::detail::make_service_id(service_id_value), \ + ::mods::detail::make_service_id(service_id_value), \ } #define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \ - IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits::id, \ - ::dusk::mods::ServiceTraits::major_version, min_minor_value, \ + IMPORT_SERVICE_EX(service_type, variable, ::mods::ServiceTraits::id, \ + ::mods::ServiceTraits::major_version, min_minor_value, \ SERVICE_IMPORT_REQUIRED) #define IMPORT_SERVICE(service_type, variable) \ IMPORT_SERVICE_VERSION( \ - service_type, variable, ::dusk::mods::ServiceTraits::minor_version) + service_type, variable, ::mods::ServiceTraits::minor_version) #define IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, min_minor_value) \ - IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits::id, \ - ::dusk::mods::ServiceTraits::major_version, min_minor_value, \ + IMPORT_SERVICE_EX(service_type, variable, ::mods::ServiceTraits::id, \ + ::mods::ServiceTraits::major_version, min_minor_value, \ SERVICE_IMPORT_OPTIONAL) #define IMPORT_OPTIONAL_SERVICE(service_type, variable) \ IMPORT_OPTIONAL_SERVICE_VERSION( \ - service_type, variable, ::dusk::mods::ServiceTraits::minor_version) + service_type, variable, ::mods::ServiceTraits::minor_version) #define EXPORT_SERVICE_AS(instance, service_id_value) \ MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##instance = { \ @@ -79,12 +79,11 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa (instance).header.major_version, \ (instance).header.minor_version, \ &(instance), \ - ::dusk::mods::detail::make_service_id(service_id_value), \ + ::mods::detail::make_service_id(service_id_value), \ } #define EXPORT_SERVICE(instance) \ - EXPORT_SERVICE_AS( \ - instance, ::dusk::mods::ServiceTraits>::id) + EXPORT_SERVICE_AS(instance, ::mods::ServiceTraits>::id) #define EXPORT_DEFERRED_SERVICE(token, service_id_value, major_value, minor_value) \ MOD_META_RECORD static constinit ModMetaExport mod_meta_export_##token = { \ @@ -92,5 +91,5 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa static_cast(major_value), \ static_cast(minor_value), \ nullptr, \ - ::dusk::mods::detail::make_service_id(service_id_value), \ + ::mods::detail::make_service_id(service_id_value), \ } diff --git a/sdk/include/mods/svc/camera.h b/sdk/include/mods/svc/camera.h index ec7724a1de..f6de5b8146 100644 --- a/sdk/include/mods/svc/camera.h +++ b/sdk/include/mods/svc/camera.h @@ -57,7 +57,7 @@ typedef struct CameraService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/config.h b/sdk/include/mods/svc/config.h index 50c518bd40..cd4afdbbdf 100644 --- a/sdk/include/mods/svc/config.h +++ b/sdk/include/mods/svc/config.h @@ -100,7 +100,7 @@ typedef struct ConfigService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/game.h b/sdk/include/mods/svc/game.h index bdcf2d71f0..0a3585b912 100644 --- a/sdk/include/mods/svc/game.h +++ b/sdk/include/mods/svc/game.h @@ -23,7 +23,7 @@ typedef struct GameService { #include template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/gfx.h b/sdk/include/mods/svc/gfx.h index fad1b0bf9a..03b24ed986 100644 --- a/sdk/include/mods/svc/gfx.h +++ b/sdk/include/mods/svc/gfx.h @@ -206,7 +206,7 @@ typedef struct GfxService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/hook.h b/sdk/include/mods/svc/hook.h index 987c438fa0..3d4e9c0472 100644 --- a/sdk/include/mods/svc/hook.h +++ b/sdk/include/mods/svc/hook.h @@ -46,7 +46,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 dusk::mods::arg / arg_ref from mods/hook.hpp. `userdata` is the pointer + * them through mods::arg / arg_ref from mods/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); @@ -118,7 +118,7 @@ typedef struct HookService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/host.h b/sdk/include/mods/svc/host.h index 0f7851c7e1..9e278d5790 100644 --- a/sdk/include/mods/svc/host.h +++ b/sdk/include/mods/svc/host.h @@ -107,7 +107,7 @@ typedef struct HostService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/log.h b/sdk/include/mods/svc/log.h index 0dbf4ec48e..0bb9ca74ed 100644 --- a/sdk/include/mods/svc/log.h +++ b/sdk/include/mods/svc/log.h @@ -40,7 +40,7 @@ typedef struct LogService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/overlay.h b/sdk/include/mods/svc/overlay.h index 02f60bd727..76e24c878b 100644 --- a/sdk/include/mods/svc/overlay.h +++ b/sdk/include/mods/svc/overlay.h @@ -50,7 +50,7 @@ typedef struct OverlayService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/resource.h b/sdk/include/mods/svc/resource.h index 1a8251d83a..05a22754a3 100644 --- a/sdk/include/mods/svc/resource.h +++ b/sdk/include/mods/svc/resource.h @@ -45,7 +45,7 @@ typedef struct ResourceService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/texture.h b/sdk/include/mods/svc/texture.h index bb648cf860..cdcfbe7e2a 100644 --- a/sdk/include/mods/svc/texture.h +++ b/sdk/include/mods/svc/texture.h @@ -81,7 +81,7 @@ typedef struct TextureService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index 73df25cb47..e19ec65f6c 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -277,7 +277,7 @@ typedef struct UiService { #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { 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; From 12261dbc491c6a06ec48588f3a9101d01ea2b8c4 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 15 Jul 2026 12:04:41 -0600 Subject: [PATCH 3/7] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 1dde08fa0d..81f12f31d2 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 1dde08fa0d0030133788a6250a81c8b9c44f246f +Subproject commit 81f12f31d23ec822d8bde2031c91e94c470911eb From e9ebb174f12603d43e2495c00e2964b482577edd Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 15 Jul 2026 12:44:27 -0600 Subject: [PATCH 4/7] Mod SDK: Fix Windows ARM detection; ignore dotfiles in dir copies --- cmake/ModSDK.cmake | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/cmake/ModSDK.cmake b/cmake/ModSDK.cmake index 0371a7d61b..10a87e8525 100644 --- a/cmake/ModSDK.cmake +++ b/cmake/ModSDK.cmake @@ -13,6 +13,18 @@ function(_mod_lib_info out_platform_var out_name_var) message(FATAL_ERROR "add_mod: universal binaries are not supported") endif () set(_arch "${CMAKE_OSX_ARCHITECTURES}") + elseif (WIN32) + set(_arch_id "${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}") + if (NOT _arch_id) + set(_arch_id "${CMAKE_C_COMPILER_ARCHITECTURE_ID}") + endif () + if (_arch_id MATCHES "^ARM64(EC)?$") + set(_arch "arm64") + elseif (_arch_id STREQUAL "x64") + set(_arch "amd64") + elseif (_arch_id STREQUAL "X86") + set(_arch "x86") + endif () endif () string(TOLOWER "${CMAKE_SYSTEM_NAME}" _platform) if (_platform STREQUAL "darwin") @@ -74,7 +86,15 @@ function(_mod_collect_assets out_var dir) endif () file(GLOB_RECURSE _files CONFIGURE_DEPENDS LIST_DIRECTORIES false "${dir}/*") - set(${out_var} ${_files} PARENT_SCOPE) + # Dotfiles (.gitkeep, .DS_Store, ...) are not packaged; see CopyModAssets.cmake. + set(_assets "") + foreach (_file IN LISTS _files) + file(RELATIVE_PATH _rel "${dir}" "${_file}") + if (NOT _rel MATCHES "(^|/)\\.") + list(APPEND _assets "${_file}") + endif () + endforeach () + set(${out_var} ${_assets} PARENT_SCOPE) endfunction() function(_mod_add_webgpu_headers target_name) @@ -290,8 +310,8 @@ function(add_mod target_name) list(APPEND _package_deps ${_res_deps}) list(APPEND _package_inputs "${_res_dir}" ${_res_deps}) list(APPEND _zip_args res) - list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory - "${_res_dir}" "${_stage}/res") + list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} "-DSRC=${_res_dir}" + "-DDST=${_stage}/res" -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/CopyModAssets.cmake") endif () if (ARG_OVERLAY_DIR) _mod_resolve_source_path(_overlay_dir "${ARG_OVERLAY_DIR}") @@ -299,8 +319,8 @@ function(add_mod target_name) list(APPEND _package_deps ${_overlay_deps}) list(APPEND _package_inputs "${_overlay_dir}" ${_overlay_deps}) list(APPEND _zip_args overlay) - list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory - "${_overlay_dir}" "${_stage}/overlay") + list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} "-DSRC=${_overlay_dir}" + "-DDST=${_stage}/overlay" -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/CopyModAssets.cmake") endif () if (ARG_TEXTURES_DIR) _mod_resolve_source_path(_textures_dir "${ARG_TEXTURES_DIR}") @@ -308,8 +328,8 @@ function(add_mod target_name) list(APPEND _package_deps ${_textures_deps}) list(APPEND _package_inputs "${_textures_dir}" ${_textures_deps}) list(APPEND _zip_args textures) - list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} -E copy_directory - "${_textures_dir}" "${_stage}/textures") + list(APPEND _extra_cmds COMMAND ${CMAKE_COMMAND} "-DSRC=${_textures_dir}" + "-DDST=${_stage}/textures" -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/CopyModAssets.cmake") endif () set(_bundle_cmds "") From 76b56cd8b81809fce0a5c2a44e2f6d437591132f Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 15 Jul 2026 12:46:57 -0600 Subject: [PATCH 5/7] Forgot to stage this --- cmake/CopyModAssets.cmake | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 cmake/CopyModAssets.cmake diff --git a/cmake/CopyModAssets.cmake b/cmake/CopyModAssets.cmake new file mode 100644 index 0000000000..ca16da6018 --- /dev/null +++ b/cmake/CopyModAssets.cmake @@ -0,0 +1,10 @@ +# Copies a mod asset directory for packaging, skipping dotfiles and dot-directories +# (.gitkeep, .DS_Store, ...). Usage: cmake -DSRC= -DDST= -P CopyModAssets.cmake +file(MAKE_DIRECTORY "${DST}") +file(GLOB_RECURSE _files RELATIVE "${SRC}" "${SRC}/*") +foreach (_file IN LISTS _files) + if (NOT _file MATCHES "(^|/)\\.") + get_filename_component(_dir "${_file}" DIRECTORY) + file(COPY "${SRC}/${_file}" DESTINATION "${DST}/${_dir}") + endif () +endforeach () From ec51ba298b7086fa1f7f9cf590fb79041a0a7f81 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 15 Jul 2026 13:07:22 -0600 Subject: [PATCH 6/7] Update docs/modding.md --- docs/modding.md | 50 ++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/modding.md b/docs/modding.md index 8afe8701e5..2600b4721d 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -28,7 +28,8 @@ function, read and write data fields, and hook the vast majority of game functio ## Getting Started -Fork the [mod template](../mods/template_mod/), a self-contained CMake project that uses the Dusklight mod SDK. +Fork the [mod template](https://github.com/TwilitRealm/mod-template), a self-contained CMake project that uses the +Dusklight mod SDK. ``` my_mod/ @@ -43,19 +44,22 @@ my_mod/ **CMakeLists.txt:** ```cmake -cmake_minimum_required(VERSION 3.25) +cmake_minimum_required(VERSION 3.26) project(my_mod CXX) -set(DUSKLIGHT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dusklight" CACHE PATH "Path to dusklight source root") +if (NOT DUSKLIGHT_VERSION) + set(DUSKLIGHT_VERSION "76b56cd8b81809fce0a5c2a44e2f6d437591132f") +endif () +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchDusklight.cmake") add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL) add_mod(my_mod - FEATURES game # optional + FEATURES game # remove for service/asset-only mods; add webgpu for GfxService SOURCES src/mod.cpp MOD_JSON mod.json - RES_DIR res # optional - OVERLAY_DIR overlay # optional - TEXTURES_DIR textures # optional + RES_DIR res # mod resources, including icon.png and banner.png + OVERLAY_DIR overlay # game file overlays; remove if unused + TEXTURES_DIR textures # texture replacements; remove if unused ) ``` @@ -65,11 +69,7 @@ Available features: - `webgpu`: Allows importing the WebGPU API (`webgpu/webgpu.h`). Must be enabled when using [GfxService](#gfxservice-modssvcgfxh). -Building produces `my_mod.dusk` in `build//mods/` (configurable via the `DUSK_MODS_OUTPUT_DIR` cache variable). -Dusklight searches a `mods/` directory next to the app in addition to the user directory, so a dev build launched from -`build//` picks these up automatically: rebuild, relaunch (or click Reload), done. - -For a regular game install, copy the `.dusk` into the user mods folder: +Building produces `my_mod.dusk` in `build/mods/`. Copy the `.dusk` into the user mods folder: - Windows: `%APPDATA%\TwilitRealm\Dusklight\mods` - Linux: `~/.local/share/TwilitRealm/Dusklight/mods` @@ -498,14 +498,14 @@ Run before the original. Return `HOOK_SKIP_ORIGINAL` to cancel it (post-hooks st ```cpp HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata) { - daAlink_c* link = dusk::mods::arg(args, 0); // arg 0 is `this` + daAlink_c* link = mods::arg(args, 0); // arg 0 is `this` if (link->shape_angle.y > 10000) { return HOOK_SKIP_ORIGINAL; } return HOOK_CONTINUE; } -dusk::mods::hook_add_pre(svc_hook, on_pos_move_pre); +mods::hook_add_pre(svc_hook, on_pos_move_pre); ``` ### Post-hooks @@ -516,7 +516,7 @@ if any. ```cpp void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... } -dusk::mods::hook_add_post(svc_hook, on_pos_move_post); +mods::hook_add_post(svc_hook, on_pos_move_post); ``` ### Replace-hooks @@ -525,13 +525,13 @@ Substitute the original entirely. Call through to it via the declaration's `g_or ```cpp void on_execute_replace(ModContext*, void* args, void* retval, void*) { - int result = LinkExecute::g_orig(dusk::mods::arg(args, 0)); + int result = LinkExecute::g_orig(mods::arg(args, 0)); if (retval != nullptr) { *static_cast(retval) = result; } } -dusk::mods::hook_replace(svc_hook, on_execute_replace); +mods::hook_replace(svc_hook, on_execute_replace); ``` By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`, @@ -547,7 +547,7 @@ symbol name instead. You must supply the signature along with the name. DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack", void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit); -dusk::mods::hook_add_pre(svc_hook, on_hookshot_hit_pre); +mods::hook_add_pre(svc_hook, on_hookshot_hit_pre); ... HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the original ``` @@ -571,8 +571,8 @@ call. declaration order. ```cpp -T value = dusk::mods::arg(args, n); // copy -T& ref = dusk::mods::arg_ref(args, n); // read/write reference +T value = mods::arg(args, n); // copy +T& ref = mods::arg_ref(args, n); // read/write reference ``` ```cpp @@ -580,14 +580,14 @@ DEFINE_HOOK(fopAcM_createItem, CreateItem); // fpc_ProcID fopAcM_createItem(..., int itemNo, ...): turn heart drops into green rupees HookAction on_create_item_pre(ModContext*, void* args, void*, void*) { - int& itemNo = dusk::mods::arg_ref(args, 1); + int& itemNo = mods::arg_ref(args, 1); if (itemNo == dItemNo_HEART_e) { itemNo = dItemNo_GREEN_RUPEE_e; } return HOOK_CONTINUE; } -dusk::mods::hook_add_pre(svc_hook, on_create_item_pre); +mods::hook_add_pre(svc_hook, on_create_item_pre); ``` For reference parameters (e.g. `const cXyz& pos`), `arg_ref` yields a direct reference. @@ -646,13 +646,13 @@ the stack for the whole session (e.g. the outermost main loop); a mod that does Service calls report failure through `ModResult` return values (`MOD_OK`, `MOD_UNAVAILABLE`, `MOD_INVALID_ARGUMENT`, ...). Lifecycle exports additionally receive a `ModError*`: fill it (e.g. with -`dusk::mods::set_error(error, code, "message")`) and return the code, and the loader disables the mod and shows the +`mods::set_error(error, code, "message")`) and return the code, and the loader disables the mod and shows the message to the user. ```cpp MOD_EXPORT ModResult mod_initialize(ModError* error) { if (!load_my_data()) { - return dusk::mods::set_error(error, MOD_ERROR, "failed to load data"); + return mods::set_error(error, MOD_ERROR, "failed to load data"); } return MOD_OK; } @@ -686,7 +686,7 @@ typedef struct MyModService { #ifdef __cplusplus #include "mods/service.hpp" template <> -struct dusk::mods::ServiceTraits { +struct mods::ServiceTraits { static constexpr const char* id = MY_MOD_SERVICE_ID; static constexpr uint16_t major_version = MY_MOD_SERVICE_MAJOR; static constexpr uint16_t minor_version = MY_MOD_SERVICE_MINOR; From 1bae8a5e6a812217ca33ba533e707ecfa64b1553 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Thu, 16 Jul 2026 00:10:07 -0600 Subject: [PATCH 7/7] DuskDsp: Fix streamed audio position tracking --- src/dusk/audio/DuskDsp.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index 1f304d4b3e..4009b37e39 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -457,10 +457,7 @@ static int ReadChannelSamplesChunk( auto aramBase = static_cast(ARGetStorageAddress()) + channel.mWaveAramAddress; - // Streaming logic directly modifies mSamplesLeft. - // So we use that as our tracking of where we are. - auto curSamplePosition = channel.mEndSample - channel.mSamplesLeft; - + auto curSamplePosition = channel.mSamplePosition; u32 skipSamples = curSamplePosition % channel.mSamplesPerBlock; if (skipSamples != 0) { // We need to start reading in the middle of a block. This can happen thanks to loops.