mirror of
https://github.com/sal063/AC6_recomp
synced 2026-09-03 02:23:48 -04:00
412 lines
20 KiB
C++
412 lines
20 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
|
|
*
|
|
* @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 };
|
|
|
|
// 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;
|
|
Lifecycle lifecycle = Lifecycle::kHotReload;
|
|
Constraints constraints;
|
|
std::string default_value;
|
|
bool is_debug_only = false;
|
|
};
|
|
|
|
std::vector<FlagEntry>& GetRegistry();
|
|
void RegisterFlag(FlagEntry entry);
|
|
bool SetFlagByName(std::string_view name, std::string_view value);
|
|
std::string GetFlagByName(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);
|
|
|
|
struct FlagRegistrar {
|
|
FlagEntry* entry_ptr = nullptr;
|
|
|
|
explicit FlagRegistrar(FlagEntry e) {
|
|
// Register immediately and store pointer to the registered entry for chaining
|
|
RegisterFlag(std::move(e));
|
|
auto& registry = GetRegistry();
|
|
entry_ptr = ®istry.back();
|
|
}
|
|
|
|
// Move constructor for copy-initialization in macros
|
|
FlagRegistrar(FlagRegistrar&& other) noexcept : entry_ptr(other.entry_ptr) {
|
|
other.entry_ptr = nullptr;
|
|
}
|
|
|
|
// Chain methods are rvalue-ref-qualified to work with temporaries
|
|
FlagRegistrar&& range(double min_val, double max_val) && {
|
|
entry_ptr->constraints.min = min_val;
|
|
entry_ptr->constraints.max = max_val;
|
|
return std::move(*this);
|
|
}
|
|
|
|
FlagRegistrar&& allowed(std::initializer_list<std::string> values) && {
|
|
entry_ptr->constraints.allowed_values = values;
|
|
return std::move(*this);
|
|
}
|
|
|
|
FlagRegistrar&& lifecycle(Lifecycle lc) && {
|
|
entry_ptr->lifecycle = lc;
|
|
return std::move(*this);
|
|
}
|
|
|
|
FlagRegistrar&& debug_only() && {
|
|
entry_ptr->is_debug_only = true;
|
|
return std::move(*this);
|
|
}
|
|
|
|
FlagRegistrar&& validator(std::function<bool(std::string_view)> fn) && {
|
|
entry_ptr->constraints.custom_validator = std::move(fn);
|
|
return std::move(*this);
|
|
}
|
|
|
|
~FlagRegistrar() = default;
|
|
|
|
// Non-copyable (prevent double registration)
|
|
FlagRegistrar(const FlagRegistrar&) = delete;
|
|
FlagRegistrar& operator=(const FlagRegistrar&) = delete;
|
|
FlagRegistrar& operator=(FlagRegistrar&&) = delete;
|
|
};
|
|
|
|
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 files that need access to a cvar defined elsewhere)
|
|
#define REXCVAR_DECLARE(type, name) extern type FLAGS_##name
|
|
|
|
// Get a cvar value
|
|
#define REXCVAR_GET(name) (FLAGS_##name)
|
|
|
|
// Set a cvar value
|
|
#define REXCVAR_SET(name, value) (FLAGS_##name = (value))
|
|
|
|
// 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 = (default_val); \
|
|
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 = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return FLAGS_##name ? "true" : "false"; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
(default_val) ? "true" : "false", \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_INT32(name, default_val, category, desc) \
|
|
int32_t FLAGS_##name = (default_val); \
|
|
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 = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name); }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_INT64(name, default_val, category, desc) \
|
|
int64_t FLAGS_##name = (default_val); \
|
|
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 = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name); }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_UINT32(name, default_val, category, desc) \
|
|
uint32_t FLAGS_##name = (default_val); \
|
|
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 = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name); }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_UINT64(name, default_val, category, desc) \
|
|
uint64_t FLAGS_##name = (default_val); \
|
|
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 = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name); }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_DOUBLE(name, default_val, category, desc) \
|
|
double FLAGS_##name = (default_val); \
|
|
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 = val; \
|
|
return true; \
|
|
}, \
|
|
[]() { return std::to_string(FLAGS_##name); }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
std::to_string(default_val), \
|
|
false})
|
|
|
|
#define REXCVAR_DEFINE_STRING(name, default_val, category, desc) \
|
|
std::string FLAGS_##name = (default_val); \
|
|
static auto _cvar_reg_##name = ::rex::cvar::FlagRegistrar({#name, \
|
|
::rex::cvar::FlagType::String, \
|
|
category, \
|
|
desc, \
|
|
[](std::string_view v) { \
|
|
FLAGS_##name = std::string(v); \
|
|
return true; \
|
|
}, \
|
|
[]() { return FLAGS_##name; }, \
|
|
::rex::cvar::Lifecycle::kHotReload, \
|
|
{}, \
|
|
default_val, \
|
|
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
|