--- /dev/null
+<Router AppAssembly="@typeof(DebugPlugin).Assembly">
+ <Found Context="routeData">
+ <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
+ </Found>
+ <NotFound>
+ <LayoutView Layout="@typeof(MainLayout)">
+ <p>Sorry, there's nothing at this address.</p>
+ </LayoutView>
+ </NotFound>
+</Router>
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net5.0</TargetFramework>
+ <OutputType>Library</OutputType>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Server.Api\Impostor.Server.Api.csproj" />
+ </ItemGroup>
+
+</Project>
\ No newline at end of file
--- /dev/null
+@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
+
+<div class="container">
+ <h2>Games</h2>
+ @if (GameManager.Games.Any())
+ {
+ <table class="table table-striped">
+ <thead>
+ <tr>
+ <th>Code</th>
+ <th>Players</th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var game in GameManager.Games)
+ {
+ <tr>
+ <td>@game.Code</td>
+ <td>
+ <ul class="mb-0">
+ @foreach (var player in game.Players)
+ {
+ <li>@player.Client.Name</li>
+ }
+ </ul>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ }
+ else
+ {
+ <div class="text-center">
+ <i class="text-muted">There are no active games.</i>
+ </div>
+ }
+</div>
+
+@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
--- /dev/null
+@page "/"
+@namespace Impostor.Plugins.Debugger.Pages
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+ <title>Impostor Debugger</title>
+ <base href="~/"/>
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
+</head>
+<body>
+<component type="typeof(App)" render-mode="Server"/>
+
+<script src="_framework/blazor.server.js"></script>
+</body>
+</html>
\ No newline at end of file
--- /dev/null
+@inherits LayoutComponentBase
+
+<div class="page">
+ @Body
+</div>
\ No newline at end of file
--- /dev/null
+@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
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;
}
/// <summary>
/// <summary>
/// The events that the listener is listening to.
/// </summary>
- public Type[] Events { get; set; }
+ public Type? Event { get; set; }
/// <summary>
/// If set to true, the listener will be called regardless of the <see cref="IEventCancelable.IsCancelled"/>.
--- /dev/null
+using Impostor.Server.Games;
+
+namespace Impostor.Server.Events
+{
+ /// <summary>
+ /// Called whenever a new <see cref="IGame"/> is destroyed.
+ /// </summary>
+ public sealed class GameDestroyedEvent : IGameEvent
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GameDestroyedEvent"/> class.
+ /// </summary>
+ /// <param name="game">Instance of the game.</param>
+ public GameDestroyedEvent(IGame game)
+ {
+ Game = game;
+ }
+
+ /// <inheritdoc/>
+ public IGame Game { get; }
+ }
+}
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+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
-using System.Threading.Tasks;
+using System;
+using System.Threading.Tasks;
namespace Impostor.Server.Events.Managers
{
public interface IEventManager
{
+ /// <summary>
+ /// Register a temporary event listener.
+ /// </summary>
+ /// <param name="callback">Event callback.</param>
+ /// <returns>Disposable that unregisters the callback from the event manager.</returns>
+ /// <typeparam name="TEvent">Type of the event.</typeparam>
+ IDisposable Register<TEvent>(Func<IServiceProvider, TEvent, ValueTask> callback)
+ where TEvent : IEvent;
+
+ /// <summary>
+ /// Register a temporary event listener.
+ /// </summary>
+ /// <param name="listener">Event listener.</param>
+ /// <param name="invoker">Middleware between the events, which can be used to swap to the correct thread dispatcher.</param>
+ /// <returns>Disposable that unregisters the callback from the event manager.</returns>
+ /// <typeparam name="TListener">Type of the event listener.</typeparam>
+ IDisposable RegisterListener<TListener>(TListener listener, Func<Func<Task>, Task>? invoker = null)
+ where TListener : IEventListener;
+
/// <summary>
/// Returns true if an event with the type <see cref="TEvent"/> is registered.
/// </summary>
+++ /dev/null
-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
--- /dev/null
+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
int HostId { get; }
IGameMessageWriter CreateMessage(MessageType type);
-
- bool TryGetPlayer(int id, [NotNullWhen(true)] out IClientPlayer player);
-
- /// <summary>
- /// Register a new client to the game.
- /// </summary>
- /// <param name="client">Client to register.</param>
- /// <returns>Join result.</returns>
- ValueTask<GameJoinResult> AddClientAsync(IClient client);
-
- /// <summary>
- /// Kicks all the players from the game to end the game.
- /// </summary>
- /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
- 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
using System.Collections.Generic;
-using System.Threading.Tasks;
-using Impostor.Shared.Innersloth;
-using Impostor.Shared.Innersloth.Data;
namespace Impostor.Server.Games.Managers
{
{
IEnumerable<IGame> Games { get; }
- ValueTask<IGame> CreateAsync(GameOptionsData options);
-
IGame? Find(GameCode code);
-
- IEnumerable<IGame> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10);
-
- ValueTask RemoveAsync(GameCode code);
}
}
\ No newline at end of file
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0-rc.1.20451.14" />
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0-rc.1.20451.14" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0-rc.1.20451.14" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cgame/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=games_005Cextensions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=net_005Cextensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
+++ /dev/null
-using System.Threading.Tasks;
-
-namespace Impostor.Server.Net.Factories
-{
- public interface IClientFactory
- {
- /// <summary>
- /// Creates a client for the Hazel <see cref="connection"/>.
- /// </summary>
- /// <param name="connection">Hazel connection.</param>
- /// <param name="name"></param>
- /// <param name="clientVersion"></param>
- IClient Create(IConnection connection, string name, int clientVersion);
- }
-}
\ No newline at end of file
/// <summary>
/// Gets or sets the current game data of the <see cref="IClient"/>.
/// </summary>
- IClientPlayer? Player { get; set; }
-
- ValueTask HandleMessageAsync(IMessage message);
-
- ValueTask HandleDisconnectAsync(string reason);
+ IClientPlayer? Player { get; }
}
}
\ No newline at end of file
bool IsConnected { get; }
/// <summary>
- /// Gets or sets the client of the connection.
+ /// Gets the client of the connection.
/// </summary>
- IClient? Client { get; set; }
+ IClient? Client { get; }
/// <summary>
/// Create a message writer that can be send to the connection.
-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<IClient> Clients { get; }
}
}
\ No newline at end of file
+++ /dev/null
-using System.Net;
-using System.Threading.Tasks;
-using Impostor.Server.Games;
-using Impostor.Server.Net.Messages;
-
-namespace Impostor.Server.Net.Manager
-{
- /// <summary>
- /// Represents the matchmaker which will listen for incoming connections.
- /// </summary>
- public interface IMatchmaker
- {
- /// <summary>
- /// Starts the matchmaker on the given endpoint.
- /// </summary>
- /// <param name="ipEndPoint">Endpoint where the matchmaker should listen to.</param>
- /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
- ValueTask StartAsync(IPEndPoint ipEndPoint);
-
- /// <summary>
- /// Stop the matchmaker.
- /// </summary>
- /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
- ValueTask StopAsync();
-
- /// <summary>
- /// Create a message writer that can be send to players in the game.
- /// </summary>
- /// <param name="game">The game.</param>
- /// <param name="messageType">Type of the message.</param>
- /// <returns>Message writer for the given game.</returns>
- IGameMessageWriter CreateGameMessageWriter(IGame game, MessageType messageType);
- }
-}
\ No newline at end of file
using System.Threading.Tasks;
using Impostor.Server.Events;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
namespace Impostor.Server.Plugins
{
ValueTask DisableAsync();
ValueTask ReloadAsync();
+
+ void ConfigureHost(IHostBuilder host);
+
+ void ConfigureServices(IServiceCollection services);
}
}
\ No newline at end of file
using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
namespace Impostor.Server.Plugins
{
{
return default;
}
+
+ public virtual void ConfigureHost(IHostBuilder host)
+ {
+ }
+
+ public virtual void ConfigureServices(IServiceCollection services)
+ {
+ }
}
}
\ No newline at end of file
+++ /dev/null
-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<IMatchmaker, HazelMatchmaker>();
- return services;
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-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<HazelConnection> _logger;
-
- public HazelConnection(Connection innerConnection, ILogger<HazelConnection> 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
+++ /dev/null
-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<HazelMatchmaker> _logger;
- private readonly ILogger<HazelConnection> _connectionLogger;
- private UdpConnectionListener _connection;
-
- public HazelMatchmaker(
- ILogger<HazelMatchmaker> logger,
- IClientManager clientManager,
- ILogger<HazelConnection> 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
+++ /dev/null
-<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
- <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
- <DebugType Condition=" '$(Configuration)' == 'Release' ">None</DebugType>
- </PropertyGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\..\submodules\Hazel-Networking\Hazel\Hazel.csproj" />
- <ProjectReference Include="..\Impostor.Server.Api\Impostor.Server.Api.csproj" />
- </ItemGroup>
-
- <ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0-rc.1.20451.14" />
- </ItemGroup>
-
- <ItemGroup>
- <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
- <_Parameter1>Impostor.Tests</_Parameter1>
- </AssemblyAttribute>
- </ItemGroup>
-
-</Project>
+++ /dev/null
-<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
- <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
+++ /dev/null
-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<byte> Buffer { get; }
- public int Position { get; set; }
- public int Length => Buffer.Length;
-
- public BufferMessageReader(byte tag, ReadOnlyMemory<byte> 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<byte> ReadBytesAndSize()
- {
- var len = ReadPackedInt32();
- return ReadBytes(len);
- }
-
- public ReadOnlyMemory<byte> 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
+++ /dev/null
-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
+++ /dev/null
-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<Connection> GetConnections(Func<IClientPlayer, bool> filter)
- {
- return _game.Players
- .Where(filter)
- .Select(p => p.Client.Connection)
- .OfType<HazelConnection>()
- .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
+++ /dev/null
-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
+++ /dev/null
-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<byte> 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
--- /dev/null
+// 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
--- /dev/null
+using System.Collections.Generic;
+
+// ReSharper disable once CheckNamespace
+namespace Impostor.Server.Net.Manager
+{
+ internal partial class ClientManager : IClientManager
+ {
+ IEnumerable<IClient> IClientManager.Clients => _clients.Values;
+ }
+}
\ No newline at end of file
--- /dev/null
+using Impostor.Server.Games;
+
+// ReSharper disable once CheckNamespace
+namespace Impostor.Server.Net.State
+{
+ internal partial class ClientPlayer
+ {
+ /// <inheritdoc />
+ IClient IClientPlayer.Client => Client;
+
+ /// <inheritdoc />
+ IGame IClientPlayer.Game => Game;
+ }
+}
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+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<IGame> IGameManager.Games => _games.Select(kv => kv.Value);
+
+ IGame IGameManager.Find(GameCode code) => Find(code);
+ }
+}
\ No newline at end of file
--- /dev/null
+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
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;
{
internal class EventManager : IEventManager
{
+ private readonly ConcurrentDictionary<Type, object> _temporaryEventListeners;
private readonly IServiceProvider _serviceProvider;
public EventManager(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
+ _temporaryEventListeners = new ConcurrentDictionary<Type, object>();
+ }
+
+ /// <inheritdoc />
+ public IDisposable Register<TEvent>(Func<IServiceProvider, TEvent, ValueTask> callback)
+ where TEvent : IEvent
+ {
+ var register = (TemporaryEventRegister<TEvent>) _temporaryEventListeners.GetOrAdd(
+ typeof(TEvent),
+ _ => new TemporaryEventRegister<TEvent>());
+
+ return register.Add(callback);
+ }
+
+ /// <inheritdoc />
+ public IDisposable RegisterListener<TListener>(TListener listener, Func<Func<Task>, 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);
}
/// <inheritdoc />
{
await eventListener.InvokeAsync(handler, @event, scope.ServiceProvider);
}
+
+ if (_temporaryEventListeners.TryGetValue(typeof(T), out var cb))
+ {
+ await ((TemporaryEventRegister<T>) cb).CallAsync(scope.ServiceProvider, @event);
+ }
}
finally
{
}
}
}
+
+ private IDisposable RegisterListenerImpl<TEvent>(object obj, RegisteredEventListener listener, Func<Func<Task>, Task> invoker = null)
+ where TEvent : IEvent
+ {
+ return invoker == null
+ ? Register<TEvent>((provider, @event) => listener.InvokeAsync(obj, @event, provider))
+ : Register<TEvent>((provider, @event) => new ValueTask(invoker(() => listener.InvokeAsync(obj, @event, provider).AsTask())));
+ }
}
}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+
+namespace Impostor.Server.Events
+{
+ /// <summary>
+ /// Disposes multiple <see cref="IDisposable"/>.
+ /// </summary>
+ internal class MultiDisposable : IDisposable
+ {
+ private readonly IEnumerable<IDisposable> _disposables;
+
+ public MultiDisposable(IEnumerable<IDisposable> disposables)
+ {
+ _disposables = disposables;
+ }
+
+ public void Dispose()
+ {
+ foreach (var disposable in _disposables)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+}
\ No newline at end of file
{
var methodArgument = methodArguments[i];
- if (methodArgument.ParameterType == EventType)
+ if (typeof(IEvent).IsAssignableFrom(methodArgument.ParameterType)
+ && methodArgument.ParameterType.IsAssignableFrom(EventType))
{
arguments[i] = @event;
}
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))
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
.Compile();
}
- public static IEnumerable<RegisteredEventListener> FromType(Type type)
+ public static IReadOnlyList<RegisteredEventListener> 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();
});
}
// Register the event.
- var attribute = methodType.GetCustomAttribute<EventListenerAttribute>(false);
-
- if (attribute == null)
+ foreach (var attribute in methodType.GetCustomAttributes<EventListenerAttribute>(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);
}
}
}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Impostor.Server.Events
+{
+ internal class TemporaryEventRegister<T>
+ where T : IEvent
+ {
+ private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
+ private readonly List<Func<IServiceProvider, T, ValueTask>> _callbacks = new List<Func<IServiceProvider, T, ValueTask>>();
+
+ 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<IServiceProvider, T, ValueTask> callback)
+ {
+ semaphoreSlim.Wait();
+
+ try
+ {
+ _callbacks.Add(callback);
+ }
+ finally
+ {
+ semaphoreSlim.Release();
+ }
+
+ return new UnregisterEvent(this, callback);
+ }
+
+ private void Remove(Func<IServiceProvider, T, ValueTask> callback)
+ {
+ semaphoreSlim.Wait();
+
+ try
+ {
+ _callbacks.Remove(callback);
+ }
+ finally
+ {
+ semaphoreSlim.Release();
+ }
+ }
+
+ private class UnregisterEvent : IDisposable
+ {
+ private readonly TemporaryEventRegister<T> _register;
+ private readonly Func<IServiceProvider, T, ValueTask> _callback;
+
+ public UnregisterEvent(TemporaryEventRegister<T> register, Func<IServiceProvider, T, ValueTask> callback)
+ {
+ _register = register;
+ _callback = callback;
+ }
+
+ public void Dispose()
+ {
+ _register.Remove(_callback);
+ }
+ }
+ }
+}
\ No newline at end of file
</PropertyGroup>
<ItemGroup>
+ <ProjectReference Include="..\..\submodules\Hazel-Networking\Hazel\Hazel.csproj" />
<ProjectReference Include="..\Impostor.Server.Api\Impostor.Server.Api.csproj" />
- <ProjectReference Include="..\Impostor.Server.Hazel\Impostor.Server.Hazel.csproj" />
<ProjectReference Include="..\Impostor.Shared\Impostor.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="5.0.0-rc.1.20451.7" />
+ <PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="5.0.0-rc.1.20451.14" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0-rc.1.20451.14" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
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;
internal class Client : ClientBase
{
private readonly ILogger<Client> _logger;
- private readonly IClientManager _clientManager;
- private readonly IGameManager _gameManager;
+ private readonly ClientManager _clientManager;
+ private readonly GameManager _gameManager;
- public Client(ILogger<Client> logger, IClientManager clientManager, IGameManager gameManager, string name, IConnection connection)
+ public Client(ILogger<Client> logger, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection)
: base(name, connection)
{
_logger = logger;
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;
public string Name { get; }
- public IConnection Connection { get; }
+ public HazelConnection Connection { get; }
public bool IsBot => false;
public IDictionary<object, object> Items { get; }
- public IClientPlayer Player { get; set; }
+ public ClientPlayer Player { get; set; }
public abstract ValueTask HandleMessageAsync(IMessage message);
using System;
using System.Threading.Tasks;
+using Impostor.Server.Hazel;
using Microsoft.Extensions.DependencyInjection;
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<TClient>(_serviceProvider, name, connection);
connection.Client = client;
--- /dev/null
+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
--- /dev/null
+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<HazelConnection> _logger;
+
+ public HazelConnection(Connection innerConnection, ILogger<HazelConnection> 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
--- /dev/null
+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<byte> Buffer { get; }
+ public int Position { get; set; }
+ public int Length => Buffer.Length;
+
+ public BufferMessageReader(byte tag, ReadOnlyMemory<byte> 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<byte> ReadBytesAndSize()
+ {
+ var len = ReadPackedInt32();
+ return ReadBytes(len);
+ }
+
+ public ReadOnlyMemory<byte> 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
--- /dev/null
+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
--- /dev/null
+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<Connection> GetConnections(Func<IClientPlayer, bool> filter)
+ {
+ return _game.Players
+ .Where(filter)
+ .Select(p => p.Client.Connection)
+ .OfType<HazelConnection>()
+ .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
--- /dev/null
+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
--- /dev/null
+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<byte> 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
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;
namespace Impostor.Server.Net.Manager
{
- internal class ClientManager : IClientManager
+ internal partial class ClientManager
{
public static HashSet<int> SupportedVersions { get; } = new HashSet<int>
{
};
private readonly ILogger<ClientManager> _logger;
- private readonly ConcurrentDictionary<int, IClient> _clients;
+ private readonly ConcurrentDictionary<int, ClientBase> _clients;
private readonly IClientFactory _clientFactory;
private int _idLast;
{
_logger = logger;
_clientFactory = clientFactory;
- _clients = new ConcurrentDictionary<int, IClient>();
+ _clients = new ConcurrentDictionary<int, ClientBase>();
}
+ public IEnumerable<ClientBase> Clients => _clients.Values;
+
public int NextId()
{
var clientId = Interlocked.Increment(ref _idLast);
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)
{
}
var client = _clientFactory.Create(connection, name, clientVersion);
-
- Register(client);
- }
-
- public void Register(IClient client)
- {
var id = NextId();
client.Id = id;
namespace Impostor.Server.Net.Manager
{
- internal class GameManager : IGameManager
+ internal partial class GameManager
{
private readonly ILogger<GameManager> _logger;
private readonly INodeLocator _nodeLocator;
_games = new ConcurrentDictionary<int, Game>();
}
- public IEnumerable<IGame> Games => _games.Select(kv => kv.Value);
-
- public async ValueTask<IGame> CreateAsync(GameOptionsData options)
+ public async ValueTask<Game> CreateAsync(GameOptionsData options)
{
// TODO: Prevent duplicates when using server redirector using INodeProvider.
var gameCode = GameCode.Create();
return game;
}
- public IGame Find(GameCode code)
+ public Game Find(GameCode code)
{
_games.TryGetValue(code, out var game);
return game;
}
- public IEnumerable<IGame> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10)
+ public IEnumerable<Game> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10)
{
var results = 0;
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
--- /dev/null
+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<Matchmaker> _logger;
+ private readonly ILogger<HazelConnection> _connectionLogger;
+ private UdpConnectionListener _connection;
+
+ public Matchmaker(
+ ILogger<Matchmaker> logger,
+ ClientManager clientManager,
+ ILogger<HazelConnection> 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
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;
private readonly ILogger<MatchmakerService> _logger;
private readonly ServerConfig _serverConfig;
private readonly ServerRedirectorConfig _redirectorConfig;
- private readonly IMatchmaker _matchmaker;
+ private readonly Matchmaker _matchmaker;
public MatchmakerService(
ILogger<MatchmakerService> logger,
IOptions<ServerConfig> serverConfig,
IOptions<ServerRedirectorConfig> redirectorConfig,
- IMatchmaker matchmaker)
+ Matchmaker matchmaker)
{
_logger = logger;
_serverConfig = serverConfig.Value;
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,
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;
{
private static readonly ILogger Logger = Log.ForContext<ClientRedirector>();
- 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)
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; }
/// <inheritdoc />
public LimboStates Limbo { get; set; }
- /// <inheritdoc />
- IClient IClientPlayer.Client => Client;
-
- /// <inheritdoc />
- IGame IClientPlayer.Game => Game;
-
/// <inheritdoc />
public ValueTask KickAsync()
{
private readonly List<InnerNetObject> _allObjects = new List<InnerNetObject>();
private readonly Dictionary<uint, InnerNetObject> _allObjectsFast = new Dictionary<uint, InnerNetObject>();
- 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)
{
await packet.SendToAllAsync();
}
- public async ValueTask<GameJoinResult> AddClientAsync(IClient client)
+ public async ValueTask<GameJoinResult> AddClientAsync(ClientBase client)
{
// Check if the IP of the player is banned.
if (client.Connection != null && _bannedIps.Contains(client.Connection.EndPoint.Address))
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))
}
}
- 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.
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;
{
internal partial class Game
{
- private void PlayerAdd(IClientPlayer player)
+ private async ValueTask PlayerAdd(ClientPlayer player)
{
// Store player.
if (!_players.TryAdd(player.Client.Id, player))
{
HostId = player.Client.Id;
}
+
+ await _eventManager.CallAsync(new PlayerJoinedGameEvent(this, player));
}
private async ValueTask<bool> PlayerRemove(int playerId, bool isBan = false)
_bannedIps.Add(player.Client.Connection.EndPoint.Address);
}
+ await _eventManager.CallAsync(new PlayerLeftGameEvent(this, player, isBan));
+
return true;
}
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;
namespace Impostor.Server.Net.State
{
- internal partial class Game : IGame
+ internal partial class Game
{
private static readonly ILogger Logger = Log.ForContext<Game>();
private readonly IServiceProvider _serviceProvider;
- private readonly IGameManager _gameManager;
- private readonly IClientManager _clientManager;
- private readonly IMatchmaker _matchmaker;
- private readonly ConcurrentDictionary<int, IClientPlayer> _players;
+ private readonly GameManager _gameManager;
+ private readonly ClientManager _clientManager;
+ private readonly Matchmaker _matchmaker;
+ private readonly ConcurrentDictionary<int, ClientPlayer> _players;
private readonly HashSet<IPAddress> _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<int, IClientPlayer>();
+ _players = new ConcurrentDictionary<int, ClientPlayer>();
_bannedIps = new HashSet<IPAddress>();
PublicIp = publicIp;
Options = options;
_matchmaker = matchmaker;
_clientManager = clientManager;
+ _eventManager = eventManager;
Items = new ConcurrentDictionary<object, object>();
}
public int PlayerCount => _players.Count;
- public IClientPlayer Host => _players[HostId];
+ public ClientPlayer Host => _players[HostId];
public IEnumerable<IClientPlayer> 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))
{
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);
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+using System.Collections.Generic;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginConfig
+ {
+ public List<string> Paths { get; set; } = new List<string>();
+
+ public List<string> LibraryPaths { get; set; } = new List<string>();
+ }
+}
\ No newline at end of file
--- /dev/null
+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<IAssemblyInformation>();
+ var context = AssemblyLoadContext.Default;
+
+ // Add the plugins and libraries.
+ var pluginPaths = new List<string>(config.Paths);
+ var libraryPaths = new List<string>(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<IPlugin>()
+ .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<string> paths,
+ Matcher matcher,
+ ICollection<IAssemblyInformation> 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
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;
}
}
- 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<PluginConfig>();
+
+ return Host.CreateDefaultBuilder(args)
#if DEBUG
.UseEnvironment(Environment.GetEnvironmentVariable("IMPOSTOR_ENV") ?? "Development")
#else
#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) =>
{
services.Configure<DebugConfig>(host.Configuration.GetSection(DebugConfig.Section));
#endif
services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
- services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
+ services.Configure<ServerRedirectorConfig>(
+ host.Configuration.GetSection(ServerRedirectorConfig.Section));
if (redirector.Enabled)
{
services.AddSingleton<INodeLocator, NodeLocatorNoOp>();
}
- services.AddSingleton<IClientManager, ClientManager>();
+ services.AddSingleton<ClientManager>();
+ services.AddSingleton<IClientManager>(p => p.GetRequiredService<ClientManager>());
if (redirector.Enabled && redirector.Master)
{
else
{
services.AddSingleton<IClientFactory, ClientFactory<Client>>();
- services.AddSingleton<IGameManager, GameManager>();
+ services.AddSingleton<GameManager>();
+ services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>());
}
services.AddSingleton<IEventManager, EventManager>();
- services.UseHazelMatchmaking();
+ services.AddSingleton<Matchmaker>();
services.AddHostedService<MatchmakerService>();
})
+ .UsePluginLoader(pluginConfig)
.UseConsoleLifetime()
.UseSerilog();
+ }
}
}
\ No newline at end of file
--- /dev/null
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("Impostor.Tests")]
\ No newline at end of file
Assert.Equal(Test1, reader.ReadInt32());
Assert.Equal(Test2, reader.ReadInt32());
}
-
+
[Fact]
public void ReadProperBool()
{
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\..\submodules\Hazel-Networking\Hazel\Hazel.csproj" />
- <ProjectReference Include="..\Impostor.Server.Hazel\Impostor.Server.Hazel.csproj" />
- <ProjectReference Include="..\Impostor.Shared\Impostor.Shared.csproj" />
+ <ProjectReference Include="..\Impostor.Server\Impostor.Server.csproj" />
</ItemGroup>
</Project>
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}"
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
{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
{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
{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