Compare commits

...

8 Commits

Author SHA1 Message Date
Roflmuffin
983d91491d fix: update gamedata 2024-02-29 12:01:01 +10:00
BuSheeZy
71507b1e25 fix: allow using an empty flag array in overrides (#351)
Co-authored-by: Ryan Bucshon <busheezy@users.noreply.github.com>
Co-authored-by: Michael Wilson <roflmuffin@users.noreply.github.com>
2024-02-29 01:35:21 +00:00
B3none
cfe14b35fe [no ci] Bump the min api version in the fake convars example plugin (#350) 2024-02-28 15:08:23 +10:00
Poggu
5a6cdf0da3 feat: improve entity validation (#348) 2024-02-27 16:12:56 +10:00
Michael Wilson
a5399dd5fe feat: add FakeConVar class (#325) 2024-02-26 16:55:19 +10:00
Roflmuffin
ab996c34e9 fix: use function reference for next frame tasks
fixes #178
2024-02-26 09:20:56 +10:00
B3none
6e2e25b96e Re-implemented css_lang command (#343) 2024-02-26 08:56:16 +10:00
Roflmuffin
1142c9f063 [no ci] remove untriaged label when milestoning issues 2024-02-24 14:58:01 +10:00
35 changed files with 628 additions and 57 deletions

View File

@@ -0,0 +1,20 @@
name: Label new issues
on:
issues:
types:
- milestoned
jobs:
label_issues:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Remove label
run: gh issue edit "$NUMBER" --remove-label "$LABELS"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABELS: untriaged

View File

@@ -28,8 +28,8 @@
},
"CCSPlayerController_Respawn": {
"offsets": {
"windows": 245,
"linux": 247
"windows": 244,
"linux": 246
}
},
"CBasePlayerController_SetPawn": {

View File

@@ -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)]

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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;
}
}
}

View File

@@ -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.

View File

@@ -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>

View File

@@ -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(175)]
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));
}
}

View File

@@ -102,9 +102,18 @@ public class AdminTests
[Fact]
public void ShouldAddCommandPermissionOverridesAtRuntime()
{
Assert.False(AdminManager.CommandIsOverriden("runtime_command"));
AdminManager.AddPermissionOverride("runtime_command", "@runtime/override");
Assert.True(AdminManager.CommandIsOverriden("runtime_command"));
Assert.Equal("@runtime/override", AdminManager.GetPermissionOverrides("runtime_command").Single());
Assert.False(AdminManager.CommandIsOverriden("runtime_command_a"));
AdminManager.AddPermissionOverride("runtime_command_a", "@runtime/override");
Assert.True(AdminManager.CommandIsOverriden("runtime_command_a"));
Assert.Equal("@runtime/override", AdminManager.GetPermissionOverrides("runtime_command_a").Single());
}
[Fact]
public void ShouldAddCommandPermissionOverridesWithEmpty()
{
Assert.False(AdminManager.CommandIsOverriden("runtime_command_b"));
AdminManager.AddPermissionOverride("runtime_command_b");
Assert.True(AdminManager.CommandIsOverriden("runtime_command_b"));
Assert.False(AdminManager.GetPermissionOverrides("runtime_command_b").Any());
}
}

View File

@@ -427,6 +427,18 @@
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.NativeAPI.QueueTaskForNextFrame(System.IntPtr)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.NativeAPI.QueueTaskForNextWorldUpdate(System.IntPtr)</Target>
<Left>.\ApiCompat\v151.dll</Left>
<Right>obj\Debug\net7.0\CounterStrikeSharp.API.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:CounterStrikeSharp.API.Core.Plugin.Host.PluginManager.#ctor(CounterStrikeSharp.API.Core.Hosting.IScriptHostConfiguration,Microsoft.Extensions.Logging.ILogger{CounterStrikeSharp.API.Core.Plugin.Host.PluginManager},System.IServiceProvider)</Target>
@@ -1039,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>

View File

