diff --git a/docfx/examples/WithFakeConvars.md b/docfx/examples/WithFakeConvars.md
new file mode 100644
index 000000000..d32370e7e
--- /dev/null
+++ b/docfx/examples/WithFakeConvars.md
@@ -0,0 +1,5 @@
+[!INCLUDE [WithFakeConvars](../../examples/WithFakeConvars/README.md)]
+
+View project on Github
+
+[!code-csharp[](../../examples/WithFakeConvars/WithFakeConvarsPlugin.cs)]
\ No newline at end of file
diff --git a/docfx/examples/toc.yml b/docfx/examples/toc.yml
index 465bb9897..4fe532dd1 100644
--- a/docfx/examples/toc.yml
+++ b/docfx/examples/toc.yml
@@ -3,6 +3,8 @@ items:
href: HelloWorld.md
- name: Commands
href: WithCommands.md
+ - name: Fake ConVars
+ href: WithFakeConvars.md
- name: Config
href: WithConfig.md
- name: Dependency Injection
diff --git a/examples/WithFakeConvars/ConVars.cs b/examples/WithFakeConvars/ConVars.cs
new file mode 100644
index 000000000..63cf1d0c3
--- /dev/null
+++ b/examples/WithFakeConvars/ConVars.cs
@@ -0,0 +1,9 @@
+using CounterStrikeSharp.API.Modules.Cvars;
+
+namespace WithFakeConvars;
+
+public static class ConVars
+{
+ // This convar is registered from the plugin instance but can be used anywhere.
+ public static FakeConVar ExampleStaticCvar = new("example_static", "An example static cvar");
+}
\ No newline at end of file
diff --git a/examples/WithFakeConvars/EvenNumberValidator.cs b/examples/WithFakeConvars/EvenNumberValidator.cs
new file mode 100644
index 000000000..d13b216d5
--- /dev/null
+++ b/examples/WithFakeConvars/EvenNumberValidator.cs
@@ -0,0 +1,21 @@
+using CounterStrikeSharp.API.Modules.Cvars.Validators;
+
+namespace WithFakeConvars;
+
+// This is an example of a custom validator that checks if a number is even.
+public class EvenNumberValidator : IValidator
+{
+ public bool Validate(int value, out string? errorMessage)
+ {
+ if (value % 2 == 0)
+ {
+ errorMessage = null;
+ return true;
+ }
+ else
+ {
+ errorMessage = "Value must be an even number";
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/WithFakeConvars/README.md b/examples/WithFakeConvars/README.md
new file mode 100644
index 000000000..55f6fc066
--- /dev/null
+++ b/examples/WithFakeConvars/README.md
@@ -0,0 +1,2 @@
+# With Fake Convars
+This is an example that shows how to register "fake" convars, which are actually console commands that track their internal state.
\ No newline at end of file
diff --git a/examples/WithFakeConvars/WithFakeConvars.csproj b/examples/WithFakeConvars/WithFakeConvars.csproj
new file mode 100644
index 000000000..080fe0c26
--- /dev/null
+++ b/examples/WithFakeConvars/WithFakeConvars.csproj
@@ -0,0 +1,12 @@
+
+
+ net7.0
+ enable
+ enable
+ false
+ false
+
+
+
+
+
diff --git a/examples/WithFakeConvars/WithFakeConvarsPlugin.cs b/examples/WithFakeConvars/WithFakeConvarsPlugin.cs
new file mode 100644
index 000000000..d6f36d4c1
--- /dev/null
+++ b/examples/WithFakeConvars/WithFakeConvarsPlugin.cs
@@ -0,0 +1,58 @@
+using CounterStrikeSharp.API;
+using CounterStrikeSharp.API.Core;
+using CounterStrikeSharp.API.Core.Attributes;
+using CounterStrikeSharp.API.Modules.Cvars;
+using CounterStrikeSharp.API.Modules.Cvars.Validators;
+
+namespace WithFakeConvars;
+
+[MinimumApiVersion(168)]
+public class WithFakeConvarsPlugin : BasePlugin
+{
+ public override string ModuleName => "Example: With Fake Convars";
+ public override string ModuleVersion => "1.0.0";
+ public override string ModuleAuthor => "CounterStrikeSharp & Contributors";
+ public override string ModuleDescription => "A simple plugin that registers some console variables";
+
+ // FakeConVar is a class that can be used to create custom console variables.
+ // You can specify a name, description, default value, and custom validators.
+ public FakeConVar BoolCvar = new("example_bool", "An example boolean cvar", true);
+
+ // Range validator is an inbuilt validator that can be used to ensure that a value is within a certain range.
+ public FakeConVar ExampleIntCvar = new("example_int", "An example integer cvar", 10, flags: ConVarFlags.FCVAR_NONE, new RangeValidator(0, 100));
+
+ public FakeConVar ExampleFloatCvar = new("example_float", "An example float cvar", 10, flags: ConVarFlags.FCVAR_NONE, new RangeValidator(5, 20));
+ public FakeConVar ExampleStringCvar = new("example_string", "An example string cvar", "default");
+
+ // Replicated, Cheat & Protected flags are supported.
+ public FakeConVar ExamplePublicCvar = new("example_public_float", "An example public float cvar", 5,
+ ConVarFlags.FCVAR_REPLICATED);
+
+ // Can only be changed if sv_cheats is enabled.
+ public FakeConVar ExampleCheatCvar = new("example_cheat_float", "An example cheat float cvar", 5,
+ ConVarFlags.FCVAR_CHEAT);
+
+ // Protected cvars do not output their value when queried.
+ public FakeConVar ExampleProtectedCvar = new("example_protected_float", "An example cheat float cvar", 5,
+ ConVarFlags.FCVAR_PROTECTED);
+
+ // You can create your own custom validators by implementing the IValidator interface.
+ public FakeConVar ExampleEvenNumberCvar = new("example_even_number", "An example even number cvar", 0, flags: ConVarFlags.FCVAR_NONE, new EvenNumberValidator());
+
+ public FakeConVar RequiresRestartCvar = new("example_requires_restart", "A cvar that requires a restart when changed");
+
+ public override void Load(bool hotReload)
+ {
+ // You can subscribe to the ValueChanged event to execute code when the value of a cvar changes.
+ // In this example, we restart the game when the value of RequiresRestartCvar is greater than 5.
+ RequiresRestartCvar.ValueChanged += (sender, value) =>
+ {
+ if (value > 5)
+ {
+ Server.ExecuteCommand("mp_restartgame 1");
+ }
+ };
+
+ RegisterFakeConVars(typeof(ConVars));
+ }
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml b/managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml
index a95db9354..aed5411ea 100644
--- a/managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml
+++ b/managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml
@@ -1051,4 +1051,16 @@
.\ApiCompat\v151.dll
obj\Debug\net7.0\CounterStrikeSharp.API.dll
+
+ CP1002
+ Microsoft.Extensions.Localization.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
+ .\ApiCompat\v151.dll
+ obj\Debug\net7.0\CounterStrikeSharp.API.dll
+
+
+ CP1002
+ Serilog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10
+ .\ApiCompat\v151.dll
+ obj\Debug\net7.0\CounterStrikeSharp.API.dll
+
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/ConVarFlags.cs b/managed/CounterStrikeSharp.API/ConVarFlags.cs
index c8df80cd5..af4f8bda0 100644
--- a/managed/CounterStrikeSharp.API/ConVarFlags.cs
+++ b/managed/CounterStrikeSharp.API/ConVarFlags.cs
@@ -23,6 +23,7 @@ namespace CounterStrikeSharp.API
[Flags]
public enum ConVarFlags : Int64
{
+ FCVAR_NONE = 0,
FCVAR_LINKED_CONCOMMAND = (1 << 0),
FCVAR_DEVELOPMENTONLY =
diff --git a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs
index 813e90fed..60ff15abe 100644
--- a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs
+++ b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs
@@ -28,6 +28,7 @@
using CounterStrikeSharp.API.Modules.Events;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Config;
+using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
@@ -308,6 +309,7 @@ public void RegisterAllAttributes(object instance)
this.RegisterAttributeHandlers(instance);
this.RegisterConsoleCommandAttributeHandlers(instance);
this.RegisterEntityOutputAttributeHandlers(instance);
+ this.RegisterFakeConVars(instance);
}
public void InitializeConfig(object instance, Type pluginType)
@@ -410,6 +412,43 @@ public void RegisterEntityOutputAttributeHandlers(object instance)
}
}
+ public void RegisterFakeConVars(Type type, object instance = null)
+ {
+ var convars = type
+ .GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
+ .Where(prop => prop.FieldType.IsGenericType &&
+ prop.FieldType.GetGenericTypeDefinition() == typeof(FakeConVar<>));
+
+ foreach (var prop in convars)
+ {
+ object propValue = prop.GetValue(instance); // FakeConvar> instance
+ var propValueType = prop.FieldType.GenericTypeArguments[0];
+ var name = prop.FieldType.GetProperty("Name", BindingFlags.Public | BindingFlags.Instance)
+ .GetValue(propValue);
+
+ var description = prop.FieldType.GetProperty("Description", BindingFlags.Public | BindingFlags.Instance)
+ .GetValue(propValue);
+
+ MethodInfo executeCommandMethod = prop.FieldType
+ .GetMethod("ExecuteCommand", BindingFlags.Instance | BindingFlags.NonPublic);
+
+ this.AddCommand((string)name, (string) description, (caller, command) =>
+ {
+ executeCommandMethod.Invoke(propValue, new object[] {caller, command});
+ });
+ }
+ }
+
+ ///
+ /// Used to bind a fake ConVar to a plugin command. Only required for ConVars that are not public properties of the plugin class.
+ ///
+ ///
+ ///
+ public void RegisterFakeConVars(object instance)
+ {
+ RegisterFakeConVars(instance.GetType(), instance);
+ }
+
public void HookEntityOutput(string classname, string outputName, EntityIO.EntityOutputHandler handler, HookMode mode = HookMode.Pre)
{
var subscriber = new CallbackSubscriber(handler, handler,
diff --git a/managed/CounterStrikeSharp.API/Modules/Cvars/FakeConVar.cs b/managed/CounterStrikeSharp.API/Modules/Cvars/FakeConVar.cs
new file mode 100644
index 000000000..c867f476a
--- /dev/null
+++ b/managed/CounterStrikeSharp.API/Modules/Cvars/FakeConVar.cs
@@ -0,0 +1,140 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using CounterStrikeSharp.API.Modules.Commands;
+using CounterStrikeSharp.API.Modules.Cvars.Validators;
+
+namespace CounterStrikeSharp.API.Modules.Cvars;
+
+public class FakeConVar where T : IComparable
+{
+ private readonly IEnumerable>? _customValidators;
+
+ public FakeConVar(string name, string description, T defaultValue = default(T), ConVarFlags flags = ConVarFlags.FCVAR_NONE,
+ params IValidator[] customValidators)
+ {
+ _customValidators = customValidators;
+ Name = name;
+ Description = description;
+ Value = defaultValue;
+ Flags = flags;
+ }
+
+ public ConVarFlags Flags { get; set; }
+
+ public string Name { get; }
+ public string Description { get; }
+
+ public event EventHandler ValueChanged;
+
+ private T _value;
+
+ public T Value
+ {
+ get => _value;
+ set => SetValue(value);
+ }
+
+ internal void ExecuteCommand(CCSPlayerController? player, CommandInfo args)
+ {
+ if (player != null && !Flags.HasFlag(ConVarFlags.FCVAR_REPLICATED))
+ {
+ return;
+ }
+
+ if (args.ArgCount < 2)
+ {
+ if (Flags.HasFlag(ConVarFlags.FCVAR_PROTECTED) && player != null)
+ {
+ args.ReplyToCommand($"{args.GetArg(0)} = ");
+ }
+ else
+ {
+ args.ReplyToCommand($"{args.GetArg(0)} = {Value.ToString()}");
+ }
+
+ return;
+ }
+
+ if (Flags.HasFlag(ConVarFlags.FCVAR_CHEAT))
+ {
+ var cheats = ConVar.Find("sv_cheats")!.GetPrimitiveValue();
+ if (!cheats)
+ {
+ args.ReplyToCommand($"SV: Convar '{Name}' is cheat protected, change ignored");
+ return;
+ }
+ }
+
+ if (player != null)
+ {
+ return;
+ }
+
+ try
+ {
+ // TODO(dotnet8): Replace with IParsable
+ bool success = true;
+ T parsedValue = default(T);
+ TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
+ if (converter.CanConvertFrom(typeof(string)))
+ {
+ try
+ {
+ parsedValue = (T)converter.ConvertFromString(args.ArgString);
+ }
+ catch
+ {
+ success = typeof(T) == typeof(bool) && TryConvertCustomBoolean(args.ArgString, out parsedValue);
+ }
+ }
+
+ if (!success)
+ {
+ args.ReplyToCommand($"Error: String '{args.GetArg(1)}' can't be converted to {typeof(T).Name}");
+ args.ReplyToCommand($"Failed to parse input ConVar '{Name}' from string '{args.GetArg(1)}'");
+ return;
+ }
+
+ SetValue(parsedValue);
+ }
+ catch (Exception ex)
+ {
+ args.ReplyToCommand($"Error: {ex.Message}");
+ }
+ }
+
+ private bool TryConvertCustomBoolean(string input, out T result)
+ {
+ input = input.Trim().ToLowerInvariant();
+ if (input == "1" || input == "true")
+ {
+ result = (T)(object)true;
+ return true;
+ }
+ else if (input == "0" || input == "false")
+ {
+ result = (T)(object)false;
+ return true;
+ }
+
+ result = default(T);
+ return false;
+ }
+
+ private void SetValue(T value)
+ {
+ if (_customValidators != null)
+ {
+ foreach (var validator in _customValidators)
+ {
+ if (!validator.Validate(value, out var error))
+ {
+ throw new ArgumentException($"{error ?? "Invalid value provided"}");
+ }
+ }
+ }
+
+ _value = value;
+ ValueChanged?.Invoke(this, _value);
+ }
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/Modules/Cvars/Validators/IValidator.cs b/managed/CounterStrikeSharp.API/Modules/Cvars/Validators/IValidator.cs
new file mode 100644
index 000000000..e3f4dbc09
--- /dev/null
+++ b/managed/CounterStrikeSharp.API/Modules/Cvars/Validators/IValidator.cs
@@ -0,0 +1,6 @@
+namespace CounterStrikeSharp.API.Modules.Cvars.Validators;
+
+public interface IValidator
+{
+ bool Validate(T value, out string? errorMessage);
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.API/Modules/Cvars/Validators/RangeValidator.cs b/managed/CounterStrikeSharp.API/Modules/Cvars/Validators/RangeValidator.cs
new file mode 100644
index 000000000..fcb2666dc
--- /dev/null
+++ b/managed/CounterStrikeSharp.API/Modules/Cvars/Validators/RangeValidator.cs
@@ -0,0 +1,27 @@
+namespace CounterStrikeSharp.API.Modules.Cvars.Validators;
+
+public class RangeValidator : IValidator where T : IComparable
+{
+ private readonly T _min;
+ private readonly T _max;
+
+ public RangeValidator(T min, T max)
+ {
+ _min = min;
+ _max = max;
+ }
+
+ public bool Validate(T value, out string? errorMessage)
+ {
+ if (value.CompareTo(_min) >= 0 && value.CompareTo(_max) <= 0)
+ {
+ errorMessage = null;
+ return true;
+ }
+ else
+ {
+ errorMessage = $"Value must be between {_min} and {_max}";
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/managed/CounterStrikeSharp.sln b/managed/CounterStrikeSharp.sln
index 1fd97fcfd..25ce62def 100644
--- a/managed/CounterStrikeSharp.sln
+++ b/managed/CounterStrikeSharp.sln
@@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithTranslations", "..\exam
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithVoiceOverrides", "..\examples\WithVoiceOverrides\WithVoiceOverrides.csproj", "{6FA3107D-42AF-42A0-BF51-2230D13268B5}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WithFakeConvars", "..\examples\WithFakeConvars\WithFakeConvars.csproj", "{1309954E-FAF7-47A5-9FF9-C7263B33E4E3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -98,6 +100,10 @@ Global
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6FA3107D-42AF-42A0-BF51-2230D13268B5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1309954E-FAF7-47A5-9FF9-C7263B33E4E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1309954E-FAF7-47A5-9FF9-C7263B33E4E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1309954E-FAF7-47A5-9FF9-C7263B33E4E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1309954E-FAF7-47A5-9FF9-C7263B33E4E3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{57E64289-5D69-4AA1-BEF0-D0D96A55EE8F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
@@ -111,5 +117,6 @@ Global
{31EABE0B-871F-497B-BF36-37FFC6FAD15F} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{BB44E08E-CCA8-4E22-A132-11B2F69D1890} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
{6FA3107D-42AF-42A0-BF51-2230D13268B5} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
+ {1309954E-FAF7-47A5-9FF9-C7263B33E4E3} = {7DF99C35-881D-4FF2-B1C9-246BD3DECB9A}
EndGlobalSection
EndGlobal