Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,19 @@ FetchContent_Declare(funchook
set(FUNCHOOK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FUNCHOOK_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(FUNCHOOK_INSTALL OFF CACHE BOOL "" FORCE)
# funchook auto-detects its CPU backend from CMAKE_SYSTEM_PROCESSOR (the build
# host). That's wrong when Apple cross-compiles to a different single target arch
# (e.g. an arm64 runner building x86_64), which makes it compile the wrong backend.
# funchook only auto-detects when FUNCHOOK_CPU is unset, so pre-set it from the
# real target arch.
if (APPLE AND CMAKE_OSX_ARCHITECTURES AND NOT CMAKE_OSX_ARCHITECTURES MATCHES ";")
if (CMAKE_OSX_ARCHITECTURES MATCHES "x86_64|i.86")
set(FUNCHOOK_CPU x86)
elseif (CMAKE_OSX_ARCHITECTURES MATCHES "arm64|aarch64")
set(FUNCHOOK_CPU arm64)
endif ()
endif ()

FetchContent_MakeAvailable(cxxopts json miniz funchook)

if (DUSK_ENABLE_SENTRY_NATIVE)
Expand Down Expand Up @@ -559,6 +572,13 @@ endif ()

target_compile_definitions(${DUSK_MAIN_TARGET} PRIVATE ${GAME_COMPILE_DEFS} DUSK_BUILDING_GAME=1)
target_include_directories(${DUSK_MAIN_TARGET} PRIVATE ${GAME_INCLUDE_DIRS})

# Keep game functions patchable by code-mod hooks (funchook): GCC's partial
# inlining splits functions into ".part.0" wrappers whose prologue funchook
# can't relocate (FUNCHOOK_ERROR_CANNOT_FIX_IP_RELATIVE), so hooks on them fail.
if (DUSK_ENABLE_CODE_MODS AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(${DUSK_MAIN_TARGET} PRIVATE -fno-partial-inlining)
endif ()
target_link_libraries(${DUSK_MAIN_TARGET} PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_precompile_headers(${DUSK_MAIN_TARGET} PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_LIST_DIR}/include/dusk_pch.hpp>")

Expand Down Expand Up @@ -662,15 +682,15 @@ if (APPLE)
set_target_properties(
dusklight PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING}
MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST}
MACOSX_BUNDLE_BUNDLE_NAME "${DUSK_BUNDLE_NAME}"
MACOSX_BUNDLE_GUI_IDENTIFIER "${DUSK_BUNDLE_IDENTIFIER}"
MACOSX_BUNDLE_BUNDLE_VERSION "${DUSK_VERSION_STRING}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}"
MACOSX_BUNDLE_INFO_PLIST "${DUSK_INFO_PLIST}"
OUTPUT_NAME Dusklight
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${DUSK_ENTITLEMENTS}
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${DUSK_ENTITLEMENTS}"
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES"
)
endif ()
Expand Down
29 changes: 29 additions & 0 deletions include/dusk/mod_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@
typedef void* DuskPanelHandle;
typedef void* DuskElemHandle;

// A user-configurable setting a mod declares once (in mod_init) via
// define_settings. The host persists values per mod and renders a control for
// each in the game's Mods tab.
typedef enum DuskSettingType {
DUSK_SETTING_BOOL = 0,
DUSK_SETTING_INT = 1,
DUSK_SETTING_FLOAT = 2,
} DuskSettingType;

typedef struct DuskSetting {
const char* key; // stable id, unique within the mod
const char* label; // display name
const char* help; // short description (may be NULL)
DuskSettingType type;
double default_value;
double min_value; // INT/FLOAT
double max_value; // INT/FLOAT
double step; // INT/FLOAT UI increment (0 -> sensible default)
} DuskSetting;

// Place this once at file scope in your mod to declare the minimum API version required.
// The loader will refuse to initialize the mod if the engine's API version is older.
#define DUSK_REQUIRE_API_VERSION \
Expand Down Expand Up @@ -55,6 +75,15 @@ struct DuskModAPIv1 {

void (*service_publish)(const char* name, void* ptr);
void* (*service_get)(const char* name);

// Settings. Declare the calling mod's settings once from mod_init; the host
// stores them, overlays any saved values, and shows a control per setting in
// the Mods tab. setting_get/_set read/write the calling mod's live value by
// key. These resolve the "current mod" the same way the UI/log callbacks do,
// so call them from mod_init / mod_tick (cache values for use in hooks).
void (*define_settings)(const DuskSetting* settings, uint32_t count);
double (*setting_get)(const char* key);
void (*setting_set)(const char* key, double value);
};

using DuskModAPI = DuskModAPIv1;
Expand Down
70 changes: 68 additions & 2 deletions include/dusk/mod_loader.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#pragma once

#include <filesystem>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include <ranges>

Expand Down Expand Up @@ -32,6 +34,24 @@ struct ModMetadata {
std::string author;
std::string description;
bool hasCode;
int priority = 0; // load order; lower priority loads first
std::vector<std::string> dependencies; // mod ids that must load before this one
};

enum class SettingType { Bool, Int, Float };

// A mod's declared setting plus its live value. Built from the DuskSetting a mod
// passes to define_settings, with the saved value overlaid from config.json.
struct ModSetting {
std::string key;
std::string label;
std::string help;
SettingType type = SettingType::Bool;
double defaultValue = 0.0;
double minValue = 0.0;
double maxValue = 0.0;
double step = 0.0;
double value = 0.0;
};

struct NativeMod {
Expand Down Expand Up @@ -87,10 +107,19 @@ struct LoadedMod {
std::string mod_path;
std::string dir;

// Enabled state is owned by the game (persisted as a CVar), not the mod.
// `enabled` is the live mirror of cvarIsEnabled.
std::unique_ptr<ConfigVar<bool>> cvarIsEnabled;

bool active = false;
bool enabled = true;
bool fromDir = true; // bundle source kind (dir vs .dusk), for hot-reload
bool active = false; // currently loaded + initialized
bool load_failed = false;
std::string load_error; // reason shown in the UI when load_failed

std::string source_lib; // on-disk file to watch for hot-reload
std::filesystem::file_time_type lib_mtime{}; // its mtime when last (re)loaded

std::vector<ModSetting> settings;

NativeModStatus native_status = NativeModStatus::None;
std::unique_ptr<NativeMod> native;
Expand All @@ -110,6 +139,13 @@ class ModLoader {
void tick();
void shutdown();

// Hot-reload: cheap per-frame check that reloads any mod whose library file
// changed on disk (driven by a background file watcher). Called from tick().
void update();

// Rescan the mods directory and load any newly-added mods (additive).
void refresh();

[[nodiscard]] auto mods() const {
return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; });
}
Expand All @@ -118,15 +154,45 @@ class ModLoader {
return mods() | std::views::filter([](const auto& m) { return m.active; });
}

[[nodiscard]] const std::filesystem::path& modsDir() const { return m_modsDir; }

// Lookup + live enable/disable + settings, addressed by mod id (used by UI).
LoadedMod* find(std::string_view id);
[[nodiscard]] bool isEnabled(std::string_view id);
// True when every dependency is installed and currently active (loaded ok).
[[nodiscard]] bool depsSatisfied(const LoadedMod& mod);
void setEnabled(std::string_view id, bool enabled);
[[nodiscard]] double getSetting(std::string_view id, std::string_view key);
void setSetting(std::string_view id, std::string_view key, double value);

// Called by the mod API on behalf of the calling (current) mod.
void modDefineSettings(LoadedMod& mod, const DuskSetting* arr, uint32_t count);
[[nodiscard]] double modGetSetting(LoadedMod& mod, const char* key);
void modSetSetting(LoadedMod& mod, const char* key, double value);

private:
std::vector<std::unique_ptr<LoadedMod>> m_mods;
std::filesystem::path m_modsDir;
bool m_initialized = false;

void tryLoadDusk(const std::filesystem::path& modPath, bool fromDir);
void tryLoadNativeMod(LoadedMod& mod);
void computeLoadOrder(); // reorder m_mods: dependencies first, then priority
bool checkDependencies(LoadedMod& mod); // verify deps active; sets load_error on fail
void buildAPI(LoadedMod& mod);
void initOverlayFiles();

bool initMod(LoadedMod& mod); // buildAPI + fn_init (native already loaded)
void unloadMod(LoadedMod& mod); // clear hooks + fn_cleanup + unload native lib
void reloadMod(LoadedMod& mod); // hot-reload a single mod in place

[[nodiscard]] std::filesystem::path configPath(const LoadedMod& mod) const;
[[nodiscard]] std::filesystem::path schemaPath(const LoadedMod& mod) const;
void readConfig(LoadedMod& mod); // settings values only (enabled is a CVar)
void writeConfig(const LoadedMod& mod);
void writeSchema(const LoadedMod& mod);
void parseSchema(LoadedMod& mod);
void applySetting(LoadedMod& mod, std::string_view key, double value);
};

using ModIndex = std::ranges::range_difference_t<decltype(std::declval<ModLoader>().mods())>;
Expand Down
9 changes: 9 additions & 0 deletions src/dusk/hook_system.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "dusk/hook_system.hpp"
#include "dusk/logging.h"
#include "dusk/mod_loader.hpp"

#include <cstdint>
#include <cstring>
Expand Down Expand Up @@ -78,6 +79,14 @@ void hookInstallByAddr(void* fn_addr, void* tramp_fn, void** orig_store) {
DuskLog.warn(
"HookSystem: funchook failed for {:p} (prepare={} install={})", fn_addr, prep, inst);
funchook_destroy(fh);
// Surface the failure on the mod so it shows as failed (toast + UI marker)
// instead of silently running with a dead hook.
if (auto* mod = static_cast<LoadedMod*>(modding::g_dusk_hook_current_mod)) {
mod->load_failed = true;
if (mod->load_error.empty()) {
mod->load_error = "a hook could not be installed (function not patchable)";
}
}
return;
}

Expand Down
Loading
Loading