@@ -23,6 +23,7 @@ namespace CounterStrikeSharp.API
[Flags]
public enum ConVarFlags : Int64
{
FCVAR_NONE = 0,
FCVAR_LINKED_CONCOMMAND = (1 << 0),
FCVAR_DEVELOPMENTONLY =

View File

@@ -502,20 +502,20 @@ namespace CounterStrikeSharp.API.Core
}
}
public static void QueueTaskForNextFrame(IntPtr callback){
public static void QueueTaskForNextFrame(InputArgument callback){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(callback);
ScriptContext.GlobalScriptContext.Push((InputArgument)callback);
ScriptContext.GlobalScriptContext.SetIdentifier(0x9FE394D8);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();
}
}
public static void QueueTaskForNextWorldUpdate(IntPtr callback){
public static void QueueTaskForNextWorldUpdate(InputArgument callback){
lock (ScriptContext.GlobalScriptContext.Lock) {
ScriptContext.GlobalScriptContext.Reset();
ScriptContext.GlobalScriptContext.Push(callback);
ScriptContext.GlobalScriptContext.Push((InputArgument)callback);
ScriptContext.GlobalScriptContext.SetIdentifier(0xAD51A0C9);
ScriptContext.GlobalScriptContext.Invoke();
ScriptContext.GlobalScriptContext.CheckErrors();

View File

@@ -245,20 +245,17 @@ namespace CounterStrikeSharp.API.Core
break;
}
}
[CommandHelper(usage: "[language code, e.g. \"de\", \"pl\", \"en\"]",
whoCanExecute: CommandUsage.CLIENT_AND_SERVER)]
private void OnLangCommand(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
SteamID steamId = (SteamID)player.SteamID;
var steamId = (SteamID)player.SteamID;
if (command.ArgCount == 1)
{
var language = _playerLanguageManager.GetLanguage(steamId);
command.ReplyToCommand(string.Format("Current language is \"{0}\" ({1})", language.Name,
language.NativeName));
command.ReplyToCommand($"Current language is \"{language.Name}\" ({language.NativeName})");
return;
}
@@ -297,6 +294,11 @@ namespace CounterStrikeSharp.API.Core
" stop / unload - Unloads a plugin currently loaded.\n" +
" restart / reload - Reloads a plugin currently loaded.",
});
_commandManager.RegisterCommand(new("css_lang", "Set Counter-Strike Sharp language.", OnLangCommand)
{
ExecutableBy = CommandUsage.CLIENT_AND_SERVER,
UsageHint = "[language code, e.g. \"de\", \"pl\", \"en\"]",
});
}
}
}

View File

@@ -28,6 +28,7 @@ using CounterStrikeSharp.API.Modules.Commands;
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 @@ namespace CounterStrikeSharp.API.Core
this.RegisterAttributeHandlers(instance);
this.RegisterConsoleCommandAttributeHandlers(instance);
this.RegisterEntityOutputAttributeHandlers(instance);
this.RegisterFakeConVars(instance);
}
public void InitializeConfig(object instance, Type pluginType)
@@ -410,6 +412,43 @@ namespace CounterStrikeSharp.API.Core
}
}
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});
});
}
}
/// <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,

View File

