Merge branch 'main' of https://github.com/TwilitRealm/dusk into randomizer

This commit is contained in:
gymnast86
2026-07-13 15:52:24 -07:00
175 changed files with 17712 additions and 680 deletions
+69 -7
View File
@@ -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;
+82 -6
View File
@@ -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>;
+10
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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;
+245
View File
@@ -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
+12
View File
@@ -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
+2 -1
View File
@@ -7,7 +7,8 @@
namespace dusk {
using namespace config;
using config::ConfigVar;
using config::ActionBindConfigVar;
enum class BloomMode : int {
Off = 0,
+1
View File
@@ -37,5 +37,6 @@ struct SpeedrunInfo {
extern SpeedrunInfo m_speedrunInfo;
void resetForSpeedrunMode();
void restoreFromSpeedrunMode();
} // namespace dusk
+5
View File
@@ -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();