From 82495fa216fdd53239987faa75117ba26a5fb2ba Mon Sep 17 00:00:00 2001 From: Roflmuffin Date: Sat, 14 Oct 2023 23:11:37 +1000 Subject: [PATCH] feat: simplify listeners code --- .../Core/Attributes/ListenerNameAttribute.cs | 14 ++ .../ConsoleCommandAttribute.cs | 3 +- .../GameEventHandlerAttribute.cs | 3 +- .../CounterStrikeSharp.API/Core/BasePlugin.cs | 141 ++++-------------- .../Core/Listeners.g.cs | 33 ++++ .../Modules/Listeners/Listeners.cs | 54 ------- managed/TestPlugin/TestPlugin.cs | 13 +- src/scripting/listeners/general.yaml | 3 + src/scripting/listeners/players.yaml | 5 + tooling/CodeGen.Natives/Program.cs | 1 + .../Scripts/GenerateListeners.cs | 74 +++++++++ 11 files changed, 168 insertions(+), 176 deletions(-) create mode 100644 managed/CounterStrikeSharp.API/Core/Attributes/ListenerNameAttribute.cs rename managed/CounterStrikeSharp.API/Core/Attributes/{ => Registration}/ConsoleCommandAttribute.cs (78%) rename managed/CounterStrikeSharp.API/Core/Attributes/{ => Registration}/GameEventHandlerAttribute.cs (63%) create mode 100644 managed/CounterStrikeSharp.API/Core/Listeners.g.cs delete mode 100644 managed/CounterStrikeSharp.API/Modules/Listeners/Listeners.cs create mode 100644 src/scripting/listeners/general.yaml create mode 100644 src/scripting/listeners/players.yaml create mode 100644 tooling/CodeGen.Natives/Scripts/GenerateListeners.cs diff --git a/managed/CounterStrikeSharp.API/Core/Attributes/ListenerNameAttribute.cs b/managed/CounterStrikeSharp.API/Core/Attributes/ListenerNameAttribute.cs new file mode 100644 index 000000000..ea8083c20 --- /dev/null +++ b/managed/CounterStrikeSharp.API/Core/Attributes/ListenerNameAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace CounterStrikeSharp.API.Core.Attributes; + +[AttributeUsage(AttributeTargets.Delegate, Inherited = false)] +public class ListenerNameAttribute : Attribute +{ + public string Name { get; init; } + + public ListenerNameAttribute(string name) + { + Name = name; + } +} \ No newline at end of file diff --git a/managed/CounterStrikeSharp.API/Core/Attributes/ConsoleCommandAttribute.cs b/managed/CounterStrikeSharp.API/Core/Attributes/Registration/ConsoleCommandAttribute.cs similarity index 78% rename from managed/CounterStrikeSharp.API/Core/Attributes/ConsoleCommandAttribute.cs rename to managed/CounterStrikeSharp.API/Core/Attributes/Registration/ConsoleCommandAttribute.cs index 94cd9e84b..f64fe85a3 100644 --- a/managed/CounterStrikeSharp.API/Core/Attributes/ConsoleCommandAttribute.cs +++ b/managed/CounterStrikeSharp.API/Core/Attributes/Registration/ConsoleCommandAttribute.cs @@ -1,7 +1,6 @@ using System; -using CounterStrikeSharp.API.Modules.Events; -namespace CounterStrikeSharp.API.Core.Attributes; +namespace CounterStrikeSharp.API.Core.Attributes.Registration; [AttributeUsage(AttributeTargets.Method)] public class ConsoleCommandAttribute : Attribute diff --git a/managed/CounterStrikeSharp.API/Core/Attributes/GameEventHandlerAttribute.cs b/managed/CounterStrikeSharp.API/Core/Attributes/Registration/GameEventHandlerAttribute.cs similarity index 63% rename from managed/CounterStrikeSharp.API/Core/Attributes/GameEventHandlerAttribute.cs rename to managed/CounterStrikeSharp.API/Core/Attributes/Registration/GameEventHandlerAttribute.cs index 1944b3cb3..774622f75 100644 --- a/managed/CounterStrikeSharp.API/Core/Attributes/GameEventHandlerAttribute.cs +++ b/managed/CounterStrikeSharp.API/Core/Attributes/Registration/GameEventHandlerAttribute.cs @@ -1,7 +1,6 @@ using System; -using CounterStrikeSharp.API.Modules.Events; -namespace CounterStrikeSharp.API.Core.Attributes; +namespace CounterStrikeSharp.API.Core.Attributes.Registration; [AttributeUsage(AttributeTargets.Method)] public class GameEventHandlerAttribute : Attribute diff --git a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs index 3e5eccf5c..0d4d4a0c9 100644 --- a/managed/CounterStrikeSharp.API/Core/BasePlugin.cs +++ b/managed/CounterStrikeSharp.API/Core/BasePlugin.cs @@ -18,10 +18,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Loader; using CounterStrikeSharp.API.Core.Attributes; +using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Events; using CounterStrikeSharp.API.Modules.Listeners; @@ -187,41 +189,38 @@ public void UnhookConVarChange(ConVar convar, ConVar.ConVarChangedCallback handl CommandHandlers.Remove(handler); } }*/ - - private void AddListener(string name, Listeners.SourceEventHandler handler, - Action input = null, Action output = null) where T : EventArgs, new() + + // Adds global listener, e.g. OnTick, OnClientConnect + protected void RegisterListener(T handler) where T : Delegate { - var wrappedHandler = new Action(context => + var listenerName = typeof(T).GetCustomAttribute()?.Name; + if (string.IsNullOrEmpty(listenerName)) { - var eventArgs = new T(); - - // Before crossing the border, gets all the correct data from the context - input?.Invoke(eventArgs, context); + throw new Exception("Listener of type T is invalid and does not have a name attribute"); + } + + var parameterTypes = typeof(T).GetMethod("Invoke").GetParameters().Select(p => p.ParameterType).ToArray(); - // Invoke the actual event. - handler?.Invoke(eventArgs); + Console.WriteLine($"Registering listener for {listenerName} with {parameterTypes.Length}"); + + var wrappedHandler = new Action(context => + { + var args = new object[parameterTypes.Length]; + for (int i = 0; i < parameterTypes.Length; i++) + { + args[i] = context.GetArgument(parameterTypes[i], i); + } - // After crossing the border, puts all the correct "return" data back onto the context - output?.Invoke(eventArgs, context); + handler.DynamicInvoke(args); }); + + var subscriber = new CallbackSubscriber(handler, wrappedHandler, () => { RemoveListener(listenerName, handler); }); - var subscriber = new CallbackSubscriber(handler, wrappedHandler, () => { RemoveListener(name, handler); }); - - NativeAPI.AddListener(name, subscriber.GetInputArgument()); + NativeAPI.AddListener(listenerName, subscriber.GetInputArgument()); Listeners[handler] = subscriber; } - public void RemoveListener(string name, Listeners.SourceEventHandler handler) - where T : EventArgs, new() - { - if (!Listeners.TryGetValue(handler, out var subscriber)) return; - - NativeAPI.RemoveListener(name, subscriber.GetInputArgument()); - FunctionReference.Remove(subscriber.GetReferenceIdentifier()); - Listeners.Remove(handler); - } - - public void RemoveListener(string name, Delegate handler) + protected void RemoveListener(string name, Delegate handler) { if (!Listeners.TryGetValue(handler, out var subscriber)) return; @@ -236,95 +235,7 @@ public Timer AddTimer(float interval, Action callback, TimerFlags? flags = null) Timers.Add(timer); return timer; } - - public event Listeners.SourceEventHandler OnClientConnect - { - add => AddListener("OnClientConnect", value, - (args, context) => - { - args.PlayerIndex = context.GetArgument(0); - args.Name = context.GetArgument(1); - args.Address = context.GetArgument(2); - } - ); - remove => RemoveListener("OnClientConnect", value); - } - - public event Listeners.SourceEventHandler OnClientConnected - { - add => AddListener("OnClientConnected", value, - (args, context) => args.PlayerSlot = context.GetArgument(0)); - remove => RemoveListener("OnClientConnected", value); - } - - public event Listeners.SourceEventHandler OnClientDisconnect - { - add => AddListener("OnClientDisconnect", value, - (args, context) => args.PlayerSlot = context.GetArgument(0)); - remove => RemoveListener("OnClientDisconnect", value); - } - - public event Listeners.SourceEventHandler OnMapStart - { - add => AddListener("OnMapStart", value, - (args, context) => args.MapName = context.GetArgument(0)); - remove => RemoveListener("OnMapStart", value); - } - - public event Listeners.SourceEventHandler OnTick - { - add => AddListener("OnTick", value); - remove => RemoveListener("OnTick", value); - } - - public event Listeners.SourceEventHandler OnMapEnd - { - add => AddListener("OnMapEnd", value); - remove => RemoveListener("OnMapEnd", value); - } - - public event Listeners.SourceEventHandler OnClientDisconnectPost - { - add => AddListener("OnClientDisconnectPost", value, - (args, context) => args.PlayerSlot = context.GetArgument(0)); - remove => RemoveListener("OnClientDisconnectPost", value); - } - - public event Listeners.SourceEventHandler OnClientPutInServer - { - add => AddListener("OnClientPutInServer", value, - (args, context) => args.PlayerSlot = context.GetArgument(0)); - remove => RemoveListener("OnClientPutInServer", value); - } - - public event Listeners.SourceEventHandler OnEntityCreated - { - add => AddListener("OnEntityCreated", value, - (args, context) => - { - args.EntityIndex = context.GetArgument(0); - args.Classname = context.GetArgument(1); - }); - remove => RemoveListener("OnEntityCreated", value); - } - - public event Listeners.SourceEventHandler OnEntitySpawned - { - add => AddListener("OnEntitySpawned", value, - (args, context) => - { - args.EntityIndex = context.GetArgument(0); - args.Classname = context.GetArgument(1); - }); - remove => RemoveListener("OnEntitySpawned", value); - } - - public event Listeners.SourceEventHandler OnEntityDeleted - { - add => AddListener("OnEntityDeleted", value, - (args, context) => { args.EntityIndex = context.GetArgument(0); }); - remove => RemoveListener("OnEntityDeleted", value); - } + public void RegisterAllAttributes(object instance) { diff --git a/managed/CounterStrikeSharp.API/Core/Listeners.g.cs b/managed/CounterStrikeSharp.API/Core/Listeners.g.cs new file mode 100644 index 000000000..1897d8634 --- /dev/null +++ b/managed/CounterStrikeSharp.API/Core/Listeners.g.cs @@ -0,0 +1,33 @@ + +using System; +using CounterStrikeSharp.API.Core.Attributes; + +namespace CounterStrikeSharp.API.Core +{ + public partial class Listeners { + + [ListenerName("OnTick")] + public delegate void OnTick(); + + [ListenerName("OnMapStart")] + public delegate void OnMapStart(string mapName); + + [ListenerName("OnMapEnd")] + public delegate void OnMapEnd(); + + [ListenerName("OnClientConnect")] + public delegate void OnClientConnect(int index, string name, string ipAddress); + + [ListenerName("OnClientConnected")] + public delegate void OnClientConnected(int index); + + [ListenerName("OnClientPutInServer")] + public delegate void OnClientPutInServer(int index); + + [ListenerName("OnClientDisconnect")] + public delegate void OnClientDisconnect(int index); + + [ListenerName("OnClientDisconnectPost")] + public delegate void OnClientDisconnectPost(int index); + } +} diff --git a/managed/CounterStrikeSharp.API/Modules/Listeners/Listeners.cs b/managed/CounterStrikeSharp.API/Modules/Listeners/Listeners.cs deleted file mode 100644 index d002211cd..000000000 --- a/managed/CounterStrikeSharp.API/Modules/Listeners/Listeners.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 . * - */ - -using System; - -namespace CounterStrikeSharp.API.Modules.Listeners -{ - public partial class Listeners - { - public delegate void SourceEventHandler(object e); - - public delegate void SourceEventHandler(T e); - - public class MapStartArgs : EventArgs - { - public string MapName { get; set; } - } - - public class PlayerArgs : EventArgs - { - public int PlayerSlot { get; internal set; } - public bool Cancel { get; set; } - public string CancelReason { get; set; } - } - - public class EntityArgs : EventArgs - { - public int EntityIndex { get; internal set; } - public string Classname { get; set; } - } - - public class PlayerConnectArgs : EventArgs - { - public int PlayerIndex { get; internal set; } - public string Name { get; internal set; } - public string Address { get; internal set; } - public bool Cancel { get; set; } - public string CancelReason { get; set; } - } - } -} \ No newline at end of file diff --git a/managed/TestPlugin/TestPlugin.cs b/managed/TestPlugin/TestPlugin.cs index f7ce1de3b..921f368b8 100644 --- a/managed/TestPlugin/TestPlugin.cs +++ b/managed/TestPlugin/TestPlugin.cs @@ -19,6 +19,7 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; +using CounterStrikeSharp.API.Core.Attributes.Registration; using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Events; using CounterStrikeSharp.API.Modules.Memory; @@ -43,10 +44,16 @@ public override void Load(bool hotReload) { Log($"{@event.Userid}, {@event.X},{@event.Y},{@event.Z}"); }); - + // Hook global listeners defined by CounterStrikeSharp - OnMapStart += args => { Log($"Map {args.MapName} has started!"); }; - OnClientConnect += args => { Log($"Client {args.Name} from {args.Address} has connected!"); }; + RegisterListener(mapName => + { + Log($"Map {mapName} has started!"); + }); + RegisterListener((index, name, ip) => + { + Log($"Client {name} from {ip} has connected!"); + }); // You can use `ModuleDirectory` to get the directory of the plugin (for storing config files, saving database files etc.) File.WriteAllText(Path.Join(ModuleDirectory, "example.txt"), diff --git a/src/scripting/listeners/general.yaml b/src/scripting/listeners/general.yaml new file mode 100644 index 000000000..3a1ed6310 --- /dev/null +++ b/src/scripting/listeners/general.yaml @@ -0,0 +1,3 @@ +OnTick: +OnMapStart: mapName:string +OnMapEnd: \ No newline at end of file diff --git a/src/scripting/listeners/players.yaml b/src/scripting/listeners/players.yaml new file mode 100644 index 000000000..5c9d29162 --- /dev/null +++ b/src/scripting/listeners/players.yaml @@ -0,0 +1,5 @@ +OnClientConnect: index:int, name:string, ipAddress:string +OnClientConnected: index:int +OnClientPutInServer: index:int +OnClientDisconnect: index:int +OnClientDisconnectPost: index:int \ No newline at end of file diff --git a/tooling/CodeGen.Natives/Program.cs b/tooling/CodeGen.Natives/Program.cs index 8cd6e3c2e..a479b06dd 100644 --- a/tooling/CodeGen.Natives/Program.cs +++ b/tooling/CodeGen.Natives/Program.cs @@ -24,6 +24,7 @@ static void Main(string[] args) { Generators.GenerateNatives(); Generators.GenerateGameEvents(); + Generators.GenerateListeners(); } } } \ No newline at end of file diff --git a/tooling/CodeGen.Natives/Scripts/GenerateListeners.cs b/tooling/CodeGen.Natives/Scripts/GenerateListeners.cs new file mode 100644 index 000000000..996e950a6 --- /dev/null +++ b/tooling/CodeGen.Natives/Scripts/GenerateListeners.cs @@ -0,0 +1,74 @@ +using YamlDotNet.Serialization; + +namespace CodeGen.Natives.Scripts; + +public partial class Generators +{ + public readonly record struct ListenerDefinition(string Name, Dictionary Arguments); + + public static void GenerateListeners() + { + var pathToSearch = Path.Join(Helpers.GetRootDirectory(), "src/scripting/listeners"); + + var deserializer = new DeserializerBuilder() + .Build(); + + var listeners = new List(); + foreach (string file in Directory.EnumerateFiles(pathToSearch, "*yaml", SearchOption.AllDirectories) + .OrderBy(Path.GetFileName)) + { + var deserialized = deserializer.Deserialize>(File.ReadAllText(file)); + + foreach (var listenerName in deserialized.Keys) + { + var parameterString = deserialized[listenerName]; + + var parameters = new Dictionary(); + + if (!string.IsNullOrEmpty(parameterString)) + { + // Get each parameter, then map its type to the dictionary + // i.e. callback: pointer, name: string + parameters = parameterString + .Split(',') + .Select(part => part.Split(':')) + .ToDictionary( + pair => pair[0].Trim(), + pair => pair[1].Trim() + ); + } + + listeners.Add(new(listenerName, parameters)); + } + } + + + var outputString = string.Join("\n", listeners.Select(listener => + { + var arguments = string.Join(", ", + listener.Arguments.Select(pair => $"{Mapping.GetCSharpType(pair.Value)} {pair.Key}")).Trim(); + + return $@" + [ListenerName(""{listener.Name}"")] + public delegate void {listener.Name}({arguments});"; + })); + + + var result = $@" +using System; +using CounterStrikeSharp.API.Core.Attributes; + +namespace CounterStrikeSharp.API.Core +{{ + public partial class Listeners {{ + {outputString} + }} +}} +"; + + Console.WriteLine($"Generated C# bindings for {listeners.Count} listeners successfully."); + + File.WriteAllText(Path.Join(Helpers.GetRootDirectory(), "managed/CounterStrikeSharp.API/Core/Listeners.g.cs"), + result); + } +} \ No newline at end of file