@@ -22,9 +22,22 @@ using Microsoft.Extensions.Logging;
namespace CounterStrikeSharp.API.Core
{
/// <summary>
/// Describes the lifetime of a function reference.
/// </summary>
public enum FunctionLifetime
{
/// <summary>Delegate will be removed after the first invocation.</summary>
SingleUse,
/// <summary>Delegate will remain in memory for the lifetime of the application.</summary>
Permanent
}
public class FunctionReference
{
private readonly Delegate m_method;
public FunctionLifetime Lifetime { get; set; } = FunctionLifetime.Permanent;
public unsafe delegate void CallbackDelegate(fxScriptContext* context);
private CallbackDelegate s_callback;
@@ -59,12 +72,13 @@ namespace CounterStrikeSharp.API.Core
if (typeof(NativeObject).IsAssignableFrom(param.ParameterType))
{
obj = Activator.CreateInstance(param.ParameterType,
new[] {scriptContext.GetArgument(typeof(IntPtr), i)});
new[] { scriptContext.GetArgument(typeof(IntPtr), i) });
}
else
{
obj = scriptContext.GetArgument(param.ParameterType, i);
}
return obj;
}).ToArray();
@@ -79,6 +93,15 @@ namespace CounterStrikeSharp.API.Core
{
Application.Instance.Logger.LogError(e, "Error invoking callback");
}
finally
{
if (Lifetime == FunctionLifetime.SingleUse)
{
Remove(Identifier);
if (references.ContainsKey(m_method))
references.Remove(m_method);
}
}
});
s_callback = dg;
}
@@ -98,7 +121,7 @@ namespace CounterStrikeSharp.API.Core
var referenceId = Register(reference);
reference.Identifier = referenceId;
return reference;
}

View File

