|
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<string, Variable> Registry = new(); |
|
|
|
public string Key { get; } |
|
private object RawValue = default; |
|
private object InitialValue = default; |
|
public TypeCode Type { get; init; } |
|
|
|
[DebuggerBrowsable(DebuggerBrowsableState.Never)] |
|
public Action<Variable> 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<ConVarAttribute>() != 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<Variable> update_callback = default/*, Action<Variable> 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(); |
|
} |