From: Gerard Smit Date: Mon, 19 Oct 2020 09:31:25 +0000 (+0200) Subject: Plugin loader (#59) X-Git-Tag: v1.2.2~96^2~74 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=852badb48e71d28fe252191332223380d71530db;p=rhonda%2Fimpostor.git Plugin loader (#59) * Parse GameData headers * Added GameData spawn * GameData playerinfo parsing * Added BufferMessageReader tests * Fix InnerGameData * Added CustomNetworkTransform and VoteBanSystem * Revert changes * Finish CustomNetworkTransform * Implemented a simple plugin loader * Replaced Reference with PackageReference Note to myself: don't trust Rider auto-add reference * Use classes and moved all API-related stuff to one folder * Added PlayerJoinedGameEvent and PlayerLeftGameEvent * Removed Impostor.Server.Hazel * Moved more API-specific stuff Co-authored-by: AeonLucid --- diff --git a/src/Impostor.Plugins.Debugger/App.razor b/src/Impostor.Plugins.Debugger/App.razor new file mode 100644 index 0000000..38e8633 --- /dev/null +++ b/src/Impostor.Plugins.Debugger/App.razor @@ -0,0 +1,10 @@ + + + + + + +

Sorry, there's nothing at this address.

+
+
+
\ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/DebugPlugin.cs b/src/Impostor.Plugins.Debugger/DebugPlugin.cs new file mode 100644 index 0000000..de18d73 --- /dev/null +++ b/src/Impostor.Plugins.Debugger/DebugPlugin.cs @@ -0,0 +1,36 @@ +using System; +using Impostor.Server.Plugins; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Impostor.Plugins.Debugger +{ + public class DebugPlugin : PluginBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(); + services.AddServerSideBlazor(); + } + + public override void ConfigureHost(IHostBuilder host) + { + host.ConfigureWebHostDefaults(webBuilder => + { + webBuilder.Configure(app => + { + app.UseStaticFiles(); + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); + }); + }); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj b/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj new file mode 100644 index 0000000..1852d26 --- /dev/null +++ b/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + Library + + + + + + + \ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/Pages/Index.razor b/src/Impostor.Plugins.Debugger/Pages/Index.razor new file mode 100644 index 0000000..7ca76ed --- /dev/null +++ b/src/Impostor.Plugins.Debugger/Pages/Index.razor @@ -0,0 +1,68 @@ +@page "/" +@using Impostor.Server.Events +@using Impostor.Server.Events.Managers +@using Impostor.Server.Games.Managers +@implements IDisposable +@implements IEventListener +@inject IEventManager EventManager +@inject IGameManager GameManager + +
+

Games

+ @if (GameManager.Games.Any()) + { + + + + + + + + + @foreach (var game in GameManager.Games) + { + + + + + } + +
CodePlayers
@game.Code +
    + @foreach (var player in game.Players) + { +
  • @player.Client.Name
  • + } +
+
+ } + else + { +
+ There are no active games. +
+ } +
+ +@code { + private IDisposable _disposable; + + [EventListener(typeof(GameCreatedEvent))] + [EventListener(typeof(GameDestroyedEvent))] + [EventListener(typeof(PlayerJoinedGameEvent))] + [EventListener(typeof(PlayerLeftGameEvent))] + public void OnGameCreated(IGameEvent e) + { + StateHasChanged(); + } + + protected override void OnInitialized() + { + _disposable = EventManager.RegisterListener(this, InvokeAsync); + } + + public void Dispose() + { + _disposable?.Dispose(); + } +} \ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml b/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml new file mode 100644 index 0000000..eed3aaf --- /dev/null +++ b/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml @@ -0,0 +1,19 @@ +@page "/" +@namespace Impostor.Plugins.Debugger.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + Impostor Debugger + + + + + + + + + \ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor b/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor new file mode 100644 index 0000000..07da9d6 --- /dev/null +++ b/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor @@ -0,0 +1,5 @@ +@inherits LayoutComponentBase + +
+ @Body +
\ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/_Imports.razor b/src/Impostor.Plugins.Debugger/_Imports.razor new file mode 100644 index 0000000..109ec4e --- /dev/null +++ b/src/Impostor.Plugins.Debugger/_Imports.razor @@ -0,0 +1,8 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using Impostor.Plugins.Debugger.Shared \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs b/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs index bea45c1..c8b9360 100644 --- a/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs +++ b/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs @@ -2,19 +2,18 @@ namespace Impostor.Server.Events { - [AttributeUsage(AttributeTargets.Method)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class EventListenerAttribute : Attribute { public EventListenerAttribute(EventPriority priority = EventPriority.Normal) { Priority = priority; - Events = new Type[0]; } public EventListenerAttribute(Type @event, EventPriority priority = EventPriority.Normal) { Priority = priority; - Events = new[] { @event }; + Event = @event; } /// @@ -25,7 +24,7 @@ namespace Impostor.Server.Events /// /// The events that the listener is listening to. /// - public Type[] Events { get; set; } + public Type? Event { get; set; } /// /// If set to true, the listener will be called regardless of the . diff --git a/src/Impostor.Server.Api/Events/Game/GameDestroyedEvent.cs b/src/Impostor.Server.Api/Events/Game/GameDestroyedEvent.cs new file mode 100644 index 0000000..5d32143 --- /dev/null +++ b/src/Impostor.Server.Api/Events/Game/GameDestroyedEvent.cs @@ -0,0 +1,22 @@ +using Impostor.Server.Games; + +namespace Impostor.Server.Events +{ + /// + /// Called whenever a new is destroyed. + /// + public sealed class GameDestroyedEvent : IGameEvent + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the game. + public GameDestroyedEvent(IGame game) + { + Game = game; + } + + /// + public IGame Game { get; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/Game/PlayerJoinedGameEvent.cs b/src/Impostor.Server.Api/Events/Game/PlayerJoinedGameEvent.cs new file mode 100644 index 0000000..0a47f64 --- /dev/null +++ b/src/Impostor.Server.Api/Events/Game/PlayerJoinedGameEvent.cs @@ -0,0 +1,18 @@ +using Impostor.Server.Games; +using Impostor.Server.Net; + +namespace Impostor.Server.Events +{ + public class PlayerJoinedGameEvent : IGameEvent + { + public PlayerJoinedGameEvent(IGame game, IClientPlayer player) + { + Game = game; + Player = player; + } + + public IGame Game { get; } + + public IClientPlayer Player { get; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/Game/PlayerLeftGameEvent.cs b/src/Impostor.Server.Api/Events/Game/PlayerLeftGameEvent.cs new file mode 100644 index 0000000..e17c333 --- /dev/null +++ b/src/Impostor.Server.Api/Events/Game/PlayerLeftGameEvent.cs @@ -0,0 +1,21 @@ +using Impostor.Server.Games; +using Impostor.Server.Net; + +namespace Impostor.Server.Events +{ + public class PlayerLeftGameEvent : IGameEvent + { + public PlayerLeftGameEvent(IGame game, IClientPlayer player, bool isBan) + { + Game = game; + Player = player; + IsBan = isBan; + } + + public IGame Game { get; } + + public IClientPlayer Player { get; } + + public bool IsBan { get; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/Managers/IEventManager.cs b/src/Impostor.Server.Api/Events/Managers/IEventManager.cs index 488e38c..11ae01d 100644 --- a/src/Impostor.Server.Api/Events/Managers/IEventManager.cs +++ b/src/Impostor.Server.Api/Events/Managers/IEventManager.cs @@ -1,9 +1,29 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Impostor.Server.Events.Managers { public interface IEventManager { + /// + /// Register a temporary event listener. + /// + /// Event callback. + /// Disposable that unregisters the callback from the event manager. + /// Type of the event. + IDisposable Register(Func callback) + where TEvent : IEvent; + + /// + /// Register a temporary event listener. + /// + /// Event listener. + /// Middleware between the events, which can be used to swap to the correct thread dispatcher. + /// Disposable that unregisters the callback from the event manager. + /// Type of the event listener. + IDisposable RegisterListener(TListener listener, Func, Task>? invoker = null) + where TListener : IEventListener; + /// /// Returns true if an event with the type is registered. /// diff --git a/src/Impostor.Server.Api/Extensions/GameManagerExtensions.cs b/src/Impostor.Server.Api/Extensions/GameManagerExtensions.cs deleted file mode 100644 index df89168..0000000 --- a/src/Impostor.Server.Api/Extensions/GameManagerExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Linq; -using Impostor.Server.Games.Managers; -using Impostor.Shared.Innersloth.Data; - -namespace Impostor.Server -{ - public static class GameManagerExtensions - { - public static int GetGameCount(this IGameManager manager, MapFlags map) - { - return manager.Games.Count(game => map.HasFlag((MapFlags)(1 << game.Options.MapId))); - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Games/Extensions/GameManagerExtensions.cs b/src/Impostor.Server.Api/Games/Extensions/GameManagerExtensions.cs new file mode 100644 index 0000000..db173c2 --- /dev/null +++ b/src/Impostor.Server.Api/Games/Extensions/GameManagerExtensions.cs @@ -0,0 +1,14 @@ +using System.Linq; +using Impostor.Server.Games.Managers; +using Impostor.Shared.Innersloth.Data; + +namespace Impostor.Server.Games +{ + public static class GameManagerExtensions + { + public static int GetGameCount(this IGameManager manager, MapFlags map) + { + return manager.Games.Count(game => map.HasFlag((MapFlags)(1 << game.Options.MapId))); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Games/IGame.cs b/src/Impostor.Server.Api/Games/IGame.cs index 84a301d..c4931b7 100644 --- a/src/Impostor.Server.Api/Games/IGame.cs +++ b/src/Impostor.Server.Api/Games/IGame.cs @@ -32,32 +32,5 @@ namespace Impostor.Server.Games int HostId { get; } IGameMessageWriter CreateMessage(MessageType type); - - bool TryGetPlayer(int id, [NotNullWhen(true)] out IClientPlayer player); - - /// - /// Register a new client to the game. - /// - /// Client to register. - /// Join result. - ValueTask AddClientAsync(IClient client); - - /// - /// Kicks all the players from the game to end the game. - /// - /// A representing the asynchronous operation. - ValueTask EndAsync(); - - ValueTask HandleStartGame(IMessageReader reader); - - ValueTask HandleEndGame(IMessageReader reader); - - ValueTask HandleKickPlayer(int playerId, bool isBan); - - ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason); - - ValueTask HandleAlterGame(IMessageReader message, IClientPlayer sender, bool isPublic); - - ValueTask HandleGameData(IMessageReader parent, IClientPlayer sender, bool toPlayer); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Games/Managers/IGameManager.cs b/src/Impostor.Server.Api/Games/Managers/IGameManager.cs index 9cb9646..d089772 100644 --- a/src/Impostor.Server.Api/Games/Managers/IGameManager.cs +++ b/src/Impostor.Server.Api/Games/Managers/IGameManager.cs @@ -1,7 +1,4 @@ using System.Collections.Generic; -using System.Threading.Tasks; -using Impostor.Shared.Innersloth; -using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Games.Managers { @@ -9,12 +6,6 @@ namespace Impostor.Server.Games.Managers { IEnumerable Games { get; } - ValueTask CreateAsync(GameOptionsData options); - IGame? Find(GameCode code); - - IEnumerable FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10); - - ValueTask RemoveAsync(GameCode code); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Impostor.Server.Api.csproj b/src/Impostor.Server.Api/Impostor.Server.Api.csproj index 1518fb6..0e72d47 100644 --- a/src/Impostor.Server.Api/Impostor.Server.Api.csproj +++ b/src/Impostor.Server.Api/Impostor.Server.Api.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings b/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings index c8f8b80..2df7445 100644 --- a/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings +++ b/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings @@ -4,4 +4,5 @@ True True True + True True \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs b/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs deleted file mode 100644 index ac0faec..0000000 --- a/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading.Tasks; - -namespace Impostor.Server.Net.Factories -{ - public interface IClientFactory - { - /// - /// Creates a client for the Hazel . - /// - /// Hazel connection. - /// - /// - IClient Create(IConnection connection, string name, int clientVersion); - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/IClient.cs b/src/Impostor.Server.Api/Net/IClient.cs index 0283a75..4d6d22d 100644 --- a/src/Impostor.Server.Api/Net/IClient.cs +++ b/src/Impostor.Server.Api/Net/IClient.cs @@ -57,10 +57,6 @@ namespace Impostor.Server.Net /// /// Gets or sets the current game data of the . /// - IClientPlayer? Player { get; set; } - - ValueTask HandleMessageAsync(IMessage message); - - ValueTask HandleDisconnectAsync(string reason); + IClientPlayer? Player { get; } } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/IConnection.cs b/src/Impostor.Server.Api/Net/IConnection.cs index 371c5ac..69d9119 100644 --- a/src/Impostor.Server.Api/Net/IConnection.cs +++ b/src/Impostor.Server.Api/Net/IConnection.cs @@ -22,9 +22,9 @@ namespace Impostor.Server.Net bool IsConnected { get; } /// - /// Gets or sets the client of the connection. + /// Gets the client of the connection. /// - IClient? Client { get; set; } + IClient? Client { get; } /// /// Create a message writer that can be send to the connection. diff --git a/src/Impostor.Server.Api/Net/Manager/IClientManager.cs b/src/Impostor.Server.Api/Net/Manager/IClientManager.cs index 46da848..6405a65 100644 --- a/src/Impostor.Server.Api/Net/Manager/IClientManager.cs +++ b/src/Impostor.Server.Api/Net/Manager/IClientManager.cs @@ -1,15 +1,9 @@ -using System.Threading.Tasks; +using System.Collections.Generic; namespace Impostor.Server.Net.Manager { public interface IClientManager { - ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion); - - void Register(IClient client); - - void Remove(IClient client); - - bool Validate(IClient client); + IEnumerable Clients { get; } } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs b/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs deleted file mode 100644 index 19ef796..0000000 --- a/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Net; -using System.Threading.Tasks; -using Impostor.Server.Games; -using Impostor.Server.Net.Messages; - -namespace Impostor.Server.Net.Manager -{ - /// - /// Represents the matchmaker which will listen for incoming connections. - /// - public interface IMatchmaker - { - /// - /// Starts the matchmaker on the given endpoint. - /// - /// Endpoint where the matchmaker should listen to. - /// A representing the asynchronous operation. - ValueTask StartAsync(IPEndPoint ipEndPoint); - - /// - /// Stop the matchmaker. - /// - /// A representing the asynchronous operation. - ValueTask StopAsync(); - - /// - /// Create a message writer that can be send to players in the game. - /// - /// The game. - /// Type of the message. - /// Message writer for the given game. - IGameMessageWriter CreateGameMessageWriter(IGame game, MessageType messageType); - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Plugins/IPlugin.cs b/src/Impostor.Server.Api/Plugins/IPlugin.cs index d31520b..e5f8645 100644 --- a/src/Impostor.Server.Api/Plugins/IPlugin.cs +++ b/src/Impostor.Server.Api/Plugins/IPlugin.cs @@ -1,5 +1,7 @@ using System.Threading.Tasks; using Impostor.Server.Events; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Impostor.Server.Plugins { @@ -10,5 +12,9 @@ namespace Impostor.Server.Plugins ValueTask DisableAsync(); ValueTask ReloadAsync(); + + void ConfigureHost(IHostBuilder host); + + void ConfigureServices(IServiceCollection services); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Plugins/PluginBase.cs b/src/Impostor.Server.Api/Plugins/PluginBase.cs index c392bba..5ef6cf3 100644 --- a/src/Impostor.Server.Api/Plugins/PluginBase.cs +++ b/src/Impostor.Server.Api/Plugins/PluginBase.cs @@ -1,4 +1,6 @@ using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Impostor.Server.Plugins { @@ -18,5 +20,13 @@ namespace Impostor.Server.Plugins { return default; } + + public virtual void ConfigureHost(IHostBuilder host) + { + } + + public virtual void ConfigureServices(IServiceCollection services) + { + } } } \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Extensions/ServiceExtensions.cs b/src/Impostor.Server.Hazel/Extensions/ServiceExtensions.cs deleted file mode 100644 index b97a65f..0000000 --- a/src/Impostor.Server.Hazel/Extensions/ServiceExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Impostor.Server.Net.Manager; -using Microsoft.Extensions.DependencyInjection; - -namespace Impostor.Server.Hazel -{ - public static class ServiceExtensions - { - public static IServiceCollection UseHazelMatchmaking(this IServiceCollection services) - { - services.AddSingleton(); - return services; - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/HazelConnection.cs b/src/Impostor.Server.Hazel/HazelConnection.cs deleted file mode 100644 index b0f5d5e..0000000 --- a/src/Impostor.Server.Hazel/HazelConnection.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using Hazel; -using Impostor.Server.Hazel.Messages; -using Impostor.Server.Net; -using Impostor.Server.Net.Messages; -using Microsoft.Extensions.Logging; - -namespace Impostor.Server.Hazel -{ - internal class HazelConnection : IConnection - { - private readonly ILogger _logger; - - public HazelConnection(Connection innerConnection, ILogger logger) - { - _logger = logger; - InnerConnection = innerConnection; - innerConnection.DataReceived = ConnectionOnDataReceived; - innerConnection.Disconnected = ConnectionOnDisconnected; - } - - public Connection InnerConnection { get; } - - public IPEndPoint EndPoint => InnerConnection.EndPoint; - - public bool IsConnected => InnerConnection.State == ConnectionState.Connected; - - public IClient Client { get; set; } - - private async ValueTask ConnectionOnDisconnected(DisconnectedEventArgs e) - { - if (Client != null) - { - await Client.HandleDisconnectAsync(e.Reason); - } - } - - private async ValueTask ConnectionOnDataReceived(DataReceivedEventArgs e) - { - if (Client == null) - { - _logger.LogWarning("Client was null."); - return; - } - - while (true) - { - if (e.Message.Position >= e.Message.Length) - { - break; - } - - var reader = e.Message.ReadMessage(); - var type = e.SendOption switch - { - SendOption.None => MessageType.Unreliable, - SendOption.Reliable => MessageType.Reliable, - _ => throw new NotSupportedException() - }; - - using var message = new HazelMessage(reader, type); - - await Client.HandleMessageAsync(message); - } - } - - public IConnectionMessageWriter CreateMessage(MessageType messageType) - { - return new HazelConnectionMessageWriter(messageType, this); - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/HazelMatchmaker.cs b/src/Impostor.Server.Hazel/HazelMatchmaker.cs deleted file mode 100644 index cc647a1..0000000 --- a/src/Impostor.Server.Hazel/HazelMatchmaker.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; -using Hazel; -using Hazel.Udp; -using Impostor.Server.Games; -using Impostor.Server.Hazel.Messages; -using Impostor.Server.Net.Manager; -using Impostor.Server.Net.Messages; -using Microsoft.Extensions.Logging; - -namespace Impostor.Server.Hazel -{ - internal class HazelMatchmaker : IMatchmaker - { - private readonly IClientManager _clientManager; - private readonly ILogger _logger; - private readonly ILogger _connectionLogger; - private UdpConnectionListener _connection; - - public HazelMatchmaker( - ILogger logger, - IClientManager clientManager, - ILogger connectionLogger) - { - _logger = logger; - _clientManager = clientManager; - _connectionLogger = connectionLogger; - } - - public async ValueTask StartAsync(IPEndPoint ipEndPoint) - { - var mode = ipEndPoint.AddressFamily switch - { - AddressFamily.InterNetwork => IPMode.IPv4, - AddressFamily.InterNetworkV6 => IPMode.IPv6, - _ => throw new InvalidOperationException() - }; - - _connection = new UdpConnectionListener(ipEndPoint, mode); - _connection.NewConnection = OnNewConnection; - - await _connection.StartAsync(); - } - - public async ValueTask StopAsync() - { - await _connection.DisposeAsync(); - } - - private async ValueTask OnNewConnection(NewConnectionEventArgs e) - { - // Handshake. - var clientVersion = e.HandshakeData.ReadInt32(); - var name = e.HandshakeData.ReadString(); - - var connection = new HazelConnection(e.Connection, _connectionLogger); - - // Register client - await _clientManager.RegisterConnectionAsync(connection, name, clientVersion); - } - - public IGameMessageWriter CreateGameMessageWriter(IGame game, MessageType messageType) - { - return new HazelGameMessageWriter(messageType, game); - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj b/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj deleted file mode 100644 index 7d699cf..0000000 --- a/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - net5.0 - true - None - - - - - - - - - - - - - - <_Parameter1>Impostor.Tests - - - - diff --git a/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj.DotSettings b/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj.DotSettings deleted file mode 100644 index 17962b1..0000000 --- a/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/BufferMessageReader.cs b/src/Impostor.Server.Hazel/Messages/BufferMessageReader.cs deleted file mode 100644 index 3e560c7..0000000 --- a/src/Impostor.Server.Hazel/Messages/BufferMessageReader.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Runtime.CompilerServices; -using System.Text; -using Impostor.Server.Net.Messages; - -namespace Impostor.Server.Hazel.Messages -{ - public class BufferMessageReader : IMessageReader - { - public byte Tag { get; } - public ReadOnlyMemory Buffer { get; } - public int Position { get; set; } - public int Length => Buffer.Length; - - public BufferMessageReader(byte tag, ReadOnlyMemory buffer) - { - Tag = tag; - Buffer = buffer; - } - - public IMessageReader ReadMessage() - { - var length = ReadUInt16(); - var tag = ReadByte(); - var pos = Position; - - Position += length; - - return new BufferMessageReader(tag, Buffer.Slice(pos, length)); - } - - public bool ReadBoolean() - { - byte val = FastByte(); - return val != 0; - } - - public sbyte ReadSByte() - { - return (sbyte)FastByte(); - } - - public byte ReadByte() - { - return FastByte(); - } - - public ushort ReadUInt16() - { - var output = BinaryPrimitives.ReadUInt16LittleEndian(Buffer.Span.Slice(Position)); - Position += sizeof(ushort); - return output; - } - - public short ReadInt16() - { - var output = BinaryPrimitives.ReadInt16LittleEndian(Buffer.Span.Slice(Position)); - Position += sizeof(short); - return output; - } - - public uint ReadUInt32() - { - var output = BinaryPrimitives.ReadUInt32LittleEndian(Buffer.Span.Slice(Position)); - Position += sizeof(uint); - return output; - } - - public int ReadInt32() - { - var output = BinaryPrimitives.ReadInt32LittleEndian(Buffer.Span.Slice(Position)); - Position += sizeof(int); - return output; - } - - public float ReadSingle() - { - var output = BinaryPrimitives.ReadSingleLittleEndian(Buffer.Span.Slice(Position)); - Position += sizeof(float); - return output; - } - - public string ReadString() - { - var len = ReadPackedInt32(); - var output = Encoding.UTF8.GetString(Buffer.Span.Slice(Position, len)); - Position += len; - return output; - } - - public ReadOnlyMemory ReadBytesAndSize() - { - var len = ReadPackedInt32(); - return ReadBytes(len); - } - - public ReadOnlyMemory ReadBytes(int length) - { - var output = Buffer.Slice(Position, length); - Position += length; - return output; - } - - public int ReadPackedInt32() - { - return (int)ReadPackedUInt32(); - } - - public uint ReadPackedUInt32() - { - bool readMore = true; - int shift = 0; - uint output = 0; - - while (readMore) - { - byte b = ReadByte(); - if (b >= 0x80) - { - readMore = true; - b ^= 0x80; - } - else - { - readMore = false; - } - - output |= (uint)(b << shift); - shift += 7; - } - - return output; - } - - public void CopyTo(IMessageWriter writer) - { - writer.Write((ushort) Length); - writer.Write(Tag); - writer.Write(Buffer); - } - - public IMessageReader Slice(int start) - { - return new BufferMessageReader(Tag, Buffer.Slice(start)); - } - - public IMessageReader Slice(int start, int length) - { - return new BufferMessageReader(Tag, Buffer.Slice(start, length)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private byte FastByte() - { - return Buffer.Span[Position++]; - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs deleted file mode 100644 index 7f45619..0000000 --- a/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; -using Impostor.Server.Net; -using Impostor.Server.Net.Messages; - -namespace Impostor.Server.Hazel.Messages -{ - internal class HazelConnectionMessageWriter : HazelMessageWriter, IConnectionMessageWriter - { - private readonly HazelConnection _connection; - - public HazelConnectionMessageWriter(MessageType type, HazelConnection connection) - : base(type) - { - _connection = connection; - } - - public IConnection Connection => _connection; - - public async ValueTask SendAsync() - { - await _connection.InnerConnection.Send(Writer); - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs deleted file mode 100644 index e99533b..0000000 --- a/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Hazel; -using Impostor.Server.Games; -using Impostor.Server.Net; -using Impostor.Server.Net.Messages; - -namespace Impostor.Server.Hazel.Messages -{ - internal class HazelGameMessageWriter : HazelMessageWriter, IGameMessageWriter - { - private readonly IGame _game; - - public HazelGameMessageWriter(MessageType type, IGame game) - : base(type) - { - _game = game; - } - - private IEnumerable GetConnections(Func filter) - { - return _game.Players - .Where(filter) - .Select(p => p.Client.Connection) - .OfType() - .Select(c => c.InnerConnection); - } - - public ValueTask SendToAllAsync(LimboStates states) - { - foreach (var connection in GetConnections(x => x.Limbo.HasFlag(states))) - { - connection.Send(Writer); - } - - return default; - } - - public ValueTask SendToAllExceptAsync(int senderId, LimboStates states) - { - foreach (var connection in GetConnections(x => - x.Limbo.HasFlag(states) && - x.Client.Id != senderId)) - { - connection.Send(Writer); - } - return default; - } - - public ValueTask SendToAsync(int id) - { - if (_game.TryGetPlayer(id, out var player) - && player.Client.Connection is HazelConnection hazelConnection) - { - hazelConnection.InnerConnection.Send(Writer); - } - - return default; - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/HazelMessage.cs b/src/Impostor.Server.Hazel/Messages/HazelMessage.cs deleted file mode 100644 index 4f08eb4..0000000 --- a/src/Impostor.Server.Hazel/Messages/HazelMessage.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using Hazel; -using Impostor.Server.Net.Messages; - -namespace Impostor.Server.Hazel.Messages -{ - internal class HazelMessage : IMessage, IDisposable - { - private bool _isDisposed; - private readonly MessageReader _reader; - - public HazelMessage(MessageReader reader, MessageType type) - { - _reader = reader; - Type = type; - } - - public MessageType Type { get; } - - public IMessageReader CreateReader() - { - if (_isDisposed) - { - throw new ObjectDisposedException(nameof(_reader)); - } - - return new BufferMessageReader(_reader.Tag, _reader.Buffer); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - _isDisposed = true; - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - ~HazelMessage() - { - Dispose(false); - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs deleted file mode 100644 index 0d40313..0000000 --- a/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Net; -using Hazel; -using Impostor.Server.Games; -using Impostor.Server.Net.Messages; - -namespace Impostor.Server.Hazel.Messages -{ - internal abstract class HazelMessageWriter : IMessageWriter - { - protected readonly MessageWriter Writer; - - protected HazelMessageWriter(MessageType type) - { - Writer = MessageWriter.Get(ToSendOption(type)); - } - - private static SendOption ToSendOption(MessageType type) - { - return type switch - { - MessageType.Unreliable => SendOption.None, - MessageType.Reliable => SendOption.Reliable, - _ => throw new NotSupportedException($"Message type {type} is not supported") - }; - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - Writer.Recycle(); - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public void Write(bool value) - { - Writer.Write(value); - } - - public void Write(sbyte value) - { - Writer.Write(value); - } - - public void Write(byte value) - { - Writer.Write(value); - } - - public void Write(short value) - { - Writer.Write(value); - } - - public void Write(ushort value) - { - Writer.Write(value); - } - - public void Write(uint value) - { - Writer.Write(value); - } - - public void Write(int value) - { - Writer.Write(value); - } - - public void Write(float value) - { - Writer.Write(value); - } - - public void Write(string value) - { - Writer.Write(value); - } - - public void Write(IPAddress value) - { - Writer.Write(value.GetAddressBytes()); - } - - public void WritePacked(int value) - { - Writer.WritePacked(value); - } - - public void WritePacked(uint value) - { - Writer.WritePacked(value); - } - - public void Write(ReadOnlyMemory data) - { - Writer.Write(data.ToArray()); // TODO: Fix memory allocation. - } - - public void StartMessage(byte typeFlag) - { - Writer.StartMessage(typeFlag); - } - - public void Write(GameCode value) - { - Write(value.Value); - } - - public void EndMessage() - { - Writer.EndMessage(); - } - - public void Clear(MessageType type) - { - Writer.Clear(ToSendOption(type)); - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Api/ClientBase.Api.cs b/src/Impostor.Server/Api/ClientBase.Api.cs new file mode 100644 index 0000000..1a375d2 --- /dev/null +++ b/src/Impostor.Server/Api/ClientBase.Api.cs @@ -0,0 +1,10 @@ +// ReSharper disable once CheckNamespace +namespace Impostor.Server.Net +{ + internal abstract partial class ClientBase : IClient + { + IConnection IClient.Connection => Connection; + + IClientPlayer IClient.Player => Player; + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Api/ClientManager.Api.cs b/src/Impostor.Server/Api/ClientManager.Api.cs new file mode 100644 index 0000000..597dd5a --- /dev/null +++ b/src/Impostor.Server/Api/ClientManager.Api.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +// ReSharper disable once CheckNamespace +namespace Impostor.Server.Net.Manager +{ + internal partial class ClientManager : IClientManager + { + IEnumerable IClientManager.Clients => _clients.Values; + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Api/ClientPlayer.Api.cs b/src/Impostor.Server/Api/ClientPlayer.Api.cs new file mode 100644 index 0000000..b945f08 --- /dev/null +++ b/src/Impostor.Server/Api/ClientPlayer.Api.cs @@ -0,0 +1,14 @@ +using Impostor.Server.Games; + +// ReSharper disable once CheckNamespace +namespace Impostor.Server.Net.State +{ + internal partial class ClientPlayer + { + /// + IClient IClientPlayer.Client => Client; + + /// + IGame IClientPlayer.Game => Game; + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Api/Game.Api.cs b/src/Impostor.Server/Api/Game.Api.cs new file mode 100644 index 0000000..74743a9 --- /dev/null +++ b/src/Impostor.Server/Api/Game.Api.cs @@ -0,0 +1,10 @@ +using Impostor.Server.Games; + +// ReSharper disable once CheckNamespace +namespace Impostor.Server.Net.State +{ + internal partial class Game : IGame + { + IClientPlayer IGame.Host => Host; + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Api/GameManager.Api.cs b/src/Impostor.Server/Api/GameManager.Api.cs new file mode 100644 index 0000000..05010d3 --- /dev/null +++ b/src/Impostor.Server/Api/GameManager.Api.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Linq; +using Impostor.Server.Games; +using Impostor.Server.Games.Managers; + +// ReSharper disable once CheckNamespace +namespace Impostor.Server.Net.Manager +{ + internal partial class GameManager : IGameManager + { + IEnumerable IGameManager.Games => _games.Select(kv => kv.Value); + + IGame IGameManager.Find(GameCode code) => Find(code); + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Api/HazelConnection.Api.cs b/src/Impostor.Server/Api/HazelConnection.Api.cs new file mode 100644 index 0000000..d70a8b8 --- /dev/null +++ b/src/Impostor.Server/Api/HazelConnection.Api.cs @@ -0,0 +1,10 @@ +using Impostor.Server.Net; + +// ReSharper disable once CheckNamespace +namespace Impostor.Server.Hazel +{ + internal partial class HazelConnection : IConnection + { + IClient IConnection.Client => Client; + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Events/EventManager.cs b/src/Impostor.Server/Events/EventManager.cs index f3d33ca..69ceef5 100644 --- a/src/Impostor.Server/Events/EventManager.cs +++ b/src/Impostor.Server/Events/EventManager.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using Impostor.Server.Events.Managers; using Microsoft.Extensions.DependencyInjection; @@ -9,11 +11,49 @@ namespace Impostor.Server.Events { internal class EventManager : IEventManager { + private readonly ConcurrentDictionary _temporaryEventListeners; private readonly IServiceProvider _serviceProvider; public EventManager(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; + _temporaryEventListeners = new ConcurrentDictionary(); + } + + /// + public IDisposable Register(Func callback) + where TEvent : IEvent + { + var register = (TemporaryEventRegister) _temporaryEventListeners.GetOrAdd( + typeof(TEvent), + _ => new TemporaryEventRegister()); + + return register.Add(callback); + } + + /// + public IDisposable RegisterListener(TListener listener, Func, Task> invoker = null) + where TListener : IEventListener + { + if (listener == null) + { + throw new ArgumentNullException(nameof(listener)); + } + + var registerMethod = typeof(EventManager).GetMethod(nameof(RegisterListenerImpl), BindingFlags.Instance | BindingFlags.NonPublic); + var methods = RegisteredEventListener.FromType(listener.GetType()); + var disposes = new IDisposable[methods.Count]; + + for (var i = 0; i < methods.Count; i++) + { + var method = methods[i]; + + disposes[i] = (IDisposable) registerMethod! + .MakeGenericMethod(method.EventType) + .Invoke(this, new object[] { listener, method, invoker }); + } + + return new MultiDisposable(disposes); } /// @@ -37,6 +77,11 @@ namespace Impostor.Server.Events { await eventListener.InvokeAsync(handler, @event, scope.ServiceProvider); } + + if (_temporaryEventListeners.TryGetValue(typeof(T), out var cb)) + { + await ((TemporaryEventRegister) cb).CallAsync(scope.ServiceProvider, @event); + } } finally { @@ -67,5 +112,13 @@ namespace Impostor.Server.Events } } } + + private IDisposable RegisterListenerImpl(object obj, RegisteredEventListener listener, Func, Task> invoker = null) + where TEvent : IEvent + { + return invoker == null + ? Register((provider, @event) => listener.InvokeAsync(obj, @event, provider)) + : Register((provider, @event) => new ValueTask(invoker(() => listener.InvokeAsync(obj, @event, provider).AsTask()))); + } } } \ No newline at end of file diff --git a/src/Impostor.Server/Events/MultiDisposable.cs b/src/Impostor.Server/Events/MultiDisposable.cs new file mode 100644 index 0000000..fc6a6f0 --- /dev/null +++ b/src/Impostor.Server/Events/MultiDisposable.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Impostor.Server.Events +{ + /// + /// Disposes multiple . + /// + internal class MultiDisposable : IDisposable + { + private readonly IEnumerable _disposables; + + public MultiDisposable(IEnumerable disposables) + { + _disposables = disposables; + } + + public void Dispose() + { + foreach (var disposable in _disposables) + { + disposable.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Events/RegisteredEventListener.cs b/src/Impostor.Server/Events/RegisteredEventListener.cs index 058f099..d0021b3 100644 --- a/src/Impostor.Server/Events/RegisteredEventListener.cs +++ b/src/Impostor.Server/Events/RegisteredEventListener.cs @@ -63,7 +63,8 @@ namespace Impostor.Server.Events { var methodArgument = methodArguments[i]; - if (methodArgument.ParameterType == EventType) + if (typeof(IEvent).IsAssignableFrom(methodArgument.ParameterType) + && methodArgument.ParameterType.IsAssignableFrom(EventType)) { arguments[i] = @event; } @@ -85,17 +86,17 @@ namespace Impostor.Server.Events invoke = Expression.Block( Expression.IfThenElse( Expression.Property(@event, nameof(IEventCancelable.IsCancelled)), - Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)), + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))), Expression.Block( invoke, - Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)))), - Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask))); + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))))), + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); } else { invoke = Expression.Block( invoke, - Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask))); + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); } } else if (method.ReturnType == typeof(ValueTask)) @@ -105,9 +106,9 @@ namespace Impostor.Server.Events invoke = Expression.Block( Expression.IfThenElse( Expression.Property(@event, nameof(IEventCancelable.IsCancelled)), - Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)), + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))), Expression.Return(returnTarget, invoke)), - Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask))); + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); } } else @@ -119,12 +120,12 @@ namespace Impostor.Server.Events .Compile(); } - public static IEnumerable FromType(Type type) + public static IReadOnlyList FromType(Type type) { return Instances.GetOrAdd(type, t => { return t.GetMethods() - .Where(m => !m.IsStatic && m.GetCustomAttribute(typeof(EventListenerAttribute), false) != null) + .Where(m => !m.IsStatic && m.GetCustomAttributes(typeof(EventListenerAttribute), false).Any()) .SelectMany(m => FromMethod(t, m)) .ToArray(); }); @@ -141,34 +142,21 @@ namespace Impostor.Server.Events } // Register the event. - var attribute = methodType.GetCustomAttribute(false); - - if (attribute == null) + foreach (var attribute in methodType.GetCustomAttributes(false)) { - yield break; - } - - Type[] eventTypes; + var eventType = attribute.Event; - if (attribute.Events.Length == 0) - { - if (methodType.GetParameters().Length == 0 || !typeof(IEvent).IsAssignableFrom(methodType.GetParameters()[0].ParameterType)) + if (eventType == null) { - throw new InvalidOperationException($"The first parameter of the method {methodType.GetFriendlyName()} should be the type {nameof(IEvent)}."); - } - - eventTypes = new[] { methodType.GetParameters()[0].ParameterType }; - } - else - { - eventTypes = attribute.Events; - } + if (methodType.GetParameters().Length == 0 || !typeof(IEvent).IsAssignableFrom(methodType.GetParameters()[0].ParameterType)) + { + throw new InvalidOperationException($"The first parameter of the method {methodType.GetFriendlyName()} should be the type {nameof(IEvent)}."); + } - foreach (var eventType in eventTypes) - { - var listener = new RegisteredEventListener(eventType, methodType, attribute, listenerType); + eventType = methodType.GetParameters()[0].ParameterType; + } - yield return listener; + yield return new RegisteredEventListener(eventType, methodType, attribute, listenerType); } } } diff --git a/src/Impostor.Server/Events/TemporaryEventRegister.cs b/src/Impostor.Server/Events/TemporaryEventRegister.cs new file mode 100644 index 0000000..834fe41 --- /dev/null +++ b/src/Impostor.Server/Events/TemporaryEventRegister.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Impostor.Server.Events +{ + internal class TemporaryEventRegister + where T : IEvent + { + private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); + private readonly List> _callbacks = new List>(); + + public async ValueTask CallAsync(IServiceProvider provider, T @event) + { + await semaphoreSlim.WaitAsync(); + + try + { + foreach (var callback in _callbacks) + { + await callback.Invoke(provider, @event); + } + } + finally + { + semaphoreSlim.Release(); + } + } + + public IDisposable Add(Func callback) + { + semaphoreSlim.Wait(); + + try + { + _callbacks.Add(callback); + } + finally + { + semaphoreSlim.Release(); + } + + return new UnregisterEvent(this, callback); + } + + private void Remove(Func callback) + { + semaphoreSlim.Wait(); + + try + { + _callbacks.Remove(callback); + } + finally + { + semaphoreSlim.Release(); + } + } + + private class UnregisterEvent : IDisposable + { + private readonly TemporaryEventRegister _register; + private readonly Func _callback; + + public UnregisterEvent(TemporaryEventRegister register, Func callback) + { + _register = register; + _callback = callback; + } + + public void Dispose() + { + _register.Remove(_callback); + } + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Impostor.Server.csproj b/src/Impostor.Server/Impostor.Server.csproj index 2f44e1d..c7472cc 100644 --- a/src/Impostor.Server/Impostor.Server.csproj +++ b/src/Impostor.Server/Impostor.Server.csproj @@ -21,13 +21,14 @@ + - + diff --git a/src/Impostor.Server/Net/Client.cs b/src/Impostor.Server/Net/Client.cs index 8293540..9a7217b 100644 --- a/src/Impostor.Server/Net/Client.cs +++ b/src/Impostor.Server/Net/Client.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Impostor.Server.Data; using Impostor.Server.Games; using Impostor.Server.Games.Managers; +using Impostor.Server.Hazel; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth; @@ -16,10 +17,10 @@ namespace Impostor.Server.Net internal class Client : ClientBase { private readonly ILogger _logger; - private readonly IClientManager _clientManager; - private readonly IGameManager _gameManager; + private readonly ClientManager _clientManager; + private readonly GameManager _gameManager; - public Client(ILogger logger, IClientManager clientManager, IGameManager gameManager, string name, IConnection connection) + public Client(ILogger logger, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection) : base(name, connection) { _logger = logger; diff --git a/src/Impostor.Server/Net/ClientBase.cs b/src/Impostor.Server/Net/ClientBase.cs index 825acc1..dfeacd3 100644 --- a/src/Impostor.Server/Net/ClientBase.cs +++ b/src/Impostor.Server/Net/ClientBase.cs @@ -2,13 +2,15 @@ using System.Collections.Generic; using System.Threading.Tasks; using Hazel; +using Impostor.Server.Hazel; using Impostor.Server.Net.Messages; +using Impostor.Server.Net.State; namespace Impostor.Server.Net { - public abstract class ClientBase : IClient + internal abstract partial class ClientBase { - protected ClientBase(string name, IConnection connection) + protected ClientBase(string name, HazelConnection connection) { Name = name; Connection = connection; @@ -19,13 +21,13 @@ namespace Impostor.Server.Net public string Name { get; } - public IConnection Connection { get; } + public HazelConnection Connection { get; } public bool IsBot => false; public IDictionary Items { get; } - public IClientPlayer Player { get; set; } + public ClientPlayer Player { get; set; } public abstract ValueTask HandleMessageAsync(IMessage message); diff --git a/src/Impostor.Server/Net/Factories/ClientFactory.cs b/src/Impostor.Server/Net/Factories/ClientFactory.cs index 653eeda..e9ff13e 100644 --- a/src/Impostor.Server/Net/Factories/ClientFactory.cs +++ b/src/Impostor.Server/Net/Factories/ClientFactory.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Impostor.Server.Hazel; using Microsoft.Extensions.DependencyInjection; namespace Impostor.Server.Net.Factories @@ -14,7 +15,7 @@ namespace Impostor.Server.Net.Factories _serviceProvider = serviceProvider; } - public IClient Create(IConnection connection, string name, int clientVersion) + public ClientBase Create(HazelConnection connection, string name, int clientVersion) { var client = ActivatorUtilities.CreateInstance(_serviceProvider, name, connection); connection.Client = client; diff --git a/src/Impostor.Server/Net/Factories/IClientFactory.cs b/src/Impostor.Server/Net/Factories/IClientFactory.cs new file mode 100644 index 0000000..d09def4 --- /dev/null +++ b/src/Impostor.Server/Net/Factories/IClientFactory.cs @@ -0,0 +1,9 @@ +using Impostor.Server.Hazel; + +namespace Impostor.Server.Net.Factories +{ + internal interface IClientFactory + { + ClientBase Create(HazelConnection connection, string name, int clientVersion); + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Hazel/HazelConnection.cs b/src/Impostor.Server/Net/Hazel/HazelConnection.cs new file mode 100644 index 0000000..9e15256 --- /dev/null +++ b/src/Impostor.Server/Net/Hazel/HazelConnection.cs @@ -0,0 +1,74 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Hazel; +using Impostor.Server.Hazel.Messages; +using Impostor.Server.Net; +using Impostor.Server.Net.Messages; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Hazel +{ + internal partial class HazelConnection + { + private readonly ILogger _logger; + + public HazelConnection(Connection innerConnection, ILogger logger) + { + _logger = logger; + InnerConnection = innerConnection; + innerConnection.DataReceived = ConnectionOnDataReceived; + innerConnection.Disconnected = ConnectionOnDisconnected; + } + + public Connection InnerConnection { get; } + + public IPEndPoint EndPoint => InnerConnection.EndPoint; + + public bool IsConnected => InnerConnection.State == ConnectionState.Connected; + + public ClientBase Client { get; set; } + + private async ValueTask ConnectionOnDisconnected(DisconnectedEventArgs e) + { + if (Client != null) + { + await Client.HandleDisconnectAsync(e.Reason); + } + } + + private async ValueTask ConnectionOnDataReceived(DataReceivedEventArgs e) + { + if (Client == null) + { + _logger.LogWarning("Client was null."); + return; + } + + while (true) + { + if (e.Message.Position >= e.Message.Length) + { + break; + } + + var reader = e.Message.ReadMessage(); + var type = e.SendOption switch + { + SendOption.None => MessageType.Unreliable, + SendOption.Reliable => MessageType.Reliable, + _ => throw new NotSupportedException() + }; + + using var message = new HazelMessage(reader, type); + + await Client.HandleMessageAsync(message); + } + } + + public IConnectionMessageWriter CreateMessage(MessageType messageType) + { + return new HazelConnectionMessageWriter(messageType, this); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Hazel/Messages/BufferMessageReader.cs b/src/Impostor.Server/Net/Hazel/Messages/BufferMessageReader.cs new file mode 100644 index 0000000..3e560c7 --- /dev/null +++ b/src/Impostor.Server/Net/Hazel/Messages/BufferMessageReader.cs @@ -0,0 +1,159 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Text; +using Impostor.Server.Net.Messages; + +namespace Impostor.Server.Hazel.Messages +{ + public class BufferMessageReader : IMessageReader + { + public byte Tag { get; } + public ReadOnlyMemory Buffer { get; } + public int Position { get; set; } + public int Length => Buffer.Length; + + public BufferMessageReader(byte tag, ReadOnlyMemory buffer) + { + Tag = tag; + Buffer = buffer; + } + + public IMessageReader ReadMessage() + { + var length = ReadUInt16(); + var tag = ReadByte(); + var pos = Position; + + Position += length; + + return new BufferMessageReader(tag, Buffer.Slice(pos, length)); + } + + public bool ReadBoolean() + { + byte val = FastByte(); + return val != 0; + } + + public sbyte ReadSByte() + { + return (sbyte)FastByte(); + } + + public byte ReadByte() + { + return FastByte(); + } + + public ushort ReadUInt16() + { + var output = BinaryPrimitives.ReadUInt16LittleEndian(Buffer.Span.Slice(Position)); + Position += sizeof(ushort); + return output; + } + + public short ReadInt16() + { + var output = BinaryPrimitives.ReadInt16LittleEndian(Buffer.Span.Slice(Position)); + Position += sizeof(short); + return output; + } + + public uint ReadUInt32() + { + var output = BinaryPrimitives.ReadUInt32LittleEndian(Buffer.Span.Slice(Position)); + Position += sizeof(uint); + return output; + } + + public int ReadInt32() + { + var output = BinaryPrimitives.ReadInt32LittleEndian(Buffer.Span.Slice(Position)); + Position += sizeof(int); + return output; + } + + public float ReadSingle() + { + var output = BinaryPrimitives.ReadSingleLittleEndian(Buffer.Span.Slice(Position)); + Position += sizeof(float); + return output; + } + + public string ReadString() + { + var len = ReadPackedInt32(); + var output = Encoding.UTF8.GetString(Buffer.Span.Slice(Position, len)); + Position += len; + return output; + } + + public ReadOnlyMemory ReadBytesAndSize() + { + var len = ReadPackedInt32(); + return ReadBytes(len); + } + + public ReadOnlyMemory ReadBytes(int length) + { + var output = Buffer.Slice(Position, length); + Position += length; + return output; + } + + public int ReadPackedInt32() + { + return (int)ReadPackedUInt32(); + } + + public uint ReadPackedUInt32() + { + bool readMore = true; + int shift = 0; + uint output = 0; + + while (readMore) + { + byte b = ReadByte(); + if (b >= 0x80) + { + readMore = true; + b ^= 0x80; + } + else + { + readMore = false; + } + + output |= (uint)(b << shift); + shift += 7; + } + + return output; + } + + public void CopyTo(IMessageWriter writer) + { + writer.Write((ushort) Length); + writer.Write(Tag); + writer.Write(Buffer); + } + + public IMessageReader Slice(int start) + { + return new BufferMessageReader(Tag, Buffer.Slice(start)); + } + + public IMessageReader Slice(int start, int length) + { + return new BufferMessageReader(Tag, Buffer.Slice(start, length)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte FastByte() + { + return Buffer.Span[Position++]; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Hazel/Messages/HazelConnectionMessageWriter.cs b/src/Impostor.Server/Net/Hazel/Messages/HazelConnectionMessageWriter.cs new file mode 100644 index 0000000..7f45619 --- /dev/null +++ b/src/Impostor.Server/Net/Hazel/Messages/HazelConnectionMessageWriter.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Impostor.Server.Net; +using Impostor.Server.Net.Messages; + +namespace Impostor.Server.Hazel.Messages +{ + internal class HazelConnectionMessageWriter : HazelMessageWriter, IConnectionMessageWriter + { + private readonly HazelConnection _connection; + + public HazelConnectionMessageWriter(MessageType type, HazelConnection connection) + : base(type) + { + _connection = connection; + } + + public IConnection Connection => _connection; + + public async ValueTask SendAsync() + { + await _connection.InnerConnection.Send(Writer); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Hazel/Messages/HazelGameMessageWriter.cs b/src/Impostor.Server/Net/Hazel/Messages/HazelGameMessageWriter.cs new file mode 100644 index 0000000..a87db88 --- /dev/null +++ b/src/Impostor.Server/Net/Hazel/Messages/HazelGameMessageWriter.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Hazel; +using Impostor.Server.Games; +using Impostor.Server.Net; +using Impostor.Server.Net.Messages; +using Impostor.Server.Net.State; + +namespace Impostor.Server.Hazel.Messages +{ + internal class HazelGameMessageWriter : HazelMessageWriter, IGameMessageWriter + { + private readonly Game _game; + + public HazelGameMessageWriter(MessageType type, Game game) + : base(type) + { + _game = game; + } + + private IEnumerable GetConnections(Func filter) + { + return _game.Players + .Where(filter) + .Select(p => p.Client.Connection) + .OfType() + .Select(c => c.InnerConnection); + } + + public ValueTask SendToAllAsync(LimboStates states) + { + foreach (var connection in GetConnections(x => x.Limbo.HasFlag(states))) + { + connection.Send(Writer); + } + + return default; + } + + public ValueTask SendToAllExceptAsync(int senderId, LimboStates states) + { + foreach (var connection in GetConnections(x => + x.Limbo.HasFlag(states) && + x.Client.Id != senderId)) + { + connection.Send(Writer); + } + return default; + } + + public ValueTask SendToAsync(int id) + { + if (_game.TryGetPlayer(id, out var player) + && player.Client.Connection is HazelConnection hazelConnection) + { + hazelConnection.InnerConnection.Send(Writer); + } + + return default; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Hazel/Messages/HazelMessage.cs b/src/Impostor.Server/Net/Hazel/Messages/HazelMessage.cs new file mode 100644 index 0000000..4f08eb4 --- /dev/null +++ b/src/Impostor.Server/Net/Hazel/Messages/HazelMessage.cs @@ -0,0 +1,49 @@ +using System; +using Hazel; +using Impostor.Server.Net.Messages; + +namespace Impostor.Server.Hazel.Messages +{ + internal class HazelMessage : IMessage, IDisposable + { + private bool _isDisposed; + private readonly MessageReader _reader; + + public HazelMessage(MessageReader reader, MessageType type) + { + _reader = reader; + Type = type; + } + + public MessageType Type { get; } + + public IMessageReader CreateReader() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(_reader)); + } + + return new BufferMessageReader(_reader.Tag, _reader.Buffer); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~HazelMessage() + { + Dispose(false); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Hazel/Messages/HazelMessageWriter.cs b/src/Impostor.Server/Net/Hazel/Messages/HazelMessageWriter.cs new file mode 100644 index 0000000..0d40313 --- /dev/null +++ b/src/Impostor.Server/Net/Hazel/Messages/HazelMessageWriter.cs @@ -0,0 +1,127 @@ +using System; +using System.Net; +using Hazel; +using Impostor.Server.Games; +using Impostor.Server.Net.Messages; + +namespace Impostor.Server.Hazel.Messages +{ + internal abstract class HazelMessageWriter : IMessageWriter + { + protected readonly MessageWriter Writer; + + protected HazelMessageWriter(MessageType type) + { + Writer = MessageWriter.Get(ToSendOption(type)); + } + + private static SendOption ToSendOption(MessageType type) + { + return type switch + { + MessageType.Unreliable => SendOption.None, + MessageType.Reliable => SendOption.Reliable, + _ => throw new NotSupportedException($"Message type {type} is not supported") + }; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Writer.Recycle(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Write(bool value) + { + Writer.Write(value); + } + + public void Write(sbyte value) + { + Writer.Write(value); + } + + public void Write(byte value) + { + Writer.Write(value); + } + + public void Write(short value) + { + Writer.Write(value); + } + + public void Write(ushort value) + { + Writer.Write(value); + } + + public void Write(uint value) + { + Writer.Write(value); + } + + public void Write(int value) + { + Writer.Write(value); + } + + public void Write(float value) + { + Writer.Write(value); + } + + public void Write(string value) + { + Writer.Write(value); + } + + public void Write(IPAddress value) + { + Writer.Write(value.GetAddressBytes()); + } + + public void WritePacked(int value) + { + Writer.WritePacked(value); + } + + public void WritePacked(uint value) + { + Writer.WritePacked(value); + } + + public void Write(ReadOnlyMemory data) + { + Writer.Write(data.ToArray()); // TODO: Fix memory allocation. + } + + public void StartMessage(byte typeFlag) + { + Writer.StartMessage(typeFlag); + } + + public void Write(GameCode value) + { + Write(value.Value); + } + + public void EndMessage() + { + Writer.EndMessage(); + } + + public void Clear(MessageType type) + { + Writer.Clear(ToSendOption(type)); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Manager/ClientManager.cs b/src/Impostor.Server/Net/Manager/ClientManager.cs index a178f77..37a1402 100644 --- a/src/Impostor.Server/Net/Manager/ClientManager.cs +++ b/src/Impostor.Server/Net/Manager/ClientManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Impostor.Server.Data; +using Impostor.Server.Hazel; using Impostor.Server.Net.Factories; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth; @@ -11,7 +12,7 @@ using Microsoft.Extensions.Logging; namespace Impostor.Server.Net.Manager { - internal class ClientManager : IClientManager + internal partial class ClientManager { public static HashSet SupportedVersions { get; } = new HashSet { @@ -20,7 +21,7 @@ namespace Impostor.Server.Net.Manager }; private readonly ILogger _logger; - private readonly ConcurrentDictionary _clients; + private readonly ConcurrentDictionary _clients; private readonly IClientFactory _clientFactory; private int _idLast; @@ -28,9 +29,11 @@ namespace Impostor.Server.Net.Manager { _logger = logger; _clientFactory = clientFactory; - _clients = new ConcurrentDictionary(); + _clients = new ConcurrentDictionary(); } + public IEnumerable Clients => _clients.Values; + public int NextId() { var clientId = Interlocked.Increment(ref _idLast); @@ -47,7 +50,7 @@ namespace Impostor.Server.Net.Manager return clientId; } - public async ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion) + public async ValueTask RegisterConnectionAsync(HazelConnection connection, string name, int clientVersion) { if (name.Length > 10) { @@ -66,12 +69,6 @@ namespace Impostor.Server.Net.Manager } var client = _clientFactory.Create(connection, name, clientVersion); - - Register(client); - } - - public void Register(IClient client) - { var id = NextId(); client.Id = id; diff --git a/src/Impostor.Server/Net/Manager/GameManager.cs b/src/Impostor.Server/Net/Manager/GameManager.cs index a0e8339..e54f866 100644 --- a/src/Impostor.Server/Net/Manager/GameManager.cs +++ b/src/Impostor.Server/Net/Manager/GameManager.cs @@ -19,7 +19,7 @@ using Microsoft.Extensions.Options; namespace Impostor.Server.Net.Manager { - internal class GameManager : IGameManager + internal partial class GameManager { private readonly ILogger _logger; private readonly INodeLocator _nodeLocator; @@ -38,9 +38,7 @@ namespace Impostor.Server.Net.Manager _games = new ConcurrentDictionary(); } - public IEnumerable Games => _games.Select(kv => kv.Value); - - public async ValueTask CreateAsync(GameOptionsData options) + public async ValueTask CreateAsync(GameOptionsData options) { // TODO: Prevent duplicates when using server redirector using INodeProvider. var gameCode = GameCode.Create(); @@ -60,13 +58,13 @@ namespace Impostor.Server.Net.Manager return game; } - public IGame Find(GameCode code) + public Game Find(GameCode code) { _games.TryGetValue(code, out var game); return game; } - public IEnumerable FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10) + public IEnumerable FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10) { var results = 0; @@ -115,13 +113,15 @@ namespace Impostor.Server.Net.Manager return; } - if (!_games.TryRemove(gameCode, out _)) + if (!_games.TryRemove(gameCode, out game)) { return; } _logger.LogDebug("Remove game with code {0} ({1}).", GameCodeParser.IntToGameName(gameCode), gameCode); _nodeLocator.Remove(GameCodeParser.IntToGameName(gameCode)); + + await _eventManager.CallAsync(new GameDestroyedEvent(game)); } } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/Matchmaker.cs b/src/Impostor.Server/Net/Matchmaker.cs new file mode 100644 index 0000000..462c26c --- /dev/null +++ b/src/Impostor.Server/Net/Matchmaker.cs @@ -0,0 +1,65 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Hazel; +using Hazel.Udp; +using Impostor.Server.Games; +using Impostor.Server.Hazel.Messages; +using Impostor.Server.Net.Manager; +using Impostor.Server.Net.Messages; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Hazel +{ + internal class Matchmaker + { + private readonly ClientManager _clientManager; + private readonly ILogger _logger; + private readonly ILogger _connectionLogger; + private UdpConnectionListener _connection; + + public Matchmaker( + ILogger logger, + ClientManager clientManager, + ILogger connectionLogger) + { + _logger = logger; + _clientManager = clientManager; + _connectionLogger = connectionLogger; + } + + public async ValueTask StartAsync(IPEndPoint ipEndPoint) + { + var mode = ipEndPoint.AddressFamily switch + { + AddressFamily.InterNetwork => IPMode.IPv4, + AddressFamily.InterNetworkV6 => IPMode.IPv6, + _ => throw new InvalidOperationException() + }; + + _connection = new UdpConnectionListener(ipEndPoint, mode); + _connection.NewConnection = OnNewConnection; + + await _connection.StartAsync(); + } + + public async ValueTask StopAsync() + { + await _connection.DisposeAsync(); + } + + private async ValueTask OnNewConnection(NewConnectionEventArgs e) + { + // Handshake. + var clientVersion = e.HandshakeData.ReadInt32(); + var name = e.HandshakeData.ReadString(); + + var connection = new HazelConnection(e.Connection, _connectionLogger); + + // Register client + await _clientManager.RegisterConnectionAsync(connection, name, clientVersion); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/MatchmakerService.cs b/src/Impostor.Server/Net/MatchmakerService.cs index 1405b97..2384716 100644 --- a/src/Impostor.Server/Net/MatchmakerService.cs +++ b/src/Impostor.Server/Net/MatchmakerService.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Impostor.Server.Data; +using Impostor.Server.Hazel; using Impostor.Server.Net.Manager; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -14,13 +15,13 @@ namespace Impostor.Server.Net private readonly ILogger _logger; private readonly ServerConfig _serverConfig; private readonly ServerRedirectorConfig _redirectorConfig; - private readonly IMatchmaker _matchmaker; + private readonly Matchmaker _matchmaker; public MatchmakerService( ILogger logger, IOptions serverConfig, IOptions redirectorConfig, - IMatchmaker matchmaker) + Matchmaker matchmaker) { _logger = logger; _serverConfig = serverConfig.Value; @@ -35,7 +36,7 @@ namespace Impostor.Server.Net await _matchmaker.StartAsync(endpoint); _logger.LogInformation( - "Matchmaker is listening on {0}:{1}, the public server ip is {2}:{3}.", + "Matchmaker is listening on {0}:{1}, the public server ip is {2}:{3}.", endpoint.Address, endpoint.Port, _serverConfig.PublicIp, diff --git a/src/Impostor.Server/Net/Redirector/ClientRedirector.cs b/src/Impostor.Server/Net/Redirector/ClientRedirector.cs index 1302363..8e18e04 100644 --- a/src/Impostor.Server/Net/Redirector/ClientRedirector.cs +++ b/src/Impostor.Server/Net/Redirector/ClientRedirector.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Impostor.Server.Data; +using Impostor.Server.Hazel; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth; @@ -13,14 +14,14 @@ namespace Impostor.Server.Net.Redirector { private static readonly ILogger Logger = Log.ForContext(); - private readonly IClientManager _clientManager; + private readonly ClientManager _clientManager; private readonly INodeProvider _nodeProvider; private readonly INodeLocator _nodeLocator; public ClientRedirector( string name, - IConnection connection, - IClientManager clientManager, + HazelConnection connection, + ClientManager clientManager, INodeProvider nodeProvider, INodeLocator nodeLocator) : base(name, connection) diff --git a/src/Impostor.Server/Net/State/ClientPlayer.cs b/src/Impostor.Server/Net/State/ClientPlayer.cs index b6734c4..a744e88 100644 --- a/src/Impostor.Server/Net/State/ClientPlayer.cs +++ b/src/Impostor.Server/Net/State/ClientPlayer.cs @@ -4,28 +4,22 @@ using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net.State { - internal class ClientPlayer : IClientPlayer + internal partial class ClientPlayer : IClientPlayer { - public ClientPlayer(IClient client, Game game) + public ClientPlayer(ClientBase client, Game game) { Game = game; Client = client; Limbo = LimboStates.PreSpawn; } - public IClient Client { get; } + public ClientBase Client { get; } public Game Game { get; } /// public LimboStates Limbo { get; set; } - /// - IClient IClientPlayer.Client => Client; - - /// - IGame IClientPlayer.Game => Game; - /// public ValueTask KickAsync() { diff --git a/src/Impostor.Server/Net/State/Game.Data.cs b/src/Impostor.Server/Net/State/Game.Data.cs index a1ba361..d3577b3 100644 --- a/src/Impostor.Server/Net/State/Game.Data.cs +++ b/src/Impostor.Server/Net/State/Game.Data.cs @@ -26,10 +26,10 @@ namespace Impostor.Server.Net.State private readonly List _allObjects = new List(); private readonly Dictionary _allObjectsFast = new Dictionary(); - public async ValueTask HandleGameData(IMessageReader parent, IClientPlayer sender, bool toPlayer) + public async ValueTask HandleGameData(IMessageReader parent, ClientPlayer sender, bool toPlayer) { // Find target player. - IClientPlayer target = null; + ClientPlayer target = null; if (toPlayer) { diff --git a/src/Impostor.Server/Net/State/Game.Incoming.cs b/src/Impostor.Server/Net/State/Game.Incoming.cs index f439995..888f6d4 100644 --- a/src/Impostor.Server/Net/State/Game.Incoming.cs +++ b/src/Impostor.Server/Net/State/Game.Incoming.cs @@ -17,7 +17,7 @@ namespace Impostor.Server.Net.State await packet.SendToAllAsync(); } - public async ValueTask AddClientAsync(IClient client) + public async ValueTask AddClientAsync(ClientBase client) { // Check if the IP of the player is banned. if (client.Connection != null && _bannedIps.Contains(client.Connection.EndPoint.Address)) @@ -140,14 +140,14 @@ namespace Impostor.Server.Net.State await message.SendToAllExceptAsync(playerId); } - private async ValueTask HandleJoinGameNew(IClientPlayer sender, bool isNew) + private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew) { Logger.Information("{0} - Player {1} ({2}) is joining.", Code, sender.Client.Name, sender.Client.Id); // Add player to the game. if (isNew) { - PlayerAdd(sender); + await PlayerAdd(sender); } using (var message = CreateMessage(MessageType.Reliable)) @@ -162,14 +162,14 @@ namespace Impostor.Server.Net.State } } - private async ValueTask HandleJoinGameNext(IClientPlayer sender, bool isNew) + private async ValueTask HandleJoinGameNext(ClientPlayer sender, bool isNew) { Logger.Information("{0} - Player {1} ({2}) is rejoining.", Code, sender.Client.Name, sender.Client.Id); // Add player to the game. if (isNew) { - PlayerAdd(sender); + await PlayerAdd(sender); } // Check if the host joined and let everyone join. diff --git a/src/Impostor.Server/Net/State/Game.State.cs b/src/Impostor.Server/Net/State/Game.State.cs index 5e9b38b..fdca5d4 100644 --- a/src/Impostor.Server/Net/State/Game.State.cs +++ b/src/Impostor.Server/Net/State/Game.State.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using Impostor.Server.Events; using Impostor.Server.Exceptions; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth.Data; @@ -8,7 +9,7 @@ namespace Impostor.Server.Net.State { internal partial class Game { - private void PlayerAdd(IClientPlayer player) + private async ValueTask PlayerAdd(ClientPlayer player) { // Store player. if (!_players.TryAdd(player.Client.Id, player)) @@ -21,6 +22,8 @@ namespace Impostor.Server.Net.State { HostId = player.Client.Id; } + + await _eventManager.CallAsync(new PlayerJoinedGameEvent(this, player)); } private async ValueTask PlayerRemove(int playerId, bool isBan = false) @@ -55,6 +58,8 @@ namespace Impostor.Server.Net.State _bannedIps.Add(player.Client.Connection.EndPoint.Address); } + await _eventManager.CallAsync(new PlayerLeftGameEvent(this, player, isBan)); + return true; } diff --git a/src/Impostor.Server/Net/State/Game.cs b/src/Impostor.Server/Net/State/Game.cs index ca68fa7..c0d06f6 100644 --- a/src/Impostor.Server/Net/State/Game.cs +++ b/src/Impostor.Server/Net/State/Game.cs @@ -4,8 +4,11 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; +using Impostor.Server.Events.Managers; using Impostor.Server.Games; using Impostor.Server.Games.Managers; +using Impostor.Server.Hazel; +using Impostor.Server.Hazel.Messages; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Messages; using Impostor.Server.Net.Redirector; @@ -16,30 +19,32 @@ using ILogger = Serilog.ILogger; namespace Impostor.Server.Net.State { - internal partial class Game : IGame + internal partial class Game { private static readonly ILogger Logger = Log.ForContext(); private readonly IServiceProvider _serviceProvider; - private readonly IGameManager _gameManager; - private readonly IClientManager _clientManager; - private readonly IMatchmaker _matchmaker; - private readonly ConcurrentDictionary _players; + private readonly GameManager _gameManager; + private readonly ClientManager _clientManager; + private readonly Matchmaker _matchmaker; + private readonly ConcurrentDictionary _players; private readonly HashSet _bannedIps; + private readonly IEventManager _eventManager; public Game( IServiceProvider serviceProvider, - IGameManager gameManager, + GameManager gameManager, INodeLocator nodeLocator, IPEndPoint publicIp, GameCode code, GameOptionsData options, - IMatchmaker matchmaker, - IClientManager clientManager) + Matchmaker matchmaker, + ClientManager clientManager, + IEventManager eventManager) { _serviceProvider = serviceProvider; _gameManager = gameManager; - _players = new ConcurrentDictionary(); + _players = new ConcurrentDictionary(); _bannedIps = new HashSet(); PublicIp = publicIp; @@ -49,6 +54,7 @@ namespace Impostor.Server.Net.State Options = options; _matchmaker = matchmaker; _clientManager = clientManager; + _eventManager = eventManager; Items = new ConcurrentDictionary(); } @@ -68,16 +74,16 @@ namespace Impostor.Server.Net.State public int PlayerCount => _players.Count; - public IClientPlayer Host => _players[HostId]; + public ClientPlayer Host => _players[HostId]; public IEnumerable Players => _players.Select(p => p.Value); public IGameMessageWriter CreateMessage(MessageType type) { - return _matchmaker.CreateGameMessageWriter(this, type); + return new HazelGameMessageWriter(type, this); } - public bool TryGetPlayer(int id, out IClientPlayer player) + public bool TryGetPlayer(int id, out ClientPlayer player) { if (_players.TryGetValue(id, out var result)) { @@ -94,7 +100,7 @@ namespace Impostor.Server.Net.State return _gameManager.RemoveAsync(Code); } - private ValueTask BroadcastJoinMessage(IGameMessageWriter message, bool clear, IClientPlayer player) + private ValueTask BroadcastJoinMessage(IGameMessageWriter message, bool clear, ClientPlayer player) { Message01JoinGame.SerializeJoin(message, clear, Code, player.Client.Id, HostId); diff --git a/src/Impostor.Server/Plugins/AssemblyInformation.cs b/src/Impostor.Server/Plugins/AssemblyInformation.cs new file mode 100644 index 0000000..5f6aee1 --- /dev/null +++ b/src/Impostor.Server/Plugins/AssemblyInformation.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public class AssemblyInformation : IAssemblyInformation + { + private Assembly _assembly; + + public AssemblyInformation(AssemblyName assemblyName, string path, bool isPlugin) + { + AssemblyName = assemblyName; + Path = path; + IsPlugin = isPlugin; + } + + public string Path { get; } + + public bool IsPlugin { get; } + + public AssemblyName AssemblyName { get; } + + public Assembly Load(AssemblyLoadContext context) + { + if (_assembly != null) + { + return _assembly; + } + + using var stream = File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read); + + _assembly = context.LoadFromStream(stream); + + return _assembly; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Plugins/IAssemblyInformation.cs b/src/Impostor.Server/Plugins/IAssemblyInformation.cs new file mode 100644 index 0000000..fb36e92 --- /dev/null +++ b/src/Impostor.Server/Plugins/IAssemblyInformation.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public interface IAssemblyInformation + { + AssemblyName AssemblyName { get; } + + bool IsPlugin { get; } + + Assembly Load(AssemblyLoadContext context); + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs b/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs new file mode 100644 index 0000000..720367c --- /dev/null +++ b/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public class LoadedAssemblyInformation : IAssemblyInformation + { + private readonly Assembly _assembly; + + public LoadedAssemblyInformation(Assembly assembly) + { + AssemblyName = assembly.GetName(); + _assembly = assembly; + } + + public AssemblyName AssemblyName { get; } + + public bool IsPlugin => false; + + public Assembly Load(AssemblyLoadContext context) + { + return _assembly; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Plugins/PluginConfig.cs b/src/Impostor.Server/Plugins/PluginConfig.cs new file mode 100644 index 0000000..22dc9e9 --- /dev/null +++ b/src/Impostor.Server/Plugins/PluginConfig.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Impostor.Server.Plugins +{ + public class PluginConfig + { + public List Paths { get; set; } = new List(); + + public List LibraryPaths { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Plugins/PluginLoader.cs b/src/Impostor.Server/Plugins/PluginLoader.cs new file mode 100644 index 0000000..4fc0997 --- /dev/null +++ b/src/Impostor.Server/Plugins/PluginLoader.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.Hosting; + +namespace Impostor.Server.Plugins +{ + public static class PluginLoader + { + public static IHostBuilder UsePluginLoader(this IHostBuilder builder, PluginConfig config) + { + var assemblyInfos = new List(); + var context = AssemblyLoadContext.Default; + + // Add the plugins and libraries. + var pluginPaths = new List(config.Paths); + var libraryPaths = new List(config.LibraryPaths); + + var rootFolder = Assembly.GetEntryAssembly()?.Location; + if (rootFolder != null) + { + pluginPaths.Add(Path.Combine(rootFolder, "plugins")); + libraryPaths.Add(Path.Combine(rootFolder, "libraries")); + } + + var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); + matcher.AddInclude("*.dll"); + matcher.AddExclude("Impostor.Server.Api.dll"); + matcher.AddExclude("Impostor.Shared.dll"); + + RegisterAssemblies(pluginPaths, matcher, assemblyInfos, true); + RegisterAssemblies(libraryPaths, matcher, assemblyInfos, false); + + // Register the resolver to the current context. + // TODO: Move this to a new context so we can unload/reload plugins. + context.Resolving += (loadContext, name) => + { + var info = assemblyInfos.FirstOrDefault(a => a.AssemblyName.Name == name.Name); + + return info?.Load(loadContext); + }; + + // TODO: Catch uncaught exceptions. + var assemblies = assemblyInfos + .Where(a => a.IsPlugin) + .Select(a => context.LoadFromAssemblyName(a.AssemblyName)) + .ToList(); + + var plugins = assemblies + .SelectMany(a => a.GetTypes()) + .Where(typeof(IPlugin).IsAssignableFrom) + .Select(Activator.CreateInstance) + .Cast() + .ToList(); + + foreach (var plugin in plugins) + { + plugin.ConfigureHost(builder); + } + + builder.ConfigureServices(services => + { + foreach (var plugin in plugins) + { + plugin.ConfigureServices(services); + } + }); + + return builder; + } + + private static void RegisterAssemblies( + IEnumerable paths, + Matcher matcher, + ICollection assemblyInfos, + bool isPlugin) + { + foreach (var path in paths.SelectMany(matcher.GetResultsInFullPath)) + { + AssemblyName assemblyName; + + try + { + assemblyName = AssemblyName.GetAssemblyName(path); + } + catch (BadImageFormatException) + { + continue; + } + + assemblyInfos.Add(new AssemblyInformation(assemblyName, path, isPlugin)); + } + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index 31433b2..43bc6f6 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -8,6 +8,7 @@ using Impostor.Server.Net; using Impostor.Server.Net.Factories; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Redirector; +using Impostor.Server.Plugins; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -49,8 +50,25 @@ namespace Impostor.Server } } - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) + private static IConfiguration CreateConfiguration(string[] args) + { + var configurationBuilder = new ConfigurationBuilder(); + + configurationBuilder.AddJsonFile("config.json", true); + configurationBuilder.AddJsonFile("config.Development.json", true); + configurationBuilder.AddEnvironmentVariables(prefix: "IMPOSTOR_"); + configurationBuilder.AddCommandLine(args); + + return configurationBuilder.Build(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) + { + var configuration = CreateConfiguration(args); + var pluginConfig = configuration.GetSection("PluginLoader") + .Get(); + + return Host.CreateDefaultBuilder(args) #if DEBUG .UseEnvironment(Environment.GetEnvironmentVariable("IMPOSTOR_ENV") ?? "Development") #else @@ -58,10 +76,7 @@ namespace Impostor.Server #endif .ConfigureAppConfiguration(builder => { - builder.AddJsonFile("config.json", true); - builder.AddJsonFile("config.Development.json", true); - builder.AddEnvironmentVariables(prefix: "IMPOSTOR_"); - builder.AddCommandLine(args); + builder.AddConfiguration(configuration); }) .ConfigureServices((host, services) => { @@ -73,7 +88,8 @@ namespace Impostor.Server services.Configure(host.Configuration.GetSection(DebugConfig.Section)); #endif services.Configure(host.Configuration.GetSection(ServerConfig.Section)); - services.Configure(host.Configuration.GetSection(ServerRedirectorConfig.Section)); + services.Configure( + host.Configuration.GetSection(ServerRedirectorConfig.Section)); if (redirector.Enabled) { @@ -115,7 +131,8 @@ namespace Impostor.Server services.AddSingleton(); } - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(p => p.GetRequiredService()); if (redirector.Enabled && redirector.Master) { @@ -126,14 +143,17 @@ namespace Impostor.Server else { services.AddSingleton>(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(p => p.GetRequiredService()); } services.AddSingleton(); - services.UseHazelMatchmaking(); + services.AddSingleton(); services.AddHostedService(); }) + .UsePluginLoader(pluginConfig) .UseConsoleLifetime() .UseSerilog(); + } } } \ No newline at end of file diff --git a/src/Impostor.Server/Properties/AssemblyInfo.cs b/src/Impostor.Server/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..eb56780 --- /dev/null +++ b/src/Impostor.Server/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Impostor.Tests")] \ No newline at end of file diff --git a/src/Impostor.Tests/Hazel/BufferMessageReaderTests.cs b/src/Impostor.Tests/Hazel/BufferMessageReaderTests.cs index f896e69..0b71ed7 100644 --- a/src/Impostor.Tests/Hazel/BufferMessageReaderTests.cs +++ b/src/Impostor.Tests/Hazel/BufferMessageReaderTests.cs @@ -28,7 +28,7 @@ namespace Impostor.Tests.Hazel Assert.Equal(Test1, reader.ReadInt32()); Assert.Equal(Test2, reader.ReadInt32()); } - + [Fact] public void ReadProperBool() { diff --git a/src/Impostor.Tests/Impostor.Tests.csproj b/src/Impostor.Tests/Impostor.Tests.csproj index 536b4e5..407e920 100644 --- a/src/Impostor.Tests/Impostor.Tests.csproj +++ b/src/Impostor.Tests/Impostor.Tests.csproj @@ -14,9 +14,7 @@ - - - + diff --git a/src/Impostor.sln b/src/Impostor.sln index 3ebbabc..d35c75f 100644 --- a/src/Impostor.sln +++ b/src/Impostor.sln @@ -21,8 +21,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Server", "Impostor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Server.Api", "Impostor.Server.Api\Impostor.Server.Api.csproj", "{E096A7D7-D693-4A13-A526-38CC574D84F8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Server.Hazel", "Impostor.Server.Hazel\Impostor.Server.Hazel.csproj", "{C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "patcher", "patcher", "{94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.Shared", "Impostor.Patcher\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj", "{7C3EB599-2292-4532-B280-D5BED1094DD4}" @@ -31,6 +29,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.WinForms", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.Cli", "Impostor.Patcher\Impostor.Patcher.Cli\Impostor.Patcher.Cli.csproj", "{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Debugger", "Impostor.Plugins.Debugger\Impostor.Plugins.Debugger.csproj", "{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -91,14 +93,6 @@ Global {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.Build.0 = Release|Any CPU {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.ActiveCfg = Release|Any CPU {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.Build.0 = Release|Any CPU - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Debug|x86.ActiveCfg = Debug|Any CPU - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Debug|x86.Build.0 = Debug|Any CPU - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Release|Any CPU.Build.0 = Release|Any CPU - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Release|x86.ActiveCfg = Release|Any CPU - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Release|x86.Build.0 = Release|Any CPU {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -115,6 +109,14 @@ Global {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|Any CPU.Build.0 = Release|Any CPU {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|x86.ActiveCfg = Release|Any CPU {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|x86.Build.0 = Release|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|x86.ActiveCfg = Debug|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|x86.Build.0 = Debug|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|Any CPU.Build.0 = Release|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|x86.ActiveCfg = Release|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -128,8 +130,8 @@ Global {804CF172-0C87-4423-9688-BD97D549891E} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F} {1B0390AF-A4F3-4FE4-B093-708B0135C0B3} = {F2B205ED-4250-412E-9992-B11B7D6CE136} {E096A7D7-D693-4A13-A526-38CC574D84F8} = {F2B205ED-4250-412E-9992-B11B7D6CE136} - {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306} = {F2B205ED-4250-412E-9992-B11B7D6CE136} {7C3EB599-2292-4532-B280-D5BED1094DD4} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F} {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F} + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124} EndGlobalSection EndGlobal