@@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using System;
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
@@ -6,14 +7,20 @@ namespace CounterStrikeSharp.API.Core;
public partial class CBaseEntity
{
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void Teleport(Vector position, QAngle angles, Vector velocity)
{
Guard.IsValidEntity(this);
VirtualFunction.CreateVoid<IntPtr, IntPtr, IntPtr, IntPtr>(Handle, GameData.GetOffset("CBaseEntity_Teleport"))(
Handle, position.Handle, angles.Handle, velocity.Handle);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void DispatchSpawn()
{
Guard.IsValidEntity(this);
VirtualFunctions.CBaseEntity_DispatchSpawn(Handle, IntPtr.Zero);
}
@@ -25,7 +32,13 @@ public partial class CBaseEntity
/// <summary>
/// Shorthand for accessing an entity's CBodyComponent?.SceneNode?.AbsRotation;
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public QAngle? AbsRotation => CBodyComponent?.SceneNode?.AbsRotation;
public T? GetVData<T>() where T : CEntitySubclassVDataBase => (T)Activator.CreateInstance(typeof(T), Marshal.ReadIntPtr(SubclassID.Handle + 4));
public T? GetVData<T>() where T : CEntitySubclassVDataBase
{
Guard.IsValidEntity(this);
return (T) Activator.CreateInstance(typeof(T), Marshal.ReadIntPtr(SubclassID.Handle + 4));
}
}

View File

@@ -20,8 +20,11 @@ namespace CounterStrikeSharp.API.Core;
public partial class CBaseModelEntity
{
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void SetModel(string model)
{
Guard.IsValidEntity(this);
VirtualFunctions.SetModel(Handle, model);
}
}

View File

@@ -1,5 +1,4 @@
using System;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Entities.Constants;
@@ -10,10 +9,13 @@ namespace CounterStrikeSharp.API.Core;
public partial class CBasePlayerController
{
public void SetPawn(CBasePlayerPawn? pawn)
{
if (pawn is null) return;
if (!pawn.IsValid) return;
VirtualFunctions.CBasePlayerController_SetPawnFunc.Invoke(this, pawn, true, false);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void SetPawn(CBasePlayerPawn? pawn)
{
Guard.IsValidEntity(this);
if (pawn is null) return;
if (!pawn.IsValid) return;
VirtualFunctions.CBasePlayerController_SetPawnFunc.Invoke(this, pawn, true, false);
}
}

View File

@@ -10,17 +10,24 @@ public partial class CBasePlayerPawn
/// </summary>
/// <param name="explode"></param>
/// <param name="force"></param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void CommitSuicide(bool explode, bool force)
{
Guard.IsValidEntity(this);
VirtualFunction.CreateVoid<IntPtr, bool, bool>(Handle, GameData.GetOffset("CBasePlayerPawn_CommitSuicide"))(Handle, explode, force);
}
/// <summary>
/// Remove Player Item
/// </summary>
/// <param name="weapon"></param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void RemovePlayerItem(CBasePlayerWeapon weapon)
{
Guard.IsValidEntity(this);
Guard.IsValidEntity(weapon);
VirtualFunctions.RemovePlayerItemVirtual(Handle, weapon.Handle);
}
}

View File

@@ -13,8 +13,11 @@ public partial class CCSPlayerController
public int? UserId => NativeAPI.GetUseridFromIndex((int)Index);
public CsTeam Team => (CsTeam)TeamNum;
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public IntPtr GiveNamedItem(string item)
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return 0;
if (PlayerPawn.Value == null) return 0;
if (!PlayerPawn.Value.IsValid) return 0;
@@ -34,25 +37,37 @@ public partial class CCSPlayerController
return GiveNamedItem(itemString);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void PrintToConsole(string message)
{
Guard.IsValidEntity(this);
NativeAPI.PrintToConsole((int)Index, $"{message}\n\0");
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void PrintToChat(string message)
{
Guard.IsValidEntity(this);
VirtualFunctions.ClientPrint(Handle, HudDestination.Chat, message, 0, 0, 0, 0);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void PrintToCenter(string message)
{
Guard.IsValidEntity(this);
VirtualFunctions.ClientPrint(Handle, HudDestination.Center, message, 0, 0, 0, 0);
}
public void PrintToCenterHtml(string message) => PrintToCenterHtml(message, 5);
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void PrintToCenterHtml(string message, int duration)
{
Guard.IsValidEntity(this);
var @event = new EventShowSurvivalRespawnStatus(true)
{
LocToken = message,
@@ -65,8 +80,10 @@ public partial class CCSPlayerController
/// <summary>
/// Drops the active player weapon on the ground.
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void DropActiveWeapon()
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return;
if (PlayerPawn.Value == null) return;
if (!PlayerPawn.Value.IsValid) return;
@@ -83,8 +100,10 @@ public partial class CCSPlayerController
/// <summary>
/// Removes every weapon from the player.
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void RemoveWeapons()
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return;
if (PlayerPawn.Value == null) return;
if (!PlayerPawn.Value.IsValid) return;
@@ -99,8 +118,10 @@ public partial class CCSPlayerController
/// </summary>
/// <param name="explode"></param>
/// <param name="force"></param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void CommitSuicide(bool explode, bool force)
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return;
if (PlayerPawn.Value == null) return;
if (!PlayerPawn.Value.IsValid) return;
@@ -111,8 +132,10 @@ public partial class CCSPlayerController
/// <summary>
/// Respawn player
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void Respawn()
{
Guard.IsValidEntity(this);
if (!PlayerPawn.IsValid) return;
if (PlayerPawn.Value == null) return;
if (!PlayerPawn.Value.IsValid) return;
@@ -128,8 +151,11 @@ public partial class CCSPlayerController
/// Forcibly switches the team of the player, the player will remain alive and keep their weapons.
/// </summary>
/// <param name="team">The team to switch to</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void SwitchTeam(CsTeam team)
{
Guard.IsValidEntity(this);
VirtualFunctions.SwitchTeam(Handle, (byte)team);
}
@@ -140,8 +166,11 @@ public partial class CCSPlayerController
/// </remarks>
/// </summary>
/// <param name="team">The team to change to</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void ChangeTeam(CsTeam team)
{
Guard.IsValidEntity(this);
VirtualFunction.CreateVoid<IntPtr, CsTeam>(Handle, GameData.GetOffset("CCSPlayerController_ChangeTeam"))(Handle,
team);
}
@@ -151,8 +180,11 @@ public partial class CCSPlayerController
/// </summary>
/// <param name="conVar">Name of the convar to retrieve</param>
/// <returns>ConVar string value</returns>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public string GetConVarValue(string conVar)
{
Guard.IsValidEntity(this);
return NativeAPI.GetClientConvarValue(Slot, conVar);
}
@@ -171,13 +203,12 @@ public partial class CCSPlayerController
/// </summary>
/// <param name="conVar">Console variable name</param>
/// <param name="value">String value to set</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
/// <exception cref="InvalidOperationException">Player is not a bot</exception>
public void SetFakeClientConVar(string conVar, string value)
{
if (!IsBot)
{
throw new InvalidOperationException("'SetFakeClientConVar' can only be called for fake clients (bots)");
}
Guard.IsValidEntity(this);
if (!IsBot) throw new InvalidOperationException("'SetFakeClientConVar' can only be called for fake clients (bots)");
NativeAPI.SetFakeClientConvarValue(Slot, conVar, value);
}
@@ -207,27 +238,45 @@ public partial class CCSPlayerController
/// Note: Only works for some commands, marked with the FCVAR_CLIENT_CAN_EXECUTE flag (not many).
/// </summary>
/// <param name="command"></param>
public void ExecuteClientCommand(string command) => NativeAPI.IssueClientCommand(Slot, command);
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void ExecuteClientCommand(string command)
{
Guard.IsValidEntity(this);
NativeAPI.IssueClientCommand(Slot, command);
}
/// <summary>
/// Issue the specified command directly from the server (mimics the server executing the command with the given player context).
/// <remarks>Works with server commands like `kill`, `explode`, `noclip`, etc. </remarks>
/// </summary>
/// <param name="command"></param>
public void ExecuteClientCommandFromServer(string command) => NativeAPI.IssueClientCommandFromServer(Slot, command);
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void ExecuteClientCommandFromServer(string command)
{
Guard.IsValidEntity(this);
NativeAPI.IssueClientCommandFromServer(Slot, command);
}
/// <summary>
/// Overrides who a player can hear in voice chat.
/// </summary>
/// <param name="sender">Player talking in the voice chat</param>
/// <param name="override">Whether the talker should be heard</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void SetListenOverride(CCSPlayerController sender, ListenOverride @override)
{
Guard.IsValidEntity(this);
NativeAPI.SetClientListening(Handle, sender.Handle, (Byte)@override);
}
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public ListenOverride GetListenOverride(CCSPlayerController sender)
{
Guard.IsValidEntity(this);
return NativeAPI.GetClientListening(Handle, sender.Handle);
}
@@ -236,27 +285,31 @@ public partial class CCSPlayerController
/// <summary>
/// Returns the authorized SteamID of this user which has been validated with the SteamAPI.
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public SteamID? AuthorizedSteamID
{
get
{
if (!IsValid) return null;
Guard.IsValidEntity(this);
var authorizedSteamId = NativeAPI.GetPlayerAuthorizedSteamid(Slot);
if ((long)authorizedSteamId == -1) return null;
return (SteamID)authorizedSteamId;
}
}
/// <summary>
/// Returns the IP address (and possibly port) of this player.
/// <remarks>Returns 127.0.0.1 if the player is a bot.</remarks>
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public string? IpAddress
{
get
{
if (!IsValid) return null;
Guard.IsValidEntity(this);
var ipAddress = NativeAPI.GetPlayerIpAddress(Slot);
if (string.IsNullOrWhiteSpace(ipAddress)) return null;
@@ -267,9 +320,20 @@ public partial class CCSPlayerController
/// <summary>
/// Determines how the player interacts with voice chat.
/// </summary>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public VoiceFlags VoiceFlags
{
get => (VoiceFlags)NativeAPI.GetClientVoiceFlags(Handle);
set => NativeAPI.SetClientVoiceFlags(Handle, (Byte)value);
get
{
Guard.IsValidEntity(this);
return (VoiceFlags)NativeAPI.GetClientVoiceFlags(Handle);
}
set
{
Guard.IsValidEntity(this);
NativeAPI.SetClientVoiceFlags(Handle, (Byte)value);
}
}
}

View File

@@ -23,16 +23,26 @@ public partial class CCSPlayer_ItemServices
/// <summary>
/// Drops the active player weapon on the ground.
/// </summary>
/// <exception cref="InvalidOperationException">ItemServices points to null</exception>
public void DropActivePlayerWeapon(CBasePlayerWeapon activeWeapon)
{
if(Handle == IntPtr.Zero)
throw new InvalidOperationException("ItemServices points to null.");
Guard.IsValidEntity(activeWeapon);
VirtualFunction.CreateVoid<nint, nint>(Handle, GameData.GetOffset("CCSPlayer_ItemServices_DropActivePlayerWeapon"))(Handle, activeWeapon.Handle);
}
/// <summary>
/// Removes every weapon from the player.
/// </summary>
/// <exception cref="InvalidOperationException">ItemServices points to null</exception>
public void RemoveWeapons()
{
if (Handle == IntPtr.Zero)
throw new InvalidOperationException("ItemServices points to null.");
VirtualFunction.CreateVoid<nint>(Handle, GameData.GetOffset("CCSPlayer_ItemServices_RemoveWeapons"))(Handle);
}
}

View File

@@ -32,7 +32,12 @@ public partial class CEntityInstance : IEquatable<CEntityInstance>
public string DesignerName => IsValid ? Entity?.DesignerName : null;
public void Remove() => VirtualFunctions.UTIL_Remove(this.Handle);
public void Remove()
{
Guard.IsValidEntity(this);
VirtualFunctions.UTIL_Remove(this.Handle);
}
public bool Equals(CEntityInstance? other)
{
@@ -58,7 +63,7 @@ public partial class CEntityInstance : IEquatable<CEntityInstance>
{
return !Equals(left, right);
}
/// <summary>
/// Calls a named input method on an entity.
/// <example>
@@ -72,8 +77,11 @@ public partial class CEntityInstance : IEquatable<CEntityInstance>
/// <param name="caller">Entity that is sending the event, <see langword="null"/> for no entity</param>
/// <param name="value">String variant value to send with the event</param>
/// <param name="outputId">Unknown, defaults to 0</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public void AcceptInput(string inputName, CEntityInstance? activator = null, CEntityInstance? caller = null, string value = "", int outputId = 0)
{
Guard.IsValidEntity(this);
VirtualFunctions.AcceptInput(Handle, inputName, activator?.Handle ?? IntPtr.Zero, caller?.Handle ?? IntPtr.Zero, value, 0);
}
}

View File

@@ -23,8 +23,11 @@ public partial class CGameSceneNode
/// <summary>
/// Gets the <see cref="CSkeletonInstance"/> instance from the node.
/// </summary>
/// <exception cref="InvalidOperationException">GameSceneNode points to null</exception>
public CSkeletonInstance GetSkeletonInstance()
{
if (Handle == IntPtr.Zero) throw new InvalidOperationException("GameSceneNode points to null.");
return new CSkeletonInstance(VirtualFunction.Create<nint, nint>(Handle, GameData.GetOffset("CGameSceneNode_GetSkeletonInstance"))(Handle));
}
}

View File

@@ -0,0 +1,11 @@
namespace CounterStrikeSharp.API
{
public static class Guard
{
public static void IsValidEntity(CEntityInstance ent)
{
if (!ent.IsValid)
throw new InvalidOperationException("Entity is not valid");
}
}
}

View File

@@ -62,7 +62,7 @@ namespace CounterStrikeSharp.API.Modules.Admin
{
CommandOverrides.TryGetValue(commandName, out var overrideDef);
if (overrideDef == null) return false;
return overrideDef.Enabled && overrideDef?.Flags.Count() > 0;
return overrideDef.Enabled;
}
/// <summary>

View File

@@ -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,
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);
}
}

View File

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

View File

@@ -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;
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@@ -78,11 +79,15 @@ public class Schema
public static T GetSchemaValue<T>(IntPtr handle, string className, string propertyName)
{
if (handle == IntPtr.Zero) throw new ArgumentNullException(nameof(handle), "Schema target points to null.");
return NativeAPI.GetSchemaValueByName<T>(handle, (int)typeof(T).ToDataType(), className, propertyName);
}
public static void SetSchemaValue<T>(IntPtr handle, string className, string propertyName, T value)
{
if (handle == IntPtr.Zero) throw new ArgumentNullException(nameof(handle), "Schema target points to null.");
if (CoreConfig.FollowCS2ServerGuidelines && _cs2BadList.Contains(propertyName))
{
throw new Exception($"Cannot set or get '{className}::{propertyName}' with \"FollowCS2ServerGuidelines\" option enabled.");
@@ -93,11 +98,15 @@ public class Schema
public static T GetDeclaredClass<T>(IntPtr pointer, string className, string memberName)
{
if (pointer == IntPtr.Zero) throw new ArgumentNullException(nameof(pointer), "Schema target points to null.");
return (T)Activator.CreateInstance(typeof(T), pointer + GetSchemaOffset(className, memberName));
}
public static unsafe ref T GetRef<T>(IntPtr pointer, string className, string memberName)
{
if (pointer == IntPtr.Zero) throw new ArgumentNullException(nameof(pointer), "Schema target points to null.");
return ref Unsafe.AsRef<T>((void*)(pointer + GetSchemaOffset(className, memberName)));
}
@@ -114,6 +123,8 @@ public class Schema
public static T GetPointer<T>(IntPtr pointer, string className, string memberName)
{
if (pointer == IntPtr.Zero) throw new ArgumentNullException(nameof(pointer), "Schema target points to null.");
var pointerTo = Marshal.ReadIntPtr(pointer + GetSchemaOffset(className, memberName));
if (pointerTo == IntPtr.Zero)
{
@@ -125,6 +136,8 @@ public class Schema
public static unsafe Span<T> GetFixedArray<T>(IntPtr pointer, string className, string memberName, int count)
{
if (pointer == IntPtr.Zero) throw new ArgumentNullException(nameof(pointer), "Schema target points to null.");
Span<T> span = new((void*)(pointer + GetSchemaOffset(className, memberName)), count);
return span;
}

View File

@@ -40,10 +40,6 @@ namespace CounterStrikeSharp.API
public static float GameFrameTime => NativeAPI.GetGameFrameTime();
public static double EngineTime => NativeAPI.GetEngineTime();
public static void PrecacheModel(string name) => NativeAPI.PrecacheModel(name);
// public static void PrecacheSound(string name) => Sound.PrecacheSound(name);
// Currently only used to keep the delegate from being garbage collected
private static List<Action> nextFrameTasks = new List<Action>();
/// <summary>
/// Queue a task to be executed on the next game frame.
@@ -51,9 +47,9 @@ namespace CounterStrikeSharp.API
/// </summary>
public static void NextFrame(Action task)
{
nextFrameTasks.Add(task);
var ptr = Marshal.GetFunctionPointerForDelegate(task);
NativeAPI.QueueTaskForNextFrame(ptr);
var functionReference = FunctionReference.Create(task);
functionReference.Lifetime = FunctionLifetime.SingleUse;
NativeAPI.QueueTaskForNextFrame(functionReference);
}
/// <summary>
@@ -63,9 +59,9 @@ namespace CounterStrikeSharp.API
/// <param name="task"></param>
public static void NextWorldUpdate(Action task)
{
nextFrameTasks.Add(task);
var ptr = Marshal.GetFunctionPointerForDelegate(task);
NativeAPI.QueueTaskForNextWorldUpdate(ptr);
var functionReference = FunctionReference.Create(task);
functionReference.Lifetime = FunctionLifetime.SingleUse;
NativeAPI.QueueTaskForNextWorldUpdate(functionReference);
}
public static void PrintToChatAll(string message)

View File

@@ -209,8 +209,11 @@ namespace CounterStrikeSharp.API
/// <param name="className" example="CBaseEntity">Schema field class name</param>
/// <param name="fieldName" example="m_iHealth">Schema field name</param>
/// <param name="extraOffset">Any additional offset to the schema field</param>
/// <exception cref="InvalidOperationException">Entity is not valid</exception>
public static void SetStateChanged(CBaseEntity entity, string className, string fieldName, int extraOffset = 0)
{
Guard.IsValidEntity(entity);
if (!Schema.IsSchemaFieldNetworked(className, fieldName))
{
Application.Instance.Logger.LogWarning("Field {ClassName}:{FieldName} is not networked, but SetStateChanged was called on it.", className, fieldName);

View File

@@ -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

View File

@@ -21,8 +21,8 @@ TRACE_FILTER_PROXY_SET_TRACE_TYPE_CALLBACK: trace_filter:pointer, callback:point
TRACE_FILTER_PROXY_SET_SHOULD_HIT_ENTITY_CALLBACK: trace_filter:pointer, callback:pointer -> void
NEW_TRACE_RESULT: -> pointer
GET_TICKED_TIME: -> double
QUEUE_TASK_FOR_NEXT_FRAME: callback:pointer -> void
QUEUE_TASK_FOR_NEXT_WORLD_UPDATE: callback:pointer -> void
QUEUE_TASK_FOR_NEXT_FRAME: callback:func -> void
QUEUE_TASK_FOR_NEXT_WORLD_UPDATE: callback:func -> void
GET_VALVE_INTERFACE: interfaceType:int, interfaceName:string -> pointer
GET_COMMAND_PARAM_VALUE: param:string, dataType:DataType_t, defaultValue:any -> any
PRINT_TO_SERVER_CONSOLE: msg:string -> void

View File

@@ -29,6 +29,12 @@
namespace counterstrikesharp {
CBaseEntity* GetEntityFromIndex(ScriptContext& script_context) {
if (!globals::entitySystem)
{
script_context.ThrowNativeError("Entity system is not yet initialized");
return nullptr;
}
auto entityIndex = script_context.GetArgument<int>(0);
return globals::entitySystem->GetBaseEntity(CEntityIndex(entityIndex));
@@ -47,6 +53,11 @@ const char* GetDesignerName(ScriptContext& scriptContext) {
}
void* GetEntityPointerFromHandle(ScriptContext& scriptContext) {
if (!globals::entitySystem) {
scriptContext.ThrowNativeError("Entity system is not yet initialized");
return nullptr;
}
auto handle = scriptContext.GetArgument<CEntityHandle*>(0);
if (!handle->IsValid()) {
@@ -57,6 +68,11 @@ void* GetEntityPointerFromHandle(ScriptContext& scriptContext) {
}
void* GetEntityPointerFromRef(ScriptContext& scriptContext) {
if (!globals::entitySystem) {
scriptContext.ThrowNativeError("Entity system yet is not initialized");
return nullptr;
}
auto ref = scriptContext.GetArgument<unsigned int>(0);
if (ref == INVALID_EHANDLE_INDEX) {
@@ -87,6 +103,11 @@ unsigned int GetRefFromEntityPointer(ScriptContext& scriptContext) {
}
bool IsRefValidEntity(ScriptContext& scriptContext) {
if (!globals::entitySystem) {
scriptContext.ThrowNativeError("Entity system yet is not initialized");
return false;
}
auto ref = scriptContext.GetArgument<unsigned int>(0);
if (ref == INVALID_EHANDLE_INDEX) {
@@ -110,10 +131,20 @@ void PrintToConsole(ScriptContext& scriptContext) {
}
CEntityIdentity* GetFirstActiveEntity(ScriptContext& script_context) {
if (!globals::entitySystem) {
script_context.ThrowNativeError("Entity system yet is not initialized");
return nullptr;
}
return globals::entitySystem->m_EntityList.m_pFirstActiveEntity;
}
void* GetConcreteEntityListPointer(ScriptContext& script_context) {
if (!globals::entitySystem) {
script_context.ThrowNativeError("Entity system yet is not initialized");
return nullptr;
}
return &globals::entitySystem->m_EntityList;
}