mirror of
https://github.com/sal063/AC6_recomp
synced 2026-09-06 19:30:56 -04:00
521 lines
27 KiB
C++
521 lines
27 KiB
C++
/**
|
|
* @file cvar.h
|
|
* @brief cvar (configuration variable) system
|
|
*
|
|
* @section cvar_defining Defining CVars
|
|
*
|
|
* Define cvars in a single .cpp file using REXCVAR_DEFINE_* macros:
|
|
* @code
|
|
* // In flags.cpp or your module's .cpp file
|
|
* REXCVAR_DEFINE_BOOL(my_flag, false, "Category", "Description");
|
|
* REXCVAR_DEFINE_INT32(my_int, 42, "Category", "An integer setting");
|
|
* REXCVAR_DEFINE_STRING(my_string, "default", "Category", "A string setting");
|
|
* @endcode
|
|
*
|
|
* Available types: BOOL, INT32, INT64, UINT32, UINT64, DOUBLE, STRING, COMMAND
|
|
*
|
|
* @section cvar_declaring Declaring CVars (for use in other files)
|
|
*
|
|
* @code
|
|
* // In a header or other .cpp that needs access
|
|
* REXCVAR_DECLARE(bool, my_flag);
|
|
* REXCVAR_DECLARE(int32_t, my_int);
|
|
* REXCVAR_DECLARE(std::string, my_string);
|
|
* @endcode
|
|
*
|
|
* @section cvar_access Getting and Setting Values
|
|
*
|
|
* @code
|
|
* // Type-safe access (preferred for known cvars)
|
|
* bool value = REXCVAR_GET(my_flag);
|
|
* REXCVAR_SET(my_flag, true);
|
|
*
|
|
* // String-based access (for dynamic/runtime lookup)
|
|
* std::string str_val = rex::cvar::GetFlagByName("my_flag");
|
|
* rex::cvar::SetFlagByName("my_flag", "true");
|
|
* @endcode
|
|
*
|
|
* @section cvar_metadata Adding Metadata
|
|
*
|
|
* Chain metadata methods after the DEFINE macro:
|
|
* @code
|
|
* REXCVAR_DEFINE_INT32(scale, 1, "GPU", "Resolution scale")
|
|
* .range(1, 8)
|
|
* .lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
|
*
|
|
* REXCVAR_DEFINE_STRING(backend, "d3d12", "GPU", "Render backend")
|
|
* .allowed({"d3d12", "vulkan"})
|
|
* .lifecycle(rex::cvar::Lifecycle::kInitOnly);
|
|
*
|
|
* REXCVAR_DEFINE_BOOL(debug_overlay, false, "Debug", "Show overlay")
|
|
* .debug_only();
|
|
* @endcode
|
|
*
|
|
* @section cvar_guidelines Metadata Guidelines
|
|
*
|
|
* - .lifecycle(kInitOnly) - Device/backend selection that cannot change after init
|
|
* - .lifecycle(kRequiresRestart) - Settings that need restart to take effect
|
|
* - .lifecycle(kHotReload) - Default; can change at runtime with immediate effect
|
|
* - .range(min, max) - Numeric bounds validation
|
|
* - .allowed({...}) - String enum validation
|
|
* - .debug_only() - Mark as debug-only (for filtering in release UIs)
|
|
* - .validator(fn) - Custom validation function
|
|
*
|
|
* @section cvar_query Querying CVars
|
|
*
|
|
* @code
|
|
* // List all cvars
|
|
* auto all = rex::cvar::ListFlags();
|
|
*
|
|
* // Query by category or lifecycle
|
|
* auto gpu_flags = rex::cvar::ListFlagsByCategory("GPU");
|
|
* auto init_only = rex::cvar::ListFlagsByLifecycle(rex::cvar::Lifecycle::kInitOnly);
|
|
*
|
|
* // Get metadata for a specific cvar
|
|
* const auto* info = rex::cvar::GetFlagInfo("my_flag");
|
|
* if (info) {
|
|
* // Access info->lifecycle, info->constraints, info->description, etc.
|
|
* }
|
|
*
|
|
* // Check for modified values
|
|
* auto modified = rex::cvar::ListModifiedFlags();
|
|
* @endcode
|
|
*
|
|
* @copyright Copyright (c) 2026 Tom Clay
|
|
* @license BSD 3-Clause License
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <charconv>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace rex::cvar {
|
|
|
|
//=============================================================================
|
|
// Initialization API
|
|
//=============================================================================
|
|
|
|
std::vector<std::string> Init(int argc, char** argv);
|
|
void LoadConfig(const std::filesystem::path& config_path);
|
|
void ApplyEnvironment();
|
|
void FinalizeInit();
|
|
bool IsFinalized();
|
|
void SaveConfig(const std::filesystem::path& config_path);
|
|
|
|
//=============================================================================
|
|
// Flag Registry
|
|
//=============================================================================
|
|
|
|
enum class FlagType { Boolean, Int32, Int64, Uint32, Uint64, Double, String, Command };
|
|
|
|
// Lifecycle: when can this flag be modified?
|
|
enum class Lifecycle {
|
|
kInitOnly, // Can only be set during initialization (before FinalizeInit)
|
|
kHotReload, // Can be changed at runtime with immediate effect
|
|
kRequiresRestart // Can be changed, but only takes effect after restart
|
|
};
|
|
|
|
// Validation constraints
|
|
struct Constraints {
|
|
std::optional<double> min;
|
|
std::optional<double> max;
|
|
std::vector<std::string> allowed_values;
|
|
std::function<bool(std::string_view)> custom_validator;
|
|
|
|
bool HasRangeConstraint() const { return min.has_value() || max.has_value(); }
|
|
bool HasAllowedValues() const { return !allowed_values.empty(); }
|
|
};
|
|
|
|
struct FlagEntry {
|
|
std::string name;
|
|
FlagType type;
|
|
std::string category;
|
|
std::string description;
|
|
std::function<bool(std::string_view)> setter;
|
|
std::function<std::string()> getter;
|
|
std::function<void()> command_callback;
|
|
Lifecycle lifecycle = Lifecycle::kHotReload;
|
|
Constraints constraints;
|
|
std::string default_value;
|
|
bool is_debug_only = false;
|
|
};
|
|
|
|
std::vector<FlagEntry>& GetRegistry();
|
|
|
|
/**
|
|
* Returns the registered entry's index, or nullopt if the name was already
|
|
* registered (logged at ERROR).
|
|
*/
|
|
std::optional<size_t> RegisterFlag(FlagEntry entry);
|
|
|
|
/**
|
|
* Removes a flag from the registry. Used by `FlagRegistrar`'s destructor so
|
|
* that DLL unload tears down the lambdas captured in each FlagEntry.
|
|
*/
|
|
void UnregisterFlag(std::string_view name);
|
|
|
|
bool SetFlagByName(std::string_view name, std::string_view value);
|
|
std::string GetFlagByName(std::string_view name);
|
|
|
|
// Typed registry query. Cross-DLL access path that does not require linking
|
|
// the DLL where the cvar is defined. Slower than REXCVAR_GET (string parse +
|
|
// hash lookup), so prefer REXCVAR_GET when the defining DLL is already on the
|
|
// link line. Returns a value-initialized T when the cvar is missing or its
|
|
// stored string fails to parse.
|
|
template <typename T>
|
|
T Query(std::string_view name);
|
|
|
|
template <>
|
|
bool Query<bool>(std::string_view name);
|
|
template <>
|
|
int32_t Query<int32_t>(std::string_view name);
|
|
template <>
|
|
int64_t Query<int64_t>(std::string_view name);
|
|
template <>
|
|
uint32_t Query<uint32_t>(std::string_view name);
|
|
template <>
|
|
uint64_t Query<uint64_t>(std::string_view name);
|
|
template <>
|
|
double Query<double>(std::string_view name);
|
|
template <>
|
|
std::string Query<std::string>(std::string_view name);
|
|
|
|
std::vector<std::string> ListFlags();
|
|
std::vector<std::string> ListFlagsByCategory(std::string_view category);
|
|
std::vector<std::string> ListFlagsByLifecycle(Lifecycle lc);
|
|
const FlagEntry* GetFlagInfo(std::string_view name);
|
|
std::vector<std::string> GetPendingRestartFlags();
|
|
void ClearPendingRestartFlags();
|
|
void ResetToDefault(std::string_view name);
|
|
void ResetAllToDefaults();
|
|
bool HasNonDefaultValue(std::string_view name);
|
|
std::vector<std::string> ListModifiedFlags();
|
|
std::string SerializeToTOML();
|
|
std::string SerializeToTOML(std::string_view category);
|
|
|
|
/// Callback invoked when a CVAR value changes
|
|
/// @param name The CVAR name
|
|
/// @param new_value The new value as a string
|
|
using ChangeCallback = std::function<void(std::string_view name, std::string_view new_value)>;
|
|
|
|
/// Register a callback to be invoked when a specific CVAR changes
|
|
void RegisterChangeCallback(std::string_view name, ChangeCallback callback);
|
|
|
|
/// Unregister all callbacks for a specific CVAR
|
|
void UnregisterChangeCallbacks(std::string_view name);
|
|
|
|
/**
|
|
* RAII handle for a registered flag. Destructor unregisters by name; on
|
|
* duplicate-name registration `owned_name_` is empty so chain methods and
|
|
* the destructor become no-ops and the original owner's entry is untouched.
|
|
*/
|
|
struct FlagRegistrar {
|
|
std::string owned_name_; // empty when registration was rejected
|
|
|
|
explicit FlagRegistrar(FlagEntry e) {
|
|
std::string name = e.name;
|
|
if (RegisterFlag(std::move(e)).has_value()) {
|
|
owned_name_ = std::move(name);
|
|
}
|
|
}
|
|
|
|
FlagRegistrar(FlagRegistrar&& other) noexcept : owned_name_(std::move(other.owned_name_)) {
|
|
other.owned_name_.clear();
|
|
}
|
|
|
|
~FlagRegistrar() {
|
|
if (!owned_name_.empty()) {
|
|
UnregisterFlag(owned_name_);
|
|
}
|
|
}
|
|
|
|
// Chain methods mutate the registered entry by name lookup.
|
|
FlagRegistrar&& range(double min_val, double max_val) && {
|
|
apply_([=](FlagEntry& entry) {
|
|
entry.constraints.min = min_val;
|
|
entry.constraints.max = max_val;
|
|
});
|
|
return std::move(*this);
|
|
}
|
|
|
|
FlagRegistrar&& allowed(std::initializer_list<std::string> values) && {
|
|
std::vector<std::string> vals(values);
|
|
apply_([vals = std::move(vals)](FlagEntry& entry) { entry.constraints.allowed_values = vals; });
|
|
return std::move(*this);
|
|
}
|
|
|
|
FlagRegistrar&& lifecycle(Lifecycle lc) && {
|
|
apply_([=](FlagEntry& entry) { entry.lifecycle = lc; });
|
|
return std::move(*this);
|
|
}
|
|
|
|
FlagRegistrar&& debug_only() && {
|
|
apply_([](FlagEntry& entry) { entry.is_debug_only = true; });
|
|
return std::move(*this);
|
|
}
|
|
|
|
FlagRegistrar&& validator(std::function<bool(std::string_view)> fn) && {
|
|
apply_([fn = std::move(fn)](FlagEntry& entry) mutable {
|
|
entry.constraints.custom_validator = std::move(fn);
|
|
});
|
|
return std::move(*this);
|
|
}
|
|
|
|
// Non-copyable (prevent double registration)
|
|
FlagRegistrar(const FlagRegistrar&) = delete;
|
|
FlagRegistrar& operator=(const FlagRegistrar&) = delete;
|
|
FlagRegistrar& operator=(FlagRegistrar&&) = delete;
|
|
|
|
private:
|
|
void apply_(std::function<void(FlagEntry&)> fn);
|
|
};
|
|
|
|
inline bool ParseDouble(std::string_view s, double& out) {
|
|
if (s.empty())
|
|
return false;
|
|
char* end = nullptr;
|
|
std::string str(s);
|
|
out = std::strtod(str.c_str(), &end);
|
|
return end != str.c_str() && *end == '\0';
|
|
}
|
|
|
|
} // namespace rex::cvar
|
|
|
|
//=============================================================================
|
|
// CVar Macros
|
|
//=============================================================================
|
|
|
|
// Declare a cvar (use in headers and TUs that need to read it).
|
|
// The accessor function returns a reference to the cvar's storage. Storage
|
|
// lives as a static-local inside whichever DLL contains the matching
|
|
// REXCVAR_DEFINE_*. Cross-DLL access goes through the import lib.
|
|
#define REXCVAR_DECLARE(type, name) type& FLAGS_##name##_storage_()
|
|
|
|
// Get a cvar value
|
|
#define REXCVAR_GET(name) (FLAGS_##name##_storage_())
|
|
|
|
// Set a cvar value
|
|
#define REXCVAR_SET(name, value) (FLAGS_##name##_storage_() = (value))
|
|
|
|
// Cross-module typed query that goes through the cvar registry by name.
|
|
// Use this when the defining DLL is not on the consumer's link line (e.g.,
|
|
// across one-way subsystem dependencies where adding the reverse link would
|
|
// create a cycle). Slower than REXCVAR_GET; prefer REXCVAR_GET when possible.
|
|
#define REXCVAR_QUERY(type, name) (::rex::cvar::Query<type>(#name))
|
|
|
|
// Define cvars (use in one .cpp file per cvar)
|
|
// The FlagRegistrar registers the flag in its destructor, allowing method chaining.
|
|
#define REXCVAR_DEFINE_BOOL(name, default_val, category, desc) \
|
|
bool& FLAGS_##name##_storage_() { \
|
|
static bool storage = (default_val); \
|
|
return storage; \
|
|
} \
|
|
static auto _cvar_reg_##name = \
|
|
::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::Boolean, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view v) { \
|
|
bool val = (v == "true" || v == "1" || v == "yes"); \
|
|
FLAGS_##name##_storage_() = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return FLAGS_##name##_storage_() ? "true" : "false"; }, \
|
|
[]() { return; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
(default_val) ? "true" : "false", \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_INT32(name, default_val, category, desc) \
|
|
int32_t& FLAGS_##name##_storage_() { \
|
|
static int32_t storage = (default_val); \
|
|
return storage; \
|
|
} \
|
|
static auto _cvar_reg_##name = \
|
|
::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::Int32, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view v) { \
|
|
int32_t val = 0; \
|
|
auto [ptr, ec] = \
|
|
std::from_chars(v.data(), v.data() + v.size(), val); \
|
|
if (ec != std::errc()) \
|
|
return false; \
|
|
FLAGS_##name##_storage_() = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
|
|
[]() { return; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_INT64(name, default_val, category, desc) \
|
|
int64_t& FLAGS_##name##_storage_() { \
|
|
static int64_t storage = (default_val); \
|
|
return storage; \
|
|
} \
|
|
static auto _cvar_reg_##name = \
|
|
::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::Int64, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view v) { \
|
|
int64_t val = 0; \
|
|
auto [ptr, ec] = \
|
|
std::from_chars(v.data(), v.data() + v.size(), val); \
|
|
if (ec != std::errc()) \
|
|
return false; \
|
|
FLAGS_##name##_storage_() = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
|
|
[]() { return; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_UINT32(name, default_val, category, desc) \
|
|
uint32_t& FLAGS_##name##_storage_() { \
|
|
static uint32_t storage = (default_val); \
|
|
return storage; \
|
|
} \
|
|
static auto _cvar_reg_##name = \
|
|
::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::Uint32, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view v) { \
|
|
uint32_t val = 0; \
|
|
auto [ptr, ec] = \
|
|
std::from_chars(v.data(), v.data() + v.size(), val); \
|
|
if (ec != std::errc()) \
|
|
return false; \
|
|
FLAGS_##name##_storage_() = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
|
|
[]() { return; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_UINT64(name, default_val, category, desc) \
|
|
uint64_t& FLAGS_##name##_storage_() { \
|
|
static uint64_t storage = (default_val); \
|
|
return storage; \
|
|
} \
|
|
static auto _cvar_reg_##name = \
|
|
::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::Uint64, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view v) { \
|
|
uint64_t val = 0; \
|
|
auto [ptr, ec] = \
|
|
std::from_chars(v.data(), v.data() + v.size(), val); \
|
|
if (ec != std::errc()) \
|
|
return false; \
|
|
FLAGS_##name##_storage_() = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
|
|
[]() { return; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_DOUBLE(name, default_val, category, desc) \
|
|
double& FLAGS_##name##_storage_() { \
|
|
static double storage = (default_val); \
|
|
return storage; \
|
|
} \
|
|
static auto _cvar_reg_##name = \
|
|
::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::Double, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view v) { \
|
|
double val = 0; \
|
|
if (!::rex::cvar::ParseDouble(v, val)) \
|
|
return false; \
|
|
FLAGS_##name##_storage_() = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name##_storage_()); }, \
|
|
[]() { return; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_STRING(name, default_val, category, desc) \
|
|
std::string& FLAGS_##name##_storage_() { \
|
|
static std::string storage = (default_val); \
|
|
return storage; \
|
|
} \
|
|
static auto _cvar_reg_##name = \
|
|
::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::String, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view v) { \
|
|
FLAGS_##name##_storage_() = std::string(v); \
|
|
return true; \
|
|
}, \
|
|
[]() { return FLAGS_##name##_storage_(); }, \
|
|
[]() { return; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
default_val, \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_COMMAND(name, callback, category, desc) \
|
|
std::function<void()>& FLAGS_##name##_storage_() { \
|
|
static std::function<void()> storage = (callback); \
|
|
return storage; \
|
|
} \
|
|
static auto _cvar_reg_##name = \
|
|
::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::Command, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view) { return false; }, \
|
|
[]() { return "<command>"; }, \
|
|
callback, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
"<command>", \
|
|
false})
|
|
|
|
namespace rex::cvar {
|
|
namespace testing {
|
|
|
|
class ScopedLifecycleOverride {
|
|
public:
|
|
ScopedLifecycleOverride();
|
|
~ScopedLifecycleOverride();
|
|
|
|
ScopedLifecycleOverride(const ScopedLifecycleOverride&) = delete;
|
|
ScopedLifecycleOverride& operator=(const ScopedLifecycleOverride&) = delete;
|
|
};
|
|
|
|
void ResetAllForTesting();
|
|
|
|
} // namespace testing
|
|
} // namespace rex::cvar
|