Files
RobustToolbox/Robust.Shared/Configuration/ConfigurationManager.cs
83c2a1be11 [Dependency] source generator part 2 (#6550)
* [Dependency] source generator

No more reflection, no more codegen at runtime

Also various changes to Roslyn helpers to make this easier to write.

Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback.

No fallback is available for [field: Dependency] properties, due to a Roslyn bug.

Code Fixes exist. We love Roslyn

* Apply dependencies generator changes to all code

* Release notes

* Preprocessor got hands

* Handle nullable dependencies

These are bad but gotta deal with it.

* Apply suggestions from code review

Co-authored-by: Moony <moony@hellomouse.net>

* Fine, let's not use collection expressions

---------

Co-authored-by: Moony <moony@hellomouse.net>
2026-05-08 12:38:33 +02:00

1084 lines
37 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Nett;
using Robust.Shared.Collections;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.Configuration
{
/// <summary>
/// Stores and manages global configuration variables.
/// </summary>
[Virtual]
internal partial class ConfigurationManager : IConfigurationManagerInternal
{
[Dependency] private IGameTiming _gameTiming = default!;
[Dependency] private ILogManager _logManager = default!;
private const char TABLE_DELIMITER = '.';
protected readonly Dictionary<string, ConfigVar> _configVars = new();
private ConfigFileStorage? _configFile;
protected bool _isServer;
protected readonly ReaderWriterLockSlim Lock = new();
private ISawmill _sawmill = default!;
public event Action<CVarChangeInfo>? OnCVarValueChanged;
/// <summary>
/// Constructs a new ConfigurationManager.
/// </summary>
public ConfigurationManager()
{
}
public void Initialize(bool isServer)
{
_isServer = isServer;
_sawmill = _logManager.GetSawmill("cfg");
}
public virtual void Shutdown()
{
using var _ = Lock.WriteGuard();
_configVars.Clear();
_configFile = null;
}
/// <inheritdoc />
public HashSet<string> LoadFromTomlStream(Stream file)
{
TomlTable tblRoot;
try
{
tblRoot = Toml.ReadStream(file);
}
catch (Exception e)
{
_sawmill.Error("Unable to load configuration from table:\n{0}", e);
return [];
}
return LoadFromTomlTable(tblRoot);
}
private HashSet<string> LoadFromTomlTable(TomlTable table)
{
var loaded = new HashSet<string>();
var callbackEvents = new ValueList<ValueChangedInvoke>();
try
{
// Ensure callbacks are raised OUTSIDE the write lock.
using (Lock.WriteGuard())
{
foreach (var (cvar, value) in ParseCVarValuesFromToml(table))
{
loaded.Add(cvar);
LoadParsedVar(cvar, value, ref callbackEvents);
}
}
}
finally
{
RunDeferredInvokeCallbacks(in callbackEvents);
}
return loaded;
}
private void RunDeferredInvokeCallbacks(in ValueList<ValueChangedInvoke> callbackEvents)
{
foreach (var callback in callbackEvents)
{
InvokeValueChanged(callback);
}
}
private void LoadParsedVar(
string cvar,
object value,
ref ValueList<ValueChangedInvoke> changedInvokes)
{
if (_configVars.TryGetValue(cvar, out var cfgVar))
{
// overwrite the value with the saved one
var oldValue = GetConfigVarValue(cfgVar);
var convertedValue = value;
if (cfgVar.Type != value.GetType())
{
try
{
convertedValue = ConvertToCVarType(value, cfgVar.Type!);
}
catch
{
_sawmill.Error($"Parsed cvar does not match registered cvar type. Name: {cvar}. Code Type: {cfgVar.Type}. Parsed type: {value.GetType()}");
return;
}
}
changedInvokes.Add(SetupInvokeValueChanged(cfgVar, convertedValue, oldValue));
cfgVar.Value = convertedValue;
}
else
{
//or add another unregistered CVar
cfgVar = AddUnregisteredCVar(cvar, value);
}
cfgVar.ConfigModified = true;
}
private ConfigVar AddUnregisteredCVar(string name, object value)
{
//Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered.
var cfgVar = new ConfigVar(name, null!, CVar.NONE) { Value = value };
_configVars.Add(name, cfgVar);
return cfgVar;
}
public HashSet<string> LoadDefaultsFromTomlStream(Stream stream)
{
var tblRoot = Toml.ReadStream(stream);
var loaded = new HashSet<string>();
var callbackEvents = new ValueList<ValueChangedInvoke>();
// Ensure callbacks are raised OUTSIDE the write lock.
using (Lock.WriteGuard())
{
foreach (var (cVarName, value) in ParseCVarValuesFromToml(tblRoot))
{
if (!_configVars.TryGetValue(cVarName, out var cVar) || !cVar.Registered)
{
_sawmill.Error($"Trying to set unregistered variable '{cVarName}'");
continue;
}
var convertedValue = value;
if (cVar.Type != value.GetType())
{
try
{
convertedValue = ConvertToCVarType(value, cVar.Type!);
}
catch
{
_sawmill.Error($"Override TOML parsed cvar does not match registered cvar type. Name: {cVarName}. Code Type: {cVar.Type}. Toml type: {value.GetType()}");
continue;
}
}
if (cVar.OverrideValue == null && cVar.Value == null)
{
var oldValue = GetConfigVarValue(cVar);
callbackEvents.Add(SetupInvokeValueChanged(cVar, convertedValue, oldValue));
}
cVar.DefaultValue = convertedValue;
}
}
foreach (var callback in callbackEvents)
{
InvokeValueChanged(callback);
}
return loaded;
}
/// <inheritdoc />
public HashSet<string> LoadFromFile(string configFile)
{
try
{
HashSet<string> result;
using (var file = File.OpenRead(configFile))
{
result = LoadFromTomlStream(file);
}
SetSaveFile(configFile);
ApplyRollback();
_sawmill.Info($"Configuration loaded from file");
return result;
}
catch (Exception e)
{
_sawmill.Error("Unable to load configuration file:\n{0}", e);
return new HashSet<string>(0);
}
}
public void SetSaveFile(string configFile)
{
_configFile = new ConfigFileStorageDisk { Path = configFile };
}
public void SetVirtualConfig()
{
_configFile = new ConfigFileStorageVirtual();
}
public void CheckUnusedCVars()
{
if (!GetCVar(CVars.CfgCheckUnused))
return;
using (Lock.ReadGuard())
{
foreach (var cVar in _configVars.Values)
{
if (cVar.Registered)
continue;
_sawmill.Warning("Unknown CVar found (typo in config?): {CVar}", cVar.Name);
}
}
}
/// <inheritdoc />
public void SaveToTomlStream(Stream stream, IEnumerable<string> cvars)
{
var table = SaveToTomlTable(cvars);
Toml.WriteStream(table, stream);
}
private TomlTable SaveToTomlTable(IEnumerable<string> cvars, Func<string, object>? overrideValue = null)
{
var tblRoot = Toml.Create();
using (Lock.ReadGuard())
{
foreach (var name in cvars)
{
if (!_configVars.TryGetValue(name, out var cVar))
continue;
object? value;
if (overrideValue != null)
{
value = overrideValue(name);
}
else
{
value = cVar.Value;
if (value == null && cVar.Registered)
{
value = cVar.DefaultValue;
}
}
if (value == null)
{
_sawmill.Error($"CVar {name} has no value or default value, was the default value registered as null?");
continue;
}
var keyIndex = name.LastIndexOf(TABLE_DELIMITER);
var tblPath = name.Substring(0, keyIndex).Split(TABLE_DELIMITER);
var keyName = name.Substring(keyIndex + 1);
// locate the Table in the config tree
var table = tblRoot;
foreach (var curTblName in tblPath)
{
if (!table.TryGetValue(curTblName, out TomlObject tblObject))
{
tblObject = table.Add(curTblName, new Dictionary<string, TomlObject>()).Added;
}
table = tblObject as TomlTable ?? throw new InvalidConfigurationException(
$"[CFG] Object {curTblName} is being used like a table, but it is a {tblObject}. Are your CVar names formed properly?");
}
//runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯
// If you add a type here, add it to .Rollback.cs too!!!
// I can't share the code because of how the serialization layers work :(
switch (value)
{
case Enum val:
table.Add(keyName, (int)(object)val); // asserts Enum value != (ulong || long)
break;
case int val:
table.Add(keyName, val);
break;
case long val:
table.Add(keyName, val);
break;
case bool val:
table.Add(keyName, val);
break;
case string val:
table.Add(keyName, val);
break;
case float val:
table.Add(keyName, val);
break;
case double val:
table.Add(keyName, val);
break;
default:
_sawmill.Warning($"Cannot serialize '{name}', unsupported type.");
break;
}
}
}
return tblRoot;
}
/// <inheritdoc />
public void SaveToFile()
{
if (_configFile == null)
{
_sawmill.Warning("Cannot save the config file, because one was never loaded.");
return;
}
try
{
// Always write if it was present when reading from the config file, otherwise:
// Don't write if Archive flag is not set.
// Don't write if the cVar is the default value.
var cvars = _configVars.Where(x => x.Value.ConfigModified
|| ((x.Value.Flags & CVar.ARCHIVE) != 0 && x.Value.Value != null &&
!x.Value.Value.Equals(x.Value.DefaultValue))).Select(x => x.Key);
// Write in-memory to avoid bulldozing config file on exception.
var memoryStream = new MemoryStream();
SaveToTomlStream(memoryStream, cvars);
memoryStream.Position = 0;
switch (_configFile)
{
case ConfigFileStorageDisk disk:
{
using var file = File.Create(disk.Path);
memoryStream.CopyTo(file);
break;
}
case ConfigFileStorageVirtual @virtual:
{
@virtual.Stream.SetLength(0);
memoryStream.CopyTo(@virtual.Stream);
break;
}
default:
{
throw new UnreachableException();
}
}
_sawmill.Info($"config saved to '{_configFile}'.");
}
catch (Exception e)
{
_sawmill.Warning($"Cannot save the config file '{_configFile}'.\n {e}");
}
}
public void RegisterCVar<T>(string name, T defaultValue, CVar flags = CVar.NONE,
Action<T>? onValueChanged = null)
where T : notnull
{
RegisterCVar(name, typeof(T), defaultValue, flags);
if (onValueChanged != null)
OnValueChanged(name, onValueChanged);
}
private void RegisterCVar(string name, Type type, object defaultValue, CVar flags)
{
DebugTools.AssertEqual(defaultValue.GetType(), type);
DebugTools.Assert(!type.IsEnum || type.GetEnumUnderlyingType() == typeof(int),
$"{name}: Enum cvars must have int as underlying type.");
var only = _isServer ? CVar.CLIENTONLY : CVar.SERVERONLY;
if ((flags & only) != 0)
{
// Ignored on this side.
return;
}
using var _ = Lock.WriteGuard();
if (_configVars.TryGetValue(name, out var cVar))
{
if (cVar.Registered)
_sawmill.Error($"The variable '{name}' has already been registered.");
if (cVar.Value != null && type != cVar.Value.GetType())
{
try
{
cVar.Value = ConvertToCVarType(cVar.Value, type);
}
catch
{
_sawmill.Error($"TOML parsed cvar does not match registered cvar type. Name: {name}. Code Type: {type.Name}. Toml type: {cVar.Value.GetType().Name}");
return;
}
}
cVar.DefaultValue = defaultValue;
cVar.Flags = flags;
cVar.Register();
if (cVar.OverrideValue != null)
{
cVar.OverrideValueParsed = ParseOverrideValue(cVar.OverrideValue, type);
}
return;
}
var cvar = new ConfigVar(name, defaultValue, flags);
cvar.Register();
_configVars.Add(name, cvar);
}
public void OnValueChanged<T>(CVarDef<T> cVar, Action<T> onValueChanged, bool invokeImmediately = false)
where T : notnull
{
OnValueChanged(cVar.Name, onValueChanged, invokeImmediately);
}
public void OnValueChanged<T>(string name, Action<T> onValueChanged, bool invokeImmediately = false)
where T : notnull
{
using (Lock.WriteGuard())
{
var reg = _configVars[name];
reg.ValueChanged.AddInPlace(
(object value, in CVarChangeInfo _) => onValueChanged((T)value),
onValueChanged);
}
if (invokeImmediately)
{
onValueChanged(GetCVar<T>(name));
}
}
public void UnsubValueChanged<T>(CVarDef<T> cVar, Action<T> onValueChanged) where T : notnull
{
UnsubValueChanged(cVar.Name, onValueChanged);
}
public void UnsubValueChanged<T>(string name, Action<T> onValueChanged) where T : notnull
{
using var _ = Lock.WriteGuard();
var reg = _configVars[name];
reg.ValueChanged.RemoveInPlace(onValueChanged);
}
public void OnValueChanged<T>(CVarDef<T> cVar, CVarChanged<T> onValueChanged, bool invokeImmediately = false)
where T : notnull
{
OnValueChanged(cVar.Name, onValueChanged, invokeImmediately);
}
public void OnValueChanged<T>(string name, CVarChanged<T> onValueChanged, bool invokeImmediately = false)
where T : notnull
{
object value;
using (Lock.WriteGuard())
{
var reg = _configVars[name];
value = GetConfigVarValue(reg);
reg.ValueChanged.AddInPlace(
(object value, in CVarChangeInfo info) => onValueChanged((T)value, info),
onValueChanged);
}
if (invokeImmediately)
{
onValueChanged(GetCVar<T>(name), new CVarChangeInfo(name, _gameTiming.CurTick, value, value));
}
}
public void UnsubValueChanged<T>(CVarDef<T> cVar, CVarChanged<T> onValueChanged) where T : notnull
{
UnsubValueChanged(cVar.Name, onValueChanged);
}
public void UnsubValueChanged<T>(string name, CVarChanged<T> onValueChanged) where T : notnull
{
using var _ = Lock.WriteGuard();
var reg = _configVars[name];
reg.ValueChanged.RemoveInPlace(onValueChanged);
}
public void LoadCVarsFromAssembly(Assembly assembly)
{
foreach (var type in assembly
.GetTypes()
.Where(p => Attribute.IsDefined(p, typeof(CVarDefsAttribute))))
{
LoadCVarsFromType(type);
}
}
public void LoadCVarsFromType(Type containingType)
{
foreach (var defField in containingType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
{
var fieldType = defField.FieldType;
if (!fieldType.IsGenericType || fieldType.GetGenericTypeDefinition() != typeof(CVarDef<>))
{
continue;
}
var type = fieldType.GetGenericArguments()[0];
if (!defField.IsInitOnly)
{
throw new InvalidOperationException(
$"Found CVarDef '{defField.Name}' on '{defField.DeclaringType?.FullName}' that is not readonly. Please mark it as readonly.");
}
var def = (CVarDef?)defField.GetValue(null);
if (def == null)
{
throw new InvalidOperationException(
$"CVarDef '{defField.Name}' on '{defField.DeclaringType?.FullName}' is null.");
}
RegisterCVar(def.Name, type, def.DefaultValue, def.Flags);
}
}
/// <inheritdoc />
public bool IsCVarRegistered(string name)
{
using var _ = Lock.ReadGuard();
return _configVars.TryGetValue(name, out var cVar) && cVar.Registered;
}
public CVar GetCVarFlags(string name)
{
using var _ = Lock.ReadGuard();
return _configVars[name].Flags;
}
/// <inheritdoc />
public IEnumerable<string> GetRegisteredCVars()
{
using var _ = Lock.ReadGuard();
// Have to .ToArray() so the lock is held for the whole iteration operation.
// This function is only currently used for the cvar ? command so I'm not too worried.
return _configVars.Where(c => c.Value.Registered).Select(p => p.Key).ToArray();
}
/// <inheritdoc />
public virtual void SetCVar(string name, object value, bool force = false)
{
SetCVarInternal(name, value, _gameTiming.CurTick);
}
protected void SetCVarInternal(string name, object value, GameTick intendedTick)
{
ValueChangedInvoke? invoke = null;
using (Lock.WriteGuard())
{
//TODO: Make flags work, required non-derpy net system.
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
{
if (!Equals(cVar.OverrideValueParsed ?? cVar.Value, value))
{
var oldValue = GetConfigVarValue(cVar);
invoke = SetupInvokeValueChanged(cVar, value, oldValue, intendedTick);
// Setting an overriden var just turns off the override, basically.
cVar.OverrideValue = null;
cVar.OverrideValueParsed = null;
cVar.Value = value;
}
}
else
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
}
if (invoke != null)
InvokeValueChanged(invoke.Value);
}
public void SetCVar<T>(CVarDef<T> def, T value, bool force = false) where T : notnull
{
SetCVar(def.Name, value, force);
}
public void OverrideDefault(string name, object value)
{
ValueChangedInvoke? invoke = null;
using (Lock.WriteGuard())
{
//TODO: Make flags work, required non-derpy net system.
if (!_configVars.TryGetValue(name, out var cVar) || !cVar.Registered)
throw new InvalidConfigurationException($"Trying to set unregistered variable '{name}'");
if (cVar.OverrideValue == null && cVar.Value == null)
{
var oldValue = GetConfigVarValue(cVar);
invoke = SetupInvokeValueChanged(cVar, value, oldValue);
}
cVar.DefaultValue = value;
}
if (invoke != null)
InvokeValueChanged(invoke.Value);
}
public void OverrideDefault<T>(CVarDef<T> def, T value) where T : notnull
{
OverrideDefault(def.Name, value);
}
public object GetCVar(string name)
{
using var _ = Lock.ReadGuard();
if (_configVars.TryGetValue(name, out var cVar) && cVar.Registered)
return GetConfigVarValue(cVar);
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
}
/// <inheritdoc />
public T GetCVar<T>(string name)
{
return (T)GetCVar(name);
}
public T GetCVar<T>(CVarDef<T> def) where T : notnull
{
return GetCVar<T>(def.Name);
}
public Type GetCVarType(string name)
{
using var _ = Lock.ReadGuard();
if (!_configVars.TryGetValue(name, out var cVar) || !cVar.Registered)
{
throw new InvalidConfigurationException($"Trying to get type of unregistered variable '{name}'");
}
// If it's null it's a string, since the rest is primitives which aren't null.
return cVar.Type!;
}
protected static object GetConfigVarValue(ConfigVar cVar)
{
return cVar.OverrideValueParsed ?? cVar.Value ?? cVar.DefaultValue;
}
public void OverrideConVars(IEnumerable<(string key, string value)> cVars)
{
var invokes = new ValueList<ValueChangedInvoke>();
using (Lock.WriteGuard())
{
foreach (var (key, value) in cVars)
{
if (_configVars.TryGetValue(key, out var cfgVar))
{
cfgVar.OverrideValue = value;
if (!cfgVar.Registered)
continue;
var newValue = ParseOverrideValue(value, cfgVar.Type!);
var oldValue = GetConfigVarValue(cfgVar);
invokes.Add(SetupInvokeValueChanged(cfgVar, newValue, oldValue));
cfgVar.OverrideValueParsed = newValue;
}
else
{
//or add another unregistered CVar
//Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered.
var cVar = new ConfigVar(key, null!, CVar.NONE) { OverrideValue = value };
_configVars.Add(key, cVar);
}
}
}
foreach (var invoke in invokes)
{
InvokeValueChanged(invoke);
}
}
private static object ParseOverrideValue(string value, Type? type)
{
if (type == typeof(int))
{
return int.Parse(value);
}
if (type == typeof(bool))
{
return bool.Parse(value);
}
if (type == typeof(float))
{
return float.Parse(value, CultureInfo.InvariantCulture);
}
if (type?.IsEnum ?? false)
{
return Enum.Parse(type, value);
}
if (type == typeof(long))
{
return long.Parse(value);
}
if (type == typeof(ushort))
{
return ushort.Parse(value);
}
// Must be a string.
return value;
}
/// <summary>
/// Converts a TomlObject into its native type.
/// </summary>
/// <param name="obj">The object to convert.</param>
/// <returns>The boxed native type of the TomlObject.</returns>
private static object TypeConvert(TomlObject obj)
{
var tmlType = obj.TomlType;
switch (tmlType)
{
case TomlObjectType.Bool:
return obj.Get<bool>();
case TomlObjectType.Float:
return obj.Get<float>();
case TomlObjectType.Int:
var val = obj.Get<long>();
if (val is >= int.MinValue and <= int.MaxValue)
return obj.Get<int>();
return val;
case TomlObjectType.String:
return obj.Get<string>();
default:
throw new InvalidConfigurationException($"Cannot convert {tmlType}.");
}
}
private void InvokeValueChanged(in ValueChangedInvoke invoke)
{
try
{
OnCVarValueChanged?.Invoke(invoke.Info);
}
catch (Exception e)
{
_sawmill.Error($"Error while running OnCVarValueChanged callback: {e}");
}
foreach (var entry in invoke.Invoke.Entries)
{
try
{
entry.Value!.Invoke(invoke.Value, in invoke.Info);
}
catch (Exception e)
{
_sawmill.Error($"Error while running OnValueChanged callback: {e}");
}
}
}
private ValueChangedInvoke SetupInvokeValueChanged(ConfigVar var, object newValue, object oldValue, GameTick? tick = null)
{
tick ??= _gameTiming.CurTick;
var info = new CVarChangeInfo(var.Name, tick.Value, newValue, oldValue);
return new ValueChangedInvoke(info, var.ValueChanged);
}
private IEnumerable<(string cvar, object value)> ParseCVarValuesFromToml(TomlTable tblRoot)
{
return ProcessTomlObject(tblRoot, "");
IEnumerable<(string cvar, object value)> ProcessTomlObject(TomlObject obj, string tablePath)
{
if (obj is TomlTable table)
{
foreach (var kvTml in table)
{
string newPath;
if ((kvTml.Value is TomlTable))
newPath = tablePath + kvTml.Key + TABLE_DELIMITER;
else
newPath = tablePath + kvTml.Key;
foreach (var tuple in ProcessTomlObject(kvTml.Value, newPath))
{
yield return tuple;
}
}
yield break;
}
var tomlValue = TypeConvert(obj);
yield return (tablePath, tomlValue);
}
}
/// <summary>
/// Try to convert a compatible value to the actual registration type of a CVar.
/// </summary>
/// <remarks>
/// When CVars are parsed from TOML, their in-code type is not known.
/// This function does the necessary conversions from e.g. int to long.
/// </remarks>
/// <param name="value">
/// The value to convert.
/// This must be a simple type like strings or integers.
/// </param>
/// <param name="cVar">
/// The registration type of the CVar.
/// </param>
/// <returns></returns>
private static object ConvertToCVarType(object value, Type cVar)
{
if (cVar.IsEnum)
return Enum.Parse(cVar, value.ToString() ?? string.Empty);
return Convert.ChangeType(value, cVar);
}
internal List<Delegate> GetSubs(string name)
{
using (Lock.ReadGuard())
{
var list = new List<Delegate>();
if (!_configVars.TryGetValue(name, out var cVar))
throw new InvalidConfigurationException($"Trying to get unregistered variable '{name}'");
foreach (var entry in cVar.ValueChanged.Entries)
{
list.Add((Delegate) entry.Equality!);
}
return list;
}
}
/// <summary>
/// Holds the data for a single configuration variable.
/// </summary>
protected sealed class ConfigVar
{
/// <summary>
/// Constructs a CVar.
/// </summary>
/// <param name="name">The name of the CVar. This needs to contain only printable characters.
/// Underscores '_' are reserved. Everything before the last underscore is a table identifier,
/// everything after is the CVar name in the TOML document.</param>
/// <param name="defaultValue">The default value of this CVar.</param>
/// <param name="flags">Optional flags to modify the behavior of this CVar.</param>
public ConfigVar(string name, object defaultValue, CVar flags)
{
Name = name;
Flags = flags;
_defaultValue = defaultValue;
}
/// <summary>
/// The type of the cvar's value. This may be null until the cvar is registered.
/// </summary>
public Type? Type { get; internal set; }
/// <summary>
/// The name of the CVar.
/// </summary>
public string Name { get; }
/// <summary>
/// The default value of this CVar.
/// </summary>
public object DefaultValue
{
get => _defaultValue;
set
{
if (Registered)
DebugTools.AssertEqual(value.GetType(), Type);
_defaultValue = value;
}
}
/// <summary>
/// Optional flags to modify the behavior of this CVar.
/// </summary>
public CVar Flags { get; set; }
/// <summary>
/// The current value of this CVar.
/// </summary>
public object? Value
{
get => _value;
set
{
if (value != null && Registered)
DebugTools.AssertEqual(value.GetType(), Type);
_value = value;
}
}
/// <summary>
/// Has this CVar been registered in code?
/// </summary>
public bool Registered { get; private set; }
public void Register()
{
if (Registered)
{
DebugTools.AssertNotNull(DefaultValue);
DebugTools.AssertEqual(DefaultValue.GetType(), Type);
DebugTools.Assert(Value == null || Value.GetType() == Type);
DebugTools.Assert(OverrideValueParsed == null || OverrideValueParsed.GetType() == Type);
return;
}
if (_defaultValue == null)
throw new NullReferenceException("Must specify default value before registering");
if (Value != null && DefaultValue.GetType() != Value.GetType())
throw new Exception($"The cvar value & default value must be of the same type");
if (OverrideValueParsed != null && DefaultValue.GetType() != OverrideValueParsed.GetType())
throw new Exception($"The cvar override value & default value must be of the same type");
Type = DefaultValue.GetType();
Registered = true;
}
/// <summary>
/// Was the CVar present in the config file?
/// If so we need to always re-save it even if it's not ARCHIVE.
/// </summary>
public bool ConfigModified;
public InvokeList<ValueChangedDelegate> ValueChanged;
private object _defaultValue;
private object? _value;
private object? _overrideValueParsed;
// We don't know what the type of the var is until it's registered.
// So we can't actually parse them until then.
// So we keep the raw string around.
public string? OverrideValue { get; set; }
public object? OverrideValueParsed
{
get => _overrideValueParsed;
set
{
if (value != null && Registered)
DebugTools.AssertEqual(value.GetType(), Type);
_overrideValueParsed = value;
}
}
}
/// <summary>
/// All data we need to invoke a deferred ValueChanged handler outside of a write lock.
/// </summary>
private struct ValueChangedInvoke
{
public InvokeList<ValueChangedDelegate> Invoke;
public object Value => Info.NewValue;
public CVarChangeInfo Info;
public ValueChangedInvoke(CVarChangeInfo info, InvokeList<ValueChangedDelegate> invoke) : this()
{
Info = info;
Invoke = invoke;
}
}
protected delegate void ValueChangedDelegate(object value, in CVarChangeInfo info);
private abstract class ConfigFileStorage;
private sealed class ConfigFileStorageDisk : ConfigFileStorage
{
public required string Path;
public override string ToString()
{
return Path;
}
}
private sealed class ConfigFileStorageVirtual : ConfigFileStorage
{
// I did not realize when adding this class that there is currently no way to *load* this data again.
// Oh well, might be useful for a future unit test.
public readonly MemoryStream Stream = new();
public override string ToString()
{
return "<VIRTUAL>";
}
}
}
[Serializable]
[Virtual]
public class InvalidConfigurationException : Exception
{
public InvalidConfigurationException()
{
}
public InvalidConfigurationException(string message) : base(message)
{
}
public InvalidConfigurationException(string message, Exception inner) : base(message, inner)
{
}
}
}