Skip to content

Instantly share code, notes, and snippets.

@nathan130200
Last active March 27, 2022 12:27
Show Gist options
  • Save nathan130200/ad669d340ca643b3dc678a9c400315dd to your computer and use it in GitHub Desktop.
Save nathan130200/ad669d340ca643b3dc678a9c400315dd to your computer and use it in GitHub Desktop.
Console variable system like in modern games to C#

Con Var

Basic console variables port like in modern games to C# (aka CVars)

Define your holder classes.

Holder classes will receive static readonly fields with your variables registred like in example, classes must be annotated with [ConVar] attribute.

Example:

[ConVar]
class MyObject 
{
  static readonly Variable
    enableVerbose = Variable.Register("verbosityEnable", true), // defaults to Boolean
    verbosityLevel = Variable.Register("verbosityLevel", 0U) // defaults to UInt32
    ; 
    
    void CheckLogVerbosity(){
      if((bool)enableVerbose){
        // do something if this var is set to true.
        
        if((uint)verbosityLevel > 1) {
         // we received "--verbosityLevel=1" on command line or config file.
        }
      }
    }
}

You can also watch variable updates to perform some stuff on updated value, for example Socket port validation example:

// you can also specify manually typecode so value will be enforced on variable registration.
Variable.Register("tcp_port", 3005U, type: TypeCode.UInt32, update_callback: v =>
{
    var value = (ushort)v.Value;

    if (value <= 0 || value >= ushort.MaxValue) // value provided is invalid
        v.ResetValue(); // oh noo! user provided invalid value, reset back to initial value (or 3005U as we defined above)
})

Additional Notes

  • I've implemented an single command called exec. This command will search a given file, read its contents as plain text, then parse all variables from there and import variable changes.
  • Also implemented an command called "dump", after execute autoexec.cfg will dump all variables and their current values (even modified by config files or command line).
  • All variables that is inside this file will be merged into existing variables, so if you set +var1=val1 then set after +var1=val2 the value val2 will override previous value on var1 cvar.
  • I've implemented circular command check, so if you type exec myconfig.cfg and save as myconfig.cfg and try execute, will skip this line in file.
  • By default an file called autoexec.cfg is parsed and executed once all variables are registred in cache, so important stuff you wanna set, prefer this file.
public static class Util
{
public static T SafeConvert<T>(object raw, T defaults)
where T : new()
{
try
{
return (T) Convert.ChangeType(raw, Convert.GetTypeCode(defaults));
}
catch
{
return defaults;
}
}
}
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();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment