mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-25 05:58:42 -04:00
Merge branch 'main' of https://github.com/TwilitRealm/dusk into randomizer
This commit is contained in:
@@ -20,7 +20,7 @@ public:
|
||||
|
||||
/* 0x14 */ cM3dGAab mM3dGAab;
|
||||
/* 0x30 */ cBgS_ShdwDraw_Callback mCallbackFun;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x34 */ int field_0x34;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -48,6 +48,8 @@ public:
|
||||
/* 0x1370 */ Z2FxLineMgr mFxLineMgr;
|
||||
#if DEBUG
|
||||
/* 0x13BC */ Z2DebugSys mDebugSys;
|
||||
#elif PARTIAL_DEBUG
|
||||
alignas(Z2DebugSys) u8 mDebugSys[sizeof(Z2DebugSys)];
|
||||
#endif
|
||||
}; // Size: 0x138C
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ public:
|
||||
JAISoundHandle* getMainBgmHandle() { return &mMainBgmHandle; }
|
||||
JAISoundHandle* getSubBgmHandle() { return &mSubBgmHandle; }
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
f32 field_0x00_debug;
|
||||
u8 field_0x04_debug;
|
||||
#endif
|
||||
|
||||
@@ -100,13 +100,13 @@ public:
|
||||
bool isForceBattle() { return forceBattle_; }
|
||||
JSUList<Z2CreatureEnemy>* getEnemyList() { return &field_0x0; }
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
JSUList<Z2SoundObjBase>* getAllList() { return &allList_; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
/* 0x00 */ JSUList<Z2CreatureEnemy> field_0x0;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x0C */ JSUList<Z2SoundObjBase> allList_;
|
||||
#endif
|
||||
/* 0x0C */ Z2EnemyArea enemyArea_;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
struct Z2SoundStarter;
|
||||
|
||||
class Z2SoundObjBase : public Z2SoundHandles
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
, public JSULink<Z2SoundObjBase>
|
||||
#endif
|
||||
{
|
||||
|
||||
@@ -45,7 +45,7 @@ private:
|
||||
|
||||
class dAttParam_c : public JORReflexible {
|
||||
public:
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x04 */ s8 mHIOChildNo;
|
||||
#endif
|
||||
|
||||
@@ -66,7 +66,7 @@ public:
|
||||
/* 0x35 */ u8 mAttnCursorDisappearFrames;
|
||||
/* 0x38 */ f32 field_0x38;
|
||||
/* 0x3C */ f32 field_0x3c;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x44 */ s32 mDebugDispPosX;
|
||||
/* 0x48 */ s32 mDebugDispPosY;
|
||||
#endif
|
||||
|
||||
@@ -201,7 +201,7 @@ private:
|
||||
/* 0x02C */ u32 m_flags;
|
||||
/* 0x030 */ cXyz* pm_pos;
|
||||
/* 0x034 */ cXyz* pm_old_pos;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x038 */ cXyz unk_0x38;
|
||||
#endif
|
||||
/* 0x038 */ cXyz* pm_speed;
|
||||
@@ -229,7 +229,7 @@ private:
|
||||
/* 0x0CC */ f32 field_0xcc;
|
||||
/* 0x0D0 */ f32 m_wtr_chk_offset;
|
||||
/* 0x0D4 */ cBgS_PolyInfo* pm_out_poly_info;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x0E4 */ cXyz unk_0xe4;
|
||||
#endif
|
||||
/* 0x0D8 */ f32 field_0xd8;
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ public:
|
||||
|
||||
// /* 0x0000 */ cCcS mCCcS;
|
||||
/* 0x284C */ dCcMassS_Mng mMass_Mng;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x2AD0 */ u8 m_is_mass_all_timer;
|
||||
#endif
|
||||
}; // Size = 0x2AC4
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
#include <cstring>
|
||||
|
||||
#include "tracy/Tracy.hpp"
|
||||
#include "dusk/profiling.hpp"
|
||||
|
||||
enum dComIfG_ButtonStatus {
|
||||
/* 0x00 */ BUTTON_STATUS_NONE,
|
||||
@@ -1044,7 +1044,7 @@ public:
|
||||
/* 0x1DE09 */ u8 field_0x1de09;
|
||||
/* 0x1DE0A */ u8 field_0x1de0a;
|
||||
/* 0x1DE0B */ u8 mIsDebugMode;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x1DE0C */ OSStopwatch mStopwatch;
|
||||
#endif
|
||||
|
||||
@@ -1056,7 +1056,7 @@ public:
|
||||
|
||||
STATIC_ASSERT(122384 == sizeof(dComIfG_inf_c));
|
||||
|
||||
extern dComIfG_inf_c g_dComIfG_gameInfo;
|
||||
DUSK_GAME_EXTERN dComIfG_inf_c g_dComIfG_gameInfo;
|
||||
extern GXColor g_blackColor;
|
||||
extern GXColor g_clearColor;
|
||||
extern GXColor g_whiteColor;
|
||||
|
||||
@@ -41,7 +41,7 @@ public:
|
||||
BASE_ROOM5,
|
||||
BASE_DEMO,
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
BASE_DEBUG,
|
||||
#endif
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ public:
|
||||
/* 0x09B8 */ DUNGEON_LIGHT dungeonlight[8];
|
||||
/* 0x0C18 */ BOSS_LIGHT field_0x0c18[8];
|
||||
/* 0x0D58 */ BOSS_LIGHT field_0x0d58[6];
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x0E48 */ NAVYCHAN navy;
|
||||
/* 0x0E58 */ u8 field_0xe58[0xE68 - 0xE58]; // part of NAVYCHAN?
|
||||
#endif
|
||||
@@ -471,7 +471,7 @@ public:
|
||||
/* 0x130C */ u8 staffroll_next_timer;
|
||||
}; // Size: 0x1310
|
||||
|
||||
extern dScnKy_env_light_c g_env_light;
|
||||
DUSK_GAME_EXTERN dScnKy_env_light_c g_env_light;
|
||||
|
||||
STATIC_ASSERT(sizeof(dScnKy_env_light_c) == 4880);
|
||||
|
||||
|
||||
@@ -521,13 +521,13 @@ private:
|
||||
/* 0x019 */ u8 field_0x19;
|
||||
/* 0x01A */ u8 field_0x1a;
|
||||
/* 0x01B */ u8 field_0x1b;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x01C */ dPa_simpleEcallBack field_0x1c[48];
|
||||
#else
|
||||
/* 0x01C */ dPa_simpleEcallBack field_0x1c[25];
|
||||
#endif
|
||||
/* 0x210 */ level_c field_0x210;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
u8 mSceneCount;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ private:
|
||||
/* 0x18 */ JKRHeap* heap;
|
||||
/* 0x1C */ JKRSolidHeap* mDataHeap;
|
||||
/* 0x20 */ void** mRes;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x24 */ int mSize;
|
||||
#endif
|
||||
}; // Size: 0x24
|
||||
|
||||
+4
-1
@@ -1041,7 +1041,7 @@ public:
|
||||
|
||||
static const int ZONE_MAX = 0x20;
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x000 */ u8 unk_0x0;
|
||||
/* 0x001 */ char unk_0x1;
|
||||
/* 0x000 */ u8 unk_0x2[0x48 - 0x2];
|
||||
@@ -1062,6 +1062,9 @@ public:
|
||||
/* 0xF30 */ s64 mSaveTotalTime;
|
||||
#if DEBUG
|
||||
/* 0xF80 */ flagFile_c mFlagFile;
|
||||
#elif PARTIAL_DEBUG
|
||||
// flagFile_c's ctor/virtuals are only defined under #if DEBUG (d_save.cpp)
|
||||
alignas(flagFile_c) u8 mFlagFile[sizeof(flagFile_c)];
|
||||
#endif
|
||||
}; // Size: 0xF38
|
||||
|
||||
|
||||
+4
-4
@@ -538,7 +538,7 @@ public:
|
||||
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const = 0;
|
||||
/* vt[87] */ virtual void setDoor(stage_tgsc_class*) = 0;
|
||||
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const = 0;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
virtual void setUnit(void*) = 0;
|
||||
virtual void* getUnit() = 0;
|
||||
#endif
|
||||
@@ -796,7 +796,7 @@ public:
|
||||
virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
||||
virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
||||
virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
virtual void setUnit(void* i_Unit) { mUnit = i_Unit; }
|
||||
virtual void* getUnit() { return mUnit; }
|
||||
#endif
|
||||
@@ -845,7 +845,7 @@ public:
|
||||
/* 0x54 */ stage_tgsc_class* mDrTg;
|
||||
/* 0x58 */ stage_tgsc_class* mDoor;
|
||||
/* 0x5C */ dStage_FloorInfo_c* mFloorInfo;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x60 */ void* mUnit;
|
||||
#endif
|
||||
/* 0x60 */ u16 mPlayerNum;
|
||||
@@ -990,7 +990,7 @@ public:
|
||||
/* vt[86] */ virtual stage_tgsc_class* getDrTg(void) const { return mDrTg; }
|
||||
/* vt[87] */ virtual void setDoor(stage_tgsc_class* i_Door) { mDoor = i_Door; }
|
||||
/* vt[88] */ virtual stage_tgsc_class* getDoor(void) const { return mDoor; }
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
virtual void setUnit(void* i_Unit) {
|
||||
UNUSED(i_Unit);
|
||||
OSReport("stage non unit list data !!\n");
|
||||
|
||||
@@ -97,6 +97,9 @@ public:
|
||||
private:
|
||||
#if DEBUG
|
||||
/* 0x00 */ dVibTest_c mVibTest;
|
||||
#elif PARTIAL_DEBUG
|
||||
// dVibTest_c's ctor/virtuals are only defined under #if DEBUG (d_vibration.cpp)
|
||||
alignas(dVibTest_c) u8 mVibTest[sizeof(dVibTest_c)];
|
||||
#endif
|
||||
|
||||
class {
|
||||
|
||||
+69
-7
@@ -1,9 +1,11 @@
|
||||
#ifndef DUSK_CONFIG_HPP
|
||||
#define DUSK_CONFIG_HPP
|
||||
|
||||
#include <concepts>
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <stdexcept>
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include "config_var.hpp"
|
||||
|
||||
namespace dusk::config {
|
||||
@@ -40,7 +42,7 @@ public:
|
||||
[[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0;
|
||||
};
|
||||
|
||||
template<ConfigValue T>
|
||||
template <ConfigValue T>
|
||||
class ConfigImpl : public ConfigImplBase {
|
||||
// Just downcasting the references...
|
||||
void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final {
|
||||
@@ -90,20 +92,28 @@ public:
|
||||
void Register(ConfigVarBase& configVar);
|
||||
|
||||
/**
|
||||
* \brief Indicate that all registrations have happened and everything should lock in.
|
||||
* \brief Unregister a CVar, detaching it from the config system.
|
||||
*
|
||||
* If the CVar carries a user-set value (Value or Speedrun layer), it is stashed as an
|
||||
* unregistered key: Save() keeps writing it, and a later Register() of the same name restores
|
||||
* it through the normal back-fill path. The CVar may be destroyed after this returns.
|
||||
*/
|
||||
void FinishRegistration();
|
||||
void unregister(ConfigVarBase& configVar);
|
||||
|
||||
/**
|
||||
* \brief Load config from the standard user preferences location.
|
||||
*/
|
||||
void LoadFromUserPreferences();
|
||||
void LoadFromFileName(const char* path);
|
||||
void load_from_user_preferences();
|
||||
void load_from_file_name(const char* path);
|
||||
|
||||
void load_arg_override(std::string_view name, std::string_view value);
|
||||
|
||||
void shutdown();
|
||||
|
||||
/**
|
||||
* \brief Save the config to file.
|
||||
*/
|
||||
void Save();
|
||||
void save();
|
||||
|
||||
/**
|
||||
* \brief Get a registered CVar by name.
|
||||
@@ -124,6 +134,58 @@ void ClearAllActionBindings(int port);
|
||||
*/
|
||||
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
|
||||
|
||||
/**
|
||||
* \brief Type-erased change callback. previousValue points at the value before the mutation
|
||||
* (a `const T*` for a `ConfigVar<T>`) and is valid only for the duration of the call.
|
||||
*/
|
||||
using ChangeCallback = std::function<void(ConfigVarBase& cVar, const void* previousValue)>;
|
||||
|
||||
/**
|
||||
* \brief Token identifying a change subscription. 0 is never a valid token.
|
||||
*/
|
||||
using Subscription = u64;
|
||||
|
||||
/**
|
||||
* \brief Subscribe to changes of the named CVar (registered or not yet registered).
|
||||
*
|
||||
* Fired synchronously on the mutating thread (in practice the game thread) whenever the CVar's
|
||||
* effective value changes at runtime: setValue, override/speedrun setters and clears. Values
|
||||
* applied by config load or launch arguments do *not* notify: loads happen during startup
|
||||
* before the subsystems callbacks push values into are initialized, and each subsystem reads
|
||||
* its initial value itself at its own init. Callbacks may mutate other CVars; a nested
|
||||
* mutation of the same CVar applies but does not re-notify.
|
||||
*/
|
||||
Subscription subscribe(std::string_view name, ChangeCallback callback);
|
||||
|
||||
/**
|
||||
* \brief Typed convenience overload: the callback receives the current and previous values.
|
||||
*/
|
||||
template <ConfigValue T, typename Callback>
|
||||
requires std::invocable<Callback, const T&, const T&> Subscription subscribe(
|
||||
ConfigVar<T>& cVar, Callback&& callback) {
|
||||
return subscribe(cVar.getName(),
|
||||
[&cVar, cb = std::forward<Callback>(callback)](ConfigVarBase&, const void* previousValue) {
|
||||
cb(cVar.getValue(), *static_cast<const T*>(previousValue));
|
||||
});
|
||||
}
|
||||
|
||||
void unsubscribe(Subscription token);
|
||||
|
||||
/**
|
||||
* \brief Register a CVar and attach a change callback in one step.
|
||||
*
|
||||
* Useful for pushing settings into external systems (e.g. aurora) from one place instead of
|
||||
* every UI setter. The callback fires only for runtime changes (see subscribe); not when
|
||||
* loaded from config or launch arguments.
|
||||
*/
|
||||
template <ConfigValue T, typename Callback>
|
||||
requires std::invocable<Callback, const T&, const T&> Subscription Register(
|
||||
ConfigVar<T>& cVar, Callback&& onChange) {
|
||||
auto subscription = subscribe(cVar, std::forward<Callback>(onChange));
|
||||
Register(static_cast<ConfigVarBase&>(cVar));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
const ConfigImplBase* GetConfigImpl() {
|
||||
static ConfigImpl<T> config;
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
#define DUSK_CONFIG_VAR_HPP
|
||||
|
||||
#include "dolphin/types.h"
|
||||
#include <type_traits>
|
||||
#include <concepts>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
/**
|
||||
* The configuration system.
|
||||
@@ -69,7 +71,7 @@ protected:
|
||||
/**
|
||||
* The name of this CVar, used in the configuration file.
|
||||
*/
|
||||
const char* name;
|
||||
std::string name;
|
||||
|
||||
/**
|
||||
* Whether this CVar has been registered with the global managing logic.
|
||||
@@ -87,8 +89,8 @@ protected:
|
||||
*/
|
||||
const ConfigImplBase* impl;
|
||||
|
||||
ConfigVarBase(const char* name, const ConfigImplBase* impl);
|
||||
virtual ~ConfigVarBase() = default;
|
||||
ConfigVarBase(const ConfigVarBase&) = delete;
|
||||
ConfigVarBase(std::string name, const ConfigImplBase* impl);
|
||||
|
||||
/**
|
||||
* Check that the CVar is registered, aborting if this is not the case.
|
||||
@@ -98,7 +100,22 @@ protected:
|
||||
abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any change subscriber (see config::subscribe) is attached to this CVar's name.
|
||||
*/
|
||||
[[nodiscard]] bool has_subscribers() const;
|
||||
|
||||
/**
|
||||
* Notify change subscribers (see config::subscribe) that the effective value of this CVar
|
||||
* changed. Called by mutators after the change has been applied; previousValue points at
|
||||
* the old value (a `const T*` for a `ConfigVar<T>`), valid only for the duration of the
|
||||
* call.
|
||||
*/
|
||||
void notify_changed(const void* previousValue);
|
||||
|
||||
public:
|
||||
virtual ~ConfigVarBase();
|
||||
|
||||
/**
|
||||
* Get the name of this CVar, used in the configuration file.
|
||||
*/
|
||||
@@ -121,6 +138,7 @@ public:
|
||||
* This is necessary to make it legal to access.
|
||||
*/
|
||||
void markRegistered();
|
||||
void unmarkRegistered();
|
||||
|
||||
/**
|
||||
* Clear a speedrun-mode override if one is active on this CVar.
|
||||
@@ -155,6 +173,7 @@ template <typename T>
|
||||
concept ConfigValue =
|
||||
!std::is_const_v<T>
|
||||
&& !std::is_volatile_v<T>
|
||||
&& std::equality_comparable<T>
|
||||
&& (std::is_same_v<T, bool>
|
||||
|| ConfigValueInteger<T>
|
||||
|| std::is_same_v<T, f32>
|
||||
@@ -166,6 +185,9 @@ concept ConfigValue =
|
||||
template <ConfigValue T>
|
||||
const ConfigImplBase* GetConfigImpl();
|
||||
|
||||
template <ConfigValue T>
|
||||
class ConfigImpl;
|
||||
|
||||
template <typename T>
|
||||
struct ConfigEnumRange {
|
||||
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
|
||||
@@ -192,10 +214,12 @@ public:
|
||||
* @param arg Arguments to forward to construct the default value.
|
||||
*/
|
||||
template <typename... Args>
|
||||
ConfigVar(const char* name, Args&&... arg)
|
||||
: ConfigVarBase(name, GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
|
||||
ConfigVar(std::string name, Args&&... arg)
|
||||
: ConfigVarBase(std::move(name), GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
|
||||
value(), overrideValue() {}
|
||||
|
||||
ConfigVar(ConfigVar const&) = delete;
|
||||
|
||||
/**
|
||||
* \brief Get the current value of the CVar.
|
||||
*
|
||||
@@ -234,6 +258,7 @@ public:
|
||||
*/
|
||||
void setValue(T newValue, bool replaceOverride = true) {
|
||||
checkRegistered();
|
||||
const auto previous = previous_for_notify();
|
||||
value = std::move(newValue);
|
||||
|
||||
if (replaceOverride) {
|
||||
@@ -242,6 +267,7 @@ public:
|
||||
} else if (layer != ConfigVarLayer::Override) {
|
||||
layer = ConfigVarLayer::Value;
|
||||
}
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
|
||||
operator const T&() {
|
||||
@@ -258,8 +284,10 @@ public:
|
||||
*/
|
||||
void setOverrideValue(T newValue) {
|
||||
checkRegistered();
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Override;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,25 +301,31 @@ public:
|
||||
void setSpeedrunValue(T newValue) {
|
||||
checkRegistered();
|
||||
if (layer != ConfigVarLayer::Override) {
|
||||
const auto previous = previous_for_notify();
|
||||
priorLayer = layer;
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Speedrun;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
void clearOverride() {
|
||||
checkRegistered();
|
||||
if (layer == ConfigVarLayer::Override) {
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = {};
|
||||
layer = ConfigVarLayer::Value;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
void clearSpeedrunOverride() override {
|
||||
checkRegistered();
|
||||
if (layer == ConfigVarLayer::Speedrun) {
|
||||
const auto previous = previous_for_notify();
|
||||
overrideValue = {};
|
||||
layer = priorLayer;
|
||||
notify_if_changed(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +339,48 @@ public:
|
||||
const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer;
|
||||
return effectiveLayer == ConfigVarLayer::Default ? defaultValue : value;
|
||||
}
|
||||
|
||||
private:
|
||||
// The config loader applies values through the silent load_* methods below.
|
||||
friend class ConfigImpl<T>;
|
||||
|
||||
/**
|
||||
* Copy of the effective value before a mutation, taken only when someone is subscribed.
|
||||
*/
|
||||
[[nodiscard]] std::optional<T> previous_for_notify() const {
|
||||
return has_subscribers() ? std::optional<T>{getValue()} : std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify subscribers if the effective value actually changed across a mutation.
|
||||
*/
|
||||
void notify_if_changed(const std::optional<T>& previous) {
|
||||
if (previous.has_value() && !(getValue() == *previous)) {
|
||||
notify_changed(&*previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* setValue(newValue, false) without notifying change subscribers. Used when loading config:
|
||||
* loads happen during startup before the subsystems change callbacks push values into are
|
||||
* initialized, and each subsystem applies the loaded value itself at its own init.
|
||||
*/
|
||||
void load_value(T newValue) {
|
||||
checkRegistered();
|
||||
value = std::move(newValue);
|
||||
if (layer != ConfigVarLayer::Override) {
|
||||
layer = ConfigVarLayer::Value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* setOverrideValue without notifying change subscribers (see load_value).
|
||||
*/
|
||||
void load_override_value(T newValue) {
|
||||
checkRegistered();
|
||||
overrideValue = std::move(newValue);
|
||||
layer = ConfigVarLayer::Override;
|
||||
}
|
||||
};
|
||||
|
||||
using ActionBindConfigVar = ConfigVar<int>;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/gfx.h"
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
void gfx_run_stage(GfxStage stage, const view_class* gameView = nullptr,
|
||||
const view_port_class* gameViewport = nullptr);
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <dolphin/gx/GXExtra.h>
|
||||
#include "tracy/Tracy.hpp"
|
||||
|
||||
#include "profiling.hpp"
|
||||
|
||||
#if DUSK_GFX_DEBUG_GROUPS
|
||||
#define GX_DEBUG_GROUP(name, ...) \
|
||||
|
||||
@@ -12,6 +12,18 @@ extern bool RestartRequested;
|
||||
extern std::filesystem::path ConfigPath;
|
||||
extern std::filesystem::path CachePath;
|
||||
|
||||
extern uint8_t SaveRequested;
|
||||
struct StageRequest {
|
||||
std::string stage;
|
||||
bool set;
|
||||
s8 room;
|
||||
s16 point;
|
||||
s8 layer;
|
||||
};
|
||||
extern StageRequest StageRequested;
|
||||
|
||||
|
||||
|
||||
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
|
||||
(defined(TARGET_OS_TV) && TARGET_OS_TV)
|
||||
inline constexpr bool SupportsProcessRestart = false;
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/config_var.hpp"
|
||||
#include "mods/api.h"
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
class ModBundle;
|
||||
} // namespace dusk::mods
|
||||
|
||||
struct ModContext {
|
||||
dusk::mods::LoadedMod* mod = nullptr;
|
||||
};
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
class NativeModule;
|
||||
} // namespace dusk::mods::loader
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
struct ModDependencyEdge {
|
||||
LoadedMod* mod = nullptr;
|
||||
bool required = false;
|
||||
};
|
||||
|
||||
struct ModManifestInfo {
|
||||
struct Import {
|
||||
std::string id;
|
||||
uint16_t major = 0;
|
||||
bool required = false;
|
||||
bool operator==(const Import&) const = default;
|
||||
};
|
||||
struct Export {
|
||||
std::string id;
|
||||
uint16_t major = 0;
|
||||
bool operator==(const Export&) const = default;
|
||||
};
|
||||
std::vector<Import> imports;
|
||||
std::vector<Export> exports;
|
||||
bool operator==(const ModManifestInfo&) const = default;
|
||||
};
|
||||
|
||||
struct ModMetadata {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::string author;
|
||||
std::string description;
|
||||
std::string iconPath;
|
||||
std::string bannerPath;
|
||||
};
|
||||
|
||||
struct ModSearchDir {
|
||||
std::filesystem::path path;
|
||||
// Directory bundles dlopen their native lib in place instead of extracting it to the cache.
|
||||
// Required where extracted code cannot run (iOS), desirable for signed/read-only installs.
|
||||
bool inPlaceNative = false;
|
||||
// Native library location for platforms that restrict placement (e.g. iOS/tvOS Frameworks/)
|
||||
std::filesystem::path nativeLibDir;
|
||||
};
|
||||
|
||||
struct NativeMod {
|
||||
std::unique_ptr<loader::NativeModule> handle;
|
||||
const ModManifest* manifest = nullptr;
|
||||
ModContext** contextSymbol = nullptr;
|
||||
|
||||
ModInitializeFn fn_initialize = nullptr;
|
||||
ModUpdateFn fn_update = nullptr;
|
||||
ModShutdownFn fn_shutdown = nullptr;
|
||||
};
|
||||
|
||||
enum class NativeModStatus : u8 {
|
||||
/**
|
||||
* Mod does not have native code included.
|
||||
*/
|
||||
None,
|
||||
|
||||
/**
|
||||
* Native code mod loaded successfully.
|
||||
*
|
||||
* Note that this only indicates load status of the native library. If the native lib throws in
|
||||
* its init function, it will still be disabled!
|
||||
*/
|
||||
Loaded,
|
||||
|
||||
/**
|
||||
* This build was compiled without native mod support!
|
||||
*/
|
||||
BuildDisabled,
|
||||
|
||||
/**
|
||||
* Mod ships native libraries, but none matches this build's platform and architecture.
|
||||
*/
|
||||
ModMissingPlatform,
|
||||
|
||||
/**
|
||||
* Mod is built for a different ABI version than this build of the game.
|
||||
*/
|
||||
ApiVersionMismatch,
|
||||
|
||||
/**
|
||||
* Mod is missing a required native API export.
|
||||
*/
|
||||
MissingExport,
|
||||
|
||||
/**
|
||||
* Unknown error loading the native mod.
|
||||
*/
|
||||
Unknown,
|
||||
};
|
||||
|
||||
struct LoadedMod {
|
||||
ModMetadata metadata;
|
||||
std::string modPath;
|
||||
std::string dir;
|
||||
|
||||
uint32_t searchDirIndex = 0;
|
||||
// Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported.
|
||||
bool inPlace = false;
|
||||
|
||||
std::unique_ptr<ConfigVar<bool> > cvarIsEnabled;
|
||||
config::Subscription enabledSubscription = 0;
|
||||
|
||||
bool active = false;
|
||||
bool loadFailed = false;
|
||||
std::string failureReason;
|
||||
|
||||
// mod_initialize succeeded; a mod_shutdown is owed on deactivation.
|
||||
bool initialized = false;
|
||||
// Static service exports are currently present in the registry.
|
||||
bool servicesRegistered = false;
|
||||
// Lifecycle state last applied by the loader; diffed against cvarIsEnabled to pick up
|
||||
// runtime enable/disable requests.
|
||||
bool enabledApplied = false;
|
||||
// Deactivated because a provider it imports from was disabled, not by its own cvar.
|
||||
bool suspendedByProvider = false;
|
||||
// Bumped per native lib extraction so every dlopen sees a fresh path (and thus a fresh
|
||||
// image with fresh statics; a previous dlclose may not fully unmap). Also bumped by
|
||||
// asset-only reloads, so it doubles as a generation for anything caching per-mod content.
|
||||
uint32_t cacheGeneration = 0;
|
||||
// Currently extracted native library, empty if none.
|
||||
std::string nativePath;
|
||||
|
||||
NativeModStatus nativeStatus = NativeModStatus::None;
|
||||
std::unique_ptr<NativeMod> native;
|
||||
std::unique_ptr<ModContext> context;
|
||||
|
||||
// Shared with overlay file registrations so in-flight DVD reads survive disable/reload.
|
||||
std::shared_ptr<ModBundle> bundle;
|
||||
|
||||
ModManifestInfo manifestInfo;
|
||||
|
||||
// Mods this mod imports services from, and mods importing services from this mod.
|
||||
std::vector<ModDependencyEdge> dependencies;
|
||||
std::vector<ModDependencyEdge> dependents;
|
||||
};
|
||||
|
||||
class ModLoader {
|
||||
public:
|
||||
static ModLoader& instance();
|
||||
|
||||
void set_search_dirs(std::vector<ModSearchDir> dirs) { m_searchDirs = std::move(dirs); }
|
||||
void set_cache_dir(std::filesystem::path dir) { m_cacheDir = std::move(dir); }
|
||||
void init();
|
||||
void tick();
|
||||
void shutdown();
|
||||
|
||||
void request_enable(std::string_view id);
|
||||
void request_disable(std::string_view id);
|
||||
void request_reload(std::string_view id);
|
||||
void notify_mod_failure(LoadedMod& mod, bool firstFailure);
|
||||
|
||||
[[nodiscard]] auto mods() const {
|
||||
return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; });
|
||||
}
|
||||
|
||||
[[nodiscard]] auto active_mods() const {
|
||||
return mods() | std::views::filter([](const auto& m) { return m.active; });
|
||||
}
|
||||
|
||||
private:
|
||||
enum class RequestKind : u8 { Enable, Disable, Reload };
|
||||
struct Request {
|
||||
std::string modId;
|
||||
RequestKind kind;
|
||||
};
|
||||
// ModLoader::tick runs inside fapGm_Execute, so code from an unloading mod can still be
|
||||
// live on the stack (its frame unwinds after the tick). dlclose is therefore deferred to
|
||||
// the next tick, by which point every per-frame entry into the mod should have returned.
|
||||
struct RetiredNative {
|
||||
std::unique_ptr<NativeMod> native;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
std::vector<std::unique_ptr<LoadedMod> > m_mods;
|
||||
std::vector<ModSearchDir> m_searchDirs;
|
||||
std::filesystem::path m_cacheDir;
|
||||
std::vector<Request> m_pendingRequests;
|
||||
std::vector<std::string> m_pendingFailures;
|
||||
std::vector<RetiredNative> m_retiredNatives;
|
||||
bool m_initialized = false;
|
||||
bool m_startupComplete = false;
|
||||
|
||||
void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex);
|
||||
void load_native(LoadedMod& mod, const std::string& dllEntry);
|
||||
// Resolved <nativeLibDir>/<mod id><ext> if it exists on disk, empty otherwise.
|
||||
[[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const;
|
||||
void unload_native(LoadedMod& mod);
|
||||
// Registers exports (if needed), resolves imports and runs mod_initialize.
|
||||
// Returns whether the mod ended up active; failures go through fail_mod.
|
||||
bool activate_mod(LoadedMod& mod);
|
||||
// Runs mod_shutdown (if needed), detaches the mod from every service, and unloads the
|
||||
// native lib. Must only run with no mod code on the stack (startup, shutdown, or top of tick).
|
||||
void deactivate_mod(LoadedMod& mod);
|
||||
void init_services();
|
||||
bool register_static_service_exports(LoadedMod& mod);
|
||||
bool resolve_service_imports(LoadedMod& mod);
|
||||
[[nodiscard]] std::string describe_missing_import(
|
||||
const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const;
|
||||
|
||||
LoadedMod* find_mod(std::string_view id) const;
|
||||
void drain_retired_natives();
|
||||
void apply_pending_requests();
|
||||
void flush_toasts();
|
||||
void on_enabled_changed(LoadedMod& mod);
|
||||
// Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the
|
||||
// bundle from disk, then reactivates whatever the current cvar/provider state allows.
|
||||
void apply_lifecycle_change(LoadedMod& target, bool reload);
|
||||
// `target` plus transitive active/suspended dependents, in m_mods (init) order.
|
||||
std::vector<LoadedMod*> collect_lifecycle_set(LoadedMod& target);
|
||||
bool reload_bundle(LoadedMod& mod);
|
||||
bool ensure_native_loaded(LoadedMod& mod);
|
||||
};
|
||||
|
||||
using ModIndex = std::ranges::range_difference_t<decltype(std::declval<ModLoader>().mods())>;
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<tracy/Tracy.hpp>)
|
||||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef ZoneScoped
|
||||
#define ZoneScoped
|
||||
#define ZoneScopedN(name)
|
||||
#endif
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
namespace dusk {
|
||||
|
||||
using namespace config;
|
||||
using config::ConfigVar;
|
||||
using config::ActionBindConfigVar;
|
||||
|
||||
enum class BloomMode : int {
|
||||
Off = 0,
|
||||
|
||||
@@ -37,5 +37,6 @@ struct SpeedrunInfo {
|
||||
extern SpeedrunInfo m_speedrunInfo;
|
||||
|
||||
void resetForSpeedrunMode();
|
||||
void restoreFromSpeedrunMode();
|
||||
|
||||
} // namespace dusk
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
#ifndef DUSK_TEXTURE_REPLACEMENTS_HPP
|
||||
#define DUSK_TEXTURE_REPLACEMENTS_HPP
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace dusk::texture_replacements {
|
||||
|
||||
// Mod replacements are prioritized *over* user replacements (<data folder>/texture_replacements/)
|
||||
inline constexpr int32_t kUserTextureReplacementPriority = -1'000'000;
|
||||
|
||||
void reload();
|
||||
void set_enabled(bool enabled);
|
||||
void shutdown();
|
||||
|
||||
@@ -36,7 +36,7 @@ typedef struct node_create_request {
|
||||
/* 0x58 */ s16 name;
|
||||
/* 0x5C */ void* data;
|
||||
/* 0x60 */ s16 unk_0x60;
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
/* 0x64 */ int unk_0x64;
|
||||
/* 0x68 */ int unk_0x68;
|
||||
#endif
|
||||
|
||||
@@ -114,6 +114,19 @@ inline int __builtin_clz(unsigned int v) {
|
||||
|
||||
#endif
|
||||
|
||||
// Data symbols exported from the main exe need dllimport on the mod side.
|
||||
// DUSK_BUILDING_GAME is defined for the game build so the same headers work in both.
|
||||
#if defined(TARGET_PC) && defined(_WIN32) && !defined(DUSK_BUILDING_GAME)
|
||||
#define DUSK_GAME_EXTERN extern __declspec(dllimport)
|
||||
#define DUSK_GAME_DATA __declspec(dllimport)
|
||||
#elif defined(TARGET_PC) && defined(_WIN32) && defined(DUSK_BUILDING_GAME)
|
||||
#define DUSK_GAME_EXTERN extern __declspec(dllexport)
|
||||
#define DUSK_GAME_DATA __declspec(dllexport)
|
||||
#else
|
||||
#define DUSK_GAME_EXTERN extern
|
||||
#define DUSK_GAME_DATA
|
||||
#endif
|
||||
|
||||
#define FAST_DIV(x, n) (x >> (n / 2))
|
||||
|
||||
#define SQUARE(x) ((x) * (x))
|
||||
|
||||
@@ -11,13 +11,13 @@ class mDoAud_zelAudio_c : public Z2AudioMgr {
|
||||
public:
|
||||
void reset();
|
||||
mDoAud_zelAudio_c() {
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
setMode(2);
|
||||
#endif
|
||||
}
|
||||
~mDoAud_zelAudio_c() {}
|
||||
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
u8 getMode() { return field_0x13bd; }
|
||||
void setMode(u8 mode) { field_0x13bd = mode; }
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ public:
|
||||
/* 0x4 */ s8 mNo;
|
||||
/* 0x5 */ u8 mCount;
|
||||
#else
|
||||
#if PARTIAL_DEBUG
|
||||
// Initialized here since the DEBUG ctor doesn't run.
|
||||
/* 0x4 */ s8 mNo = -1;
|
||||
/* 0x5 */ u8 mCount = 0;
|
||||
#endif
|
||||
virtual ~mDoHIO_entry_c() {}
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
extern "C" {
|
||||
#else
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define MOD_EXPORT __declspec(dllexport)
|
||||
#else
|
||||
#define MOD_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define MOD_EXTERN_C extern "C"
|
||||
#else
|
||||
#define MOD_EXTERN_C
|
||||
#endif
|
||||
|
||||
#define MOD_ABI_VERSION 5u
|
||||
#define MOD_ERROR_MESSAGE_SIZE 512u
|
||||
|
||||
typedef struct ModContext ModContext;
|
||||
|
||||
typedef enum ModResult {
|
||||
MOD_OK = 0,
|
||||
MOD_ERROR = 1,
|
||||
MOD_UNAVAILABLE = 2,
|
||||
MOD_UNSUPPORTED = 3,
|
||||
MOD_CONFLICT = 4,
|
||||
MOD_INVALID_ARGUMENT = 5,
|
||||
} ModResult;
|
||||
|
||||
static_assert(sizeof(ModResult) == 4, "mod SDK enums must be int-sized; do not build mods with -fshort-enums");
|
||||
|
||||
typedef struct ModError {
|
||||
uint32_t struct_size;
|
||||
ModResult code;
|
||||
char message[MOD_ERROR_MESSAGE_SIZE];
|
||||
} ModError;
|
||||
|
||||
#define MOD_ERROR_INIT {sizeof(ModError), MOD_OK, {0}}
|
||||
|
||||
/*
|
||||
* Opaque per-mod context, populated by the host before mod_initialize is called.
|
||||
* Pass it as the first argument to every service call; it identifies the calling
|
||||
* mod for attribution (logging, resource lookup, hook ownership, etc.).
|
||||
*/
|
||||
MOD_EXPORT extern ModContext* mod_ctx;
|
||||
|
||||
/*
|
||||
* Service versioning contract:
|
||||
*
|
||||
* A service is a struct of function pointers, beginning with a ServiceHeader.
|
||||
* Compatibility is tracked with a major/minor version pair:
|
||||
*
|
||||
* - A major version bump is a breaking change. Different majors are distinct
|
||||
* services; the registry never matches an import against a different major.
|
||||
* - A minor version bump may only append fields to the end of the struct.
|
||||
* Existing fields must keep their offsets and semantics.
|
||||
*
|
||||
* Providers: exporting minor N means every function pointer introduced at or
|
||||
* below N is populated (non-NULL). struct_size reflects the compiled struct.
|
||||
*
|
||||
* Importers: importing with min_minor_version N guarantees (enforced at load
|
||||
* time) that the resolved service is at least minor N, so any field introduced
|
||||
* at or below N may be used unconditionally, with no availability checks.
|
||||
* Fields newer than the declared min_minor_version must be gated behind
|
||||
* SERVICE_HAS plus a NULL check on the pointer itself.
|
||||
*
|
||||
* Load ordering: a manifest import of another mod's service (required or
|
||||
* optional) guarantees that the provider's mod_initialize completed before the
|
||||
* importer's runs, and deferred services published during the provider's
|
||||
* initialization resolve into import slots just like static exports. If a
|
||||
* provider fails to load, mods that required its services fail in turn. Mods
|
||||
* whose required imports form a cycle all fail to load; a cycle involving an
|
||||
* optional import is broken by dropping the ordering guarantee (not the
|
||||
* resolution) of that optional import. Dynamic lookups via
|
||||
* HostService::get_service carry no ordering guarantee: they see whatever has
|
||||
* been published at call time.
|
||||
*/
|
||||
typedef struct ServiceHeader {
|
||||
uint32_t struct_size;
|
||||
uint16_t major_version;
|
||||
uint16_t minor_version;
|
||||
} ServiceHeader;
|
||||
|
||||
#define SERVICE_HEADER(service_type, major, minor) {sizeof(service_type), (major), (minor)}
|
||||
|
||||
#define SERVICE_HAS(service, service_type, field) \
|
||||
((service) != NULL && \
|
||||
(service)->header.struct_size >= \
|
||||
(uint32_t)(offsetof(service_type, field) + sizeof(((service_type*)0)->field)))
|
||||
|
||||
typedef enum ServiceImportFlags {
|
||||
SERVICE_IMPORT_REQUIRED = 0u,
|
||||
SERVICE_IMPORT_OPTIONAL = 1u << 0u,
|
||||
} ServiceImportFlags;
|
||||
|
||||
typedef enum ServiceExportFlags {
|
||||
SERVICE_EXPORT_STATIC = 0u,
|
||||
SERVICE_EXPORT_DEFERRED = 1u << 0u,
|
||||
} ServiceExportFlags;
|
||||
|
||||
typedef struct ServiceImport {
|
||||
uint32_t struct_size;
|
||||
const char* service_id;
|
||||
uint16_t major_version;
|
||||
uint16_t min_minor_version;
|
||||
uint32_t flags;
|
||||
void* slot;
|
||||
} ServiceImport;
|
||||
|
||||
typedef struct ServiceExport {
|
||||
uint32_t struct_size;
|
||||
const char* service_id;
|
||||
uint16_t major_version;
|
||||
uint16_t minor_version;
|
||||
uint32_t flags;
|
||||
const void* service;
|
||||
} ServiceExport;
|
||||
|
||||
typedef struct ModManifest {
|
||||
uint32_t struct_size;
|
||||
uint32_t abi_version;
|
||||
const ServiceImport* imports;
|
||||
size_t import_count;
|
||||
const ServiceExport* exports;
|
||||
size_t export_count;
|
||||
} ModManifest;
|
||||
|
||||
typedef const ModManifest* (*ModGetManifestFn)(void);
|
||||
|
||||
typedef ModResult (*ModInitializeFn)(ModError* out_error);
|
||||
typedef ModResult (*ModUpdateFn)(ModError* out_error);
|
||||
typedef ModResult (*ModShutdownFn)(ModError* out_error);
|
||||
|
||||
MOD_EXPORT const ModManifest* mod_get_manifest(void);
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* out_error);
|
||||
MOD_EXPORT ModResult mod_update(ModError* out_error);
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError* out_error);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,464 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace dusk::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]);
|
||||
}
|
||||
|
||||
template <class F>
|
||||
void* mfp_addr(F fn) noexcept {
|
||||
void* p = nullptr;
|
||||
static_assert(sizeof(fn) >= sizeof(void*), "unexpected function pointer size");
|
||||
std::memcpy(&p, &fn, sizeof(void*));
|
||||
return p;
|
||||
}
|
||||
|
||||
/* A string usable as a template argument: carries the hook target's symbol name and
|
||||
* makes each NamedHook instantiation's static state unique. */
|
||||
template <size_t N>
|
||||
struct FixedString {
|
||||
char chars[N]{};
|
||||
constexpr FixedString(const char (&s)[N]) noexcept {
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
chars[i] = s[i];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <class T>
|
||||
constexpr std::string_view class_name() {
|
||||
#if defined(__clang__) || defined(__GNUC__)
|
||||
// "... class_name() [T = daAlink_c]" / "... [with T = daAlink_c; ...]"
|
||||
constexpr std::string_view fn = __PRETTY_FUNCTION__;
|
||||
constexpr size_t start = fn.find("T = ") + 4;
|
||||
return fn.substr(start, fn.find_first_of(";]", start) - start);
|
||||
#elif defined(_MSC_VER)
|
||||
// "... class_name<class daAlink_c>(void)"
|
||||
constexpr std::string_view fn = __FUNCSIG__;
|
||||
constexpr size_t start = fn.find("class_name<") + 11;
|
||||
constexpr std::string_view name = fn.substr(start, fn.rfind(">(") - start);
|
||||
if constexpr (name.starts_with("class ")) {
|
||||
return name.substr(6);
|
||||
} else if constexpr (name.starts_with("struct ")) {
|
||||
return name.substr(7);
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
#else
|
||||
#error "unsupported compiler"
|
||||
#endif
|
||||
}
|
||||
|
||||
/* The manifest name of C's vtable. Only unscoped, non-template class names are
|
||||
* supported (an empty result fails resolution and the install reports it). */
|
||||
template <class C>
|
||||
constexpr auto vtable_symbol() {
|
||||
constexpr std::string_view name = class_name<C>();
|
||||
constexpr bool simple = name.find_first_of(":<> ") == std::string_view::npos;
|
||||
// "_ZTV" + decimal length + name / "??_7" + name + "@@6B@", NUL-terminated
|
||||
std::array<char, name.size() + 12> out{};
|
||||
if constexpr (!simple) {
|
||||
return out;
|
||||
}
|
||||
size_t n = 0;
|
||||
#if defined(_WIN32)
|
||||
for (char c : {'?', '?', '_', '7'}) {
|
||||
out[n++] = c;
|
||||
}
|
||||
for (char c : name) {
|
||||
out[n++] = c;
|
||||
}
|
||||
for (char c : {'@', '@', '6', 'B', '@'}) {
|
||||
out[n++] = c;
|
||||
}
|
||||
#else
|
||||
for (char c : {'_', 'Z', 'T', 'V'}) {
|
||||
out[n++] = c;
|
||||
}
|
||||
size_t len = name.size();
|
||||
char digits[8]{};
|
||||
size_t d = 0;
|
||||
while (len != 0) {
|
||||
digits[d++] = static_cast<char>('0' + len % 10);
|
||||
len /= 10;
|
||||
}
|
||||
while (d != 0) {
|
||||
out[n++] = digits[--d];
|
||||
}
|
||||
for (char c : name) {
|
||||
out[n++] = c;
|
||||
}
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at.
|
||||
* Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */
|
||||
inline size_t vcall_slot_offset(const void*& fn) noexcept {
|
||||
constexpr size_t npos = static_cast<size_t>(-1);
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
const auto* p = static_cast<const uint8_t*>(fn);
|
||||
for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs
|
||||
int32_t rel;
|
||||
std::memcpy(&rel, p + 1, 4);
|
||||
p += 5 + rel;
|
||||
}
|
||||
fn = p;
|
||||
// The vptr load. Unoptimized clang-cl thunks spill/reload rcx first
|
||||
// (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window.
|
||||
const uint8_t* q = nullptr;
|
||||
for (int i = 0; i <= 12; ++i) {
|
||||
if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx]
|
||||
q = p + i + 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (q == nullptr) {
|
||||
return npos;
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC)
|
||||
return 0;
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8]
|
||||
return static_cast<int8_t>(q[2]);
|
||||
}
|
||||
if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32]
|
||||
int32_t off;
|
||||
std::memcpy(&off, q + 2, 4);
|
||||
return off;
|
||||
}
|
||||
// clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax
|
||||
// distinguishes the thunk from an ordinary getter that begins the same way.
|
||||
if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) {
|
||||
size_t off = 0;
|
||||
const uint8_t* r = q + 3;
|
||||
if (q[2] == 0x40) {
|
||||
off = static_cast<int8_t>(q[3]);
|
||||
r = q + 4;
|
||||
} else if (q[2] == 0x80) {
|
||||
int32_t off32;
|
||||
std::memcpy(&off32, q + 3, 4);
|
||||
off = off32;
|
||||
r = q + 7;
|
||||
}
|
||||
for (int i = 0; i <= 8; ++i) {
|
||||
if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional)
|
||||
return off;
|
||||
}
|
||||
}
|
||||
}
|
||||
return npos;
|
||||
#elif defined(_M_ARM64) || defined(__aarch64__)
|
||||
const auto* p = static_cast<const uint8_t*>(fn);
|
||||
uint32_t insn[3];
|
||||
for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs
|
||||
std::memcpy(insn, p, 4);
|
||||
if ((insn[0] & 0xFC000000u) != 0x14000000u) {
|
||||
break;
|
||||
}
|
||||
const auto imm26 = static_cast<int32_t>(insn[0] << 6) >> 6;
|
||||
p += static_cast<intptr_t>(imm26) * 4;
|
||||
}
|
||||
fn = p;
|
||||
std::memcpy(insn, p, 12);
|
||||
// ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs
|
||||
if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) {
|
||||
return npos;
|
||||
}
|
||||
const uint32_t t = insn[0] & 0x1Fu;
|
||||
if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) {
|
||||
return npos;
|
||||
}
|
||||
const uint32_t s = insn[1] & 0x1Fu;
|
||||
if (insn[2] != (0xD61F0000u | (s << 5))) {
|
||||
return npos;
|
||||
}
|
||||
return ((insn[1] >> 10) & 0xFFFu) * 8;
|
||||
#else
|
||||
(void)fn;
|
||||
return npos;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Code address of the member function a mfp designates. Virtual mfps don't carry
|
||||
* one; recover it from the class's vtable (resolved from the symbol manifest), so
|
||||
* Hook works uniformly on virtual and non-virtual members. */
|
||||
template <class C, class F>
|
||||
ModResult member_target(const HookService* hooks, F mfp, void** out) {
|
||||
*out = nullptr;
|
||||
uintptr_t words[sizeof(F) > sizeof(uintptr_t) ? 2 : 1] = {};
|
||||
std::memcpy(words, &mfp, sizeof(words) < sizeof(F) ? sizeof(words) : sizeof(F));
|
||||
|
||||
#if defined(_WIN32)
|
||||
const void* fn = reinterpret_cast<const void*>(words[0]);
|
||||
const size_t slot = vcall_slot_offset(fn);
|
||||
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
|
||||
*out = const_cast<void*>(fn);
|
||||
return MOD_OK;
|
||||
}
|
||||
void* vtable = nullptr;
|
||||
const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol<C>().data(), &vtable, nullptr);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
}
|
||||
// ??_7 points at the first slot.
|
||||
*out = *reinterpret_cast<void**>(static_cast<char*>(vtable) + slot);
|
||||
#else
|
||||
#if defined(__aarch64__) || defined(__arm__)
|
||||
// AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function
|
||||
// addresses can't spare their low bit), and ptr holds the slot offset directly.
|
||||
const bool isVirtual = (words[1] & 1) != 0;
|
||||
const uintptr_t thisAdjust = words[1] >> 1;
|
||||
const uintptr_t slotOffset = words[0];
|
||||
#else
|
||||
// Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1.
|
||||
const bool isVirtual = (words[0] & 1) != 0;
|
||||
const uintptr_t thisAdjust = words[1];
|
||||
const uintptr_t slotOffset = words[0] - 1;
|
||||
#endif
|
||||
if (!isVirtual) { // non-virtual: the address itself
|
||||
*out = reinterpret_cast<void*>(words[0]);
|
||||
return MOD_OK;
|
||||
}
|
||||
if (thisAdjust != 0) {
|
||||
// this-adjusting mfp (member of a secondary base): the slot offset is
|
||||
// relative to a vtable we can't locate. Hook the overrider by name instead.
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
void* vtable = nullptr;
|
||||
const ModResult resolved = hooks->resolve(mod_ctx, vtable_symbol<C>().data(), &vtable, nullptr);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
}
|
||||
// _ZTV points at the offset-to-top slot; the address point mfps index from is
|
||||
// two pointers in (past offset-to-top and the typeinfo pointer).
|
||||
*out = *reinterpret_cast<void**>(static_cast<char*>(vtable) + 2 * sizeof(void*) + slotOffset);
|
||||
#endif
|
||||
return *out != nullptr ? MOD_OK : MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/* Trampoline generator + per-target state shared by Hook and NamedHook. Tag makes
|
||||
* each hooked target's statics distinct; the target address is filled in at install. */
|
||||
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 hook on a function named at compile time (&daAlink_c::execute, &free_fn).
|
||||
* Member functions may be virtual: the install decodes the member function pointer and hooks the
|
||||
* class's own overrider.
|
||||
*/
|
||||
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...> {
|
||||
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||
return detail::member_target<C>(hooks, Target, out);
|
||||
}
|
||||
};
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...) const>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {
|
||||
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||
return detail::member_target<C>(hooks, Target, out);
|
||||
}
|
||||
};
|
||||
|
||||
template <class R, class... A, R (*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {
|
||||
static ModResult resolve_target(const HookService*, void** out) {
|
||||
*out = mfp_addr(Target);
|
||||
return MOD_OK;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Typed 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.
|
||||
*
|
||||
* using HookshotHit = dusk::mods::NamedHook<
|
||||
* "daAlink_hookshotAtHitCallBack",
|
||||
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*)>;
|
||||
* dusk::mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit);
|
||||
*/
|
||||
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...> {
|
||||
static ModResult resolve_target(const HookService* hooks, void** out) {
|
||||
HookSymbolFlags flags{};
|
||||
const ModResult resolved = hooks->resolve(mod_ctx, Name.chars, out, &flags);
|
||||
if (resolved == MOD_OK && (flags & HOOK_SYMBOL_CODE) == 0) {
|
||||
*out = nullptr;
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
};
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
if (hooks == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
Entry::hooks = hooks;
|
||||
if (Entry::target == nullptr) {
|
||||
const ModResult resolved = Entry::resolve_target(hooks, &Entry::target);
|
||||
if (resolved != MOD_OK) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
|
||||
reinterpret_cast<void**>(&Entry::g_orig));
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
return hook_install<Hook<Target> >(hooks);
|
||||
}
|
||||
|
||||
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 hooks->add_pre(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_add_pre<Hook<Target> >(hooks, 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 hooks->add_post(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_add_post<Hook<Target> >(hooks, 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 hooks->replace(mod_ctx, Entry::target, callback, options);
|
||||
}
|
||||
|
||||
template <auto Target>
|
||||
ModResult hook_replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
return hook_replace<Hook<Target> >(hooks, callback, options);
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
template <class Service>
|
||||
struct ServiceTraits;
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline std::vector<ServiceImport>& imports() {
|
||||
static std::vector<ServiceImport> entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
inline std::vector<ServiceExport>& exports() {
|
||||
static std::vector<ServiceExport> entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
inline int register_import(ServiceImport entry) {
|
||||
imports().push_back(entry);
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline int register_export(ServiceExport entry) {
|
||||
exports().push_back(entry);
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline const ModManifest* manifest() {
|
||||
static ModManifest manifest{
|
||||
sizeof(ModManifest),
|
||||
MOD_ABI_VERSION,
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
0,
|
||||
};
|
||||
|
||||
auto& importEntries = imports();
|
||||
auto& exportEntries = exports();
|
||||
manifest.imports = importEntries.data();
|
||||
manifest.import_count = importEntries.size();
|
||||
manifest.exports = exportEntries.data();
|
||||
manifest.export_count = exportEntries.size();
|
||||
return &manifest;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline ModResult set_error(ModError* outError, ModResult code, const char* message) {
|
||||
if (outError != nullptr && outError->struct_size >= sizeof(ModError)) {
|
||||
outError->code = code;
|
||||
outError->message[0] = '\0';
|
||||
if (message != nullptr) {
|
||||
std::snprintf(outError->message, sizeof(outError->message), "%s", message);
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
|
||||
#define DEFINE_MOD() \
|
||||
extern "C" { \
|
||||
MOD_EXPORT ModContext* mod_ctx = nullptr; \
|
||||
MOD_EXPORT const ModManifest* mod_get_manifest(void) { \
|
||||
return ::dusk::mods::detail::manifest(); \
|
||||
} \
|
||||
}
|
||||
|
||||
// Declares `static 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.
|
||||
#define IMPORT_SERVICE_EX( \
|
||||
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
|
||||
static const service_type* variable = nullptr; \
|
||||
[[maybe_unused]] static const int mod_import_registration_##variable = \
|
||||
::dusk::mods::detail::register_import(ServiceImport{ \
|
||||
sizeof(ServiceImport), \
|
||||
(service_id_value), \
|
||||
static_cast<uint16_t>(major_value), \
|
||||
static_cast<uint16_t>(min_minor_value), \
|
||||
static_cast<uint32_t>(flags_value), \
|
||||
&(variable), \
|
||||
})
|
||||
|
||||
#define IMPORT_SERVICE_VERSION(service_type, variable, min_minor_value) \
|
||||
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
|
||||
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
|
||||
SERVICE_IMPORT_REQUIRED)
|
||||
|
||||
#define IMPORT_SERVICE(service_type, variable) IMPORT_SERVICE_VERSION(service_type, variable, 0)
|
||||
|
||||
#define IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, min_minor_value) \
|
||||
IMPORT_SERVICE_EX(service_type, variable, ::dusk::mods::ServiceTraits<service_type>::id, \
|
||||
::dusk::mods::ServiceTraits<service_type>::major_version, min_minor_value, \
|
||||
SERVICE_IMPORT_OPTIONAL)
|
||||
|
||||
#define IMPORT_OPTIONAL_SERVICE(service_type, variable) \
|
||||
IMPORT_OPTIONAL_SERVICE_VERSION(service_type, variable, 0)
|
||||
|
||||
#define EXPORT_SERVICE_AS(instance, service_id_value) \
|
||||
namespace { \
|
||||
const int mod_export_registration_##instance = \
|
||||
::dusk::mods::detail::register_export(ServiceExport{ \
|
||||
sizeof(ServiceExport), \
|
||||
(service_id_value), \
|
||||
(instance).header.major_version, \
|
||||
(instance).header.minor_version, \
|
||||
SERVICE_EXPORT_STATIC, \
|
||||
&(instance), \
|
||||
}); \
|
||||
}
|
||||
|
||||
#define EXPORT_SERVICE(instance) \
|
||||
EXPORT_SERVICE_AS( \
|
||||
instance, ::dusk::mods::ServiceTraits<std::remove_cv_t<decltype(instance)> >::id)
|
||||
|
||||
#define EXPORT_DEFERRED_SERVICE(token, service_id_value, major_value, minor_value) \
|
||||
namespace { \
|
||||
const int mod_deferred_export_registration_##token = \
|
||||
::dusk::mods::detail::register_export(ServiceExport{ \
|
||||
sizeof(ServiceExport), \
|
||||
(service_id_value), \
|
||||
static_cast<uint16_t>(major_value), \
|
||||
static_cast<uint16_t>(minor_value), \
|
||||
SERVICE_EXPORT_DEFERRED, \
|
||||
nullptr, \
|
||||
}); \
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera"
|
||||
#define CAMERA_SERVICE_MAJOR 1u
|
||||
#define CAMERA_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* Snapshot of a game camera for the frame currently being recorded.
|
||||
*
|
||||
* Matrix conventions: every matrix is a column-major float[16] using the matrix * column-vector
|
||||
* convention, ready to memcpy into a WGSL mat4x4f uniform. NOTE: this is the TRANSPOSE of the
|
||||
* game's row-major Mtx/Mtx44 layout; mods that want the raw game matrices should read the
|
||||
* view_class directly instead.
|
||||
*
|
||||
* View space is right-handed with -Z forward. Projection matrices are in WebGPU clip convention
|
||||
* and follow the renderer's depth mode: reversed-Z by default (depth 1.0 at the near plane,
|
||||
* 0.0 at far).
|
||||
*
|
||||
* Unprojecting a depth-buffer texel at uv with sampled depth d:
|
||||
* let ndc = vec3f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, d); // WebGPU framebuffer y is down
|
||||
* let world4 = world_from_proj * vec4f(ndc, 1.0);
|
||||
* let world = world4.xyz / world4.w;
|
||||
*/
|
||||
typedef struct CameraInfo {
|
||||
uint32_t struct_size;
|
||||
|
||||
float view_from_world[16]; /* the view matrix */
|
||||
float world_from_view[16]; /* its inverse; column 3 is the camera position */
|
||||
float proj_from_view[16]; /* WebGPU-convention projection (+ Aurora reversed-Z) */
|
||||
float view_from_proj[16]; /* its inverse */
|
||||
float proj_from_world[16]; /* proj_from_view * view_from_world */
|
||||
float world_from_proj[16]; /* one-step depth-buffer -> world unproject */
|
||||
|
||||
float eye[3]; /* camera position in world space */
|
||||
float fovy; /* vertical field of view, degrees */
|
||||
float aspect;
|
||||
float near_plane;
|
||||
float far_plane;
|
||||
} CameraInfo;
|
||||
|
||||
#define CAMERA_INFO_INIT {sizeof(CameraInfo)}
|
||||
|
||||
typedef struct CameraService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Snapshots a camera. game_view must be a view_class pointer, such as from a render stage
|
||||
* callback's game view. Game thread only. Returns MOD_UNAVAILABLE when the view is not a valid
|
||||
* perspective camera.
|
||||
*/
|
||||
ModResult (*get_camera)(ModContext* ctx, const void* game_view, CameraInfo* out_info);
|
||||
} CameraService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<CameraService> {
|
||||
static constexpr const char* id = CAMERA_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CAMERA_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config"
|
||||
#define CONFIG_SERVICE_MAJOR 1u
|
||||
#define CONFIG_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for a config var registered by the calling mod. 0 is never a valid handle. */
|
||||
typedef uint64_t ConfigVarHandle;
|
||||
/* Handle for a change subscription. 0 is never a valid handle. */
|
||||
typedef uint64_t ConfigSubscriptionHandle;
|
||||
|
||||
typedef enum ConfigVarType {
|
||||
CONFIG_VAR_BOOL = 0, /* bool */
|
||||
CONFIG_VAR_INT = 1, /* int64_t */
|
||||
CONFIG_VAR_FLOAT = 2, /* double */
|
||||
CONFIG_VAR_STRING = 3, /* UTF-8 */
|
||||
} ConfigVarType;
|
||||
|
||||
typedef struct ConfigVarDesc {
|
||||
uint32_t struct_size;
|
||||
/* Name fragment: 1-64 characters from [A-Za-z0-9_-]. The full config key is
|
||||
* "mod.<escaped mod id>.<name>", persisted in config.json alongside host settings.
|
||||
* "enabled" is reserved by the loader. */
|
||||
const char* name;
|
||||
ConfigVarType type;
|
||||
/* Default value; only the field matching `type` is read. */
|
||||
bool default_bool;
|
||||
int64_t default_int;
|
||||
double default_float;
|
||||
const char* default_string; /* NULL means "" */
|
||||
} ConfigVarDesc;
|
||||
|
||||
#define CONFIG_VAR_DESC_INIT {sizeof(ConfigVarDesc), NULL, CONFIG_VAR_BOOL, false, 0, 0.0, NULL}
|
||||
|
||||
/* Snapshot of a var's value; only the field matching `type` is meaningful. */
|
||||
typedef struct ConfigVarValue {
|
||||
uint32_t struct_size;
|
||||
ConfigVarType type;
|
||||
bool bool_value;
|
||||
int64_t int_value;
|
||||
double float_value;
|
||||
const char* string_value; /* NUL-terminated; NULL for non-string vars */
|
||||
size_t string_length; /* excludes the NUL */
|
||||
} ConfigVarValue;
|
||||
|
||||
/*
|
||||
* Fired on the game thread whenever the var's effective value changes at runtime: the calling mod's
|
||||
* own set_* calls and any other runtime writer. Writes that leave the value unchanged do not fire,
|
||||
* and neither do values applied from config.json or --cvar during registration. `value` holds the
|
||||
* new (current) value and `previous` the one it replaced; both snapshots are valid only for the
|
||||
* duration of the call (copy string_value if you need to keep it). Setting the same var from inside
|
||||
* its own callback applies the write but is not re-notified.
|
||||
*/
|
||||
typedef void (*ConfigChangedFn)(ModContext* ctx, ConfigVarHandle var, const ConfigVarValue* value,
|
||||
const ConfigVarValue* previous, void* user_data);
|
||||
|
||||
/*
|
||||
* Scoped configuration variables.
|
||||
*
|
||||
* Registrations are owned by the calling mod and removed automatically (subscriptions included)
|
||||
* when it is disabled, reloaded, or fails. Values are saved to config.json. Writes are debounced,
|
||||
* not flushed per set.
|
||||
*/
|
||||
typedef struct ConfigService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Register a config var. If a value for the full key was saved earlier (or set via --cvar),
|
||||
* it takes effect immediately; otherwise the var starts at the default. Registering a name
|
||||
* that is already live is MOD_CONFLICT. */
|
||||
ModResult (*register_var)(
|
||||
ModContext* ctx, const ConfigVarDesc* desc, ConfigVarHandle* out_handle);
|
||||
/* Unregister a var previously registered by the calling mod. Its persisted value is kept. */
|
||||
ModResult (*unregister_var)(ModContext* ctx, ConfigVarHandle var);
|
||||
|
||||
/* Typed accessors; the type must match the registration (MOD_INVALID_ARGUMENT otherwise). */
|
||||
ModResult (*get_bool)(ModContext* ctx, ConfigVarHandle var, bool* out_value);
|
||||
ModResult (*set_bool)(ModContext* ctx, ConfigVarHandle var, bool value);
|
||||
ModResult (*get_int)(ModContext* ctx, ConfigVarHandle var, int64_t* out_value);
|
||||
ModResult (*set_int)(ModContext* ctx, ConfigVarHandle var, int64_t value);
|
||||
ModResult (*get_float)(ModContext* ctx, ConfigVarHandle var, double* out_value);
|
||||
ModResult (*set_float)(ModContext* ctx, ConfigVarHandle var, double value);
|
||||
/* Copies the NUL-terminated value into buffer. out_length (optional) receives the full
|
||||
* length excluding the NUL regardless of buffer size; call with buffer == NULL and
|
||||
* buffer_size == 0 to query the length. A non-NULL buffer that is too small fails with
|
||||
* MOD_INVALID_ARGUMENT and writes nothing. */
|
||||
ModResult (*get_string)(
|
||||
ModContext* ctx, ConfigVarHandle var, char* buffer, size_t buffer_size, size_t* out_length);
|
||||
ModResult (*set_string)(ModContext* ctx, ConfigVarHandle var, const char* value);
|
||||
|
||||
/* Subscribe to changes of a var registered by the calling mod. out_handle may be NULL if
|
||||
* the subscription is never removed manually (cleanup on mod teardown is automatic). */
|
||||
ModResult (*subscribe)(ModContext* ctx, ConfigVarHandle var, ConfigChangedFn callback,
|
||||
void* user_data, ConfigSubscriptionHandle* out_handle);
|
||||
ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle);
|
||||
} ConfigService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<ConfigService> {
|
||||
static constexpr const char* id = CONFIG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* Mods that link or hook game code directly must import this service; service-only and asset-only
|
||||
* mods must not.
|
||||
*
|
||||
* Major version is the game-code ABI epoch: it is bumped when game-visible struct or vtable layouts
|
||||
* change incompatibly (e.g. a TARGET_PC field added to an existing game struct). The loader's
|
||||
* ordinary version check then fails mods built against the old epoch with a clear message instead
|
||||
* of letting them corrupt memory.
|
||||
*/
|
||||
#define GAME_SERVICE_ID "dev.twilitrealm.dusklight.game"
|
||||
#define GAME_SERVICE_MAJOR 1u
|
||||
#define GAME_SERVICE_MINOR 0u
|
||||
|
||||
typedef struct GameService {
|
||||
ServiceHeader header;
|
||||
} GameService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<GameService> {
|
||||
static constexpr const char* id = GAME_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,209 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
/*
|
||||
* Direct WebGPU access at various stages of the rendering pipeline. Mods use the wgpu* C API
|
||||
* (via webgpu/webgpu.h) for custom draws and compute dispatches.
|
||||
*
|
||||
* Every service function must be called on the game thread. GfxStageFn callbacks run on the game
|
||||
* thread during frame recording. push_draw, push_* and pass functions are valid from a stage
|
||||
* callback and anywhere else GX commands are being recorded.
|
||||
*
|
||||
* GfxDrawFn and GfxComputeFn callbacks run on the render worker thread while the frame is encoded.
|
||||
* They may use only the handles in their context struct and raw wgpu* calls; no other service may
|
||||
* be called from them.
|
||||
*
|
||||
* All WGPU handles provided by this service are borrowed. Handles in callback contexts are valid
|
||||
* only for the duration of the callback; views in GfxResolvedTargets are valid for the current
|
||||
* frame only. GPU objects a mod creates through raw wgpu calls are its own responsibility and
|
||||
* should be released in mod_shutdown. The device outlives all mods.
|
||||
*/
|
||||
|
||||
#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx"
|
||||
#define GFX_SERVICE_MAJOR 1u
|
||||
#define GFX_SERVICE_MINOR 0u
|
||||
|
||||
/* Maximum size for push_draw payload */
|
||||
#define GFX_INLINE_DRAW_PAYLOAD_SIZE 128u
|
||||
|
||||
/* 0 is never a valid handle. */
|
||||
typedef uint64_t GfxDrawTypeHandle;
|
||||
typedef uint64_t GfxStageHookHandle;
|
||||
typedef uint64_t GfxComputeTypeHandle;
|
||||
|
||||
/* A suballocation in one of the shared per-frame streaming buffers. */
|
||||
typedef struct GfxRange {
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
} GfxRange;
|
||||
|
||||
/*
|
||||
* Device and scene pass configuration. Valid from mod_initialize onward and stable for the
|
||||
* session. Offscreen passes from create_pass are always single-sample.
|
||||
*/
|
||||
typedef struct GfxDeviceInfo {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device; /* borrowed */
|
||||
WGPUQueue queue; /* borrowed */
|
||||
WGPUTextureFormat color_format; /* scene color target format */
|
||||
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 */
|
||||
} GfxDeviceInfo;
|
||||
|
||||
#define GFX_DEVICE_INFO_INIT \
|
||||
{sizeof(GfxDeviceInfo), NULL, NULL, WGPUTextureFormat_Undefined, WGPUTextureFormat_Undefined, \
|
||||
1u, false}
|
||||
|
||||
/*
|
||||
* Passed to GfxDrawFn on the render worker thread; valid only during the call. The pass pipeline,
|
||||
* bind group, viewport, and scissor state is restored by the host after the callback returns.
|
||||
*/
|
||||
typedef struct GfxDrawContext {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device;
|
||||
WGPUQueue queue;
|
||||
WGPURenderPassEncoder pass;
|
||||
WGPUBuffer vertex_buffer;
|
||||
WGPUBuffer index_buffer;
|
||||
WGPUBuffer uniform_buffer;
|
||||
WGPUBuffer storage_buffer;
|
||||
WGPUTextureFormat color_format;
|
||||
WGPUTextureFormat depth_format;
|
||||
uint32_t sample_count;
|
||||
uint32_t target_width;
|
||||
uint32_t target_height;
|
||||
bool uses_reversed_z;
|
||||
} GfxDrawContext;
|
||||
|
||||
typedef void (*GfxDrawFn)(ModContext* ctx, const GfxDrawContext* draw_ctx, const void* payload,
|
||||
size_t payload_size, void* user_data);
|
||||
|
||||
typedef struct GfxDrawTypeDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* optional debug label */
|
||||
GfxDrawFn draw; /* required; called from the render worker thread */
|
||||
void* user_data;
|
||||
} GfxDrawTypeDesc;
|
||||
|
||||
#define GFX_DRAW_TYPE_DESC_INIT {sizeof(GfxDrawTypeDesc), NULL, NULL, NULL}
|
||||
|
||||
typedef enum GfxStage {
|
||||
GFX_STAGE_SCENE_AFTER_TERRAIN = 0,
|
||||
GFX_STAGE_FRAME_BEFORE_HUD = 1,
|
||||
GFX_STAGE_FRAME_AFTER_HUD = 2,
|
||||
GFX_STAGE_SCENE_BEGIN = 3,
|
||||
GFX_STAGE_SCENE_AFTER_OPAQUE = 4,
|
||||
} GfxStage;
|
||||
|
||||
typedef struct GfxStageContext {
|
||||
uint32_t struct_size;
|
||||
GfxStage stage;
|
||||
const void* game_view; /* view_class* for world-camera stages; NULL otherwise */
|
||||
const void* game_viewport; /* view_port_class* for world-camera stages; NULL otherwise */
|
||||
} GfxStageContext;
|
||||
|
||||
typedef void (*GfxStageFn)(ModContext* ctx, const GfxStageContext* stage_ctx, void* user_data);
|
||||
|
||||
typedef struct GfxStageHookDesc {
|
||||
uint32_t struct_size;
|
||||
GfxStageFn callback; /* required */
|
||||
void* user_data;
|
||||
} GfxStageHookDesc;
|
||||
|
||||
#define GFX_STAGE_HOOK_DESC_INIT {sizeof(GfxStageHookDesc), NULL, NULL}
|
||||
|
||||
typedef struct GfxResolveDesc {
|
||||
uint32_t struct_size;
|
||||
bool color;
|
||||
bool depth;
|
||||
} GfxResolveDesc;
|
||||
|
||||
#define GFX_RESOLVE_DESC_INIT {sizeof(GfxResolveDesc), true, false}
|
||||
|
||||
typedef struct GfxResolvedTargets {
|
||||
uint32_t struct_size;
|
||||
WGPUTextureView color; /* single-sample snapshot in color_format */
|
||||
WGPUTextureView depth; /* single-sample raw depth snapshot, R32Float when available */
|
||||
WGPUTextureFormat color_format;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
} GfxResolvedTargets;
|
||||
|
||||
#define GFX_RESOLVED_TARGETS_INIT \
|
||||
{sizeof(GfxResolvedTargets), NULL, NULL, WGPUTextureFormat_Undefined, 0u, 0u}
|
||||
|
||||
/*
|
||||
* Passed to GfxComputeFn on the render worker thread; valid only during the call. The encoder is
|
||||
* the frame command encoder between scene render passes. Leave no pass open and never finish or
|
||||
* release the encoder.
|
||||
*/
|
||||
typedef struct GfxComputeContext {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device;
|
||||
WGPUQueue queue;
|
||||
WGPUCommandEncoder encoder;
|
||||
WGPUBuffer vertex_buffer;
|
||||
WGPUBuffer index_buffer;
|
||||
WGPUBuffer uniform_buffer;
|
||||
WGPUBuffer storage_buffer;
|
||||
} GfxComputeContext;
|
||||
|
||||
typedef void (*GfxComputeFn)(ModContext* ctx, const GfxComputeContext* compute_ctx,
|
||||
const void* payload, size_t payload_size, void* user_data);
|
||||
|
||||
typedef struct GfxComputeTypeDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* optional debug label */
|
||||
GfxComputeFn callback; /* required; called from the render worker thread */
|
||||
void* user_data;
|
||||
} GfxComputeTypeDesc;
|
||||
|
||||
#define GFX_COMPUTE_TYPE_DESC_INIT {sizeof(GfxComputeTypeDesc), NULL, NULL, NULL}
|
||||
|
||||
typedef struct GfxService {
|
||||
ServiceHeader header;
|
||||
|
||||
ModResult (*get_device_info)(ModContext* ctx, GfxDeviceInfo* out_info);
|
||||
void* (*get_proc_address)(ModContext* ctx, const char* name);
|
||||
|
||||
ModResult (*register_draw_type)(
|
||||
ModContext* ctx, const GfxDrawTypeDesc* desc, GfxDrawTypeHandle* out_handle);
|
||||
ModResult (*unregister_draw_type)(ModContext* ctx, GfxDrawTypeHandle handle);
|
||||
ModResult (*push_draw)(
|
||||
ModContext* ctx, GfxDrawTypeHandle handle, const void* payload, size_t payload_size);
|
||||
|
||||
ModResult (*register_compute_type)(
|
||||
ModContext* ctx, const GfxComputeTypeDesc* desc, GfxComputeTypeHandle* out_handle);
|
||||
ModResult (*unregister_compute_type)(ModContext* ctx, GfxComputeTypeHandle handle);
|
||||
ModResult (*push_compute)(
|
||||
ModContext* ctx, GfxComputeTypeHandle handle, const void* payload, size_t payload_size);
|
||||
|
||||
ModResult (*push_verts)(
|
||||
ModContext* ctx, const void* data, size_t size, size_t alignment, GfxRange* out_range);
|
||||
ModResult (*push_indices)(
|
||||
ModContext* ctx, const void* data, size_t size, size_t alignment, GfxRange* out_range);
|
||||
ModResult (*push_uniform)(ModContext* ctx, const void* data, size_t size, GfxRange* out_range);
|
||||
ModResult (*push_storage)(ModContext* ctx, const void* data, size_t size, GfxRange* out_range);
|
||||
|
||||
ModResult (*register_stage_hook)(ModContext* ctx, GfxStage stage, const GfxStageHookDesc* desc,
|
||||
GfxStageHookHandle* out_handle);
|
||||
ModResult (*unregister_stage_hook)(ModContext* ctx, GfxStageHookHandle handle);
|
||||
|
||||
ModResult (*resolve_pass)(
|
||||
ModContext* ctx, const GfxResolveDesc* desc, GfxResolvedTargets* out_targets);
|
||||
ModResult (*create_pass)(ModContext* ctx, uint32_t width, uint32_t height);
|
||||
} GfxService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<GfxService> {
|
||||
static constexpr const char* id = GFX_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GFX_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* 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).
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook"
|
||||
#define HOOK_SERVICE_MAJOR 1u
|
||||
#define HOOK_SERVICE_MINOR 0u
|
||||
|
||||
/* Symbol flags reported by resolve() */
|
||||
typedef enum HookSymbolFlags {
|
||||
HOOK_SYMBOL_CODE = 1u << 0u,
|
||||
HOOK_SYMBOL_DATA = 1u << 1u,
|
||||
/* Not exported/dynamically visible: hookable, but never linkable. */
|
||||
HOOK_SYMBOL_LOCAL = 1u << 2u,
|
||||
/* Other names share this address (ICF fold/alias): a hook intercepts them all. */
|
||||
HOOK_SYMBOL_MULTI_NAME = 1u << 3u,
|
||||
/* Resolved through a demangled display-name alias rather than the real symbol. */
|
||||
HOOK_SYMBOL_DISPLAY = 1u << 6u,
|
||||
} HookSymbolFlags;
|
||||
|
||||
/* A pre-hook's return value: whether to run the original function. */
|
||||
typedef enum HookAction {
|
||||
HOOK_CONTINUE = 0, /* run the original (and any lower-priority pre-hooks) */
|
||||
HOOK_SKIP_ORIGINAL = 1, /* cancel the original and remaining pre-hooks; post-hooks still run */
|
||||
} HookAction;
|
||||
|
||||
/* How replace resolves a second replace-hook on a target that already has one. */
|
||||
typedef enum HookReplacePolicy {
|
||||
HOOK_REPLACE_CONFLICT = 0, /* refuse with MOD_CONFLICT (the default) */
|
||||
HOOK_REPLACE_PRIORITY = 1, /* take over only if this options.priority is strictly higher */
|
||||
HOOK_REPLACE_OVERRIDE = 2, /* take over unconditionally */
|
||||
} 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<T> / arg_ref<T> 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);
|
||||
typedef void (*HookPostFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
typedef void (*HookReplaceFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
|
||||
typedef struct HookOptions {
|
||||
uint32_t struct_size;
|
||||
/* Higher runs first; ties break by registration order. Applies to pre/post ordering and,
|
||||
* with HOOK_REPLACE_PRIORITY, to replace-hook takeover. */
|
||||
int32_t priority;
|
||||
HookReplacePolicy replace_policy;
|
||||
void* userdata; /* passed back to the callback */
|
||||
} HookOptions;
|
||||
|
||||
#define HOOK_OPTIONS_INIT {sizeof(HookOptions), 0, HOOK_REPLACE_CONFLICT, NULL}
|
||||
|
||||
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).
|
||||
*/
|
||||
ModResult (*install)(
|
||||
ModContext* ctx, void* fn_addr, void* trampoline_fn, void** out_original_fn);
|
||||
|
||||
/*
|
||||
* Register a callback on an already-installed target. Pre runs before the original (and can
|
||||
* cancel it), post runs after (even if cancelled). Any number of mods may add pre/post to the
|
||||
* same target; they run in priority then registration order. replace installs a single
|
||||
* substitute for the original, managed by options.replace_policy, MOD_CONFLICT if refused.
|
||||
*/
|
||||
ModResult (*add_pre)(
|
||||
ModContext* ctx, void* fn_addr, HookPreFn callback, const HookOptions* options);
|
||||
ModResult (*add_post)(
|
||||
ModContext* ctx, void* fn_addr, HookPostFn callback, const HookOptions* options);
|
||||
ModResult (*replace)(
|
||||
ModContext* ctx, void* fn_addr, HookReplaceFn callback, const HookOptions* options);
|
||||
|
||||
/*
|
||||
* Run the registered callbacks for a target. The generated trampoline calls these; they
|
||||
* are not a mod-facing entry point. dispatch_pre reports through *out_skip_original
|
||||
* whether the original should be skipped (a pre-hook returned HOOK_SKIP_ORIGINAL, or a
|
||||
* replace-hook ran).
|
||||
*/
|
||||
ModResult (*dispatch_pre)(
|
||||
ModContext* ctx, void* fn_addr, void* args, void* retval, int* out_skip_original);
|
||||
ModResult (*dispatch_post)(ModContext* ctx, void* fn_addr, void* args, void* retval);
|
||||
|
||||
/*
|
||||
* Resolve a game symbol by name from the symbol manifest, including non-exported (static)
|
||||
* functions. Names can be either the platform's mangled name (i.e. the name passed to dlopen;
|
||||
* no Mach-O leading underscore) or the qualified function name without parameters (e.g.
|
||||
* "daAlink_c::execute"). out_flags (optional) receives HookSymbolFlags.
|
||||
*
|
||||
* Results: MOD_OK; MOD_UNSUPPORTED (no manifest for this build, missing or stale);
|
||||
* MOD_UNAVAILABLE (symbol not found); MOD_CONFLICT (name maps to more than one address: C++
|
||||
* overloads or per-TU statics; use the mangled name).
|
||||
*/
|
||||
ModResult (*resolve)(
|
||||
ModContext* ctx, const char* symbol, void** out_addr, HookSymbolFlags* out_flags);
|
||||
} HookService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<HookService> {
|
||||
static constexpr const char* id = HOOK_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host"
|
||||
#define HOST_SERVICE_MAJOR 2u
|
||||
#define HOST_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* Ignore unknown values: later service minors may add events.
|
||||
*/
|
||||
typedef enum ModLifecycleEvent {
|
||||
/*
|
||||
* The subject mod is gone: its mod_shutdown has run (when it initialized at all) and
|
||||
* every service has already dropped the state it held for it. The subject's library is
|
||||
* still mapped, so pointers into it are valid to compare against, but they must not be
|
||||
* called or dereferenced after the callback returns. Drop everything keyed to the
|
||||
* mod: callbacks it registered, its ModContext*, state indexed by it.
|
||||
*/
|
||||
MOD_LIFECYCLE_DETACHED = 0,
|
||||
} ModLifecycleEvent;
|
||||
|
||||
/*
|
||||
* ctx is the watching mod's own context; subject identifies the mod the event is about.
|
||||
* subject_id is valid only for the duration of the call.
|
||||
*/
|
||||
typedef void (*ModLifecycleFn)(ModContext* ctx, ModContext* subject, const char* subject_id,
|
||||
ModLifecycleEvent event, void* user_data);
|
||||
|
||||
typedef struct HostService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Version string of the current Dusklight build. (e.g. "1.4.2") */
|
||||
const char* version;
|
||||
|
||||
/* Build id of the running game binary: PDB GUID+age on Windows, LC_UUID on macOS, GNU build-id
|
||||
* on Linux. May be empty (len 0) if the identity could not be determined. */
|
||||
const uint8_t* build_id;
|
||||
uint32_t build_id_len;
|
||||
|
||||
/*
|
||||
* Look up a service by id at call time. Unlike a manifest import, this sees whatever is
|
||||
* currently published and carries no initialization-order guarantee (see mods/api.h).
|
||||
* MOD_UNAVAILABLE if no matching service is published; *out_service is null on failure.
|
||||
*/
|
||||
ModResult (*get_service)(ModContext* ctx, const char* service_id, uint16_t major_version,
|
||||
uint16_t min_minor_version, const void** out_service);
|
||||
|
||||
/*
|
||||
* Publish a service the calling mod declared as a deferred export in its manifest.
|
||||
* Must happen during mod_initialize so importers can resolve it; `service` must stay
|
||||
* valid until the mod shuts down.
|
||||
*/
|
||||
ModResult (*publish_service)(
|
||||
ModContext* ctx, const char* service_id, uint16_t major_version, const void* service);
|
||||
|
||||
/*
|
||||
* Report an unrecoverable failure. The calling mod's services stop resolving immediately
|
||||
* and the loader fully disables it at the next safe point; `message` is shown to the user.
|
||||
* Safe to call from any mod callback.
|
||||
*/
|
||||
void (*fail)(ModContext* ctx, ModResult code, const char* message);
|
||||
|
||||
/*
|
||||
* The calling mod's manifest metadata. Returned strings remain valid while the mod is
|
||||
* loaded.
|
||||
*/
|
||||
const char* (*mod_id)(ModContext* ctx);
|
||||
const char* (*mod_name)(ModContext* ctx);
|
||||
const char* (*mod_version)(ModContext* ctx);
|
||||
|
||||
/*
|
||||
* A writable scratch directory reserved for the calling mod. Contents survive disable
|
||||
* and reload within a session, but the directory is wiped at game startup.
|
||||
*/
|
||||
const char* (*mod_dir)(ModContext* ctx);
|
||||
|
||||
/*
|
||||
* Observe other mods' lifecycle events. Any mod whose service hands out per-caller state
|
||||
* (registrations, callbacks, handles) should watch for MOD_LIFECYCLE_DETACHED and drop what it
|
||||
* holds for the subject.
|
||||
*
|
||||
* Callbacks fire on the game thread at a lifecycle safe point (never mid-frame), for
|
||||
* every mod but the watcher itself (use mod_shutdown for self-cleanup).
|
||||
*/
|
||||
ModResult (*watch_mod_lifecycle)(
|
||||
ModContext* ctx, ModLifecycleFn fn, void* user_data, uint64_t* out_handle);
|
||||
ModResult (*unwatch_mod_lifecycle)(ModContext* ctx, uint64_t handle);
|
||||
} HostService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<HostService> {
|
||||
static constexpr const char* id = HOST_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOST_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* Logging into the game's console and log files. Messages are attributed to the calling mod
|
||||
* (prefixed with its ID).
|
||||
*/
|
||||
|
||||
#define LOG_SERVICE_ID "dev.twilitrealm.dusklight.log"
|
||||
#define LOG_SERVICE_MAJOR 1u
|
||||
#define LOG_SERVICE_MINOR 0u
|
||||
|
||||
typedef enum LogLevel {
|
||||
LOG_LEVEL_TRACE = 0,
|
||||
LOG_LEVEL_DEBUG = 1,
|
||||
LOG_LEVEL_INFO = 2,
|
||||
LOG_LEVEL_WARN = 3,
|
||||
LOG_LEVEL_ERROR = 4,
|
||||
} LogLevel;
|
||||
|
||||
typedef struct LogService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Write a log message at the given level.
|
||||
* `message` is a plain UTF-8 string and is copied before returning.
|
||||
*/
|
||||
void (*write)(ModContext* ctx, LogLevel level, const char* message);
|
||||
|
||||
/* Per-level shorthands for write. */
|
||||
void (*trace)(ModContext* ctx, const char* message);
|
||||
void (*debug)(ModContext* ctx, const char* message);
|
||||
void (*info)(ModContext* ctx, const char* message);
|
||||
void (*warn)(ModContext* ctx, const char* message);
|
||||
void (*error)(ModContext* ctx, const char* message);
|
||||
} LogService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<LogService> {
|
||||
static constexpr const char* id = LOG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = LOG_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay"
|
||||
#define OVERLAY_SERVICE_MAJOR 1u
|
||||
#define OVERLAY_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for a runtime overlay registration. 0 is never a valid handle. */
|
||||
typedef uint64_t OverlayHandle;
|
||||
|
||||
/*
|
||||
* Runtime DVD file overlays.
|
||||
*
|
||||
* Registrations are owned by the calling mod and removed automatically when it is disabled,
|
||||
* reloaded, or fails. Changes are applied at the next frame boundary; data the game has already
|
||||
* read stays in memory until it re-reads the file (sometimes on scene reload, sometimes on
|
||||
* restart).
|
||||
*
|
||||
* disc_path names the file to overlay: absolute with a leading '/', matched against the disc
|
||||
* case-insensitively (e.g. "/res/Stage/R04_00.arc"). Paths that do not exist on the disc are added
|
||||
* as new files.
|
||||
*
|
||||
* If multiple sources overlay the same path, the last one wins: a mod's runtime registrations beat
|
||||
* its static overlay/ files, and later-loaded mods beat earlier ones.
|
||||
*/
|
||||
typedef struct OverlayService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Overlay disc_path with a file from the calling mod's bundle (bundle-relative path, e.g.
|
||||
* "res/replacement.arc"). The file's contents are read lazily on each open, so the bundle
|
||||
* file must not change size while registered.
|
||||
*/
|
||||
ModResult (*add_file)(
|
||||
ModContext* ctx, const char* disc_path, const char* bundle_path, OverlayHandle* out_handle);
|
||||
|
||||
/*
|
||||
* Overlay disc_path with a caller-owned buffer. The data is copied; the caller may free it
|
||||
* as soon as this returns.
|
||||
*/
|
||||
ModResult (*add_buffer)(ModContext* ctx, const char* disc_path, const void* data, size_t size,
|
||||
OverlayHandle* out_handle);
|
||||
|
||||
/* Remove a runtime overlay previously added by the calling mod. */
|
||||
ModResult (*remove)(ModContext* ctx, OverlayHandle handle);
|
||||
} OverlayService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<OverlayService> {
|
||||
static constexpr const char* id = OVERLAY_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = OVERLAY_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#define RESOURCE_SERVICE_ID "dev.twilitrealm.dusklight.resource"
|
||||
#define RESOURCE_SERVICE_MAJOR 1u
|
||||
#define RESOURCE_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* A loaded resource, allocated by the service. Return every successful load with free;
|
||||
* buffers still live when the mod is disabled or reloaded are reclaimed with a warning.
|
||||
*/
|
||||
typedef struct ResourceBuffer {
|
||||
uint32_t struct_size;
|
||||
void* data;
|
||||
size_t size;
|
||||
} ResourceBuffer;
|
||||
|
||||
#define RESOURCE_BUFFER_INIT {sizeof(ResourceBuffer), NULL, 0u}
|
||||
|
||||
typedef struct ResourceService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Load a file into a fresh allocation. `relative_path` is resolved against the bundle's
|
||||
* res/ directory. Absolute paths and ".." are rejected. MOD_UNAVAILABLE if the file does not
|
||||
* exist. An empty file loads as data == NULL with size 0. Previous contents of `out_buffer` are
|
||||
* overwritten, not freed.
|
||||
*/
|
||||
ModResult (*load)(ModContext* ctx, const char* relative_path, ResourceBuffer* out_buffer);
|
||||
|
||||
/*
|
||||
* Release a loaded buffer and reset it to the empty state. Safe to call on an empty or
|
||||
* already-freed buffer.
|
||||
*/
|
||||
void (*free)(ModContext* ctx, ResourceBuffer* buffer);
|
||||
} ResourceService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<ResourceService> {
|
||||
static constexpr const char* id = RESOURCE_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = RESOURCE_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
|
||||
#define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture"
|
||||
#define TEXTURE_SERVICE_MAJOR 1u
|
||||
#define TEXTURE_SERVICE_MINOR 0u
|
||||
|
||||
/* Handle for a runtime texture replacement registration. 0 is never a valid handle. */
|
||||
typedef uint64_t TextureReplacementHandle;
|
||||
|
||||
typedef enum TextureKeyKind {
|
||||
/* Match a texture by the address of its in-memory GX texel data. */
|
||||
TEXTURE_KEY_POINTER = 0,
|
||||
/* Match by content: XXH64 of the base mip level (and of the referenced TLUT range for
|
||||
* palette formats), as encoded in replacement filenames / texture dumps. */
|
||||
TEXTURE_KEY_SOURCE = 1,
|
||||
} TextureKeyKind;
|
||||
|
||||
/* Wildcard values for TEXTURE_KEY_SOURCE hashes ("$" in the filename convention). */
|
||||
#define TEXTURE_HASH_WILDCARD UINT64_C(0xFFFFFFFFFFFFFFFF)
|
||||
#define TEXTURE_TLUT_WILDCARD UINT64_C(0xFFFFFFFFFFFFFFFE)
|
||||
|
||||
typedef struct TextureKey {
|
||||
uint32_t struct_size;
|
||||
TextureKeyKind kind;
|
||||
const void* pointer; /* TEXTURE_KEY_POINTER only */
|
||||
uint64_t texture_hash; /* TEXTURE_KEY_SOURCE */
|
||||
uint64_t tlut_hash; /* TEXTURE_KEY_SOURCE, palette formats only */
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t gx_format;
|
||||
bool has_tlut;
|
||||
} TextureKey;
|
||||
|
||||
#define TEXTURE_KEY_INIT {sizeof(TextureKey), TEXTURE_KEY_POINTER, NULL, 0u, 0u, 0u, 0u, 0u, false}
|
||||
|
||||
typedef struct TextureData {
|
||||
uint32_t struct_size;
|
||||
const void* data; /* texel data laid out in gx_format; copied by the service */
|
||||
size_t size;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t mip_count;
|
||||
uint32_t gx_format; /* any GX texture format supported by Aurora's converter */
|
||||
} TextureData;
|
||||
|
||||
#define TEXTURE_DATA_INIT {sizeof(TextureData), NULL, 0u, 0u, 0u, 1u, 0u}
|
||||
|
||||
/*
|
||||
* Runtime texture replacements.
|
||||
*
|
||||
* Registrations are owned by the calling mod and removed automatically when it is disabled,
|
||||
* reloaded, or fails. When multiple sources replace the same texture, the highest priority wins:
|
||||
* later-loaded mods beat earlier ones, and any mod beats the user's texture_replacements config
|
||||
* directory. Files shipped in a mod's textures/ directory register automatically with the same
|
||||
* ownership and priority; this service is for replacements decided at runtime.
|
||||
*/
|
||||
typedef struct TextureService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Register a replacement from raw texel data. The data is copied; the caller may free it as
|
||||
* soon as this returns. */
|
||||
ModResult (*register_data)(ModContext* ctx, const TextureKey* key, const TextureData* data,
|
||||
TextureReplacementHandle* out_handle);
|
||||
|
||||
/*
|
||||
* Register a replacement from an encoded .dds/.png inside the calling mod's bundle. The
|
||||
* filename encodes the key (same convention as the texture_replacements directory, e.g.
|
||||
* "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);
|
||||
|
||||
/* Remove a replacement previously registered by the calling mod. */
|
||||
ModResult (*unregister)(ModContext* ctx, TextureReplacementHandle handle);
|
||||
} TextureService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<TextureService> {
|
||||
static constexpr const char* id = TEXTURE_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = TEXTURE_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,284 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/api.h"
|
||||
#include "mods/svc/config.h"
|
||||
|
||||
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
|
||||
#define UI_SERVICE_MAJOR 1u
|
||||
#define UI_SERVICE_MINOR 0u
|
||||
|
||||
/*
|
||||
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, 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
|
||||
* MOD_INVALID_ARGUMENT. Element handles die with the content that owns them: a panel or tab rebuild
|
||||
* destroys the previous build's elements, so re-acquire handles inside the build callback rather
|
||||
* than caching them. Strings are UTF-8 and, in both directions, only valid for the duration of the
|
||||
* call.
|
||||
*/
|
||||
|
||||
/* 0 is never a valid handle. */
|
||||
typedef uint64_t UiWindowHandle;
|
||||
typedef uint64_t UiDialogHandle;
|
||||
typedef uint64_t UiElementHandle;
|
||||
typedef uint64_t UiStyleHandle;
|
||||
typedef uint64_t UiMenuTabHandle;
|
||||
|
||||
typedef enum UiStyleScope {
|
||||
UI_SCOPE_PRELAUNCH = 0, /* the pre-launch menu */
|
||||
UI_SCOPE_WINDOW = 1, /* every tabbed/small window, host and mod alike */
|
||||
UI_SCOPE_MENU_BAR = 2, /* the in-game menu bar */
|
||||
UI_SCOPE_OVERLAY = 3, /* the passive overlay (toasts, FPS counter, timers) */
|
||||
UI_SCOPE_TOUCH_CONTROLS = 4, /* touch controls and their editor */
|
||||
UI_SCOPE_GRAPHICS_TUNER = 5, /* the graphics tuner overlay window */
|
||||
} UiStyleScope;
|
||||
|
||||
typedef enum UiDialogVariant {
|
||||
UI_DIALOG_NORMAL = 0,
|
||||
UI_DIALOG_WARNING = 1, /* warning icon by default */
|
||||
UI_DIALOG_DANGER = 2, /* red styling, error icon by default */
|
||||
} UiDialogVariant;
|
||||
|
||||
typedef enum UiControlKind {
|
||||
UI_CONTROL_BUTTON = 0, /* action button (on_pressed) */
|
||||
UI_CONTROL_TOGGLE = 1, /* boolean on/off */
|
||||
UI_CONTROL_NUMBER = 2, /* integer stepper with min/max/step */
|
||||
UI_CONTROL_STRING = 3, /* text input */
|
||||
UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */
|
||||
} UiControlKind;
|
||||
|
||||
typedef enum UiControlBinding {
|
||||
/* Values flow through the `get`/`set` callbacks. Getters are polled every frame while the
|
||||
control is visible and must be cheap. */
|
||||
UI_BINDING_CALLBACKS = 0,
|
||||
/* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod)
|
||||
* directly: persistence, change notifications and the modified indicator (value != default) are
|
||||
* wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and
|
||||
* SELECT = int, STRING = string. Float vars are not bindable; use callbacks. */
|
||||
UI_BINDING_CONFIG_VAR = 1,
|
||||
} UiControlBinding;
|
||||
|
||||
/* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING
|
||||
* reads string_value. string_value passed to a setter is only valid during the call; a getter
|
||||
* should point it at storage owned by the mod (e.g. a static buffer) that stays valid until the
|
||||
* next call into the mod — the host copies it right after the getter returns. */
|
||||
typedef struct UiControlValue {
|
||||
uint32_t struct_size;
|
||||
bool bool_value;
|
||||
int64_t int_value;
|
||||
const char* string_value;
|
||||
} UiControlValue;
|
||||
|
||||
#define UI_CONTROL_VALUE_INIT {sizeof(UiControlValue), false, 0, NULL}
|
||||
|
||||
typedef void (*UiControlGetFn)(ModContext* ctx, void* user_data, UiControlValue* out_value);
|
||||
typedef void (*UiControlSetFn)(ModContext* ctx, void* user_data, const UiControlValue* value);
|
||||
/* Polled every frame while the control is visible. */
|
||||
typedef bool (*UiPredicateFn)(ModContext* ctx, void* user_data);
|
||||
typedef void (*UiPressedFn)(ModContext* ctx, void* user_data);
|
||||
|
||||
typedef struct UiControlDesc {
|
||||
uint32_t struct_size;
|
||||
UiControlKind kind;
|
||||
/* Row label (plain text). Required. */
|
||||
const char* label;
|
||||
/* Optional RML shown as contextual help when the control is focused or hovered. Only rendered
|
||||
* where a help pane exists (mod window tabs). */
|
||||
const char* help_rml;
|
||||
UiControlBinding binding; /* ignored for BUTTON */
|
||||
ConfigVarHandle config_var; /* UI_BINDING_CONFIG_VAR */
|
||||
UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
|
||||
UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */
|
||||
UiPressedFn on_pressed; /* BUTTON only. Required for BUTTON. */
|
||||
UiPredicateFn is_disabled; /* optional */
|
||||
/* Optional override for the modified indicator. CONFIG_VAR controls derive it from value !=
|
||||
* default when this is NULL. */
|
||||
UiPredicateFn is_modified;
|
||||
/* Passed to every callback above. */
|
||||
void* user_data;
|
||||
/* NUMBER: inclusive clamp range and step. min == max means the defaults (0 .. INT32_MAX); step
|
||||
* < 1 means 1. */
|
||||
int64_t min;
|
||||
int64_t max;
|
||||
int64_t step;
|
||||
const char* prefix; /* NUMBER: optional text before the value */
|
||||
const char* suffix; /* NUMBER: optional text after the value */
|
||||
/* SELECT: option labels (plain text). Required for SELECT. SELECT controls
|
||||
* present their options in the help pane, so they are only available where
|
||||
* one exists (mod window tabs); MOD_UNSUPPORTED elsewhere. */
|
||||
const char* const* options;
|
||||
size_t option_count;
|
||||
int32_t max_length; /* STRING: maximum input length; < 1 means unlimited */
|
||||
} UiControlDesc;
|
||||
|
||||
#define UI_CONTROL_DESC_INIT \
|
||||
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
|
||||
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0}
|
||||
|
||||
/* Build the panel contents. `panel` accepts the pane_add_* functions; it and
|
||||
* every element created in it are destroyed (handles invalidated) whenever the
|
||||
* panel is rebuilt, e.g. on tab switches. A non-MOD_OK result fails the mod. */
|
||||
typedef ModResult (*UiPanelBuildFn)(
|
||||
ModContext* ctx, UiElementHandle panel, void* user_data, ModError* out_error);
|
||||
/* Called every frame while the panel is the visible tab. */
|
||||
typedef ModResult (*UiPanelUpdateFn)(ModContext* ctx, void* user_data, ModError* out_error);
|
||||
|
||||
/* A panel rendered inside the calling mod's tab of the host Mods window. */
|
||||
typedef struct UiModsPanelDesc {
|
||||
uint32_t struct_size;
|
||||
UiPanelBuildFn build; /* required */
|
||||
UiPanelUpdateFn update;
|
||||
void* user_data;
|
||||
} UiModsPanelDesc;
|
||||
|
||||
#define UI_MODS_PANEL_DESC_INIT {sizeof(UiModsPanelDesc), NULL, NULL, NULL}
|
||||
|
||||
/* Build one tab of a mod window. `left_pane` is the interactive column,
|
||||
* `right_pane` shows contextual help (controls' help_rml and SELECT options
|
||||
* render there). Rebuilt on every tab activation. */
|
||||
typedef ModResult (*UiTabBuildFn)(ModContext* ctx, UiWindowHandle window, UiElementHandle left_pane,
|
||||
UiElementHandle right_pane, void* user_data, ModError* out_error);
|
||||
|
||||
typedef struct UiTabDesc {
|
||||
uint32_t struct_size;
|
||||
const char* title; /* required */
|
||||
UiTabBuildFn build; /* required */
|
||||
UiPanelUpdateFn update;
|
||||
void* user_data;
|
||||
} UiTabDesc;
|
||||
|
||||
#define UI_TAB_DESC_INIT {sizeof(UiTabDesc), NULL, NULL, NULL, NULL}
|
||||
|
||||
typedef void (*UiWindowClosedFn)(ModContext* ctx, UiWindowHandle window, void* user_data);
|
||||
|
||||
typedef struct UiWindowDesc {
|
||||
uint32_t struct_size;
|
||||
const UiTabDesc* tabs; /* at least one */
|
||||
size_t tab_count;
|
||||
/* Optional RCSS applied to this window's document only (on top of any
|
||||
* UI_SCOPE_WINDOW sheets, which apply automatically). */
|
||||
const char* rcss;
|
||||
/* Fired when the window is destroyed by user close or window_close. Not
|
||||
* fired during owning-mod teardown/shutdown. The handle is already invalid. */
|
||||
UiWindowClosedFn on_closed;
|
||||
void* user_data;
|
||||
} UiWindowDesc;
|
||||
|
||||
#define UI_WINDOW_DESC_INIT {sizeof(UiWindowDesc), NULL, 0u, NULL, NULL, NULL}
|
||||
|
||||
typedef void (*UiDialogActionFn)(ModContext* ctx, UiDialogHandle dialog, void* user_data);
|
||||
|
||||
/* Note: array element without struct_size; a future change requires appending
|
||||
* a v2 desc struct rather than growing this one. */
|
||||
typedef struct UiDialogAction {
|
||||
const char* label; /* required */
|
||||
UiDialogActionFn on_pressed;
|
||||
void* user_data;
|
||||
bool keep_open; /* false = the dialog closes after on_pressed returns */
|
||||
} UiDialogAction;
|
||||
|
||||
typedef struct UiDialogDesc {
|
||||
uint32_t struct_size;
|
||||
const char* title; /* plain text; required */
|
||||
const char* body_rml; /* RML body; required */
|
||||
UiDialogVariant variant;
|
||||
/* Optional icon name overriding the variant default: "warning", "error",
|
||||
* "question-mark", "verifying", "celebration". */
|
||||
const char* icon;
|
||||
const UiDialogAction* actions; /* at least one */
|
||||
size_t action_count;
|
||||
/* Fired on cancel (B/Escape) before the dialog closes; the dialog always
|
||||
* closes on dismiss. */
|
||||
UiDialogActionFn on_dismiss;
|
||||
void* user_data; /* passed to on_dismiss */
|
||||
} UiDialogDesc;
|
||||
|
||||
#define UI_DIALOG_DESC_INIT \
|
||||
{sizeof(UiDialogDesc), NULL, NULL, UI_DIALOG_NORMAL, NULL, NULL, 0u, NULL, NULL}
|
||||
|
||||
/* A tab added to the in-game menu bar. */
|
||||
typedef struct UiMenuTabDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* plain text; required */
|
||||
UiPressedFn on_selected; /* fired when the user activates the tab; required */
|
||||
void* user_data;
|
||||
} UiMenuTabDesc;
|
||||
|
||||
#define UI_MENU_TAB_DESC_INIT {sizeof(UiMenuTabDesc), NULL, NULL, NULL}
|
||||
|
||||
typedef struct UiService {
|
||||
ServiceHeader header;
|
||||
|
||||
/* Register or replace the panel shown in the calling mod's Mods-window tab. */
|
||||
ModResult (*register_mods_panel)(ModContext* ctx, const UiModsPanelDesc* desc);
|
||||
|
||||
/* Content builders. `pane` is a panel or tab pane handle; out_elem (where
|
||||
* present, optional) receives a handle for later elem_set_* updates. */
|
||||
ModResult (*pane_add_section)(ModContext* ctx, UiElementHandle pane, const char* title);
|
||||
ModResult (*pane_add_text)(
|
||||
ModContext* ctx, UiElementHandle pane, const char* text, UiElementHandle* out_elem);
|
||||
ModResult (*pane_add_rml)(
|
||||
ModContext* ctx, UiElementHandle pane, const char* rml, UiElementHandle* out_elem);
|
||||
ModResult (*pane_add_progress)(
|
||||
ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem);
|
||||
ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc,
|
||||
UiElementHandle* out_elem);
|
||||
|
||||
/* Element updates. The handle kind must match the setter (text/rml on text
|
||||
* rows, progress on progress bars). */
|
||||
ModResult (*elem_set_text)(ModContext* ctx, UiElementHandle elem, const char* text);
|
||||
ModResult (*elem_set_rml)(ModContext* ctx, UiElementHandle elem, const char* rml);
|
||||
ModResult (*elem_set_progress)(ModContext* ctx, UiElementHandle elem, float value);
|
||||
/* Set or clear an RCSS class on any element handle (rows, progress bars,
|
||||
* controls, panes), for styling via scoped or window RCSS. */
|
||||
ModResult (*elem_set_class)(
|
||||
ModContext* ctx, UiElementHandle elem, const char* name, bool active);
|
||||
|
||||
/* Push a tabbed two-pane window onto the document stack and show it. */
|
||||
ModResult (*window_push)(ModContext* ctx, const UiWindowDesc* desc, UiWindowHandle* out_window);
|
||||
ModResult (*window_close)(ModContext* ctx, UiWindowHandle window);
|
||||
|
||||
/* Push a modal dialog onto the document stack and show it. */
|
||||
ModResult (*dialog_push)(ModContext* ctx, const UiDialogDesc* desc, UiDialogHandle* out_dialog);
|
||||
ModResult (*dialog_close)(ModContext* ctx, UiDialogHandle dialog);
|
||||
ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml);
|
||||
/* Replace the dialog icon ("" removes it; names as in UiDialogDesc.icon). */
|
||||
ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon);
|
||||
/* Append one action button (same callback rules as at push). */
|
||||
ModResult (*dialog_add_action)(
|
||||
ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action);
|
||||
|
||||
/* Whether any focus-stack document is currently visible (visible documents block gamepad
|
||||
* input). */
|
||||
ModResult (*is_any_document_visible)(ModContext* ctx, bool* out_visible);
|
||||
|
||||
/* Register an RCSS sheet applied to every document of `scope`, now and in the future, until
|
||||
* unregistered or the calling mod is torn down. Sheets apply in registration order after the
|
||||
* host styles. Fails with MOD_INVALID_ARGUMENT if the RCSS does not parse. */
|
||||
ModResult (*register_styles)(
|
||||
ModContext* ctx, UiStyleScope scope, const char* rcss, UiStyleHandle* out_style);
|
||||
/* Like register_styles, but reads the RCSS from the mod bundle's res/ directory (same path
|
||||
* rules as ResourceService::load). MOD_UNAVAILABLE if the file does not exist. */
|
||||
ModResult (*register_styles_file)(
|
||||
ModContext* ctx, UiStyleScope scope, const char* path, UiStyleHandle* out_style);
|
||||
ModResult (*unregister_styles)(ModContext* ctx, UiStyleHandle style);
|
||||
|
||||
/* Add a tab to the in-game menu bar, on_selected runs with the menu open; pushing a window from
|
||||
* it stacks the window over the menu like the host tabs do. The tab is removed when
|
||||
* unregistered or the mod is torn down. */
|
||||
ModResult (*register_menu_tab)(
|
||||
ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab);
|
||||
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
|
||||
} UiService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct dusk::mods::ServiceTraits<UiService> {
|
||||
static constexpr const char* id = UI_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = UI_SERVICE_MAJOR;
|
||||
};
|
||||
#endif
|
||||
Reference in New Issue
Block a user