From 325ae9aef200432e9a22591a5d3273fe7c3c5e29 Mon Sep 17 00:00:00 2001 From: Sebastian Jura <22455534+CrosRoad95@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:42:13 +0100 Subject: [PATCH 1/6] General improvments --- .../Controllers/TestCommandController.cs | 36 +++++++++++++-- .../Commands/BaseCommandController.cs | 18 +++++--- .../Commands/BoundCommand.cs | 2 +- .../Commands/CommandControllerLogic.cs | 44 +++++++++++++++++-- .../Contexts/CommandContext.cs | 11 +++-- 5 files changed, 92 insertions(+), 19 deletions(-) diff --git a/SlipeServer.Example/Controllers/TestCommandController.cs b/SlipeServer.Example/Controllers/TestCommandController.cs index 9d08ad20..71453024 100644 --- a/SlipeServer.Example/Controllers/TestCommandController.cs +++ b/SlipeServer.Example/Controllers/TestCommandController.cs @@ -5,13 +5,12 @@ using SlipeServer.Server.Elements; using SlipeServer.Server.Enums; using SlipeServer.Server.Services; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; +using System.Reflection; namespace SlipeServer.Example.Controllers; +internal class NoAccessAttribute : Attribute; + [CommandController()] public class TestCommandController : BaseCommandController { @@ -29,11 +28,40 @@ public TestCommandController(ChatBox chatBox, IElementCollection elementCollecti this.logger.LogInformation("Instantiating {type}", typeof(TestController)); } + protected override void Invoke(Action next) + { + try + { + if (this.Context.MethodInfo.GetCustomAttribute() != null) + { + this.chatBox.OutputTo(this.Context.Player, $"You can not access command {this.Context.Command}"); + } else + { + next(); + } + } + catch (Exception ex) + { + this.chatBox.OutputTo(this.Context.Player, $"Failed to execute command {this.Context.Command}"); + } + } + public void Chat(IEnumerable words) { this.chatBox.OutputTo(this.Context.Player, string.Join(' ', words)); } + [NoAccess] + public void NoAccess() + { + this.chatBox.OutputTo(this.Context.Player, "You have accessed command with NoAccess attribute!"); + } + + public void Oops() + { + throw new Exception("oops"); + } + public void Ping() { this.chatBox.OutputTo(this.Context.Player, $"Your ping is {this.Context.Player.Client.Ping}."); diff --git a/SlipeServer.LuaControllers/Commands/BaseCommandController.cs b/SlipeServer.LuaControllers/Commands/BaseCommandController.cs index 2aeb8348..88bcc75b 100644 --- a/SlipeServer.LuaControllers/Commands/BaseCommandController.cs +++ b/SlipeServer.LuaControllers/Commands/BaseCommandController.cs @@ -1,5 +1,6 @@ using SlipeServer.LuaControllers.Contexts; using SlipeServer.Server.Elements; +using System.Reflection; namespace SlipeServer.LuaControllers; @@ -23,12 +24,17 @@ internal void SetContext(CommandContext? context) this.context.Value = context; } - internal virtual void HandleCommand(Player player, string command, IEnumerable args, Func, object?> handler) + protected virtual void Invoke(Action next) { - this.SetContext(new CommandContext(player, command)); + next.Invoke(); + } + + internal virtual void HandleCommand(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, object?> handler) + { + this.SetContext(new CommandContext(player, command, args, methodInfo)); try { - handler.Invoke(args); + Invoke(() => handler.Invoke(args)); } finally { @@ -42,15 +48,15 @@ public abstract class BaseCommandController : BaseCommandController whe { public new CommandContext Context => (base.Context as CommandContext)!; - internal override void HandleCommand(Player player, string command, IEnumerable args, Func, object?> handler) + internal override void HandleCommand(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, object?> handler) { if (player is not TPlayer tPlayer) return; - this.SetContext(new CommandContext(tPlayer, command)); + this.SetContext(new CommandContext(tPlayer, command, args, methodInfo)); try { - handler.Invoke(args); + Invoke(() => handler.Invoke(args)); } finally { diff --git a/SlipeServer.LuaControllers/Commands/BoundCommand.cs b/SlipeServer.LuaControllers/Commands/BoundCommand.cs index b530623b..d3b1aa85 100644 --- a/SlipeServer.LuaControllers/Commands/BoundCommand.cs +++ b/SlipeServer.LuaControllers/Commands/BoundCommand.cs @@ -35,6 +35,6 @@ public void HandleCommand(Player player, string command, IEnumerable ar controller = (BaseCommandController)ActivatorUtilities.CreateInstance(scope.ServiceProvider, this.ControllerType); } - controller.HandleCommand(player, command, args, (values) => this.Method.Invoke(controller, values.ToArray())); + controller.HandleCommand(player, command, args, this.Method, (values) => this.Method.Invoke(controller, values.ToArray())); } } diff --git a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs index 38231843..d9a55194 100644 --- a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs +++ b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs @@ -15,12 +15,48 @@ public class CommandArgumentList(IEnumerable arguments) : IEnumerable arguments.GetEnumerator(); } +public class ControllerArgumentException : Exception +{ + public int Index { get; } + + public ControllerArgumentException(int index, Exception innerException) : base(null, innerException) + { + this.Index = index; + } +} + +public class ControllerArgumentsMapper +{ + private Dictionary> mappings = new Dictionary>(); + + public ControllerArgumentsMapper() { } + + public void DefineMap(Type type, Func map) + { + var targetType = map.Method.GetParameters()[0].ParameterType; + if (!this.mappings.ContainsKey(targetType)) + { + this.mappings[targetType] = map; + } + } + + internal object? MapParameter(Type targetType, string value) + { + if (this.mappings.TryGetValue(targetType, out var mapFunction)) + { + return mapFunction(targetType, value); + } + + return null; + } +} + public class CommandControllerLogic { private readonly MtaServer server; private readonly CommandService commandService; private readonly ILogger logger; - private readonly Dictionary> handlers = new(); + private readonly Dictionary> handlers = []; public CommandControllerLogic( MtaServer server, @@ -120,7 +156,7 @@ private void AddHandler(string command, Type type, MethodInfo method, BaseComman var parameters = method.GetParameters(); if (parameters.Length == 0) - return Array.Empty(); + return []; if (parameters.Length == 1 && parameters[0].ParameterType.IsAssignableTo(typeof(IEnumerable))) return [ new CommandArgumentList(values) ]; @@ -132,7 +168,7 @@ private void AddHandler(string command, Type type, MethodInfo method, BaseComman else if (values.Length <= i) objects.Add(parameters[i].DefaultValue); - return objects.ToArray(); + return [.. objects]; } private void HandleCommand(string command, CommandTriggeredEventArgs e) @@ -144,7 +180,7 @@ private void HandleCommand(string command, CommandTriggeredEventArgs e) { try { - var parameters = MapParameters(e.Arguments.ToArray(), handler.Method); + var parameters = MapParameters(e.Arguments, handler.Method); handler.HandleCommand(e.Player, command, parameters); } catch (Exception exception) diff --git a/SlipeServer.LuaControllers/Contexts/CommandContext.cs b/SlipeServer.LuaControllers/Contexts/CommandContext.cs index 4246ffee..4a65ef63 100644 --- a/SlipeServer.LuaControllers/Contexts/CommandContext.cs +++ b/SlipeServer.LuaControllers/Contexts/CommandContext.cs @@ -1,26 +1,29 @@ using SlipeServer.Server.Elements; +using System.Reflection; namespace SlipeServer.LuaControllers.Contexts; - public class CommandContext { public Player Player { get; } public string Command { get; } + public IEnumerable Arguments { get; } + public MethodInfo MethodInfo { get; } - public CommandContext(Player player, string command) + public CommandContext(Player player, string command, IEnumerable arguments, MethodInfo methodInfo) { this.Player = player; this.Command = command; + this.Arguments = arguments; + this.MethodInfo = methodInfo; } } - public class CommandContext : CommandContext where TPlayer : Player { public new TPlayer Player => (base.Player as TPlayer)!; - public CommandContext(TPlayer player, string command) : base(player, command) + public CommandContext(TPlayer player, string command, IEnumerable arguments, MethodInfo methodInfo) : base(player, command, arguments, methodInfo) { } } From edfc04cc7dec60056f189da877196ad07f61be94 Mon Sep 17 00:00:00 2001 From: Sebastian Jura <22455534+CrosRoad95@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:55:59 +0100 Subject: [PATCH 2/6] Defining custom types for argument parser --- .../Controllers/TestCommandController.cs | 7 +++- .../LuaControllersExampleLogic.cs | 22 ++++++++++++ .../ServerBuilderExtensions.cs | 1 + SlipeServer.Hosting/HostBuilderExtensions.cs | 4 ++- .../Commands/CommandControllerLogic.cs | 29 +++++++-------- .../LuaControllerServerBuilderExtensions.cs | 22 ++++++++++-- SlipeServer.WebHostBuilderExample/Program.cs | 3 ++ .../Properties/launchSettings.json | 35 +++++++++---------- 8 files changed, 87 insertions(+), 36 deletions(-) create mode 100644 SlipeServer.Example/LuaControllersExampleLogic.cs diff --git a/SlipeServer.Example/Controllers/TestCommandController.cs b/SlipeServer.Example/Controllers/TestCommandController.cs index 71453024..789777a4 100644 --- a/SlipeServer.Example/Controllers/TestCommandController.cs +++ b/SlipeServer.Example/Controllers/TestCommandController.cs @@ -66,6 +66,11 @@ public void Ping() { this.chatBox.OutputTo(this.Context.Player, $"Your ping is {this.Context.Player.Client.Ping}."); } + + public void SampleClass(SampleClass sampleClass) + { + this.chatBox.OutputTo(this.Context.Player, $"sampleClass: {sampleClass.Number}"); + } [Command("tp")] [Command("teleport")] @@ -84,7 +89,7 @@ public void GiveWeapon(WeaponId weapon, ushort ammoCount = 100) this.Context.Player.AddWeapon(weapon, ammoCount, true); } - [NoCommand()] + [NoCommand] public void NoCommand() { this.chatBox.OutputTo(this.Context.Player, $"This should not run."); diff --git a/SlipeServer.Example/LuaControllersExampleLogic.cs b/SlipeServer.Example/LuaControllersExampleLogic.cs new file mode 100644 index 00000000..9efa470d --- /dev/null +++ b/SlipeServer.Example/LuaControllersExampleLogic.cs @@ -0,0 +1,22 @@ +using SlipeServer.LuaControllers.Commands; + +namespace SlipeServer.Example; + +public class SampleClass +{ + public int Number { get; set; } +} + +public class LuaControllersExampleLogic +{ + public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper) + { + mapper.DefineMap(arg => + { + return new SampleClass + { + Number = int.Parse(arg) + }; + }); + } +} diff --git a/SlipeServer.Example/ServerBuilderExtensions.cs b/SlipeServer.Example/ServerBuilderExtensions.cs index eb7407ed..0bb0aef2 100644 --- a/SlipeServer.Example/ServerBuilderExtensions.cs +++ b/SlipeServer.Example/ServerBuilderExtensions.cs @@ -8,6 +8,7 @@ public static class ServerBuilderExtensions public static ServerBuilder AddExampleLogic(this ServerBuilder builder) { builder.AddLogic(); + builder.AddLogic(); builder.AddLuaControllers(); return builder; diff --git a/SlipeServer.Hosting/HostBuilderExtensions.cs b/SlipeServer.Hosting/HostBuilderExtensions.cs index c31b5629..9dddf68c 100644 --- a/SlipeServer.Hosting/HostBuilderExtensions.cs +++ b/SlipeServer.Hosting/HostBuilderExtensions.cs @@ -1,4 +1,6 @@ -namespace SlipeServer.Hosting; +using SlipeServer.Server.ServerBuilders; + +namespace SlipeServer.Hosting; public static class HostBuilderExtensions { diff --git a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs index d9a55194..d1b738c3 100644 --- a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs +++ b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs @@ -25,18 +25,17 @@ public ControllerArgumentException(int index, Exception innerException) : base(n } } -public class ControllerArgumentsMapper +public sealed class LuaControllerArgumentsMapper { - private Dictionary> mappings = new Dictionary>(); + private readonly Dictionary> mappings = []; - public ControllerArgumentsMapper() { } + public LuaControllerArgumentsMapper() { } - public void DefineMap(Type type, Func map) + public void DefineMap(Func map) { - var targetType = map.Method.GetParameters()[0].ParameterType; - if (!this.mappings.ContainsKey(targetType)) + if (!this.mappings.ContainsKey(typeof(T))) { - this.mappings[targetType] = map; + this.mappings[typeof(T)] = map; } } @@ -44,29 +43,31 @@ public void DefineMap(Type type, Func map) { if (this.mappings.TryGetValue(targetType, out var mapFunction)) { - return mapFunction(targetType, value); + return mapFunction(value); } return null; } } -public class CommandControllerLogic +public sealed class CommandControllerLogic { private readonly MtaServer server; private readonly CommandService commandService; private readonly ILogger logger; + private readonly LuaControllerArgumentsMapper argumentsMapper; private readonly Dictionary> handlers = []; public CommandControllerLogic( MtaServer server, CommandService commandService, - ILogger logger) + ILogger logger, + LuaControllerArgumentsMapper argumentsMapper) { this.server = server; this.commandService = commandService; this.logger = logger; - + this.argumentsMapper = argumentsMapper; IndexControllers(); } @@ -77,7 +78,7 @@ private void IndexControllers() .Where(x => x.IsAssignableTo(typeof(BaseCommandController))) .Where(x => !x.IsAbstract); - foreach (var controllerType in controllerTypes ?? Array.Empty()) + foreach (var controllerType in controllerTypes ?? []) { var controllerAttribute = controllerType .GetCustomAttributes() @@ -107,7 +108,7 @@ private void AddHandler(string command, Type type, MethodInfo method, BaseComman { if (!this.handlers.ContainsKey(command)) { - this.handlers[command] = new(); + this.handlers[command] = []; this.commandService.AddCommand(command, isCaseSensitive).Triggered += (_, args) => HandleCommand(command, args); } @@ -148,7 +149,7 @@ private void AddHandler(string command, Type type, MethodInfo method, BaseComman if (targetType.IsEnum) return Enum.Parse(targetType, value, true); - return JsonSerializer.Deserialize(value, targetType); + return argumentsMapper.MapParameter(targetType, value); } private object?[] MapParameters(string[] values, MethodInfo method) diff --git a/SlipeServer.LuaControllers/LuaControllerServerBuilderExtensions.cs b/SlipeServer.LuaControllers/LuaControllerServerBuilderExtensions.cs index 3b8ad632..4f4ef4e8 100644 --- a/SlipeServer.LuaControllers/LuaControllerServerBuilderExtensions.cs +++ b/SlipeServer.LuaControllers/LuaControllerServerBuilderExtensions.cs @@ -1,13 +1,31 @@ -using SlipeServer.LuaControllers.Commands; +using Microsoft.Extensions.DependencyInjection; +using SlipeServer.LuaControllers.Commands; using SlipeServer.Server.ServerBuilders; namespace SlipeServer.LuaControllers; public static class LuaControllerServerBuilderExtensions { - public static void AddLuaControllers(this ServerBuilder builder) + public static ServerBuilder AddLuaControllers(this ServerBuilder builder) { builder.AddLogic(); builder.AddLogic(); + + builder.ConfigureServices(services => + { + services.AddLuaControllers(); + }); + + return builder; + } +} + + +public static class LuaControllerServiceCollectionExtensions +{ + public static IServiceCollection AddLuaControllers(this IServiceCollection services) + { + services.AddSingleton(); + return services; } } diff --git a/SlipeServer.WebHostBuilderExample/Program.cs b/SlipeServer.WebHostBuilderExample/Program.cs index e3158116..ba51abd6 100644 --- a/SlipeServer.WebHostBuilderExample/Program.cs +++ b/SlipeServer.WebHostBuilderExample/Program.cs @@ -10,6 +10,7 @@ using SlipeServer.Example; using SlipeServer.Example.Services; using SlipeServer.Example.Elements; +using SlipeServer.LuaControllers; Directory.SetCurrentDirectory(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly()!.Location)!); @@ -51,6 +52,8 @@ serverBuilder.AddExampleLogic(); }); +builder.Services.AddLuaControllers(); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/SlipeServer.WebHostBuilderExample/Properties/launchSettings.json b/SlipeServer.WebHostBuilderExample/Properties/launchSettings.json index 0db0016b..bda37ca0 100644 --- a/SlipeServer.WebHostBuilderExample/Properties/launchSettings.json +++ b/SlipeServer.WebHostBuilderExample/Properties/launchSettings.json @@ -1,33 +1,23 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:47744", - "sslPort": 44303 - } - }, +{ "profiles": { "http": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5298", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5298" }, "https": { "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7042;http://localhost:5298", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7042;http://localhost:5298" }, "IIS Express": { "commandName": "IISExpress", @@ -37,5 +27,14 @@ "ASPNETCORE_ENVIRONMENT": "Development" } } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:47744", + "sslPort": 44303 + } } -} +} \ No newline at end of file From 958fbf89757ec883608fd1bdce4457897e834e91 Mon Sep 17 00:00:00 2001 From: Sebastian Jura <22455534+CrosRoad95@users.noreply.github.com> Date: Thu, 31 Oct 2024 15:18:18 +0100 Subject: [PATCH 3/6] Next --- .../Controllers/TestCommandController.cs | 5 + .../LuaControllersExampleLogic.cs | 27 ++++- .../Commands/BaseCommandController.cs | 13 +- .../Commands/CommandControllerLogic.cs | 98 +--------------- .../Commands/LuaControllerArgumentsMapper.cs | 111 ++++++++++++++++++ 5 files changed, 152 insertions(+), 102 deletions(-) create mode 100644 SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs diff --git a/SlipeServer.Example/Controllers/TestCommandController.cs b/SlipeServer.Example/Controllers/TestCommandController.cs index 789777a4..3c273793 100644 --- a/SlipeServer.Example/Controllers/TestCommandController.cs +++ b/SlipeServer.Example/Controllers/TestCommandController.cs @@ -71,6 +71,11 @@ public void SampleClass(SampleClass sampleClass) { this.chatBox.OutputTo(this.Context.Player, $"sampleClass: {sampleClass.Number}"); } + + public void FindPlayer(Player player) + { + this.chatBox.OutputTo(this.Context.Player, $"player: {player}"); + } [Command("tp")] [Command("teleport")] diff --git a/SlipeServer.Example/LuaControllersExampleLogic.cs b/SlipeServer.Example/LuaControllersExampleLogic.cs index 9efa470d..3ef4bacd 100644 --- a/SlipeServer.Example/LuaControllersExampleLogic.cs +++ b/SlipeServer.Example/LuaControllersExampleLogic.cs @@ -1,4 +1,7 @@ using SlipeServer.LuaControllers.Commands; +using SlipeServer.Server.ElementCollections; +using SlipeServer.Server.Elements; +using SlipeServer.Server.Services; namespace SlipeServer.Example; @@ -9,7 +12,10 @@ public class SampleClass public class LuaControllersExampleLogic { - public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper) + private readonly IElementCollection elementCollection; + private readonly ChatBox chatBox; + + public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper, IElementCollection elementCollection, ChatBox chatBox) { mapper.DefineMap(arg => { @@ -18,5 +24,24 @@ public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper) Number = int.Parse(arg) }; }); + mapper.DefineMap(arg => + { + return elementCollection.GetByType().Where(x => x.Name.Contains(arg)).FirstOrDefault(); + }); + + mapper.ArgumentErrorOccurred += HandleArgumentErrorOccurred; + this.elementCollection = elementCollection; + this.chatBox = chatBox; + } + + private void HandleArgumentErrorOccurred(Player player, LuaControllerArgumentException ex) + { + if(ex.InnerException is ArgumentOutOfRangeException) + { + this.chatBox.OutputTo(player, "Too many or too few arguments"); + } else + { + this.chatBox.OutputTo(player, "Error while executing command"); + } } } diff --git a/SlipeServer.LuaControllers/Commands/BaseCommandController.cs b/SlipeServer.LuaControllers/Commands/BaseCommandController.cs index 88bcc75b..f21b6d0b 100644 --- a/SlipeServer.LuaControllers/Commands/BaseCommandController.cs +++ b/SlipeServer.LuaControllers/Commands/BaseCommandController.cs @@ -12,10 +12,11 @@ public CommandContext Context { get { - if (this.context.Value == null) + var value = this.context.Value; + if (value == null) throw new Exception("Can not access BaseCommandController.Context outside of command handling methods."); - return this.context.Value; + return value; } } @@ -31,14 +32,14 @@ protected virtual void Invoke(Action next) internal virtual void HandleCommand(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, object?> handler) { - this.SetContext(new CommandContext(player, command, args, methodInfo)); + SetContext(new CommandContext(player, command, args, methodInfo)); try { Invoke(() => handler.Invoke(args)); } finally { - this.SetContext(null); + SetContext(null); } } } @@ -53,14 +54,14 @@ internal override void HandleCommand(Player player, string command, IEnumerable< if (player is not TPlayer tPlayer) return; - this.SetContext(new CommandContext(tPlayer, command, args, methodInfo)); + SetContext(new CommandContext(tPlayer, command, args, methodInfo)); try { Invoke(() => handler.Invoke(args)); } finally { - this.SetContext(null); + SetContext(null); } } } diff --git a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs index d1b738c3..068180dc 100644 --- a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs +++ b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs @@ -5,7 +5,6 @@ using SlipeServer.Server.Services; using System.Collections; using System.Reflection; -using System.Text.Json; namespace SlipeServer.LuaControllers.Commands; @@ -15,41 +14,6 @@ public class CommandArgumentList(IEnumerable arguments) : IEnumerable arguments.GetEnumerator(); } -public class ControllerArgumentException : Exception -{ - public int Index { get; } - - public ControllerArgumentException(int index, Exception innerException) : base(null, innerException) - { - this.Index = index; - } -} - -public sealed class LuaControllerArgumentsMapper -{ - private readonly Dictionary> mappings = []; - - public LuaControllerArgumentsMapper() { } - - public void DefineMap(Func map) - { - if (!this.mappings.ContainsKey(typeof(T))) - { - this.mappings[typeof(T)] = map; - } - } - - internal object? MapParameter(Type targetType, string value) - { - if (this.mappings.TryGetValue(targetType, out var mapFunction)) - { - return mapFunction(value); - } - - return null; - } -} - public sealed class CommandControllerLogic { private readonly MtaServer server; @@ -115,63 +79,6 @@ private void AddHandler(string command, Type type, MethodInfo method, BaseComman this.handlers[command].Add(new BoundCommand(this.server.Services, command, type, method, controller)); } - private object? MapParameter(Type targetType, string value) - { - if (targetType == typeof(string)) - return value; - - if (targetType.IsAssignableFrom(typeof(string))) - return targetType; - - if (targetType == typeof(byte)) - return byte.Parse(value); - - if (targetType == typeof(ushort)) - return ushort.Parse(value); - if (targetType == typeof(short)) - return short.Parse(value); - - if (targetType == typeof(uint)) - return uint.Parse(value); - if (targetType == typeof(int)) - return int.Parse(value); - - if (targetType == typeof(ulong)) - return ulong.Parse(value); - if (targetType == typeof(long)) - return long.Parse(value); - - if (targetType == typeof(float)) - return float.Parse(value); - if (targetType == typeof(double)) - return double.Parse(value); - - if (targetType.IsEnum) - return Enum.Parse(targetType, value, true); - - return argumentsMapper.MapParameter(targetType, value); - } - - private object?[] MapParameters(string[] values, MethodInfo method) - { - var parameters = method.GetParameters(); - - if (parameters.Length == 0) - return []; - - if (parameters.Length == 1 && parameters[0].ParameterType.IsAssignableTo(typeof(IEnumerable))) - return [ new CommandArgumentList(values) ]; - - var objects = new List(); - for (var i = 0; i < parameters.Length; i++) - if (!parameters[i].IsOptional || values.Length > i) - objects.Add(MapParameter(parameters[i].ParameterType, values[i])); - else if (values.Length <= i) - objects.Add(parameters[i].DefaultValue); - - return [.. objects]; - } - private void HandleCommand(string command, CommandTriggeredEventArgs e) { if (!this.handlers.TryGetValue(command, out var handlers)) @@ -181,8 +88,9 @@ private void HandleCommand(string command, CommandTriggeredEventArgs e) { try { - var parameters = MapParameters(e.Arguments, handler.Method); - handler.HandleCommand(e.Player, command, parameters); + var parameters = this.argumentsMapper.MapParameters(e.Player, e.Arguments, handler.Method); + if(parameters != null) + handler.HandleCommand(e.Player, command, parameters); } catch (Exception exception) { diff --git a/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs b/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs new file mode 100644 index 00000000..c100fdf6 --- /dev/null +++ b/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs @@ -0,0 +1,111 @@ +using SlipeServer.Server.Elements; +using System.Reflection; + +namespace SlipeServer.LuaControllers.Commands; + +public class LuaControllerArgumentException : Exception +{ + public int Index { get; } + public MethodInfo MethodInfo { get; } + + public LuaControllerArgumentException(int index, MethodInfo methodInfo, Exception innerException) : base(null, innerException) + { + this.Index = index; + this.MethodInfo = methodInfo; + } +} + +public sealed class LuaControllerArgumentsMapper +{ + private readonly Dictionary> mappings = []; + public event Action? ArgumentErrorOccurred; + public LuaControllerArgumentsMapper() { } + + public void DefineMap(Func map) + { + if (!this.mappings.ContainsKey(typeof(T))) + { + this.mappings[typeof(T)] = map; + } + } + + private object? MapCustomeParameter(Type targetType, string value) + { + if (this.mappings.TryGetValue(targetType, out var mapFunction)) + { + return mapFunction(value); + } + + return null; + } + + private object? MapParameter(Type targetType, string value) + { + if (targetType == typeof(string)) + return value; + + if (targetType.IsAssignableFrom(typeof(string))) + return targetType; + + if (targetType == typeof(byte)) + return byte.Parse(value); + + if (targetType == typeof(ushort)) + return ushort.Parse(value); + if (targetType == typeof(short)) + return short.Parse(value); + + if (targetType == typeof(uint)) + return uint.Parse(value); + if (targetType == typeof(int)) + return int.Parse(value); + + if (targetType == typeof(ulong)) + return ulong.Parse(value); + if (targetType == typeof(long)) + return long.Parse(value); + + if (targetType == typeof(float)) + return float.Parse(value); + if (targetType == typeof(double)) + return double.Parse(value); + + if (targetType.IsEnum) + return Enum.Parse(targetType, value, true); + + return MapCustomeParameter(targetType, value); + } + + internal object?[]? MapParameters(Player player, string[] values, MethodInfo method) + { + var parameters = method.GetParameters(); + + if (parameters.Length == 0) + return []; + + if (parameters.Length == 1 && parameters[0].ParameterType.IsAssignableTo(typeof(IEnumerable))) + return [new CommandArgumentList(values)]; + + int i = 0; + try + { + if (parameters.Length != values.Length) + { + throw new ArgumentOutOfRangeException(); + } + var objects = new List(); + for (; i < parameters.Length; i++) + if (!parameters[i].IsOptional || values.Length > i) + objects.Add(MapParameter(parameters[i].ParameterType, values[i])); + else if (values.Length <= i) + objects.Add(parameters[i].DefaultValue); + + return [.. objects]; + } + catch (Exception ex) + { + ArgumentErrorOccurred?.Invoke(player, new LuaControllerArgumentException(i, method, ex)); + } + return null; + } +} From eb2a3e06b61bd4c8f045d553c97aa27cdb10431d Mon Sep 17 00:00:00 2001 From: Sebastian Jura <22455534+CrosRoad95@users.noreply.github.com> Date: Thu, 31 Oct 2024 16:37:25 +0100 Subject: [PATCH 4/6] Async commands --- .../Controllers/TestCommandController.cs | 29 ++++++++ .../Attributes/NoCommandAttribute.cs | 6 +- .../Commands/BaseCommandController.cs | 70 ++++++++++++++++++- .../Commands/BoundCommand.cs | 34 ++++++++- .../Commands/CommandControllerLogic.cs | 65 +++++++++++++++-- .../Contexts/CommandContext.cs | 6 +- 6 files changed, 194 insertions(+), 16 deletions(-) diff --git a/SlipeServer.Example/Controllers/TestCommandController.cs b/SlipeServer.Example/Controllers/TestCommandController.cs index 3c273793..51ce3a7b 100644 --- a/SlipeServer.Example/Controllers/TestCommandController.cs +++ b/SlipeServer.Example/Controllers/TestCommandController.cs @@ -5,6 +5,7 @@ using SlipeServer.Server.Elements; using SlipeServer.Server.Enums; using SlipeServer.Server.Services; +using System.Diagnostics; using System.Reflection; namespace SlipeServer.Example.Controllers; @@ -46,6 +47,13 @@ protected override void Invoke(Action next) } } + protected override async Task InvokeAsync(Func next) + { + var stopwatch = Stopwatch.StartNew(); + await next(); + Console.WriteLine("Executed async command in: {0}ms", stopwatch.ElapsedMilliseconds); + } + public void Chat(IEnumerable words) { this.chatBox.OutputTo(this.Context.Player, string.Join(' ', words)); @@ -76,6 +84,27 @@ public void FindPlayer(Player player) { this.chatBox.OutputTo(this.Context.Player, $"player: {player}"); } + + public async Task Async() + { + this.chatBox.OutputTo(this.Context.Player, "Executing command..."); + await Task.Delay(1000); + this.chatBox.OutputTo(this.Context.Player, "Command executed!"); + } + + public async Task AsyncLong() + { + this.chatBox.OutputTo(this.Context.Player, $"Simulating long execution..."); + try + { + await Task.Delay(10_000, this.Context.CancellationToken); + this.chatBox.OutputTo(this.Context.Player, "Long command executed!"); + } + catch (OperationCanceledException) + { + Console.WriteLine("Failed to execute long async command :("); + } + } [Command("tp")] [Command("teleport")] diff --git a/SlipeServer.LuaControllers/Attributes/NoCommandAttribute.cs b/SlipeServer.LuaControllers/Attributes/NoCommandAttribute.cs index 559cca8c..d91de080 100644 --- a/SlipeServer.LuaControllers/Attributes/NoCommandAttribute.cs +++ b/SlipeServer.LuaControllers/Attributes/NoCommandAttribute.cs @@ -1,6 +1,4 @@ namespace SlipeServer.LuaControllers.Attributes; -[AttributeUsage(AttributeTargets.Method)] -public class NoCommandAttribute : Attribute -{ -} +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class NoCommandAttribute : Attribute; diff --git a/SlipeServer.LuaControllers/Commands/BaseCommandController.cs b/SlipeServer.LuaControllers/Commands/BaseCommandController.cs index f21b6d0b..4545435a 100644 --- a/SlipeServer.LuaControllers/Commands/BaseCommandController.cs +++ b/SlipeServer.LuaControllers/Commands/BaseCommandController.cs @@ -29,10 +29,23 @@ protected virtual void Invoke(Action next) { next.Invoke(); } + + protected virtual async Task InvokeAsync(Func next) + { + await next(); + } internal virtual void HandleCommand(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, object?> handler) { - SetContext(new CommandContext(player, command, args, methodInfo)); + var cts = new CancellationTokenSource(); + player.Disconnected += (sender, e) => + { + cts.Cancel(); + }; + if (player.IsDestroyed) + cts.Cancel(); + + SetContext(new CommandContext(player, command, args, methodInfo, cts.Token)); try { Invoke(() => handler.Invoke(args)); @@ -42,6 +55,27 @@ internal virtual void HandleCommand(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, Task> handler) + { + var cts = new CancellationTokenSource(); + player.Disconnected += (sender, e) => + { + cts.Cancel(); + }; + if (player.IsDestroyed) + cts.Cancel(); + + SetContext(new CommandContext(player, command, args, methodInfo, cts.Token)); + try + { + await InvokeAsync(async () => await handler(args)); + } + finally + { + SetContext(null); + } + } } @@ -54,7 +88,15 @@ internal override void HandleCommand(Player player, string command, IEnumerable< if (player is not TPlayer tPlayer) return; - SetContext(new CommandContext(tPlayer, command, args, methodInfo)); + var cts = new CancellationTokenSource(); + tPlayer.Disconnected += (sender, e) => + { + cts.Cancel(); + }; + if(tPlayer.IsDestroyed) + cts.Cancel(); + + SetContext(new CommandContext(tPlayer, command, args, methodInfo, cts.Token)); try { Invoke(() => handler.Invoke(args)); @@ -64,4 +106,28 @@ internal override void HandleCommand(Player player, string command, IEnumerable< SetContext(null); } } + + internal override async Task HandleCommandAsync(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, Task> handler) + { + if (player is not TPlayer tPlayer) + return; + + var cts = new CancellationTokenSource(); + tPlayer.Disconnected += (sender, e) => + { + cts.Cancel(); + }; + if (tPlayer.IsDestroyed) + cts.Cancel(); + + SetContext(new CommandContext(tPlayer, command, args, methodInfo, cts.Token)); + try + { + await InvokeAsync(async () => await handler.Invoke(args)); + } + finally + { + SetContext(null); + } + } } diff --git a/SlipeServer.LuaControllers/Commands/BoundCommand.cs b/SlipeServer.LuaControllers/Commands/BoundCommand.cs index d3b1aa85..94b0263f 100644 --- a/SlipeServer.LuaControllers/Commands/BoundCommand.cs +++ b/SlipeServer.LuaControllers/Commands/BoundCommand.cs @@ -4,7 +4,7 @@ namespace SlipeServer.LuaControllers.Commands; -public class BoundCommand +public abstract class BoundCommandBase { public IServiceProvider ServiceProvider { get; } public string Command { get; set; } @@ -12,7 +12,7 @@ public class BoundCommand public BaseCommandController? ControllerInstance { get; set; } public MethodInfo Method { get; set; } - public BoundCommand( + public BoundCommandBase( IServiceProvider serviceProvider, string command, Type controllerType, @@ -25,6 +25,13 @@ public BoundCommand( this.Method = method; this.ControllerInstance = controllerInstance; } +} + +public sealed class BoundCommand : BoundCommandBase +{ + public BoundCommand(IServiceProvider serviceProvider, string command, Type controllerType, MethodInfo method, BaseCommandController? controllerInstance) : base(serviceProvider, command, controllerType, method, controllerInstance) + { + } public void HandleCommand(Player player, string command, IEnumerable args) { @@ -38,3 +45,26 @@ public void HandleCommand(Player player, string command, IEnumerable ar controller.HandleCommand(player, command, args, this.Method, (values) => this.Method.Invoke(controller, values.ToArray())); } } + +public sealed class AsyncBoundCommand : BoundCommandBase +{ + public AsyncBoundCommand(IServiceProvider serviceProvider, string command, Type controllerType, MethodInfo method, BaseCommandController? controllerInstance) : base(serviceProvider, command, controllerType, method, controllerInstance) + { + } + + public async Task HandleCommand(Player player, string command, IEnumerable args) + { + var controller = this.ControllerInstance; + if (controller == null) + { + var scope = this.ServiceProvider.CreateScope(); + controller = (BaseCommandController)ActivatorUtilities.CreateInstance(scope.ServiceProvider, this.ControllerType); + } + + await controller.HandleCommandAsync(player, command, args, this.Method, async (values) => + { + var task = (Task)this.Method.Invoke(controller, values.ToArray())!; + await task; + }); + } +} diff --git a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs index 068180dc..6f41e068 100644 --- a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs +++ b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs @@ -20,7 +20,8 @@ public sealed class CommandControllerLogic private readonly CommandService commandService; private readonly ILogger logger; private readonly LuaControllerArgumentsMapper argumentsMapper; - private readonly Dictionary> handlers = []; + private readonly Dictionary> syncHandlers = []; + private readonly Dictionary> asyncHandlers = []; public CommandControllerLogic( MtaServer server, @@ -54,7 +55,10 @@ private void IndexControllers() foreach (var method in methods) { - if (!method.GetCustomAttributes().Any()) + if (method.GetCustomAttributes().Any()) + continue; + + if (method.ReturnType == typeof(void)) { var commandAttributes = method.GetCustomAttributes(); @@ -64,24 +68,45 @@ private void IndexControllers() if (!commandAttributes.Any()) AddHandler(method.Name, controllerType, method, controller); } + else if(method.ReturnType == typeof(Task)) + { + var commandAttributes = method.GetCustomAttributes(); + + foreach (var attribute in commandAttributes) + AddAsyncHandler(attribute.Command, controllerType, method, controller, attribute.IsCaseSensitive); + + if (!commandAttributes.Any()) + AddAsyncHandler(method.Name, controllerType, method, controller); + } } } } private void AddHandler(string command, Type type, MethodInfo method, BaseCommandController? controller, bool isCaseSensitive = false) { - if (!this.handlers.ContainsKey(command)) + if (!this.syncHandlers.ContainsKey(command)) { - this.handlers[command] = []; + this.syncHandlers[command] = []; this.commandService.AddCommand(command, isCaseSensitive).Triggered += (_, args) => HandleCommand(command, args); } - this.handlers[command].Add(new BoundCommand(this.server.Services, command, type, method, controller)); + this.syncHandlers[command].Add(new BoundCommand(this.server.Services, command, type, method, controller)); + } + + private void AddAsyncHandler(string command, Type type, MethodInfo method, BaseCommandController? controller, bool isCaseSensitive = false) + { + if (!this.asyncHandlers.ContainsKey(command)) + { + this.asyncHandlers[command] = []; + this.commandService.AddCommand(command, isCaseSensitive).Triggered += (_, args) => HandleAsyncCommand(command, args); + } + + this.asyncHandlers[command].Add(new AsyncBoundCommand(this.server.Services, command, type, method, controller)); } private void HandleCommand(string command, CommandTriggeredEventArgs e) { - if (!this.handlers.TryGetValue(command, out var handlers)) + if (!this.syncHandlers.TryGetValue(command, out var handlers)) return; foreach (var handler in handlers) @@ -98,4 +123,32 @@ private void HandleCommand(string command, CommandTriggeredEventArgs e) } } } + + private async Task HandleAsyncCommand(string command, CommandTriggeredEventArgs e) + { + try + { + if (!this.asyncHandlers.TryGetValue(command, out var handlers)) + return; + + foreach (var handler in handlers) + { + try + { + var parameters = this.argumentsMapper.MapParameters(e.Player, e.Arguments, handler.Method); + if (parameters != null) + await handler.HandleCommand(e.Player, command, parameters); + } + catch (Exception exception) + { + this.logger.LogError(exception, "An error occured while handling the command {event}:\n{message}", command, exception.Message); + } + } + + } + catch (Exception ex) + { + this.logger.LogError(ex, "Unhandled exception thrown while executing command {commandName}", command); + } + } } diff --git a/SlipeServer.LuaControllers/Contexts/CommandContext.cs b/SlipeServer.LuaControllers/Contexts/CommandContext.cs index 4a65ef63..59193a4f 100644 --- a/SlipeServer.LuaControllers/Contexts/CommandContext.cs +++ b/SlipeServer.LuaControllers/Contexts/CommandContext.cs @@ -9,13 +9,15 @@ public class CommandContext public string Command { get; } public IEnumerable Arguments { get; } public MethodInfo MethodInfo { get; } + public CancellationToken CancellationToken { get; } - public CommandContext(Player player, string command, IEnumerable arguments, MethodInfo methodInfo) + public CommandContext(Player player, string command, IEnumerable arguments, MethodInfo methodInfo, CancellationToken cancellationToken) { this.Player = player; this.Command = command; this.Arguments = arguments; this.MethodInfo = methodInfo; + this.CancellationToken = cancellationToken; } } @@ -23,7 +25,7 @@ public class CommandContext : CommandContext where TPlayer : Player { public new TPlayer Player => (base.Player as TPlayer)!; - public CommandContext(TPlayer player, string command, IEnumerable arguments, MethodInfo methodInfo) : base(player, command, arguments, methodInfo) + public CommandContext(TPlayer player, string command, IEnumerable arguments, MethodInfo methodInfo, CancellationToken cancellationToken) : base(player, command, arguments, methodInfo, cancellationToken) { } } From 8a949bb6bb86acb076a6402bb8c45b50292c9852 Mon Sep 17 00:00:00 2001 From: Sebastian Jura <22455534+CrosRoad95@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:49:41 +0100 Subject: [PATCH 5/6] Improve controllers commands --- SlipeServer.Console/Logic/ServerTestLogic.cs | 20 ------- .../Controllers/TestCommandController.cs | 3 +- .../{ => Logic}/LuaControllersExampleLogic.cs | 11 ++-- .../Logic/ResourcesExampleLogic.cs | 52 +++++++++++++++++++ .../{ => Logic}/ServerExampleLogic.cs | 4 +- .../Resources/TestResource/test.lua | 0 .../Resources/TestResource/test2.lua | 0 .../Resources/TestResource/vehicleWarp.lua | 0 .../ServerBuilderExtensions.cs | 4 +- .../SlipeServer.Example.csproj | 6 +++ .../Attributes/CommandControllerAttribute.cs | 2 +- .../Attributes/NoLuaEventAttribute.cs | 6 +-- .../Commands/BaseCommandController.cs | 35 ++----------- .../Commands/CommandControllerLogic.cs | 6 +-- .../Commands/LuaControllerArgumentsMapper.cs | 46 +++++++++------- .../Contexts/LuaEventContext.cs | 2 - SlipeServer.Server/Elements/Player.cs | 17 ++++++ 17 files changed, 125 insertions(+), 89 deletions(-) rename SlipeServer.Example/{ => Logic}/LuaControllersExampleLogic.cs (73%) create mode 100644 SlipeServer.Example/Logic/ResourcesExampleLogic.cs rename SlipeServer.Example/{ => Logic}/ServerExampleLogic.cs (80%) rename {SlipeServer.Console => SlipeServer.Example}/Resources/TestResource/test.lua (100%) rename {SlipeServer.Console => SlipeServer.Example}/Resources/TestResource/test2.lua (100%) rename {SlipeServer.Console => SlipeServer.Example}/Resources/TestResource/vehicleWarp.lua (100%) diff --git a/SlipeServer.Console/Logic/ServerTestLogic.cs b/SlipeServer.Console/Logic/ServerTestLogic.cs index 8ee9ac5f..a3f40c20 100644 --- a/SlipeServer.Console/Logic/ServerTestLogic.cs +++ b/SlipeServer.Console/Logic/ServerTestLogic.cs @@ -64,9 +64,6 @@ public class ServerTestLogic private readonly WeaponConfigurationService weaponConfigurationService; private readonly GameWorld gameWorld; private readonly IElementIdGenerator elementIdGenerator; - private Resource? testResource; - private Resource? secondTestResource; - private Resource? thirdTestResource; private readonly Random random = new(); private RadarArea? RadarArea { get; set; } @@ -160,14 +157,6 @@ private void SetupTestLogic() private void SetupTestElements() { - this.testResource = this.resourceProvider.GetResource("TestResource"); - this.secondTestResource = this.resourceProvider.GetResource("SecondTestResource"); - this.secondTestResource.NoClientScripts[$"{this.secondTestResource!.Name}/testfile.lua"] = - Encoding.UTF8.GetBytes("outputChatBox(\"I AM A NOT CACHED MESSAGE\")"); - this.secondTestResource.NoClientScripts[$"blabla.lua"] = new byte[] { }; - - this.thirdTestResource = this.resourceProvider.GetResource("MetaXmlTestResource"); - new WorldObject(321, new Vector3(5, 0, 3)).AssociateWith(this.server); new Water(new Vector3[] { @@ -844,11 +833,6 @@ void Player_Disconnected(Player sender, PlayerQuitEventArgs e) } }; - this.commandService.AddCommand("latent").Triggered += (source, args) => - { - this.luaService.TriggerLatentEvent("Slipe.Test.ClientEvent", this.testResource!, this.root, 1, this.root, 50, "STRING"); - }; - this.commandService.AddCommand("dim").Triggered += (source, args) => { if (args.Arguments.Length > 0) @@ -1534,10 +1518,6 @@ private void OnPlayerJoin(CustomPlayer player) player.Weapons.First(weapon => weapon.Type == WeaponId.Ak47).Ammo = 750; player.Weapons.First(weapon => weapon.Type == WeaponId.Ak47).AmmoInClip = 25; - this.testResource?.StartFor(player); - this.secondTestResource?.StartFor(player); - this.thirdTestResource?.StartFor(player); - this.HandlePlayerSubscriptions(player); player.AcInfoReceived += (o, args) => diff --git a/SlipeServer.Example/Controllers/TestCommandController.cs b/SlipeServer.Example/Controllers/TestCommandController.cs index 51ce3a7b..86dee148 100644 --- a/SlipeServer.Example/Controllers/TestCommandController.cs +++ b/SlipeServer.Example/Controllers/TestCommandController.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using SlipeServer.Example.Logic; using SlipeServer.LuaControllers; using SlipeServer.LuaControllers.Attributes; using SlipeServer.Server.ElementCollections; @@ -12,7 +13,7 @@ namespace SlipeServer.Example.Controllers; internal class NoAccessAttribute : Attribute; -[CommandController()] +[CommandController] public class TestCommandController : BaseCommandController { private readonly ChatBox chatBox; diff --git a/SlipeServer.Example/LuaControllersExampleLogic.cs b/SlipeServer.Example/Logic/LuaControllersExampleLogic.cs similarity index 73% rename from SlipeServer.Example/LuaControllersExampleLogic.cs rename to SlipeServer.Example/Logic/LuaControllersExampleLogic.cs index 3ef4bacd..3ac11992 100644 --- a/SlipeServer.Example/LuaControllersExampleLogic.cs +++ b/SlipeServer.Example/Logic/LuaControllersExampleLogic.cs @@ -3,7 +3,7 @@ using SlipeServer.Server.Elements; using SlipeServer.Server.Services; -namespace SlipeServer.Example; +namespace SlipeServer.Example.Logic; public class SampleClass { @@ -34,14 +34,13 @@ public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper, IElementC this.chatBox = chatBox; } - private void HandleArgumentErrorOccurred(Player player, LuaControllerArgumentException ex) + private void HandleArgumentErrorOccurred(Player player, Exception exception) { - if(ex.InnerException is ArgumentOutOfRangeException) - { + if (exception is ArgumentOutOfRangeException) this.chatBox.OutputTo(player, "Too many or too few arguments"); - } else + else if (exception is LuaControllerArgumentException ex) { - this.chatBox.OutputTo(player, "Error while executing command"); + this.chatBox.OutputTo(player, $"Error while executing command, argument at index {ex.Index + 1} expected {ex.ParameterInfo.ParameterType}, got '{ex.Argument}'"); } } } diff --git a/SlipeServer.Example/Logic/ResourcesExampleLogic.cs b/SlipeServer.Example/Logic/ResourcesExampleLogic.cs new file mode 100644 index 00000000..8a4372a8 --- /dev/null +++ b/SlipeServer.Example/Logic/ResourcesExampleLogic.cs @@ -0,0 +1,52 @@ +using SlipeServer.Lua; +using SlipeServer.Server; +using SlipeServer.Server.Elements; +using SlipeServer.Server.Resources; +using SlipeServer.Server.Resources.Providers; +using SlipeServer.Server.Services; +using System.Text; + +namespace SlipeServer.Example.Logic; + +public sealed class ResourcesExampleLogic +{ + private readonly ChatBox chatBox; + private readonly CommandService commandService; + private readonly LuaEventService luaEventService; + private readonly Resource? testResource; + private readonly Resource? secondTestResource; + private readonly Resource? thirdTestResource; + private readonly RootElement rootElement; + + public ResourcesExampleLogic(MtaServer mtaServer, IResourceProvider resourceProvider, ChatBox chatBox, CommandService commandService, LuaEventService luaEventService) + { + this.chatBox = chatBox; + this.commandService = commandService; + this.luaEventService = luaEventService; + this.rootElement = mtaServer.RootElement; + this.testResource = resourceProvider.GetResource("TestResource"); + this.secondTestResource = resourceProvider.GetResource("SecondTestResource"); + this.secondTestResource.NoClientScripts[$"{secondTestResource!.Name}/testfile.lua"] = + Encoding.UTF8.GetBytes("outputChatBox(\"I AM A NOT CACHED MESSAGE\")"); + this.secondTestResource.NoClientScripts[$"blabla.lua"] = new byte[] { }; + + this.thirdTestResource = resourceProvider.GetResource("MetaXmlTestResource"); + + mtaServer.PlayerJoined += HandlePlayerJoined; + } + + private void AddCommands() + { + this.commandService.AddCommand("latent").Triggered += (source, args) => + { + this.luaEventService.TriggerLatentEvent("Slipe.Test.ClientEvent", this.testResource!, this.rootElement, 1, this.rootElement, 50, "STRING"); + }; + } + private void HandlePlayerJoined(Player player) + { + this.testResource?.StartFor(player); + this.secondTestResource?.StartFor(player); + this.thirdTestResource?.StartFor(player); + this.chatBox.OutputTo(player, "Resources started"); + } +} diff --git a/SlipeServer.Example/ServerExampleLogic.cs b/SlipeServer.Example/Logic/ServerExampleLogic.cs similarity index 80% rename from SlipeServer.Example/ServerExampleLogic.cs rename to SlipeServer.Example/Logic/ServerExampleLogic.cs index fa73f447..b99ecda6 100644 --- a/SlipeServer.Example/ServerExampleLogic.cs +++ b/SlipeServer.Example/Logic/ServerExampleLogic.cs @@ -1,7 +1,7 @@ using SlipeServer.Server.Elements; using SlipeServer.Server.Services; -namespace SlipeServer.Example; +namespace SlipeServer.Example.Logic; public class ServerExampleLogic { @@ -21,7 +21,7 @@ public ServerExampleLogic(CommandService commandService, ChatBox chatBox) private void AddCommand(string command, Action callback) { - this.commandService.AddCommand(command).Triggered += (object? sender, Server.Events.CommandTriggeredEventArgs e) => + this.commandService.AddCommand(command).Triggered += (sender, e) => { callback(e.Player); }; diff --git a/SlipeServer.Console/Resources/TestResource/test.lua b/SlipeServer.Example/Resources/TestResource/test.lua similarity index 100% rename from SlipeServer.Console/Resources/TestResource/test.lua rename to SlipeServer.Example/Resources/TestResource/test.lua diff --git a/SlipeServer.Console/Resources/TestResource/test2.lua b/SlipeServer.Example/Resources/TestResource/test2.lua similarity index 100% rename from SlipeServer.Console/Resources/TestResource/test2.lua rename to SlipeServer.Example/Resources/TestResource/test2.lua diff --git a/SlipeServer.Console/Resources/TestResource/vehicleWarp.lua b/SlipeServer.Example/Resources/TestResource/vehicleWarp.lua similarity index 100% rename from SlipeServer.Console/Resources/TestResource/vehicleWarp.lua rename to SlipeServer.Example/Resources/TestResource/vehicleWarp.lua diff --git a/SlipeServer.Example/ServerBuilderExtensions.cs b/SlipeServer.Example/ServerBuilderExtensions.cs index 0bb0aef2..a2f4631e 100644 --- a/SlipeServer.Example/ServerBuilderExtensions.cs +++ b/SlipeServer.Example/ServerBuilderExtensions.cs @@ -1,4 +1,5 @@ -using SlipeServer.LuaControllers; +using SlipeServer.Example.Logic; +using SlipeServer.LuaControllers; using SlipeServer.Server.ServerBuilders; namespace SlipeServer.Example; @@ -9,6 +10,7 @@ public static ServerBuilder AddExampleLogic(this ServerBuilder builder) { builder.AddLogic(); builder.AddLogic(); + builder.AddLogic(); builder.AddLuaControllers(); return builder; diff --git a/SlipeServer.Example/SlipeServer.Example.csproj b/SlipeServer.Example/SlipeServer.Example.csproj index 327d398a..7e0e9985 100644 --- a/SlipeServer.Example/SlipeServer.Example.csproj +++ b/SlipeServer.Example/SlipeServer.Example.csproj @@ -20,4 +20,10 @@ + + + + Always + + diff --git a/SlipeServer.LuaControllers/Attributes/CommandControllerAttribute.cs b/SlipeServer.LuaControllers/Attributes/CommandControllerAttribute.cs index 4c7ec384..2b90bb38 100644 --- a/SlipeServer.LuaControllers/Attributes/CommandControllerAttribute.cs +++ b/SlipeServer.LuaControllers/Attributes/CommandControllerAttribute.cs @@ -1,6 +1,6 @@ namespace SlipeServer.LuaControllers.Attributes; -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class CommandControllerAttribute(bool usesScopedEvents = true) : Attribute { public bool UsesScopedCommands { get; } = usesScopedEvents; diff --git a/SlipeServer.LuaControllers/Attributes/NoLuaEventAttribute.cs b/SlipeServer.LuaControllers/Attributes/NoLuaEventAttribute.cs index fd7b29bd..339880f1 100644 --- a/SlipeServer.LuaControllers/Attributes/NoLuaEventAttribute.cs +++ b/SlipeServer.LuaControllers/Attributes/NoLuaEventAttribute.cs @@ -1,6 +1,4 @@ namespace SlipeServer.LuaControllers.Attributes; -[AttributeUsage(AttributeTargets.Method)] -public class NoLuaEventAttribute : Attribute -{ -} +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class NoLuaEventAttribute : Attribute; diff --git a/SlipeServer.LuaControllers/Commands/BaseCommandController.cs b/SlipeServer.LuaControllers/Commands/BaseCommandController.cs index 4545435a..3b808bca 100644 --- a/SlipeServer.LuaControllers/Commands/BaseCommandController.cs +++ b/SlipeServer.LuaControllers/Commands/BaseCommandController.cs @@ -30,7 +30,7 @@ protected virtual void Invoke(Action next) next.Invoke(); } - protected virtual async Task InvokeAsync(Func next) + protected async virtual Task InvokeAsync(Func next) { await next(); } @@ -45,7 +45,7 @@ internal virtual void HandleCommand(Player player, string command, IEnumerable handler.Invoke(args)); @@ -58,15 +58,7 @@ internal virtual void HandleCommand(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, Task> handler) { - var cts = new CancellationTokenSource(); - player.Disconnected += (sender, e) => - { - cts.Cancel(); - }; - if (player.IsDestroyed) - cts.Cancel(); - - SetContext(new CommandContext(player, command, args, methodInfo, cts.Token)); + SetContext(new CommandContext(player, command, args, methodInfo, player.GetCancellationToken())); try { await InvokeAsync(async () => await handler(args)); @@ -78,7 +70,6 @@ internal virtual async Task HandleCommandAsync(Player player, string command, IE } } - public abstract class BaseCommandController : BaseCommandController where TPlayer : Player { public new CommandContext Context => (base.Context as CommandContext)!; @@ -88,15 +79,7 @@ internal override void HandleCommand(Player player, string command, IEnumerable< if (player is not TPlayer tPlayer) return; - var cts = new CancellationTokenSource(); - tPlayer.Disconnected += (sender, e) => - { - cts.Cancel(); - }; - if(tPlayer.IsDestroyed) - cts.Cancel(); - - SetContext(new CommandContext(tPlayer, command, args, methodInfo, cts.Token)); + SetContext(new CommandContext(tPlayer, command, args, methodInfo, tPlayer.GetCancellationToken())); try { Invoke(() => handler.Invoke(args)); @@ -112,15 +95,7 @@ internal override async Task HandleCommandAsync(Player player, string command, I if (player is not TPlayer tPlayer) return; - var cts = new CancellationTokenSource(); - tPlayer.Disconnected += (sender, e) => - { - cts.Cancel(); - }; - if (tPlayer.IsDestroyed) - cts.Cancel(); - - SetContext(new CommandContext(tPlayer, command, args, methodInfo, cts.Token)); + SetContext(new CommandContext(tPlayer, command, args, methodInfo, tPlayer.GetCancellationToken())); try { await InvokeAsync(async () => await handler.Invoke(args)); diff --git a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs index 6f41e068..71ff842e 100644 --- a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs +++ b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs @@ -18,7 +18,7 @@ public sealed class CommandControllerLogic { private readonly MtaServer server; private readonly CommandService commandService; - private readonly ILogger logger; + private readonly ILogger logger; private readonly LuaControllerArgumentsMapper argumentsMapper; private readonly Dictionary> syncHandlers = []; private readonly Dictionary> asyncHandlers = []; @@ -26,7 +26,7 @@ public sealed class CommandControllerLogic public CommandControllerLogic( MtaServer server, CommandService commandService, - ILogger logger, + ILogger logger, LuaControllerArgumentsMapper argumentsMapper) { this.server = server; @@ -124,7 +124,7 @@ private void HandleCommand(string command, CommandTriggeredEventArgs e) } } - private async Task HandleAsyncCommand(string command, CommandTriggeredEventArgs e) + private async void HandleAsyncCommand(string command, CommandTriggeredEventArgs e) { try { diff --git a/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs b/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs index c100fdf6..2ab0db26 100644 --- a/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs +++ b/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs @@ -6,19 +6,23 @@ namespace SlipeServer.LuaControllers.Commands; public class LuaControllerArgumentException : Exception { public int Index { get; } + public string Argument { get; } public MethodInfo MethodInfo { get; } + public ParameterInfo ParameterInfo { get; } - public LuaControllerArgumentException(int index, MethodInfo methodInfo, Exception innerException) : base(null, innerException) + public LuaControllerArgumentException(int index, string argument, MethodInfo methodInfo, ParameterInfo parameterInfo, Exception innerException) : base(null, innerException) { this.Index = index; + this.Argument = argument; this.MethodInfo = methodInfo; + this.ParameterInfo = parameterInfo; } } public sealed class LuaControllerArgumentsMapper { private readonly Dictionary> mappings = []; - public event Action? ArgumentErrorOccurred; + public event Action? ArgumentErrorOccurred; public LuaControllerArgumentsMapper() { } public void DefineMap(Func map) @@ -86,26 +90,30 @@ public void DefineMap(Func map) if (parameters.Length == 1 && parameters[0].ParameterType.IsAssignableTo(typeof(IEnumerable))) return [new CommandArgumentList(values)]; - int i = 0; - try + if (parameters.Length < values.Length) { - if (parameters.Length != values.Length) - { - throw new ArgumentOutOfRangeException(); - } - var objects = new List(); - for (; i < parameters.Length; i++) - if (!parameters[i].IsOptional || values.Length > i) - objects.Add(MapParameter(parameters[i].ParameterType, values[i])); - else if (values.Length <= i) - objects.Add(parameters[i].DefaultValue); - - return [.. objects]; + ArgumentErrorOccurred?.Invoke(player, new ArgumentOutOfRangeException()); + return null; } - catch (Exception ex) + var objects = new List(); + for (int i = 0; i < parameters.Length; i++) { - ArgumentErrorOccurred?.Invoke(player, new LuaControllerArgumentException(i, method, ex)); + var parameter = parameters[i]; + + try + { + if (!parameter.IsOptional || values.Length > i) + objects.Add(MapParameter(parameter.ParameterType, values[i])); + else if (values.Length <= i) + objects.Add(parameter.DefaultValue); + } + catch (Exception ex) + { + ArgumentErrorOccurred?.Invoke(player, new LuaControllerArgumentException(i, values[i], method, parameter, ex)); + return null; + } } - return null; + + return [.. objects]; } } diff --git a/SlipeServer.LuaControllers/Contexts/LuaEventContext.cs b/SlipeServer.LuaControllers/Contexts/LuaEventContext.cs index 4c2dc735..3bd182ca 100644 --- a/SlipeServer.LuaControllers/Contexts/LuaEventContext.cs +++ b/SlipeServer.LuaControllers/Contexts/LuaEventContext.cs @@ -2,7 +2,6 @@ namespace SlipeServer.LuaControllers.Contexts; - public class LuaEventContext { public Player Player { get; } @@ -17,7 +16,6 @@ public LuaEventContext(Player player, Element source, string eventName) } } - public class LuaEventContext : LuaEventContext where TPlayer: Player { public new TPlayer Player => (base.Player as TPlayer)!; diff --git a/SlipeServer.Server/Elements/Player.cs b/SlipeServer.Server/Elements/Player.cs index 2ec612ec..d69c512e 100644 --- a/SlipeServer.Server/Elements/Player.cs +++ b/SlipeServer.Server/Elements/Player.cs @@ -18,6 +18,7 @@ using SlipeServer.Server.Clients; using System.Net; using SlipeServer.Packets.Definitions.Lua.ElementRpc.Player; +using System.Threading; namespace SlipeServer.Server.Elements; @@ -548,6 +549,22 @@ internal bool ShouldSendReturnSyncPacket() return this.pureSyncPacketsCount++ % 4 == 0; } + /// + /// Returns a CancellationToken that is valid until the player leaves the server or is destroyed + /// + public CancellationToken GetCancellationToken() + { + var cts = new CancellationTokenSource(); + this.Disconnected += (sender, e) => + { + cts.Cancel(); + }; + if (this.IsDestroyed) + cts.Cancel(); + + return cts.Token; + } + public event ElementChangedEventHandler? WantedLevelChanged; public event ElementChangedEventHandler? NametagTextChanged; public event ElementChangedEventHandler? IsNametagShowingChanged; From 39a73889a9b967129107a2af9f7db348ea85a5c6 Mon Sep 17 00:00:00 2001 From: Sebastian Jura <22455534+CrosRoad95@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:14:43 +0100 Subject: [PATCH 6/6] Add tests, improve command packet and its handler --- .../Commands/CommandControllerLogic.cs | 1 + .../Definitions/Commands/CommandPacket.cs | 8 +- .../Integration/ControllersTests.cs | 391 +++++++++++++----- .../Handlers/Command/CommandPacketHandler.cs | 3 +- 4 files changed, 297 insertions(+), 106 deletions(-) diff --git a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs index 71ff842e..2d22989d 100644 --- a/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs +++ b/SlipeServer.LuaControllers/Commands/CommandControllerLogic.cs @@ -39,6 +39,7 @@ public CommandControllerLogic( private void IndexControllers() { var controllerTypes = AppDomain.CurrentDomain.GetAssemblies() + .Where(x => !x.FullName.StartsWith("Microsoft.") && !x.FullName.StartsWith("System.") && !x.FullName.StartsWith("xunit.")) .SelectMany(x => x.GetExportedTypes()) .Where(x => x.IsAssignableTo(typeof(BaseCommandController))) .Where(x => !x.IsAbstract); diff --git a/SlipeServer.Packets/Definitions/Commands/CommandPacket.cs b/SlipeServer.Packets/Definitions/Commands/CommandPacket.cs index 3ec41b24..1c1f822b 100644 --- a/SlipeServer.Packets/Definitions/Commands/CommandPacket.cs +++ b/SlipeServer.Packets/Definitions/Commands/CommandPacket.cs @@ -7,12 +7,15 @@ namespace SlipeServer.Packets.Definitions.Commands; public class CommandPacket : Packet { + public const int MinCommandLength = 1; + public const int MaxCommandLength = 255; + public override PacketId PacketId => PacketId.PACKET_ID_COMMAND; public override PacketReliability Reliability => PacketReliability.ReliableSequenced; public override PacketPriority Priority => PacketPriority.High; public string Command { get; private set; } = string.Empty; - public string[] Arguments { get; private set; } = Array.Empty(); + public string[] Arguments { get; private set; } = []; public CommandPacket() { @@ -21,6 +24,9 @@ public CommandPacket() public override void Read(byte[] bytes) { + if (bytes.Length < MinCommandLength || bytes.Length > MaxCommandLength * 4) + return; + var reader = new PacketReader(bytes); string[] commandArgs = reader.GetStringCharacters(bytes.Length).Split(' '); this.Command = commandArgs[0]; diff --git a/SlipeServer.Server.Tests/Integration/ControllersTests.cs b/SlipeServer.Server.Tests/Integration/ControllersTests.cs index bd944748..d223131a 100644 --- a/SlipeServer.Server.Tests/Integration/ControllersTests.cs +++ b/SlipeServer.Server.Tests/Integration/ControllersTests.cs @@ -1,6 +1,5 @@ using SlipeServer.Server.Services; using SlipeServer.Server.TestTools; -using System.Numerics; using Xunit; using SlipeServer.LuaControllers; using SlipeServer.LuaControllers.Attributes; @@ -8,12 +7,14 @@ using System.Reflection; using SlipeServer.Server.ElementCollections; using System.Threading.Tasks; -using System.Diagnostics; using System.Collections.Generic; using SlipeServer.Server.Elements; using System.Linq; using SlipeServer.LuaControllers.Commands; using SlipeServer.Server.Enums; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using FluentAssertions.Execution; namespace SlipeServer.Server.Tests.Integration; @@ -22,9 +23,15 @@ public class SampleClass public int Number { get; set; } } +public enum SampleEnum +{ + EnumValue1 = 1, + EnumValue2 = 2, + EnumValue3 = 3, +} + public class LuaControllersExampleLogic { - private readonly IElementCollection elementCollection; private readonly ChatBox chatBox; public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper, IElementCollection elementCollection, ChatBox chatBox) @@ -42,7 +49,6 @@ public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper, IElementC }); mapper.ArgumentErrorOccurred += HandleArgumentErrorOccurred; - this.elementCollection = elementCollection; this.chatBox = chatBox; } @@ -57,142 +63,319 @@ private void HandleArgumentErrorOccurred(Player player, Exception exception) } } -public class ControllersTestsHelper +public class ControllersTestsHelper : List { - public Action? Callback; + public int InvokeCalls { get; set; } + public int InvokeAsyncCalls { get; set; } + public TaskCompletionSource? TaskCompletionSource { get; set; } + + public void ClearAll() + { + this.InvokeCalls = 0; + this.InvokeAsyncCalls = 0; + TaskCompletionSource?.TrySetCanceled(); + TaskCompletionSource = null; + Clear(); + } } -public class ControllersTests -{ +public class NoAccessAttribute : Attribute; - internal class NoAccessAttribute : Attribute; +[CommandController] +public class TestCommandController : BaseCommandController +{ + private readonly ControllersTestsHelper controllersTestsHelper; - [CommandController] - public class TestCommandController : BaseCommandController + public TestCommandController(ControllersTestsHelper controllersTestsHelper) { - private readonly ChatBox chatBox; - private readonly IElementCollection elementCollection; - private readonly ControllersTestsHelper controllersTestsHelper; - - public TestCommandController(ChatBox chatBox, IElementCollection elementCollection, ControllersTestsHelper controllersTestsHelper) - { - this.chatBox = chatBox; - this.elementCollection = elementCollection; - this.controllersTestsHelper = controllersTestsHelper; - } + this.controllersTestsHelper = controllersTestsHelper; + } - protected override void Invoke(Action next) + protected override void Invoke(Action next) + { + if (this.Context.MethodInfo.GetCustomAttribute() == null) { - try - { - if (this.Context.MethodInfo.GetCustomAttribute() != null) - { - this.chatBox.OutputTo(this.Context.Player, $"You can not access command {this.Context.Command}"); - } else - { - next(); - } - } - catch (Exception ex) - { - this.chatBox.OutputTo(this.Context.Player, $"Failed to execute command {this.Context.Command}"); - } + this.controllersTestsHelper.InvokeCalls++; + next(); } + } - protected override async Task InvokeAsync(Func next) + protected override async Task InvokeAsync(Func next) + { + if (this.Context.MethodInfo.GetCustomAttribute() == null) { - var stopwatch = Stopwatch.StartNew(); + this.controllersTestsHelper.InvokeAsyncCalls++; await next(); - Console.WriteLine("Executed async command in: {0}ms", stopwatch.ElapsedMilliseconds); - } - - public void Chat(IEnumerable words) - { - this.chatBox.OutputTo(this.Context.Player, string.Join(' ', words)); } + } - [NoAccess] - public void NoAccess() - { - this.chatBox.OutputTo(this.Context.Player, "You have accessed command with NoAccess attribute!"); - } + public void Sample() + { + this.controllersTestsHelper.Add("Sample"); + } - public void Oops() - { - throw new Exception("oops"); - } + public void StringArgument(string arg) + { + this.controllersTestsHelper.Add("StringArgument"); + this.controllersTestsHelper.Add(arg); + } - public void Ping() - { - this.chatBox.OutputTo(this.Context.Player, $"Your ping is {this.Context.Player.Client.Ping}."); - } + public void NumberArgument(int arg) + { + this.controllersTestsHelper.Add("NumberArgument"); + this.controllersTestsHelper.Add(arg); + } + + public void EnumArgument(SampleEnum sampleEnum) + { + this.controllersTestsHelper.Add("EnumArgument"); + this.controllersTestsHelper.Add(sampleEnum); + } - public void SampleClass(SampleClass sampleClass) - { - this.chatBox.OutputTo(this.Context.Player, $"sampleClass: {sampleClass.Number}"); - } + public void VariadicArguments(IEnumerable words) + { + this.controllersTestsHelper.Add("VariadicArguments"); + this.controllersTestsHelper.Add(string.Join(' ', words)); + } + + public void Exception() + { + throw new Exception("oops"); + } - public void FindPlayer(Player player) - { - this.chatBox.OutputTo(this.Context.Player, $"player: {player}"); - } + [NoAccess] + public void NoAccess() + { + this.controllersTestsHelper.Add("NoAccess"); + } - public async Task Async() - { - this.chatBox.OutputTo(this.Context.Player, "Executing command..."); - await Task.Delay(1000); - this.chatBox.OutputTo(this.Context.Player, "Command executed!"); - } + public void CustomType(SampleClass sampleClass) + { + this.controllersTestsHelper.Add("SampleClass"); + this.controllersTestsHelper.Add(sampleClass.Number); + } - public async Task AsyncLong() - { - this.chatBox.OutputTo(this.Context.Player, $"Simulating long execution..."); - try - { - await Task.Delay(10_000, this.Context.CancellationToken); - this.chatBox.OutputTo(this.Context.Player, "Long command executed!"); - } - catch (OperationCanceledException) - { - Console.WriteLine("Failed to execute long async command :("); - } - } + [Command("commandA")] + [Command("commandB")] + public void CommandAlias() + { + this.controllersTestsHelper.Add("CommandAlias"); + } - [Command("tp")] - [Command("teleport")] - public void Teleport(float x, float y, float z) - { - this.Context.Player.Position = new(x, y, z); - } + [NoCommand] + public void NoCommand() + { + this.controllersTestsHelper.Add("NoCommand"); + } - public void SpawnAt(ushort model, float x, float y, float z) + public async Task Async() + { + this.controllersTestsHelper.Add("Pre"); + await Task.Yield(); + this.controllersTestsHelper.Add("Post"); + this.controllersTestsHelper.TaskCompletionSource!.TrySetResult(); + } + + public async Task CancelCommand() + { + try { - this.Context.Player.Spawn(new System.Numerics.Vector3(x, y, z), 0, model, 0, 0); + await Task.Delay(-1, this.Context.CancellationToken); } - - public void GiveWeapon(WeaponId weapon, ushort ammoCount = 100) + catch (OperationCanceledException) { - this.Context.Player.AddWeapon(weapon, ammoCount, true); + this.controllersTestsHelper.Add("Cancelled"); } - - [NoCommand] - public void NoCommand() + finally { - this.chatBox.OutputTo(this.Context.Player, $"This should not run."); + this.controllersTestsHelper.TaskCompletionSource!.TrySetResult(); } } +} - [Fact] - public void LuaControllerCommandsTest() +public class LuaControllersFixture +{ + public TestingServer Server { get; } + + public LuaControllersFixture() { - var mtaServer = new TestingServer((Configuration?)null, x => + this.Server = new TestingServer((Configuration?)null, x => { - x.AddLuaControllers(); x.AddLuaControllers(); x.AddLogic(); + x.ConfigureServices(services => + { + services.AddSingleton(); + }); }); + } +} + +public class ControllersTests : IClassFixture +{ + private readonly LuaControllersFixture luaControllersFixture; + private readonly TestingServer server; + private readonly ControllersTestsHelper helper; + public ControllersTests(LuaControllersFixture luaControllersFixture) + { + this.luaControllersFixture = luaControllersFixture; + this.server = this.luaControllersFixture.Server; + this.helper = this.server.GetRequiredService(); + this.helper.ClearAll(); + } + + [Fact] + public void NonExistingCommand() + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("non existing command", []); + + using var _ = new AssertionScope(); + this.helper.InvokeCalls.Should().Be(0); + this.helper.Should().BeEmpty(); + } + + [InlineData("Sample")] + [InlineData("sample")] + [InlineData("SAMPLE")] + [Theory] + public void CommandShouldBeCaseInsensitive(string command) + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand(command, []); + + using var _ = new AssertionScope(); + this.helper.InvokeCalls.Should().Be(1); + this.helper.Should().BeEquivalentTo(["Sample"]); + } + + [Fact] + public void StringArgument() + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("StringArgument", ["foo"]); + + this.helper.Should().BeEquivalentTo(["StringArgument", "foo"]); + } + + [Fact] + public void NumberArgument() + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("NumberArgument", ["123"]); + + this.helper.Should().BeEquivalentTo(new object[] { "NumberArgument", 123 }); + } + + [InlineData("EnumValue2")] + [InlineData("2")] + [Theory] + public void EnumArgument(string argument) + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("EnumArgument", [argument]); + + this.helper.Should().BeEquivalentTo(new object[] { "EnumArgument", SampleEnum.EnumValue2 }); + } + + [Fact] + public void VariadicNumberOfArguments() + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("VariadicArguments", ["a", "b", "c"]); + + this.helper.Should().BeEquivalentTo(["VariadicArguments", "a b c"]); + } + + [Fact] + public void MalfunctionCommand() + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("Exception", []); + + using var _ = new AssertionScope(); + this.helper.InvokeCalls.Should().Be(1); + this.helper.Should().BeEmpty(); + } + + [Fact] + public void NoAccessCommand() + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("NoAccess", []); + + using var _ = new AssertionScope(); + this.helper.InvokeCalls.Should().Be(0); + this.helper.Should().BeEmpty(); + } + + [Fact] + public void CustomMapperShouldWork() + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("CustomType", ["123"]); + + this.helper.Should().BeEquivalentTo(new object[] { "SampleClass", 123 }); + } + + [InlineData("commandA")] + [InlineData("commandB")] + [Theory] + public void CommandAliasShouldWork(string command) + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand(command, []); - var player = mtaServer.AddFakePlayer(); + this.helper.Should().BeEquivalentTo(["CommandAlias"]); } + [Fact] + public void NoCommandShouldNotBeCallable() + { + var player = this.server.AddFakePlayer(); + + player.TriggerCommand("NoCommand", []); + + this.helper.Should().BeEmpty(); + } + + [Fact] + public async Task AsyncCommandShouldWork() + { + var player = this.server.AddFakePlayer(); + + this.helper.TaskCompletionSource = new(); + player.TriggerCommand("Async", []); + await this.helper.TaskCompletionSource.Task; + + using var _ = new AssertionScope(); + this.helper.InvokeAsyncCalls.Should().Be(1); + this.helper.Should().BeEquivalentTo(["Pre", "Post"]); + } + + [Fact] + public async Task CancelCommandWhenPlayerDisconnect() + { + var player = this.server.AddFakePlayer(); + + this.helper.TaskCompletionSource = new(); + player.TriggerCommand("CancelCommand", []); + + player.TriggerDisconnected(QuitReason.Quit); + await this.helper.TaskCompletionSource.Task; + + using var _ = new AssertionScope(); + this.helper.InvokeAsyncCalls.Should().Be(1); + this.helper.Should().BeEquivalentTo(["Cancelled"]); + } } diff --git a/SlipeServer.Server/PacketHandling/Handlers/Command/CommandPacketHandler.cs b/SlipeServer.Server/PacketHandling/Handlers/Command/CommandPacketHandler.cs index e8ec79b6..bea2da4f 100644 --- a/SlipeServer.Server/PacketHandling/Handlers/Command/CommandPacketHandler.cs +++ b/SlipeServer.Server/PacketHandling/Handlers/Command/CommandPacketHandler.cs @@ -10,6 +10,7 @@ public class CommandPacketHandler : IPacketHandler public void HandlePacket(IClient client, CommandPacket packet) { - client.Player.TriggerCommand(packet.Command, packet.Arguments); + if(!string.IsNullOrWhiteSpace(packet.Command) && packet.Command.Length >= CommandPacket.MinCommandLength) + client.Player.TriggerCommand(packet.Command, packet.Arguments); } }