diff --git a/SlipeServer.Console/Logic/ServerTestLogic.cs b/SlipeServer.Console/Logic/ServerTestLogic.cs index 6a8ef4d6..9acc8ac4 100644 --- a/SlipeServer.Console/Logic/ServerTestLogic.cs +++ b/SlipeServer.Console/Logic/ServerTestLogic.cs @@ -9,6 +9,7 @@ using SlipeServer.Packets.Lua.Camera; using SlipeServer.Packets.Structs; using SlipeServer.Server; +using SlipeServer.Server.Concepts; using SlipeServer.Server.Constants; using SlipeServer.Server.ElementCollections; using SlipeServer.Server.Elements; @@ -185,6 +186,7 @@ private void SetupTestLogic() private void SetupTestElements() { this.testResource = this.resourceProvider.GetResource("TestResource"); + this.testResource.AddClientTaskHelper(); this.secondTestResource = this.resourceProvider.GetResource("SecondTestResource"); this.secondTestResource.NoClientScripts[$"{this.secondTestResource!.Name}/testfile.lua"] = Encoding.UTF8.GetBytes("outputChatBox(\"I AM A NOT CACHED MESSAGE\")"); diff --git a/SlipeServer.Console/Resources/TestResource/test.lua b/SlipeServer.Console/Resources/TestResource/test.lua index 5d49409f..029bbd18 100644 --- a/SlipeServer.Console/Resources/TestResource/test.lua +++ b/SlipeServer.Console/Resources/TestResource/test.lua @@ -114,4 +114,27 @@ addEventHandler("onClientRender", root, function() k = k + 1 printDebugVehicle(k, v); end -end) \ No newline at end of file +end) + +local clientTasks = {} +addEvent("testClientTask", true) +addEventHandler("testClientTask", root, function(clientTask) + outputChatBox("ClientTask created."); + clientTasks[#clientTasks + 1] = clientTask; +end) + +addCommandHandler("resolveTasks", function() + for i,task in ipairs(clientTasks)do + ClientTask.Resolve(task, "Ok"); + end + outputChatBox("Resolved: "..#clientTasks.." tasks.") + clientTasks = {} +end) + +addCommandHandler("rejectTasks", function() + for i,task in ipairs(clientTasks)do + ClientTask.Reject(task, "Ok"); + end + outputChatBox("Failed: "..#clientTasks.." tasks.") + clientTasks = {} +end) diff --git a/SlipeServer.Example/ServerExampleLogic.cs b/SlipeServer.Example/ServerExampleLogic.cs index 31133a1a..bfb2360e 100644 --- a/SlipeServer.Example/ServerExampleLogic.cs +++ b/SlipeServer.Example/ServerExampleLogic.cs @@ -1,5 +1,7 @@ using SlipeServer.Server; +using SlipeServer.Server.Concepts; using SlipeServer.Server.Elements; +using SlipeServer.Server.Exceptions; using SlipeServer.Server.Services; namespace SlipeServer.Example; @@ -8,12 +10,13 @@ public class ServerExampleLogic { private readonly CommandService commandService; private readonly ChatBox chatBox; + private readonly MtaServer mtaServer; public ServerExampleLogic(CommandService commandService, ChatBox chatBox, MtaServer mtaServer) { this.commandService = commandService; this.chatBox = chatBox; - + this.mtaServer = mtaServer; AddCommand("hello", player => { this.chatBox.OutputTo(player, "Hello world"); @@ -83,6 +86,40 @@ private void AddVehiclesCommands() vehicle.Fix(); this.chatBox.OutputTo(player, "Vehicle fixed"); }); + + AddCommand("clienttask", async player => + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var task = this.mtaServer.CreateClientTask(player, cts.Token); + + player.TriggerLuaEvent("testClientTask", player, task); + + try + { + await task; + } + catch (PlayerDisconnectedException e) // When player left the server + { + Console.WriteLine("Result: PlayerDisconnectedException"); + } + catch (InvalidOperationException e) // When client sent invalid response + { + Console.WriteLine("Result: InvalidOperationException"); + } + catch (ClientErrorException e) // When client on purpose rejected task + { + Console.WriteLine("Result: ClientErrorException"); + } + catch (OperationCanceledException e) // Exceptin from cts from above + { + Console.WriteLine("Result: OperationCanceledException"); + } + finally + { + this.chatBox.OutputTo(player, "Task completed"); + } + + }); } private void AddCommand(string command, Action callback) @@ -92,4 +129,19 @@ private void AddCommand(string command, Action callback) callback(e.Player); }; } + + private void AddCommand(string command, Func callback) + { + this.commandService.AddCommand(command).Triggered += async (object? sender, Server.Events.CommandTriggeredEventArgs e) => + { + try + { + await callback(e.Player); + } + catch(Exception ex) + { + Console.WriteLine(ex.ToString()); + } + }; + } } diff --git a/SlipeServer.Server/Concepts/ClientTask.cs b/SlipeServer.Server/Concepts/ClientTask.cs new file mode 100644 index 00000000..51b6d2aa --- /dev/null +++ b/SlipeServer.Server/Concepts/ClientTask.cs @@ -0,0 +1,131 @@ +using SlipeServer.Packets.Definitions.Lua; +using SlipeServer.Server.Elements; +using SlipeServer.Server.Exceptions; +using SlipeServer.Server.Resources; +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace SlipeServer.Server.Concepts; + +public static class ResourceExtensions +{ + public static Resource AddClientTaskHelper(this Resource resource) + { + resource.NoClientScripts["clientTaskHelper.lua"] = System.Text.UTF8Encoding.UTF8.GetBytes(ClientTask.luaHelperCode); + + return resource; + } +} + +public sealed class ClientErrorException : Exception +{ + public ClientErrorException(string? message) : base(message) + { + + } +} + +public sealed class ClientTask : LuaValue, IDisposable +{ + public const string luaHelperCode = """ + ClientTask = { + Resolve = function(clientTask, ...) + if(clientTask._completed)then + error("ClientTask already completed"); + end + clientTask._completed = true; + triggerServerEvent("clientTask_"..clientTask._id, localPlayer, "success", ...) + end, + Reject = function(clientTask, ...) + if(clientTask._completed)then + error("ClientTask already completed"); + end + clientTask._completed = true; + triggerServerEvent("clientTask_"..clientTask._id, localPlayer, "error", ...) + end, + } + """; + + private readonly TaskCompletionSource taskCompletionSource; + private readonly string eventName; + public MtaServer MtaServer { get; } + public Player Player { get; } + public string Id { get; } + + internal ClientTask(MtaServer mtaServer, Player player, string id, CancellationToken cancellationToken) : base(new LuaTable + { + ["_id"] = id + }) + { + this.taskCompletionSource = new TaskCompletionSource(); + this.MtaServer = mtaServer; + this.Player = player; + this.Id = id; + this.eventName = $"clientTask_{this.Id}"; + this.MtaServer.LuaEventTriggered += HandleLuaEventTriggered; + this.Player.Disconnected += HandleDisconnected; + cancellationToken.Register(() => + { + this.taskCompletionSource.TrySetCanceled(); + }); + } + + private void HandleDisconnected(Player sender, Elements.Events.PlayerQuitEventArgs e) + { + this.taskCompletionSource.TrySetException(new PlayerDisconnectedException(sender)); + Dispose(); + } + + private void HandleLuaEventTriggered(Events.LuaEvent luaEvent) + { + if (luaEvent.Name != this.eventName || luaEvent.Player != this.Player) + return; + + try + { + var result = luaEvent.Parameters[0].StringValue; + + if (result == "success") + { + this.taskCompletionSource.TrySetResult(); + } + else if (result == "error") + { + if(luaEvent.Parameters.Length >= 2) + { + this.taskCompletionSource.TrySetException(new ClientErrorException(luaEvent.Parameters[1].StringValue)); + } else + { + this.taskCompletionSource.TrySetException(new ClientErrorException(null)); + } + } + else + { + this.taskCompletionSource.TrySetException(new InvalidOperationException()); + } + } + catch (Exception ex) + { + this.taskCompletionSource.TrySetException(ex); + } + finally + { + Dispose(); + } + } + + public void Dispose() + { + this.MtaServer.LuaEventTriggered -= HandleLuaEventTriggered; + this.Player.Disconnected -= HandleDisconnected; + this.taskCompletionSource.TrySetException(new ObjectDisposedException(nameof(ClientTask))); + } + + public TaskAwaiter GetAwaiter() + { + return this.taskCompletionSource.Task.GetAwaiter(); + } +} diff --git a/SlipeServer.Server/Exceptions/PlayerDisconnectedException.cs b/SlipeServer.Server/Exceptions/PlayerDisconnectedException.cs new file mode 100644 index 00000000..eeed145b --- /dev/null +++ b/SlipeServer.Server/Exceptions/PlayerDisconnectedException.cs @@ -0,0 +1,14 @@ +using SlipeServer.Server.Elements; +using System; + +namespace SlipeServer.Server.Exceptions; + +public sealed class PlayerDisconnectedException : Exception +{ + public Player Player { get; } + + public PlayerDisconnectedException(Player player) + { + this.Player = player; + } +} diff --git a/SlipeServer.Server/MtaServer.cs b/SlipeServer.Server/MtaServer.cs index 99f7552d..b80afa7b 100644 --- a/SlipeServer.Server/MtaServer.cs +++ b/SlipeServer.Server/MtaServer.cs @@ -10,6 +10,7 @@ using SlipeServer.Server.AllSeeingEye; using SlipeServer.Server.Bans; using SlipeServer.Server.Clients; +using SlipeServer.Server.Concepts; using SlipeServer.Server.ElementCollections; using SlipeServer.Server.Elements; using SlipeServer.Server.Elements.IdGeneration; @@ -32,6 +33,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; namespace SlipeServer.Server; @@ -678,6 +680,17 @@ public static MtaServer Create(IServiceProvider serviceProvider, Action CreateWithDiSupport(Action builderAction) where TPlayer : Player => new MtaDiPlayerServer(builderAction); + /// + /// Creates + /// + /// + /// + public ClientTask CreateClientTask(Player player, CancellationToken cancellationToken = default) + { + var id = Guid.NewGuid().ToString(); + return new ClientTask(this, player, id, cancellationToken); + } + /// /// Triggered when any element is created on the server through the .AssociateElement method ///