From 5b9f49df69e1578c0934bf8956c5a909e74c3046 Mon Sep 17 00:00:00 2001 From: Sebastian Jura <22455534+CrosRoad95@users.noreply.github.com> Date: Wed, 13 Nov 2024 17:59:50 +0100 Subject: [PATCH 1/2] Next (+1 squashed commits) Squashed commits: [ec3080ad7] # This is a combination of 2 commits. Add parallel starting Revert port --- SlipeServer.Console/Program.cs | 2 + .../Resources/ResourceA/client.lua | 3 + .../Resources/ResourceB/client.lua | 3 + SlipeServer.Example/ResourcesLogic.cs | 44 +++++++++ .../ServerBuilderExtensions.cs | 1 + .../SlipeServer.Example.csproj | 14 +++ SlipeServer.HostBuilderExample/Program.cs | 2 + .../Resources/ResourceStartPacket.cs | 13 ++- SlipeServer.Packets/Reader/PacketReader.cs | 1 + .../TestResourceProvider.cs | 44 +++++++++ SlipeServer.Server.TestTools/TestingServer.cs | 34 ++++++- .../Services/ResourceServiceTests.cs | 83 ++++++++++++++++ .../Resources/ResourceService.cs | 96 ++++++++++++++++--- SlipeServer.WebHostBuilderExample/Program.cs | 2 + 14 files changed, 327 insertions(+), 15 deletions(-) create mode 100644 SlipeServer.Example/Resources/ResourceA/client.lua create mode 100644 SlipeServer.Example/Resources/ResourceB/client.lua create mode 100644 SlipeServer.Example/ResourcesLogic.cs create mode 100644 SlipeServer.Server.TestTools/TestResourceProvider.cs create mode 100644 SlipeServer.Server.Tests/Integration/Services/ResourceServiceTests.cs diff --git a/SlipeServer.Console/Program.cs b/SlipeServer.Console/Program.cs index f9d87e97..0deb5089 100644 --- a/SlipeServer.Console/Program.cs +++ b/SlipeServer.Console/Program.cs @@ -23,6 +23,7 @@ using System.Threading; using SlipeServer.Example; using SlipeServer.Scripting.Luau; +using SlipeServer.Server.Resources; namespace SlipeServer.Console; @@ -104,6 +105,7 @@ public Program(string[] args) services.AddScoped(); services.AddSingleton(); services.AddScoped(); + services.AddSingleton(); services.AddHttpClient(); diff --git a/SlipeServer.Example/Resources/ResourceA/client.lua b/SlipeServer.Example/Resources/ResourceA/client.lua new file mode 100644 index 00000000..b6fad1b9 --- /dev/null +++ b/SlipeServer.Example/Resources/ResourceA/client.lua @@ -0,0 +1,3 @@ +addCommandHandler("sampltest", function() + outputChatBox("ResourceA is running!") +end) \ No newline at end of file diff --git a/SlipeServer.Example/Resources/ResourceB/client.lua b/SlipeServer.Example/Resources/ResourceB/client.lua new file mode 100644 index 00000000..bd8d1c6b --- /dev/null +++ b/SlipeServer.Example/Resources/ResourceB/client.lua @@ -0,0 +1,3 @@ +addCommandHandler("sampltest", function() + outputChatBox("ResourceB is running!") +end) \ No newline at end of file diff --git a/SlipeServer.Example/ResourcesLogic.cs b/SlipeServer.Example/ResourcesLogic.cs new file mode 100644 index 00000000..af6e397e --- /dev/null +++ b/SlipeServer.Example/ResourcesLogic.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using SlipeServer.Server; +using SlipeServer.Server.Elements; +using SlipeServer.Server.Resources; +using SlipeServer.Server.Resources.Providers; +using SlipeServer.Server.Services; + +namespace SlipeServer.Example; + +public class ResourcesLogic +{ + private readonly ChatBox chatBox; + private readonly ResourceService resourceService; + private readonly ILogger logger; + + public ResourcesLogic(MtaServer mtaServer, ChatBox chatBox, IResourceProvider resourceProvider, ResourceService resourceService, ILogger logger) + { + this.chatBox = chatBox; + this.resourceService = resourceService; + this.logger = logger; + this.resourceService.StartResource("ResourceA"); + this.resourceService.StartResource("ResourceB"); + this.resourceService.AllStarted += HandleAllStarted; + mtaServer.PlayerJoined += HandlePlayerJoin; + } + + private async void HandlePlayerJoin(Player player) + { + try + { + await this.resourceService.StartResourcesForPlayer(player); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Failed to start resources for {playerName}", player.Name); + } + } + + private void HandleAllStarted(Player player) + { + Console.WriteLine("All resources started for: {0}", player.Name); + this.chatBox.Output($"All resources started for: {player.Name}"); + } +} diff --git a/SlipeServer.Example/ServerBuilderExtensions.cs b/SlipeServer.Example/ServerBuilderExtensions.cs index 49ef9c1b..7d8c8ac7 100644 --- a/SlipeServer.Example/ServerBuilderExtensions.cs +++ b/SlipeServer.Example/ServerBuilderExtensions.cs @@ -7,6 +7,7 @@ public static class ServerBuilderExtensions public static ServerBuilder AddExampleLogic(this ServerBuilder builder) { builder.AddLogic(); + builder.AddLogic(); return builder; } diff --git a/SlipeServer.Example/SlipeServer.Example.csproj b/SlipeServer.Example/SlipeServer.Example.csproj index d78e26fa..42d9da47 100644 --- a/SlipeServer.Example/SlipeServer.Example.csproj +++ b/SlipeServer.Example/SlipeServer.Example.csproj @@ -15,4 +15,18 @@ + + + + + + + + + Always + + + Always + + diff --git a/SlipeServer.HostBuilderExample/Program.cs b/SlipeServer.HostBuilderExample/Program.cs index b1f426df..6da347ba 100644 --- a/SlipeServer.HostBuilderExample/Program.cs +++ b/SlipeServer.HostBuilderExample/Program.cs @@ -4,6 +4,7 @@ using SlipeServer.Server.Resources.Serving; using SlipeServer.Server.ServerBuilders; using SlipeServer.Example; +using SlipeServer.Server.Resources; Configuration? configuration = null; @@ -25,6 +26,7 @@ services.AddHttpClient(); services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); // Use instead of logics services.TryAddSingleton(x => x.GetRequiredService>()); diff --git a/SlipeServer.Packets/Definitions/Resources/ResourceStartPacket.cs b/SlipeServer.Packets/Definitions/Resources/ResourceStartPacket.cs index cc5ecfcc..bc9fed3d 100644 --- a/SlipeServer.Packets/Definitions/Resources/ResourceStartPacket.cs +++ b/SlipeServer.Packets/Definitions/Resources/ResourceStartPacket.cs @@ -1,7 +1,9 @@ using SlipeServer.Packets.Builder; using SlipeServer.Packets.Enums; +using SlipeServer.Packets.Reader; using SlipeServer.Packets.Structs; using System.Collections.Generic; +using System.Text; namespace SlipeServer.Packets.Definitions.Resources; @@ -13,7 +15,7 @@ public class ResourceStartPacket : Packet public override PacketPriority Priority => PacketPriority.High; public string Name { get; set; } - public ushort NetId { get; } + public ushort NetId { get; set; } public ElementId ResourceDynamicElementId { get; set; } public ushort UncachedScriptCount { get; } public string MinServerVersion { get; } @@ -24,6 +26,11 @@ public class ResourceStartPacket : Packet public IEnumerable ExportedFunctions { get; } public ElementId ResourceElementId { get; } + public ResourceStartPacket() + { + + } + public ResourceStartPacket( string name, ushort netId, @@ -53,6 +60,10 @@ IEnumerable exportedFunctions public override void Read(byte[] bytes) { + var reader = new PacketReader(bytes); + var len = reader.GetByte(); + this.Name = Encoding.UTF8.GetString(reader.GetBytes(len)); + this.NetId = reader.GetUInt16(); } public override byte[] Write() diff --git a/SlipeServer.Packets/Reader/PacketReader.cs b/SlipeServer.Packets/Reader/PacketReader.cs index b61710e0..e8d5aa0c 100644 --- a/SlipeServer.Packets/Reader/PacketReader.cs +++ b/SlipeServer.Packets/Reader/PacketReader.cs @@ -96,6 +96,7 @@ public bool[] GetBits(int count) public uint GetUint32() => BitConverter.ToUInt32(GetBytes(4), 0); public ulong GetUint64() => BitConverter.ToUInt64(GetBytes(8), 0); public short GetInt16() => BitConverter.ToInt16(GetBytes(2), 0); + public ushort GetUInt16() => BitConverter.ToUInt16(GetBytes(2), 0); public int GetInt32() => BitConverter.ToInt32(GetBytes(4), 0); public long GetInt64() => BitConverter.ToInt64(GetBytes(8), 0); public float GetFloat() => BitConverter.ToSingle(GetBytes(4), 0); diff --git a/SlipeServer.Server.TestTools/TestResourceProvider.cs b/SlipeServer.Server.TestTools/TestResourceProvider.cs new file mode 100644 index 00000000..08baf656 --- /dev/null +++ b/SlipeServer.Server.TestTools/TestResourceProvider.cs @@ -0,0 +1,44 @@ +using SlipeServer.Server.Resources.Interpreters; +using SlipeServer.Server.Resources.Providers; +using SlipeServer.Server.Resources; +using System.Collections.Generic; + +namespace SlipeServer.Server.TestTools; + +public class TestResourceProvider : IResourceProvider +{ + private readonly Dictionary resources = []; + private readonly object netIdLock = new(); + private ushort netId = 0; + + public void AddResource(Resource resource) + { + this.resources[resource.Name] = resource; + } + + public Resource GetResource(string name) + { + return this.resources[name]; + } + + public IEnumerable GetResources() + { + return this.resources.Values; + } + + public void Refresh() { } + + private IEnumerable IndexResourceDirectory(string directory) => []; + + public IEnumerable GetFilesForResource(string name) => []; + + public byte[] GetFileContent(string resource, string file) => []; + + public ushort ReserveNetId() + { + lock (this.netIdLock) + return this.netId++; + } + + public void AddResourceInterpreter(IResourceInterpreter resourceInterpreter) { } +} diff --git a/SlipeServer.Server.TestTools/TestingServer.cs b/SlipeServer.Server.TestTools/TestingServer.cs index 8aaa4dc1..f5118851 100644 --- a/SlipeServer.Server.TestTools/TestingServer.cs +++ b/SlipeServer.Server.TestTools/TestingServer.cs @@ -1,5 +1,6 @@ using Castle.Core.Logging; using FluentAssertions; +using FluentAssertions.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; @@ -7,15 +8,19 @@ using SlipeServer.Packets; using SlipeServer.Packets.Definitions.Lua; using SlipeServer.Packets.Definitions.Lua.ElementRpc; +using SlipeServer.Packets.Definitions.Resources; using SlipeServer.Packets.Enums; using SlipeServer.Packets.Lua.Event; using SlipeServer.Server.Clients; using SlipeServer.Server.Elements; +using SlipeServer.Server.Resources; +using SlipeServer.Server.Resources.Providers; using SlipeServer.Server.Resources.Serving; using SlipeServer.Server.ServerBuilders; using System; using System.Collections.Generic; using System.Linq; +using static System.Runtime.InteropServices.JavaScript.JSType; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace SlipeServer.Server.TestTools; @@ -76,12 +81,14 @@ private void SetupSendPacketMocks() this.NetWrapperMock.Setup(x => x.SendPacket(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((ulong address, ushort version, Packet packet) => { + var data = packet.Write(); + this.packetReducer.EnqueuePacket(this.clients[this.NetWrapperMock.Object][address], packet.PacketId, data); this.sendPacketCalls.Add(new SendPacketCall() { Address = address, Version = version, PacketId = packet.PacketId, - Data = packet.Write(), + Data = data, Priority = packet.Priority, Reliability = packet.Reliability }); @@ -153,6 +160,26 @@ public void VerifyPacketSent(PacketId packetId, TPlayer to, byte[] data = null, ).Should().Be(count); } + public void VerifyResourceStartedPacketSent(TPlayer player, Resource resource, int count = 1) + { + var sendPacketCalls = this.sendPacketCalls + .Where(x => x.PacketId == PacketId.PACKET_ID_RESOURCE_START && x.Address == player.GetAddress()); + + int startedCount = 0; + + var packet = new ResourceStartPacket(); + foreach (var sendPacketCall in sendPacketCalls) + { + packet.Read(sendPacketCall.Data); + if(packet.NetId == resource.NetId) + { + startedCount++; + } + } + + startedCount.Should().Be(count); + } + public void VerifyLuaElementRpcPacketSent(ElementRpcFunction packetId, TPlayer to, byte[] data = null, int count = 1) { this.sendPacketCalls.Count(x => @@ -192,6 +219,11 @@ public void VerifyLuaEventTriggered(string eventName, TPlayer to, Element source count.Should().Be(expectedCount); } + public T CreateInstance(params object[] parameters) + { + return ActivatorUtilities.CreateInstance(this.serviceProvider, parameters); + } + public void ResetPacketCountVerification() => this.sendPacketCalls.Clear(); public uint GenerateBinaryAddress() => ++this.binaryAddressCounter; diff --git a/SlipeServer.Server.Tests/Integration/Services/ResourceServiceTests.cs b/SlipeServer.Server.Tests/Integration/Services/ResourceServiceTests.cs new file mode 100644 index 00000000..a3f8a5ab --- /dev/null +++ b/SlipeServer.Server.Tests/Integration/Services/ResourceServiceTests.cs @@ -0,0 +1,83 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SlipeServer.Packets.Definitions.Resources; +using SlipeServer.Packets.Enums; +using SlipeServer.Server.Clients; +using SlipeServer.Server.PacketHandling.Handlers; +using SlipeServer.Server.Resources; +using SlipeServer.Server.Resources.Providers; +using SlipeServer.Server.TestTools; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace SlipeServer.Server.Tests.Integration.Services; + +public class ClientPlayerResourceStartedPacketHandler : IPacketHandler +{ + public PacketId PacketId => PacketId.PACKET_ID_RESOURCE_START; + + public ClientPlayerResourceStartedPacketHandler() + { + } + + public void HandlePacket(IClient client, ResourceStartPacket packet) + { + client.Player.TriggerResourceStarted(packet.NetId); + } +} + +public class ResourceServiceTests +{ + class ResourceA : Resource + { + public ResourceA(MtaServer server) : base(server, server.RootElement, "ResourceA") { } + } + + class ResourceB : Resource + { + public ResourceB(MtaServer server) : base(server, server.RootElement, "ResourceB") { } + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task ResourceServiceShouldWork(bool parallel) + { + var server = new TestingServer((Configuration?)null, builder => + { + builder.ConfigureServices(services => + { + services.RemoveAll(); + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + }); + }); + + server.RegisterPacketHandler(); + + var resourceProvider = server.GetRequiredService(); + var resourceA = new ResourceA(server); + server.AddAdditionalResource(resourceA, []); + resourceProvider.AddResource(resourceA); + var resourceB = new ResourceB(server); + server.AddAdditionalResource(resourceB, []); + resourceProvider.AddResource(resourceB); + + var resourceService = server.CreateInstance(); + using var monitor = resourceService.Monitor(); + + resourceService.StartResource("ResourceA"); + resourceService.StartResource("ResourceB"); + + var player = server.AddFakePlayer(); + await resourceService.StartResourcesForPlayer(player, parallel); + + using var _ = new AssertionScope(); + monitor.OccurredEvents.Select(x => x.EventName).Should().BeEquivalentTo(["Started", "Started", "AllStarted"]); + server.VerifyResourceStartedPacketSent(player, resourceA); + server.VerifyResourceStartedPacketSent(player, resourceB); + } +} diff --git a/SlipeServer.Server/Resources/ResourceService.cs b/SlipeServer.Server/Resources/ResourceService.cs index 9a55c07e..e0061232 100644 --- a/SlipeServer.Server/Resources/ResourceService.cs +++ b/SlipeServer.Server/Resources/ResourceService.cs @@ -1,7 +1,12 @@ -using SlipeServer.Server.Elements; +using Microsoft.Extensions.Logging; +using SlipeServer.Server.Elements; +using SlipeServer.Server.Elements.Events; using SlipeServer.Server.Resources.Providers; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace SlipeServer.Server.Resources; @@ -10,31 +15,93 @@ namespace SlipeServer.Server.Resources; /// public class ResourceService { - private readonly MtaServer server; - private readonly RootElement root; private readonly IResourceProvider resourceProvider; + private readonly ILogger logger; private readonly List startedResources; public IReadOnlyCollection StartedResources => this.startedResources.AsReadOnly(); - public ResourceService(MtaServer server, RootElement root, IResourceProvider resourceProvider) + public event Action? AllStarted; + public event Action? Started; + + public ResourceService(IResourceProvider resourceProvider, ILogger logger) { - this.server = server; - this.root = root; this.resourceProvider = resourceProvider; - - this.startedResources = new List(); - - this.server.PlayerJoined += HandlePlayerJoin; + this.logger = logger; + this.startedResources = []; } - private void HandlePlayerJoin(Player player) + public async Task StartResourcesForPlayer(Player player, bool parallel = true, CancellationToken cancellationToken = default) { - foreach (var resource in this.startedResources) + var exceptions = new List(); + + if (parallel) + { + var resourcesNetIds = this.startedResources.Select(x => x.NetId).ToList(); + var tcs = new TaskCompletionSource(); + + void handleResourceStart(Player sender, PlayerResourceStartedEventArgs e) + { + if (sender == player && resourcesNetIds.Remove(e.NetId)) + { + Started?.Invoke(this.startedResources.Where(x => x.NetId == e.NetId).First(), sender); + if (resourcesNetIds.Count == 0) + tcs.SetResult(); + } + } + + void handlePlayerDisconnected(Player disconnectingPlayer, PlayerQuitEventArgs e) + { + if (player != disconnectingPlayer) + return; + + player.ResourceStarted -= handleResourceStart; + player.Disconnected -= handlePlayerDisconnected; + + tcs.SetException(new System.Exception("Player disconnected.")); + } + + player.ResourceStarted += handleResourceStart; + player.Disconnected += handlePlayerDisconnected; + cancellationToken.Register(() => + { + tcs.TrySetException(new OperationCanceledException()); + }); + + foreach (var resource in this.startedResources) + resource.StartFor(player); + + try + { + await tcs.Task; + } + finally + { + player.ResourceStarted -= handleResourceStart; + player.Disconnected -= handlePlayerDisconnected; + } + } + else { - resource.StartFor(player); + foreach (var resource in this.startedResources) + { + try + { + await resource.StartForAsync(player); + Started?.Invoke(resource, player); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } } + + this.AllStarted?.Invoke(player); + + if (exceptions.Count > 0) + throw new AggregateException(exceptions); } public Resource? StartResource(string name) @@ -62,4 +129,7 @@ public void StopResource(Resource resource) this.startedResources.Remove(resource); resource.Stop(); } + + public event Action? AllResourcesStartedForPlayer; + public event Action? Started; } diff --git a/SlipeServer.WebHostBuilderExample/Program.cs b/SlipeServer.WebHostBuilderExample/Program.cs index b5aa2290..663592dd 100644 --- a/SlipeServer.WebHostBuilderExample/Program.cs +++ b/SlipeServer.WebHostBuilderExample/Program.cs @@ -8,6 +8,7 @@ using SlipeServer.Lua; using SlipeServer.WebHostBuilderExample; using SlipeServer.Example; +using SlipeServer.Server.Resources; Directory.SetCurrentDirectory(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly()!.Location)!); @@ -34,6 +35,7 @@ builder.Services.AddLua(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddHostedService(); // Use instead of logics builder.Services.TryAddSingleton(x => x.GetRequiredService>()); From daf0c63714662563ac283a3465416129b04471ca Mon Sep 17 00:00:00 2001 From: Sebastian Jura <22455534+CrosRoad95@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:08:40 +0100 Subject: [PATCH 2/2] Addendum --- SlipeServer.Server/Resources/ResourceService.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/SlipeServer.Server/Resources/ResourceService.cs b/SlipeServer.Server/Resources/ResourceService.cs index e0061232..5a066918 100644 --- a/SlipeServer.Server/Resources/ResourceService.cs +++ b/SlipeServer.Server/Resources/ResourceService.cs @@ -16,7 +16,6 @@ namespace SlipeServer.Server.Resources; public class ResourceService { private readonly IResourceProvider resourceProvider; - private readonly ILogger logger; private readonly List startedResources; @@ -25,10 +24,9 @@ public class ResourceService public event Action? AllStarted; public event Action? Started; - public ResourceService(IResourceProvider resourceProvider, ILogger logger) + public ResourceService(IResourceProvider resourceProvider) { this.resourceProvider = resourceProvider; - this.logger = logger; this.startedResources = []; } @@ -129,7 +127,4 @@ public void StopResource(Resource resource) this.startedResources.Remove(resource); resource.Stop(); } - - public event Action? AllResourcesStartedForPlayer; - public event Action? Started; }