diff --git a/SlipeServer.Console/Logic/ServerTestLogic.cs b/SlipeServer.Console/Logic/ServerTestLogic.cs index 6a8ef4d6..d7e91879 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; } @@ -184,14 +181,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[] { @@ -868,11 +857,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) @@ -1553,10 +1537,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 9d08ad20..86dee148 100644 --- a/SlipeServer.Example/Controllers/TestCommandController.cs +++ b/SlipeServer.Example/Controllers/TestCommandController.cs @@ -1,18 +1,19 @@ using Microsoft.Extensions.Logging; +using SlipeServer.Example.Logic; using SlipeServer.LuaControllers; using SlipeServer.LuaControllers.Attributes; using SlipeServer.Server.ElementCollections; 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.Diagnostics; +using System.Reflection; namespace SlipeServer.Example.Controllers; -[CommandController()] +internal class NoAccessAttribute : Attribute; + +[CommandController] public class TestCommandController : BaseCommandController { private readonly ChatBox chatBox; @@ -29,15 +30,82 @@ 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}"); + } + } + + 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)); } + [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}."); } + + 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}"); + } + + 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")] @@ -56,7 +124,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/Logic/LuaControllersExampleLogic.cs b/SlipeServer.Example/Logic/LuaControllersExampleLogic.cs new file mode 100644 index 00000000..3ac11992 --- /dev/null +++ b/SlipeServer.Example/Logic/LuaControllersExampleLogic.cs @@ -0,0 +1,46 @@ +using SlipeServer.LuaControllers.Commands; +using SlipeServer.Server.ElementCollections; +using SlipeServer.Server.Elements; +using SlipeServer.Server.Services; + +namespace SlipeServer.Example.Logic; + +public class SampleClass +{ + public int Number { get; set; } +} + +public class LuaControllersExampleLogic +{ + private readonly IElementCollection elementCollection; + private readonly ChatBox chatBox; + + public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper, IElementCollection elementCollection, ChatBox chatBox) + { + mapper.DefineMap(arg => + { + return new SampleClass + { + 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, Exception exception) + { + if (exception is ArgumentOutOfRangeException) + this.chatBox.OutputTo(player, "Too many or too few arguments"); + else if (exception is LuaControllerArgumentException ex) + { + 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 95% rename from SlipeServer.Example/ServerExampleLogic.cs rename to SlipeServer.Example/Logic/ServerExampleLogic.cs index 31133a1a..1039f683 100644 --- a/SlipeServer.Example/ServerExampleLogic.cs +++ b/SlipeServer.Example/Logic/ServerExampleLogic.cs @@ -2,7 +2,7 @@ using SlipeServer.Server.Elements; using SlipeServer.Server.Services; -namespace SlipeServer.Example; +namespace SlipeServer.Example.Logic; public class ServerExampleLogic { @@ -87,7 +87,7 @@ private void AddVehiclesCommands() 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 eb7407ed..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; @@ -8,6 +9,8 @@ public static class ServerBuilderExtensions 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 06507a19..14680c70 100644 --- a/SlipeServer.Example/SlipeServer.Example.csproj +++ b/SlipeServer.Example/SlipeServer.Example.csproj @@ -21,4 +21,10 @@ + + + + Always + + 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/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/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/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 2aeb8348..3b808bca 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; @@ -11,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; } } @@ -23,38 +25,84 @@ 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(); + } + + protected async virtual Task InvokeAsync(Func next) + { + await next(); + } + + internal virtual void HandleCommand(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, object?> handler) + { + var cts = new CancellationTokenSource(); + player.Disconnected += (sender, e) => + { + cts.Cancel(); + }; + if (player.IsDestroyed) + cts.Cancel(); + + SetContext(new CommandContext(player, command, args, methodInfo, player.GetCancellationToken())); try { - handler.Invoke(args); + Invoke(() => handler.Invoke(args)); } finally { - this.SetContext(null); + SetContext(null); } } -} + internal virtual async Task HandleCommandAsync(Player player, string command, IEnumerable args, MethodInfo methodInfo, Func, Task> handler) + { + SetContext(new CommandContext(player, command, args, methodInfo, player.GetCancellationToken())); + try + { + await InvokeAsync(async () => await handler(args)); + } + finally + { + SetContext(null); + } + } +} public abstract class BaseCommandController : BaseCommandController where TPlayer : Player { 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; + + SetContext(new CommandContext(tPlayer, command, args, methodInfo, tPlayer.GetCancellationToken())); + try + { + Invoke(() => handler.Invoke(args)); + } + finally + { + 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; - this.SetContext(new CommandContext(tPlayer, command)); + SetContext(new CommandContext(tPlayer, command, args, methodInfo, tPlayer.GetCancellationToken())); try { - handler.Invoke(args); + await InvokeAsync(async () => await handler.Invoke(args)); } finally { - this.SetContext(null); + SetContext(null); } } } diff --git a/SlipeServer.LuaControllers/Commands/BoundCommand.cs b/SlipeServer.LuaControllers/Commands/BoundCommand.cs index b530623b..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) { @@ -35,6 +42,29 @@ 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())); + } +} + +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 38231843..2d22989d 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,33 +14,37 @@ public class CommandArgumentList(IEnumerable arguments) : IEnumerable arguments.GetEnumerator(); } -public class CommandControllerLogic +public sealed class CommandControllerLogic { private readonly MtaServer server; private readonly CommandService commandService; - private readonly ILogger logger; - private readonly Dictionary> handlers = new(); + private readonly ILogger logger; + private readonly LuaControllerArgumentsMapper argumentsMapper; + private readonly Dictionary> syncHandlers = []; + private readonly Dictionary> asyncHandlers = []; 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(); } 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); - foreach (var controllerType in controllerTypes ?? Array.Empty()) + foreach (var controllerType in controllerTypes ?? []) { var controllerAttribute = controllerType .GetCustomAttributes() @@ -53,7 +56,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(); @@ -63,89 +69,54 @@ 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] = new(); + 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 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 JsonSerializer.Deserialize(value, targetType); - } - - private object?[] MapParameters(string[] values, MethodInfo method) + + private void AddAsyncHandler(string command, Type type, MethodInfo method, BaseCommandController? controller, bool isCaseSensitive = false) { - var parameters = method.GetParameters(); - - if (parameters.Length == 0) - return Array.Empty(); - - 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); + if (!this.asyncHandlers.ContainsKey(command)) + { + this.asyncHandlers[command] = []; + this.commandService.AddCommand(command, isCaseSensitive).Triggered += (_, args) => HandleAsyncCommand(command, args); + } - return objects.ToArray(); + 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) { try { - var parameters = MapParameters(e.Arguments.ToArray(), 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) { @@ -153,4 +124,32 @@ private void HandleCommand(string command, CommandTriggeredEventArgs e) } } } + + private async void 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/Commands/LuaControllerArgumentsMapper.cs b/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs new file mode 100644 index 00000000..2ab0db26 --- /dev/null +++ b/SlipeServer.LuaControllers/Commands/LuaControllerArgumentsMapper.cs @@ -0,0 +1,119 @@ +using SlipeServer.Server.Elements; +using System.Reflection; + +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, 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 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)]; + + if (parameters.Length < values.Length) + { + ArgumentErrorOccurred?.Invoke(player, new ArgumentOutOfRangeException()); + return null; + } + var objects = new List(); + for (int i = 0; i < parameters.Length; i++) + { + 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 [.. objects]; + } +} diff --git a/SlipeServer.LuaControllers/Contexts/CommandContext.cs b/SlipeServer.LuaControllers/Contexts/CommandContext.cs index 4246ffee..59193a4f 100644 --- a/SlipeServer.LuaControllers/Contexts/CommandContext.cs +++ b/SlipeServer.LuaControllers/Contexts/CommandContext.cs @@ -1,26 +1,31 @@ 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 CancellationToken CancellationToken { get; } - public CommandContext(Player player, string command) + 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; } } - 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, CancellationToken cancellationToken) : base(player, command, arguments, methodInfo, cancellationToken) { } } 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.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.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 new file mode 100644 index 00000000..d223131a --- /dev/null +++ b/SlipeServer.Server.Tests/Integration/ControllersTests.cs @@ -0,0 +1,381 @@ +using SlipeServer.Server.Services; +using SlipeServer.Server.TestTools; +using Xunit; +using SlipeServer.LuaControllers; +using SlipeServer.LuaControllers.Attributes; +using System; +using System.Reflection; +using SlipeServer.Server.ElementCollections; +using System.Threading.Tasks; +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; + +public class SampleClass +{ + public int Number { get; set; } +} + +public enum SampleEnum +{ + EnumValue1 = 1, + EnumValue2 = 2, + EnumValue3 = 3, +} + +public class LuaControllersExampleLogic +{ + private readonly ChatBox chatBox; + + public LuaControllersExampleLogic(LuaControllerArgumentsMapper mapper, IElementCollection elementCollection, ChatBox chatBox) + { + mapper.DefineMap(arg => + { + return new SampleClass + { + Number = int.Parse(arg) + }; + }); + mapper.DefineMap(arg => + { + return elementCollection.GetByType().Where(x => x.Name.Contains(arg)).FirstOrDefault(); + }); + + mapper.ArgumentErrorOccurred += HandleArgumentErrorOccurred; + this.chatBox = chatBox; + } + + private void HandleArgumentErrorOccurred(Player player, Exception exception) + { + if (exception is ArgumentOutOfRangeException) + this.chatBox.OutputTo(player, "Too many or too few arguments"); + else if (exception is LuaControllerArgumentException ex) + { + this.chatBox.OutputTo(player, $"Error while executing command, argument at index {ex.Index + 1} expected {ex.ParameterInfo.ParameterType}, got '{ex.Argument}'"); + } + } +} + +public class ControllersTestsHelper : List +{ + 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 NoAccessAttribute : Attribute; + +[CommandController] +public class TestCommandController : BaseCommandController +{ + private readonly ControllersTestsHelper controllersTestsHelper; + + public TestCommandController(ControllersTestsHelper controllersTestsHelper) + { + this.controllersTestsHelper = controllersTestsHelper; + } + + protected override void Invoke(Action next) + { + if (this.Context.MethodInfo.GetCustomAttribute() == null) + { + this.controllersTestsHelper.InvokeCalls++; + next(); + } + } + + protected override async Task InvokeAsync(Func next) + { + if (this.Context.MethodInfo.GetCustomAttribute() == null) + { + this.controllersTestsHelper.InvokeAsyncCalls++; + await next(); + } + } + + public void Sample() + { + this.controllersTestsHelper.Add("Sample"); + } + + public void StringArgument(string arg) + { + this.controllersTestsHelper.Add("StringArgument"); + this.controllersTestsHelper.Add(arg); + } + + 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 VariadicArguments(IEnumerable words) + { + this.controllersTestsHelper.Add("VariadicArguments"); + this.controllersTestsHelper.Add(string.Join(' ', words)); + } + + public void Exception() + { + throw new Exception("oops"); + } + + [NoAccess] + public void NoAccess() + { + this.controllersTestsHelper.Add("NoAccess"); + } + + public void CustomType(SampleClass sampleClass) + { + this.controllersTestsHelper.Add("SampleClass"); + this.controllersTestsHelper.Add(sampleClass.Number); + } + + [Command("commandA")] + [Command("commandB")] + public void CommandAlias() + { + this.controllersTestsHelper.Add("CommandAlias"); + } + + [NoCommand] + public void NoCommand() + { + this.controllersTestsHelper.Add("NoCommand"); + } + + public async Task Async() + { + this.controllersTestsHelper.Add("Pre"); + await Task.Yield(); + this.controllersTestsHelper.Add("Post"); + this.controllersTestsHelper.TaskCompletionSource!.TrySetResult(); + } + + public async Task CancelCommand() + { + try + { + await Task.Delay(-1, this.Context.CancellationToken); + } + catch (OperationCanceledException) + { + this.controllersTestsHelper.Add("Cancelled"); + } + finally + { + this.controllersTestsHelper.TaskCompletionSource!.TrySetResult(); + } + } +} + +public class LuaControllersFixture +{ + public TestingServer Server { get; } + + public LuaControllersFixture() + { + this.Server = new TestingServer((Configuration?)null, x => + { + 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, []); + + 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.Tests/SlipeServer.Server.Tests.csproj b/SlipeServer.Server.Tests/SlipeServer.Server.Tests.csproj index 4f5cad06..34754d5b 100644 --- a/SlipeServer.Server.Tests/SlipeServer.Server.Tests.csproj +++ b/SlipeServer.Server.Tests/SlipeServer.Server.Tests.csproj @@ -10,6 +10,7 @@ + @@ -19,6 +20,7 @@ + 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; 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); } } 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