diff --git a/CMakeLists.txt b/CMakeLists.txt index 57904f724..d60e14321 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ set(SOURCE_FILES src/core/managers/con_command_manager.cpp src/core/managers/con_command_manager.h src/scripting/natives/natives_commands.cpp + src/scripting/natives/natives_convars.cpp src/core/memory_module.h src/core/memory_module.cpp src/core/cs2_sdk/interfaces/cgameresourceserviceserver.h diff --git a/managed/CounterStrikeSharp.API/Core/API.cs b/managed/CounterStrikeSharp.API/Core/API.cs index c1de5c22e..4236afc8f 100644 --- a/managed/CounterStrikeSharp.API/Core/API.cs +++ b/managed/CounterStrikeSharp.API/Core/API.cs @@ -218,6 +218,146 @@ public static void ReplicateConvar(int clientslot, string convarname, string con } } + public static void SetConvarFlags(ushort convar, ulong flags){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.Push(flags); + ScriptContext.GlobalScriptContext.SetIdentifier(0xB2BDCCBF); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + } + } + + public static ulong GetConvarFlags(ushort convar){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.SetIdentifier(0x94829E2B); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (ulong)ScriptContext.GlobalScriptContext.GetResult(typeof(ulong)); + } + } + + public static short GetConvarType(ushort convar){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.SetIdentifier(0xB6E0E54C); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (short)ScriptContext.GlobalScriptContext.GetResult(typeof(short)); + } + } + + public static string GetConvarName(ushort convar){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.SetIdentifier(0xB6F0E2F3); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (string)ScriptContext.GlobalScriptContext.GetResult(typeof(string)); + } + } + + public static string GetConvarHelpText(ushort convar){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.SetIdentifier(0x341D1F67); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (string)ScriptContext.GlobalScriptContext.GetResult(typeof(string)); + } + } + + public static ushort GetConvarAccessIndexByName(string name){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(name); + ScriptContext.GlobalScriptContext.SetIdentifier(0x6288420D); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (ushort)ScriptContext.GlobalScriptContext.GetResult(typeof(ushort)); + } + } + + public static T GetConvarValue(ushort convar){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.SetIdentifier(0x935B2E9F); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (T)ScriptContext.GlobalScriptContext.GetResult(typeof(T)); + } + } + + public static string GetConvarValueAsString(ushort convar){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.SetIdentifier(0x5CC184F8); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (string)ScriptContext.GlobalScriptContext.GetResult(typeof(string)); + } + } + + public static void SetConvarValueAsString(ushort convar, string value){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.Push(value); + ScriptContext.GlobalScriptContext.SetIdentifier(0x5EF52D6C); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + } + } + + public static object SetConvarValue(ushort convar, T value){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.Push(value); + ScriptContext.GlobalScriptContext.SetIdentifier(0xB3DDAA0B); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (object)ScriptContext.GlobalScriptContext.GetResult(typeof(object)); + } + } + + public static ushort CreateConvar(string name, short type, string helptext, ulong flags, bool hasmin, bool hasmax, T defaultvalue, T minvalue, T maxvalue){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(name); + ScriptContext.GlobalScriptContext.Push(type); + ScriptContext.GlobalScriptContext.Push(helptext); + ScriptContext.GlobalScriptContext.Push(flags); + ScriptContext.GlobalScriptContext.Push(hasmin); + ScriptContext.GlobalScriptContext.Push(hasmax); + ScriptContext.GlobalScriptContext.Push(defaultvalue); + ScriptContext.GlobalScriptContext.Push(minvalue); + ScriptContext.GlobalScriptContext.Push(maxvalue); + ScriptContext.GlobalScriptContext.SetIdentifier(0xF22079B9); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + return (ushort)ScriptContext.GlobalScriptContext.GetResult(typeof(ushort)); + } + } + + public static void DeleteConvar(ushort convar){ + lock (ScriptContext.GlobalScriptContext.Lock) { + ScriptContext.GlobalScriptContext.Reset(); + ScriptContext.GlobalScriptContext.Push(convar); + ScriptContext.GlobalScriptContext.SetIdentifier(0xFC28F444); + ScriptContext.GlobalScriptContext.Invoke(); + ScriptContext.GlobalScriptContext.CheckErrors(); + } + } + public static string GetStringFromSymbolLarge(IntPtr pointer){ lock (ScriptContext.GlobalScriptContext.Lock) { ScriptContext.GlobalScriptContext.Reset(); diff --git a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs index a4a2b47f0..aacc218b2 100644 --- a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs +++ b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs @@ -129,6 +129,8 @@ public void Dispose() internal readonly Dictionary EntitySingleOutputHooks = new Dictionary(); + internal readonly List ConVars = []; + public readonly List CommandDefinitions = new List(); public readonly List Timers = new List(); @@ -367,6 +369,7 @@ public void RegisterAllAttributes(object instance) this.RegisterAttributeHandlers(instance); this.RegisterConsoleCommandAttributeHandlers(instance); this.RegisterEntityOutputAttributeHandlers(instance); + this.RegisterConVars(instance); this.RegisterFakeConVars(instance); } @@ -524,6 +527,19 @@ public void RegisterFakeConVars(Type type, object instance = null) }); } } + + public void RegisterConVars(Type type, object instance = null) + { + var convars = type + .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .Where(prop => prop.FieldType.IsGenericType && + prop.FieldType.GetGenericTypeDefinition() == typeof(ConVar<>)); + + foreach (var prop in convars) + { + ConVars.Add(prop.GetValue(instance) as ConVarBase); // ConVar instance + } + } /// /// Used to bind a fake ConVar to a plugin command. Only required for ConVars that are not public properties of the plugin class. @@ -535,6 +551,10 @@ public void RegisterFakeConVars(object instance) RegisterFakeConVars(instance.GetType(), instance); } + public void RegisterConVars(object instance) { + RegisterConVars(instance.GetType(), instance); + } + /// /// Hooks an entity output. /// @@ -657,6 +677,11 @@ protected virtual void Dispose(bool disposing) subscriber.Dispose(); } + foreach (var convar in ConVars) + { + convar.Delete(); + } + foreach (var definition in CommandDefinitions) { CommandManager.RemoveCommand(definition); diff --git a/managed/CounterStrikeSharp.API/Modules/Cvars/ConVar.cs b/managed/CounterStrikeSharp.API/Modules/Cvars/ConVar.cs index 26e7b36c4..16cc16e1d 100644 --- a/managed/CounterStrikeSharp.API/Modules/Cvars/ConVar.cs +++ b/managed/CounterStrikeSharp.API/Modules/Cvars/ConVar.cs @@ -4,6 +4,7 @@ namespace CounterStrikeSharp.API.Modules.Cvars; +[Obsolete("Use ConVar instead for type-safe access to ConVars.")] public class ConVar { public IntPtr Handle { get; } @@ -127,7 +128,7 @@ public string StringValue throw new InvalidOperationException( $"ConVar is a {Type} but you are trying to get a string value."); } - + NativeAPI.SetConvarStringValue(Handle, value); } } diff --git a/managed/CounterStrikeSharp.API/Modules/Cvars/ConVarOfT.cs b/managed/CounterStrikeSharp.API/Modules/Cvars/ConVarOfT.cs new file mode 100644 index 000000000..8af1e274f --- /dev/null +++ b/managed/CounterStrikeSharp.API/Modules/Cvars/ConVarOfT.cs @@ -0,0 +1,223 @@ +using CounterStrikeSharp.API.Modules.Utils; + +namespace CounterStrikeSharp.API.Modules.Cvars; + +public class ConVarBase +{ + public ushort AccessIndex { get; protected set; } + + public string Name => NativeAPI.GetConvarName(AccessIndex); + public string Description => NativeAPI.GetConvarHelpText(AccessIndex); + + /// + /// The underlying data type of the ConVar. + /// + public ConVarType Type => (ConVarType)NativeAPI.GetConvarType(AccessIndex); + + /// + /// The ConVar flags as defined by . + /// + public ConVarFlags Flags + { + get => (ConVarFlags)NativeAPI.GetConvarFlags(AccessIndex); + set => NativeAPI.SetConvarFlags(AccessIndex, (ulong)value); + } + + public string ValueAsString + { + get => NativeAPI.GetConvarValueAsString(AccessIndex); + set => NativeAPI.SetConvarValueAsString(AccessIndex, value); + } + + /// + /// Shorthand for checking the flag. + /// + public bool Public + { + get => Flags.HasFlag(ConVarFlags.FCVAR_NOTIFY); + set + { + if (value) + { + Flags |= ConVarFlags.FCVAR_NOTIFY; + } + else + { + Flags &= ~ConVarFlags.FCVAR_NOTIFY; + } + } + } + + public void Delete() + { + if (AccessIndex == 0) + throw new InvalidOperationException("Cannot delete a ConVar that has not been created or found."); + + NativeAPI.DeleteConvar(AccessIndex); + AccessIndex = 0; + } +} + +public class ConVar : ConVarBase +{ + public ConVar(ushort accessIndex) + { + AccessIndex = accessIndex; + } + + public ConVar(string name, string description, T defaultValue = default(T), ConVarFlags flags = ConVarFlags.FCVAR_NONE, + T? minValue = default, T? maxValue = default) : this(new ConVarCreationOptions + { + Name = name, + DefaultValue = defaultValue, + Description = description, + Flags = flags, + MinValue = minValue, + MaxValue = maxValue + }) + { + } + + public ConVar(ConVarCreationOptions options) + { + var type = typeof(T); + var conVarType = type switch + { + _ when type == typeof(bool) => ConVarType.Bool, + _ when type == typeof(float) => ConVarType.Float32, + _ when type == typeof(double) => ConVarType.Float64, + _ when type == typeof(ushort) => ConVarType.UInt16, + _ when type == typeof(short) => ConVarType.Int16, + _ when type == typeof(uint) => ConVarType.UInt32, + _ when type == typeof(int) => ConVarType.Int32, + _ when type == typeof(long) => ConVarType.Int64, + _ when type == typeof(ulong) => ConVarType.UInt64, + _ when type == typeof(string) => ConVarType.String, + _ when type == typeof(QAngle) => ConVarType.Qangle, + _ when type == typeof(Vector2D) => ConVarType.Vector2, + _ when type == typeof(Vector) => ConVarType.Vector3, + _ when type == typeof(Vector4D) => ConVarType.Vector4, + _ => throw new InvalidOperationException($"Unsupported type: {type}") + }; + + AccessIndex = NativeAPI.CreateConvar(options.Name, (short)conVarType, options.Description, (UInt64)options.Flags, + options.MinValue != null, options.MaxValue != null, + options.DefaultValue, + options.MinValue, + options.MaxValue); + + if (AccessIndex == 0) + { + throw new InvalidOperationException($"Failed to create ConVar '{options.Name}' with type '{type}'."); + } + } + + public T Value + { + get + { + var type = typeof(T); + switch (Type) + { + case ConVarType.Bool: + if (type != typeof(bool)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Float32: + if (type != typeof(float)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Float64: + if (type != typeof(double)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.UInt16: + if (type != typeof(ushort)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Int16: + if (type != typeof(short)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.UInt32: + if (type != typeof(uint)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Int32: + if (type != typeof(int)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Int64: + if (type != typeof(long)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.UInt64: + if (type != typeof(ulong)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.String: + if (type != typeof(string)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Qangle: + if (type != typeof(QAngle)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Vector2: + if (type != typeof(Vector2D)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Vector3: + if (type != typeof(Vector)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + case ConVarType.Vector4: + if (type != typeof(Vector4D)) + throw new InvalidOperationException( + $"ConVar is a {Type} but you are trying to get a {type} value."); + break; + default: + throw new InvalidOperationException($"Unknown ConVar type: {Type}"); + } + + return NativeAPI.GetConvarValue(AccessIndex); + } + set => NativeAPI.SetConvarValue(AccessIndex, value); + } + + public static ConVar? Find(string name) + { + var accessIndex = NativeAPI.GetConvarAccessIndexByName(name); + if (accessIndex == 0) return null; + + return new ConVar(accessIndex); + } + + public override string ToString() + { + return $"ConVar [name={Name}, value={Value}, description={Description}, type={Type}, flags={Flags}]"; + } +} + +public sealed record ConVarCreationOptions +{ + public required string Name { get; init; } + public required T DefaultValue { get; init; } + public string Description { get; init; } = string.Empty; + public ConVarFlags Flags { get; init; } = ConVarFlags.FCVAR_NONE; + public T? MinValue { get; init; } + public T? MaxValue { get; init; } +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.Tests.Native/ConVarTests.cs b/managed/CounterStrikeSharp.Tests.Native/ConVarTests.cs new file mode 100644 index 000000000..48b359933 --- /dev/null +++ b/managed/CounterStrikeSharp.Tests.Native/ConVarTests.cs @@ -0,0 +1,239 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Modules.Cvars; +using CounterStrikeSharp.API.Modules.Utils; +using Xunit; + +namespace NativeTestsPlugin; + +public class ConVarTests +{ + [Fact] + public async Task BoolConVar() + { + Server.ExecuteCommand("sv_cheats 1"); + await WaitOneFrame(); + + var boolConVar = ConVar.Find("sv_cheats"); + Assert.NotNull(boolConVar); + Assert.Equal("sv_cheats", boolConVar.Name); + Assert.Equal(ConVarType.Bool, boolConVar.Type); + Assert.Equal(ConVarFlags.FCVAR_NOTIFY | ConVarFlags.FCVAR_REPLICATED | ConVarFlags.FCVAR_RELEASE, boolConVar.Flags); + Assert.True(boolConVar.Value); + + boolConVar.Value = false; + Assert.False(boolConVar.Value); + } + + [Fact] + public async Task IntConVar() + { + Server.ExecuteCommand("mp_td_dmgtokick 300"); + await WaitOneFrame(); + + var intConVar = ConVar.Find("mp_td_dmgtokick"); + Assert.NotNull(intConVar); + Assert.Equal("mp_td_dmgtokick", intConVar.Name); + Assert.Equal(ConVarType.Int32, intConVar.Type); + Assert.Equal(300, intConVar.Value); + + intConVar.Value = 500; + Assert.Equal(500, intConVar.Value); + } + + [Fact] + public async Task FloatConVar() + { + Server.ExecuteCommand("inferno_damage 40.0"); + await WaitOneFrame(); + + var floatConVar = ConVar.Find("inferno_damage"); + Assert.NotNull(floatConVar); + Assert.Equal(ConVarType.Float32, floatConVar.Type); + Assert.Equal(40.0, floatConVar.Value); + + floatConVar.Value = 50.0f; + Assert.Equal(50.0f, floatConVar.Value); + } + + [Fact] + public async Task VectorConVar() + { + Server.ExecuteCommand("fog_color -1 -1 -1"); + await WaitOneFrame(); + + var vectorConVar = ConVar.Find("fog_color"); + Assert.NotNull(vectorConVar); + Assert.Equal(-1, vectorConVar.Value.X); + Assert.Equal(-1, vectorConVar.Value.Y); + Assert.Equal(-1, vectorConVar.Value.Z); + + vectorConVar.Value = new Vector(0, 0, 0); + Assert.Equal(0, vectorConVar.Value.X); + Assert.Equal(0, vectorConVar.Value.Y); + Assert.Equal(0, vectorConVar.Value.Z); + } + + [Fact] + public async Task StringConVar() + { + Server.ExecuteCommand("mp_backup_round_file backup"); + await WaitOneFrame(); + + var stringConVar = ConVar.Find("mp_backup_round_file"); + Assert.NotNull(stringConVar); + Assert.Equal("backup", stringConVar.Value); + + stringConVar.Value = "new_backup"; + } + + [Fact] + public void CreateBoolConVar() + { + ConVar.Find("test_bool_convar")?.Delete(); + + var conVar = new ConVar("test_bool_convar", "Test boolean ConVar", true, ConVarFlags.FCVAR_NOTIFY); + Assert.NotNull(conVar); + Assert.Equal("test_bool_convar", conVar.Name); + Assert.Equal(ConVarType.Bool, conVar.Type); + Assert.Equal("Test boolean ConVar", conVar.Description); + Assert.Equal( + ConVarFlags.FCVAR_NOTIFY | ConVarFlags.FCVAR_GAMEDLL | ConVarFlags.FCVAR_RELEASE | ConVarFlags.FCVAR_CLIENT_CAN_EXECUTE, + conVar.Flags); + Assert.True(conVar.Value); + + conVar.Delete(); + + var found = ConVar.Find("test_bool_convar"); + Assert.Null(found); + } + + [Fact] + public void CreateVectorConVar() + { + ConVar.Find("test_vector_convar")?.Delete(); + + var conVar = new ConVar(new ConVarCreationOptions() + { + Name = "test_vector_convar", + DefaultValue = new Vector(1, 2, 3), + Description = "Test vector ConVar", + Flags = ConVarFlags.FCVAR_NOTIFY, + MinValue = new Vector(0, 0, 0), + MaxValue = new Vector(100, 100, 100) + }); + + Assert.NotNull(conVar); + Assert.Equal("test_vector_convar", conVar.Name); + Assert.Equal(ConVarType.Vector3, conVar.Type); + Assert.Equal("Test vector ConVar", conVar.Description); + Assert.Equal( + ConVarFlags.FCVAR_NOTIFY | ConVarFlags.FCVAR_GAMEDLL | ConVarFlags.FCVAR_RELEASE | ConVarFlags.FCVAR_CLIENT_CAN_EXECUTE, + conVar.Flags); + Assert.Equal(1, conVar.Value.X); + Assert.Equal(2, conVar.Value.Y); + Assert.Equal(3, conVar.Value.Z); + + conVar.Value = new Vector(500, 500, 500); + + // Test min/max constraints + Assert.Equal(100, conVar.Value.X); + Assert.Equal(100, conVar.Value.Y); + Assert.Equal(100, conVar.Value.Z); + + conVar.Delete(); + + var found = ConVar.Find("test_vector_convar"); + Assert.Null(found); + } + + [Fact] + public void CreateStringConVar() + { + ConVar.Find("test_string_convar")?.Delete(); + + var conVar = new ConVar("test_string_convar", "Test string ConVar", "default_value", ConVarFlags.FCVAR_NOTIFY); + Assert.NotNull(conVar); + Assert.Equal("test_string_convar", conVar.Name); + Assert.Equal(ConVarType.String, conVar.Type); + Assert.Equal("Test string ConVar", conVar.Description); + Assert.Equal( + ConVarFlags.FCVAR_NOTIFY | ConVarFlags.FCVAR_GAMEDLL | ConVarFlags.FCVAR_RELEASE | ConVarFlags.FCVAR_CLIENT_CAN_EXECUTE, + conVar.Flags); + Assert.Equal("default_value", conVar.Value); + + conVar.Delete(); + + var found = ConVar.Find("test_string_convar"); + Assert.Null(found); + } + + [Fact] + public void CreateFloatConVar() + { + ConVar.Find("test_float_convar")?.Delete(); + + var conVar = new ConVar(new ConVarCreationOptions() + { + Name = "test_float_convar", + DefaultValue = 1.23f, + Description = "Test float ConVar", + Flags = ConVarFlags.FCVAR_NOTIFY, + MinValue = 0f, + MaxValue = 25f + }); + Assert.NotNull(conVar); + Assert.Equal("test_float_convar", conVar.Name); + Assert.Equal(ConVarType.Float32, conVar.Type); + Assert.Equal("Test float ConVar", conVar.Description); + Assert.Equal( + ConVarFlags.FCVAR_NOTIFY | ConVarFlags.FCVAR_GAMEDLL | ConVarFlags.FCVAR_RELEASE | ConVarFlags.FCVAR_CLIENT_CAN_EXECUTE, + conVar.Flags); + Assert.Equal(1.23f, conVar.Value); + + // Test min/max constraints + conVar.Value = 50.0f; + Assert.Equal(25.0f, conVar.Value); + Assert.Equal("25.000000", conVar.ValueAsString); + + conVar.ValueAsString = "10.5"; + Assert.Equal(10.5f, conVar.Value); + + conVar.Delete(); + + var found = ConVar.Find("test_float_convar"); + Assert.Null(found); + } + + [Fact] + public void CreateIntConVar() + { + ConVar.Find("test_int_convar")?.Delete(); + + var conVar = new ConVar(new ConVarCreationOptions() + { + Name = "test_int_convar", + DefaultValue = 42, + Description = "Test int ConVar", + Flags = ConVarFlags.FCVAR_NOTIFY, + MinValue = 0, + MaxValue = 100 + }); + Assert.NotNull(conVar); + Assert.Equal("test_int_convar", conVar.Name); + Assert.Equal(ConVarType.Int32, conVar.Type); + Assert.Equal("Test int ConVar", conVar.Description); + Assert.Equal( + ConVarFlags.FCVAR_NOTIFY | ConVarFlags.FCVAR_GAMEDLL | ConVarFlags.FCVAR_RELEASE | ConVarFlags.FCVAR_CLIENT_CAN_EXECUTE, + conVar.Flags); + Assert.Equal(42, conVar.Value); + + // Test min/max constraints + conVar.Value = 150; + Assert.Equal(100, conVar.Value); + + conVar.Delete(); + + var found = ConVar.Find("test_int_convar"); + Assert.Null(found); + } +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.Tests.Native/GlobalUsings.cs b/managed/CounterStrikeSharp.Tests.Native/GlobalUsings.cs index f9b8ff4e0..378294e74 100644 --- a/managed/CounterStrikeSharp.Tests.Native/GlobalUsings.cs +++ b/managed/CounterStrikeSharp.Tests.Native/GlobalUsings.cs @@ -1,2 +1,3 @@ global using static TestUtils; -global using System; \ No newline at end of file +global using System; +global using System.Threading.Tasks; \ No newline at end of file diff --git a/managed/TestPlugin/TestPlugin.cs b/managed/TestPlugin/TestPlugin.cs index 655c02b70..093a3a2c8 100644 --- a/managed/TestPlugin/TestPlugin.cs +++ b/managed/TestPlugin/TestPlugin.cs @@ -65,6 +65,15 @@ public void OnConfigParsed(SampleConfig config) private TestInjectedClass _testInjectedClass; + public ConVar MyExampleConvar = new ConVar( + "example_convar", + "An example ConVar for testing purposes", + 42.0f, + ConVarFlags.FCVAR_NONE, + 0.0f, + 100.0f + ); + public SamplePlugin(TestInjectedClass testInjectedClass) { _testInjectedClass = testInjectedClass; @@ -124,16 +133,16 @@ private void SetupConvars() { RegisterListener(name => { - ConVar.Find("sv_cheats")?.SetValue(true); + ConVar.Find("sv_cheats")!.Value = true; - var numericCvar = ConVar.Find("mp_warmuptime"); - Logger.LogInformation("mp_warmuptime = {Value}", numericCvar?.GetPrimitiveValue()); + var numericCvar = ConVar.Find("mp_warmuptime"); + Logger.LogInformation("mp_warmuptime = {Value}", numericCvar?.Value); - var stringCvar = ConVar.Find("sv_skyname"); - Logger.LogInformation("sv_skyname = {Value}", stringCvar?.StringValue); + var stringCvar = ConVar.Find("sv_skyname"); + Logger.LogInformation("sv_skyname = {Value}", stringCvar?.Value); - var fogCvar = ConVar.Find("fog_color"); - Logger.LogInformation("fog_color = {Value}", fogCvar?.GetNativeValue()); + var fogCvar = ConVar.Find("fog_color"); + Logger.LogInformation("fog_color = {Value}", fogCvar?.Value); }); } @@ -205,10 +214,7 @@ private void SetupGameEvents() if (pawn == null) return HookResult.Continue; - Server.NextFrame(() => - { - player?.PrintToChat(activeWeapon?.DesignerName ?? "No Active Weapon"); - }); + Server.NextFrame(() => { player?.PrintToChat(activeWeapon?.DesignerName ?? "No Active Weapon"); }); // Set player to random colour pawn.Render = Color.FromArgb(Random.Shared.Next(0, 255), @@ -265,10 +271,7 @@ private void SetupListeners() }); // Hook global listeners defined by CounterStrikeSharp - RegisterListener(mapName => - { - Logger.LogInformation("Map {Map} has started!", mapName); - }); + RegisterListener(mapName => { Logger.LogInformation("Map {Map} has started!", mapName); }); RegisterListener(() => { Logger.LogInformation($"Map has ended."); }); RegisterListener((playerSlot, name, ip) => { @@ -357,21 +360,24 @@ private void SetupEntityOutputHooks() { HookEntityOutput("weapon_knife", "OnPlayerPickup", (output, _, activator, caller, _, delay) => { - Logger.LogInformation("weapon_knife called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", output.Description.Name, activator.DesignerName, caller.DesignerName, delay); + Logger.LogInformation("weapon_knife called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", + output.Description.Name, activator.DesignerName, caller.DesignerName, delay); return HookResult.Continue; }); HookEntityOutput("*", "*", (output, _, activator, caller, _, delay) => { - Logger.LogInformation("All EntityOutput ({name}, {activator}, {caller}, {delay})", output.Description.Name, activator.DesignerName, caller.DesignerName, delay); + Logger.LogInformation("All EntityOutput ({name}, {activator}, {caller}, {delay})", output.Description.Name, + activator.DesignerName, caller.DesignerName, delay); return HookResult.Continue; }); HookEntityOutput("*", "OnStartTouch", (_, name, activator, caller, _, delay) => { - Logger.LogInformation("OnStartTouch: ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay); + Logger.LogInformation("OnStartTouch: ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, + caller.DesignerName, delay); return HookResult.Continue; }); } @@ -609,9 +615,11 @@ private HookResult GenericEventHandler(T @event, GameEventInfo info) where T } [EntityOutputHook("*", "OnPlayerPickup")] - public HookResult OnPickup(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, float delay) + public HookResult OnPickup(CEntityIOOutput output, string name, CEntityInstance activator, CEntityInstance caller, CVariant value, + float delay) { - Logger.LogInformation("[EntityOutputHook Attribute] Called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", name, activator.DesignerName, caller.DesignerName, delay); + Logger.LogInformation("[EntityOutputHook Attribute] Called OnPlayerPickup ({name}, {activator}, {caller}, {delay})", name, + activator.DesignerName, caller.DesignerName, delay); return HookResult.Continue; } diff --git a/src/scripting/natives/natives_convars.cpp b/src/scripting/natives/natives_convars.cpp new file mode 100644 index 000000000..a8d8c2115 --- /dev/null +++ b/src/scripting/natives/natives_convars.cpp @@ -0,0 +1,517 @@ +/* + * This file is part of CounterStrikeSharp. + * CounterStrikeSharp is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * CounterStrikeSharp is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CounterStrikeSharp. If not, see . * + */ + +#define private public +#include "core/log.h" +#include "scripting/autonative.h" +#include "scripting/script_engine.h" + +#include +#include +#undef private + +namespace counterstrikesharp { + +static void SetConvarFlags(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto ref = ConVarRefAbstract(convarAccessIndex); + + if (!ref.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + auto flags = script_context.GetArgument(1); + ref.GetConVarData()->m_nFlags = flags; +} + +static void GetConvarFlags(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto ref = ConVarRefAbstract(convarAccessIndex); + + if (!ref.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + script_context.SetResult(ref.GetConVarData()->m_nFlags); +} + +static void GetConvarType(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto ref = ConVarRefAbstract(convarAccessIndex); + + if (!ref.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + script_context.SetResult(ref.GetConVarData()->GetType()); +} + +static void GetConvarName(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto ref = ConVarRefAbstract(convarAccessIndex); + + if (!ref.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + script_context.SetResult(ref.GetConVarData()->GetName()); +} + +static void GetConvarHelpText(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto ref = ConVarRefAbstract(convarAccessIndex); + + if (!ref.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + if (!ref.GetConVarData()->HasHelpText()) + { + return; + } + + script_context.SetResult(ref.GetConVarData()->GetHelpText()); +} + +static void GetConvarAccessIndexByName(ScriptContext& script_context) +{ + auto convarName = script_context.GetArgument(0); + ConVarRef ref(convarName); + + if (!ref.IsValidRef()) + { + script_context.SetResult(0); + return; + } + + script_context.SetResult(ref.GetAccessIndex()); +} + +static void GetConvarValueAsString(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto ref = ConVarRefAbstract(convarAccessIndex); + CSplitScreenSlot server(0); + + if (!ref.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + if (!ref.IsConVarDataValid()) + { + script_context.ThrowNativeError("Convar data is not valid for access index %d.", convarAccessIndex); + return; + } + + CBufferString buf; + ref.GetValueAsString(buf, server); + script_context.SetResult(buf.Get()); +} + +static void SetConvarValueAsString(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto ref = ConVarRefAbstract(convarAccessIndex); + CSplitScreenSlot server(0); + + if (!ref.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + if (!ref.IsConVarDataValid()) + { + script_context.ThrowNativeError("Convar data is not valid for access index %d.", convarAccessIndex); + return; + } + + auto value = script_context.GetArgument(1); + if (!ref.SetString(value, server)) + { + script_context.ThrowNativeError("Failed to set value for convar %s.", ref.GetName()); + return; + } +} + +static void GetConvarValue(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto cvar = ConVarRefAbstract(convarAccessIndex); + CSplitScreenSlot server(0); + + if (!cvar.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + if (!cvar.IsConVarDataValid()) + { + script_context.ThrowNativeError("Convar data is not valid for access index %d.", convarAccessIndex); + return; + } + + switch (cvar.GetType()) + { + case EConVarType_Int16: + { + script_context.SetResult(cvar.GetAs(server)); + break; + } + case EConVarType_UInt16: + { + script_context.SetResult(cvar.GetAs(server)); + break; + } + case EConVarType_UInt32: + { + script_context.SetResult(cvar.GetAs(server)); + break; + } + case EConVarType_Int32: + { + script_context.SetResult(cvar.GetAs(server)); + break; + } + case EConVarType_UInt64: + { + script_context.SetResult(cvar.GetAs(server)); + break; + } + case EConVarType_Int64: + { + script_context.SetResult(cvar.GetAs(server)); + break; + } + case EConVarType_Bool: + { + script_context.SetResult(cvar.GetAs(server)); + break; + } + case EConVarType_Float32: + { + script_context.SetResult((float)cvar.GetAs(server)); + break; + } + case EConVarType_Float64: + { + script_context.SetResult((double)cvar.GetAs(server)); + break; + } + case EConVarType_String: + { + script_context.SetResult(cvar.GetString(server).String()); + break; + } + case EConVarType_Color: + { + script_context.SetResult(&(cvar.GetConVarData()->ValueOrDefault(server)->m_clrValue)); + break; + } + case EConVarType_Vector2: + { + script_context.SetResult(&(cvar.GetConVarData()->ValueOrDefault(server)->m_vec2Value)); + break; + } + case EConVarType_Vector3: + { + script_context.SetResult(&(cvar.GetConVarData()->ValueOrDefault(server)->m_vec3Value)); + break; + } + case EConVarType_Vector4: + { + script_context.SetResult(&(cvar.GetConVarData()->ValueOrDefault(server)->m_vec4Value)); + break; + } + case EConVarType_Qangle: + { + script_context.SetResult(&(cvar.GetConVarData()->ValueOrDefault(server)->m_angValue)); + break; + } + default: + { + script_context.SetResult(nullptr); + } + } +} +static void SetConvarValue(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto cvar = ConVarRefAbstract(convarAccessIndex); + CSplitScreenSlot server(0); + + if (!cvar.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + if (!cvar.IsConVarDataValid()) + { + script_context.ThrowNativeError("Convar data is not valid for access index %d.", convarAccessIndex); + return; + } + + switch (cvar.GetType()) + { + case EConVarType_Int16: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_UInt16: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_UInt32: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_Int32: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_UInt64: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_Int64: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_Bool: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_Float32: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_Float64: + { + cvar.SetAs(script_context.GetArgument(1), server); + break; + } + case EConVarType_String: + { + cvar.SetString(script_context.GetArgument(1), server); + break; + } + case EConVarType_Vector2: + { + cvar.SetAs(*script_context.GetArgument(1), server); + break; + } + case EConVarType_Vector3: + { + cvar.SetAs(*script_context.GetArgument(1), server); + break; + } + case EConVarType_Vector4: + { + cvar.SetAs(*script_context.GetArgument(1), server); + break; + } + case EConVarType_Qangle: + { + cvar.SetAs(*script_context.GetArgument(1), server); + break; + } + default: + { + script_context.ThrowNativeError("Unsupported convar type: %d", cvar.GetType()); + } + } +} + +#define CREATE_CVAR(type) \ + auto createdConVar = new CConVar(name, flags, helpText, script_context.GetArgument(6), hasMin, \ + script_context.GetArgument(7), hasMax, script_context.GetArgument(8)); \ + createdConVarPtr = (void*)createdConVar; \ + createdConVarAccessIndex = createdConVar->GetAccessIndex(); + +#define CREATE_CVAR_PTR(type) \ + auto createdConVar = new CConVar(name, flags, helpText, *script_context.GetArgument(6), hasMin, \ + *script_context.GetArgument(7), hasMax, *script_context.GetArgument(8)); \ + createdConVarPtr = (void*)createdConVar; \ + createdConVarAccessIndex = createdConVar->GetAccessIndex(); + +static void CreateConVar(ScriptContext& script_context) +{ + auto name = script_context.GetArgument(0); + auto type = script_context.GetArgument(1); + auto helpText = script_context.GetArgument(2); + auto flags = script_context.GetArgument(3); + auto hasMin = script_context.GetArgument(4); + auto hasMax = script_context.GetArgument(5); + + // default, min, max is 6,7,8 + + ConVarRefAbstract cvar(name); + if (cvar.IsValidRef()) + { + script_context.ThrowNativeError("Convar with name '%s' already exists.", name); + return; + } + + uint16 createdConVarAccessIndex = 0; + void* createdConVarPtr = nullptr; + + switch (type) + { + case EConVarType_Int16: + { + CREATE_CVAR(int16); + break; + } + case EConVarType_UInt16: + { + CREATE_CVAR(uint16); + break; + } + case EConVarType_UInt32: + { + CREATE_CVAR(uint32); + break; + } + case EConVarType_Int32: + { + CREATE_CVAR(int32); + break; + } + case EConVarType_UInt64: + { + CREATE_CVAR(uint64); + break; + } + case EConVarType_Int64: + { + CREATE_CVAR(int64); + break; + } + case EConVarType_Bool: + { + CREATE_CVAR(bool); + break; + } + case EConVarType_Float32: + { + CREATE_CVAR(float32); + break; + } + case EConVarType_Float64: + { + CREATE_CVAR(float64); + break; + } + case EConVarType_String: + { + auto createdConVar = + new CConVar(name, flags, helpText, script_context.GetArgument(6), hasMin, + script_context.GetArgument(7), hasMax, script_context.GetArgument(8)); + createdConVarAccessIndex = createdConVar->GetAccessIndex(); + break; + } + case EConVarType_Vector2: + { + CREATE_CVAR_PTR(Vector2D); + break; + } + case EConVarType_Vector3: + { + CREATE_CVAR_PTR(Vector); + break; + } + case EConVarType_Vector4: + { + CREATE_CVAR_PTR(Vector4D); + break; + } + case EConVarType_Qangle: + { + CREATE_CVAR_PTR(QAngle); + break; + } + default: + { + script_context.ThrowNativeError("Unsupported convar type: %d", type); + return; + } + } + + script_context.SetResult(createdConVarAccessIndex); +} + +static void DeleteConVar(ScriptContext& script_context) +{ + auto convarAccessIndex = script_context.GetArgument(0); + auto ref = ConVarRefAbstract(convarAccessIndex); + + if (!ref.IsValidRef()) + { + script_context.ThrowNativeError("Invalid convar access index."); + return; + } + + if (ref.GetConVarData() == nullptr) + { + script_context.ThrowNativeError("Convar data is null."); + return; + } + + ref.GetConVarData()->Invalidate(); +} + +REGISTER_NATIVES(convars, { + ScriptEngine::RegisterNativeHandler("SET_CONVAR_FLAGS", SetConvarFlags); + ScriptEngine::RegisterNativeHandler("GET_CONVAR_FLAGS", GetConvarFlags); + ScriptEngine::RegisterNativeHandler("GET_CONVAR_TYPE", GetConvarType); + ScriptEngine::RegisterNativeHandler("GET_CONVAR_NAME", GetConvarName); + ScriptEngine::RegisterNativeHandler("GET_CONVAR_HELP_TEXT", GetConvarHelpText); + ScriptEngine::RegisterNativeHandler("GET_CONVAR_ACCESS_INDEX_BY_NAME", GetConvarAccessIndexByName); + ScriptEngine::RegisterNativeHandler("GET_CONVAR_VALUE", GetConvarValue); + ScriptEngine::RegisterNativeHandler("GET_CONVAR_VALUE_AS_STRING", GetConvarValueAsString); + ScriptEngine::RegisterNativeHandler("SET_CONVAR_VALUE_AS_STRING", SetConvarValueAsString); + ScriptEngine::RegisterNativeHandler("SET_CONVAR_VALUE", SetConvarValue); + ScriptEngine::RegisterNativeHandler("CREATE_CONVAR", CreateConVar); + ScriptEngine::RegisterNativeHandler("DELETE_CONVAR", DeleteConVar); +}) +} // namespace counterstrikesharp diff --git a/src/scripting/natives/natives_convars.yaml b/src/scripting/natives/natives_convars.yaml new file mode 100644 index 000000000..918084f72 --- /dev/null +++ b/src/scripting/natives/natives_convars.yaml @@ -0,0 +1,12 @@ +SET_CONVAR_FLAGS: convar:uint16,flags:uint64 -> void +GET_CONVAR_FLAGS: convar:uint16 -> uint64 +GET_CONVAR_TYPE: convar:uint16 -> int16 +GET_CONVAR_NAME: convar:uint16 -> string +GET_CONVAR_HELP_TEXT: convar:uint16 -> string +GET_CONVAR_ACCESS_INDEX_BY_NAME: name:string -> uint16 +GET_CONVAR_VALUE: convar:uint16 -> any +GET_CONVAR_VALUE_AS_STRING: convar:uint16 -> string +SET_CONVAR_VALUE_AS_STRING: convar:uint16, value:string -> void +SET_CONVAR_VALUE: convar:uint16, value:any -> void\ +CREATE_CONVAR: name:string, type:int16, helpText:string, flags:uint64, hasMin:bool, hasMax:bool, defaultValue:any, minValue:any, maxValue:any -> uint16 +DELETE_CONVAR: convar:uint16 -> void \ No newline at end of file diff --git a/tooling/CodeGen.Natives/Mapping.cs b/tooling/CodeGen.Natives/Mapping.cs index e12e2b26c..b5275a6bd 100644 --- a/tooling/CodeGen.Natives/Mapping.cs +++ b/tooling/CodeGen.Natives/Mapping.cs @@ -40,6 +40,10 @@ public static string GetCSharpType(string type) return "int"; case "uint": return "uint"; + case "int16": + return "short"; + case "uint16": + return "ushort"; case "bool": return "bool"; case "pointer":