using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.Reflection; [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public sealed class ConVarAttribute : Attribute { } public sealed class Variable { static readonly ConcurrentDictionary Registry = new(); public string Key { get; } private object RawValue = default; private object InitialValue = default; public TypeCode Type { get; init; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] public Action Callback { get; init; } internal void ResetValue() { Debug.WriteLine("ConVar: Reset '{0}' to initial value '{1}'", Key, InitialValue); RawValue = InitialValue; } public string GetString() => RawValue?.ToString() ?? string.Empty; [DebuggerBrowsable(DebuggerBrowsableState.Never)] public object Value { get => RawValue; set { if (RawValue == null || !RawValue.Equals(value)) { if (TryConvertToType(value, Type, out var cast)) { RawValue = cast; Callback?.Invoke(this); Debug.WriteLine("ConVar: Set '{0}' to '{1}'", Key, cast); } else { Debug.WriteLine("ConVar: Unable to set '{0}' (type mismatch. excepted={1}, found={2})", value, Type, Convert.GetTypeCode(value)); } } else { Debug.WriteLine("ConVar: Set '{0}' to '{1}'", Key, value); } } } static bool TryConvertToType(object raw, TypeCode code, out object result) { result = default; if (code == TypeCode.Object || code == TypeCode.DBNull) return false; try { if (code == TypeCode.String) return true; var ptr = raw?.ToString() ?? ""; if ((ptr == "0" || ptr == "1") && code == TypeCode.Boolean) { result = ptr.Equals("1"); return true; } if (string.IsNullOrEmpty(ptr) && code == TypeCode.Char) { result = default(char); return true; } result = Convert.ChangeType(raw, code); return result != null; } catch (Exception ex) { #if DEBUG Debug.WriteLine("Unable to test typecode from given string. " + ex + "\n"); #endif return false; } } Variable(string name, object def) { Key = name; RawValue = def; InitialValue = def; } static Variable() { Registry ??= new(); foreach (var type in typeof(Variable).Assembly.GetTypes()) { if (type.GetCustomAttribute() != null) { _ = type.GetTypeInfo().DeclaredFields .Where(x => x.IsStatic && x.FieldType == typeof(Variable)) .Select(x => x.GetValue(null)) .ToArray(); } } Check("exec", "autoexec"); Check("dump"); } public static void Parse(string[] source) => ParseInternal(source, true); static string NormalizePath(string path) { var ext = Path.GetExtension(path); if (string.IsNullOrEmpty(ext) || ext.Equals(".")) ext = ".cfg"; return Path.GetFullPath(Path.GetFileNameWithoutExtension(path) + ext); } public static void ParseFromFile(string fileName) { fileName = NormalizePath(fileName); ParseInternal(File.ReadAllLines(fileName), false, fileName); } static void ParseInternal(string[] source, bool isArgv = false, string path = default) { for (int i = 0; i < source.Length; i++) { var arg = source[i]; if (isArgv) { if (!arg.StartsWith("--")) continue; arg = arg[2..]; } else { if (arg.StartsWith('#') || arg.StartsWith("--")) continue; if (string.IsNullOrEmpty(arg)) continue; } string key, value; int off; if (!FindToken(arg, isArgv, out off) && off == -1) { key = arg.Trim(); value = string.Empty; } else { key = arg[0..off].TrimEnd(); value = arg[(off + 1)..].TrimStart(); } if (!Check(key, value, path) && !isArgv) Debug.WriteLine("ConVar: Unknown key '" + key + "'"); } } static bool FindToken(string s, bool b, out int n) { if ((n = s.IndexOf('=')) != -1) { if (s[0..n].Equals("exec")) return false; return true; } if (!b && (n = s.IndexOf(' ')) != -1) return true; return false; } static bool Check(string key, string value = default, string currentFile = default) { if (Registry.TryGetValue(key, out var temp)) { temp.Value = value; return true; } else { if (key.Equals("exec")) { value = NormalizePath(value); if (!File.Exists(value)) Debug.WriteLine("Exec: Unable to find file. (file: " + value + ")"); else { if (value.Equals(currentFile)) Debug.WriteLine("Exec: Skipping nested file loop. (file: " + value + ")"); else ParseFromFile(value); } return true; } else if (key.Equals("dump") && Debugger.IsAttached) { foreach (var (k, v) in Registry.OrderBy(x => x.Key)) Console.WriteLine("{0} = {1}", k, v.Value is bool b ? (b ? 1 : 0) : v.Value); return true; } } return false; } public static Variable Register(string key, object def = default, TypeCode? type = default, Action update_callback = default/*, Action validate_callback = defa*/) { Variable result; if (Registry.TryGetValue(key, out result)) return result; else { bool ok = true; try { if (type.HasValue) def = Convert.ChangeType(def, type.Value); } catch (Exception ex) { Debug.WriteLine("ConVar: Register '" + key + "' with fallback type.\n" + ex + "\n\n"); ok = false; } if (ok) Debug.WriteLine("ConVar: Register '" + key + "'"); result = Registry[key] = new Variable(key, def) { Type = type ?? Convert.GetTypeCode(def), //Value = FormatValue(def ?? string.Empty), Callback = update_callback }; } return result; } public bool GetBool(bool def = default) { if (Value?.Equals("1") == true || Value?.Equals("true") == true) return true; if (RawValue is bool b) return b; return def; } public short GetInt16(short def = default) => Util.SafeConvert(RawValue, def); public int GetInt32(int def = default) => Util.SafeConvert(RawValue, def); public long GetInt64(long def = default) => Util.SafeConvert(RawValue, def); public ushort GetUInt16(ushort def = default) => Util.SafeConvert(RawValue, def); public uint GetUInt32(uint def = default) => Util.SafeConvert(RawValue, def); public ulong GetUInt64(ulong def = default) => Util.SafeConvert(RawValue, def); public float GetFloat(float def = default) => Util.SafeConvert(RawValue, def); public double GetDouble(double def = default) => Util.SafeConvert(RawValue, def); public override string ToString() { return $"{Key}: {RawValue} ({Type})"; } public static implicit operator string(Variable v) => v.GetString(); public static explicit operator bool(Variable v) => v.GetBool(); public static explicit operator short(Variable v) => v.GetInt16(); public static explicit operator int(Variable v) => v.GetInt32(); public static explicit operator long(Variable v) => v.GetInt64(); public static explicit operator ushort(Variable v) => v.GetUInt16(); public static explicit operator uint(Variable v) => v.GetUInt32(); public static explicit operator ulong(Variable v) => v.GetUInt64(); public static explicit operator float(Variable v) => v.GetFloat(); public static explicit operator double(Variable v) => v.GetDouble(); }