Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add FakeConVar class #325

Merged
merged 9 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docfx/examples/WithFakeConvars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[!INCLUDE [WithFakeConvars](../../examples/WithFakeConvars/README.md)]

<a href="https://github.com/roflmuffin/CounterStrikeSharp/tree/main/examples/WithFakeConvars" class="btn btn-secondary">View project on Github <i class="bi bi-github"></i></a>

[!code-csharp[](../../examples/WithFakeConvars/WithFakeConvarsPlugin.cs)]
2 changes: 2 additions & 0 deletions docfx/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions examples/WithFakeConvars/ConVars.cs
Original file line number Diff line number Diff line change
@@ -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<int> ExampleStaticCvar = new("example_static", "An example static cvar");
}
21 changes: 21 additions & 0 deletions examples/WithFakeConvars/EvenNumberValidator.cs
Original file line number Diff line number Diff line change
@@ -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<int>
{
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;
}
}
}
2 changes: 2 additions & 0 deletions examples/WithFakeConvars/README.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions examples/WithFakeConvars/WithFakeConvars.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\managed\CounterStrikeSharp.API\CounterStrikeSharp.API.csproj" />
</ItemGroup>
</Project>
58 changes: 58 additions & 0 deletions examples/WithFakeConvars/WithFakeConvarsPlugin.cs
Original file line number Diff line number Diff line change
@@ -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<bool> 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<int> ExampleIntCvar = new("example_int", "An example integer cvar", 10, flags: ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 100));

public FakeConVar<float> ExampleFloatCvar = new("example_float", "An example float cvar", 10, flags: ConVarFlags.FCVAR_NONE, new RangeValidator<float>(5, 20));
public FakeConVar<string> ExampleStringCvar = new("example_string", "An example string cvar", "default");

// Replicated, Cheat & Protected flags are supported.
public FakeConVar<float> 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<float> 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<float> 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<int> ExampleEvenNumberCvar = new("example_even_number", "An example even number cvar", 0, flags: ConVarFlags.FCVAR_NONE, new EvenNumberValidator());

public FakeConVar<int> 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));
}
}
12 changes: 12 additions & 0 deletions managed/CounterStrikeSharp.API/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1051,4 +1051,16 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP1002</DiagnosticId>
<Target>Microsoft.Extensions.Localization.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP1002</DiagnosticId>
<Target>Serilog, Version=2.0.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
</Suppressions>
1 change: 1 addition & 0 deletions managed/CounterStrikeSharp.API/ConVarFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace CounterStrikeSharp.API
[Flags]
public enum ConVarFlags : Int64
{
FCVAR_NONE = 0,
FCVAR_LINKED_CONCOMMAND = (1 << 0),

FCVAR_DEVELOPMENTONLY =
Expand Down
39 changes: 39 additions & 0 deletions managed/CounterStrikeSharp.API/Core/BasePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -308,6 +309,7 @@
this.RegisterAttributeHandlers(instance);
this.RegisterConsoleCommandAttributeHandlers(instance);
this.RegisterEntityOutputAttributeHandlers(instance);
this.RegisterFakeConVars(instance);
}

public void InitializeConfig(object instance, Type pluginType)
Expand Down Expand Up @@ -410,6 +412,43 @@
}
}

public void RegisterFakeConVars(Type type, object instance = null)

Check warning on line 415 in managed/CounterStrikeSharp.API/Core/BasePlugin.cs

View workflow job for this annotation

GitHub Actions / build_managed

Cannot convert null literal to non-nullable reference type.

Check warning on line 415 in managed/CounterStrikeSharp.API/Core/BasePlugin.cs

View workflow job for this annotation

GitHub Actions / build_managed

Cannot convert null literal to non-nullable reference type.
{
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});
});
}
}

/// <summary>
/// Used to bind a fake ConVar to a plugin command. Only required for ConVars that are not public properties of the plugin class.
/// </summary>
/// <param name="convar"></param>
/// <typeparam name="T"></typeparam>
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,
Expand Down
140 changes: 140 additions & 0 deletions managed/CounterStrikeSharp.API/Modules/Cvars/FakeConVar.cs
Original file line number Diff line number Diff line change
@@ -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<T> where T : IComparable<T>
{
private readonly IEnumerable<IValidator<T>>? _customValidators;

public FakeConVar(string name, string description, T defaultValue = default(T), ConVarFlags flags = ConVarFlags.FCVAR_NONE,

Check warning on line 12 in managed/CounterStrikeSharp.API/Modules/Cvars/FakeConVar.cs

View workflow job for this annotation

GitHub Actions / build_managed

Possible null reference assignment.

Check warning on line 12 in managed/CounterStrikeSharp.API/Modules/Cvars/FakeConVar.cs

View workflow job for this annotation

GitHub Actions / build_managed

Possible null reference assignment.
params IValidator<T>[] 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<T> 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)} = <protected>");
}
else
{
args.ReplyToCommand($"{args.GetArg(0)} = {Value.ToString()}");
}

return;
}

if (Flags.HasFlag(ConVarFlags.FCVAR_CHEAT))
{
var cheats = ConVar.Find("sv_cheats")!.GetPrimitiveValue<bool>();
if (!cheats)
{
args.ReplyToCommand($"SV: Convar '{Name}' is cheat protected, change ignored");
return;
}
}

if (player != null)
{
return;
}

try
{
// TODO(dotnet8): Replace with IParsable<T>
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CounterStrikeSharp.API.Modules.Cvars.Validators;

public interface IValidator<in T>
{
bool Validate(T value, out string? errorMessage);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace CounterStrikeSharp.API.Modules.Cvars.Validators;

public class RangeValidator<T> : IValidator<T> where T : IComparable<T>
{
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;
}
}
}
Loading
Loading