+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Reactive;
-using System.Threading.Tasks;
-
-namespace System
-{
- public static class AsyncObservableExtensions
- {
- public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync)
- {
- if (source == null)
- throw new ArgumentNullException(nameof(source));
- if (onNextAsync == null)
- throw new ArgumentNullException(nameof(onNextAsync));
-
- return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, ex => new ValueTask(Task.FromException(ex)), () => default));
- }
-
- public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync, Func<Exception, ValueTask> onErrorAsync)
- {
- if (source == null)
- throw new ArgumentNullException(nameof(source));
- if (onNextAsync == null)
- throw new ArgumentNullException(nameof(onNextAsync));
- if (onErrorAsync == null)
- throw new ArgumentNullException(nameof(onErrorAsync));
-
- return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, onErrorAsync, () => default));
- }
-
- public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync, Func<ValueTask> onCompletedAsync)
- {
- if (source == null)
- throw new ArgumentNullException(nameof(source));
- if (onNextAsync == null)
- throw new ArgumentNullException(nameof(onNextAsync));
- if (onCompletedAsync == null)
- throw new ArgumentNullException(nameof(onCompletedAsync));
-
- return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, ex => new ValueTask(Task.FromException(ex)), onCompletedAsync));
- }
-
- public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync, Func<Exception, ValueTask> onErrorAsync, Func<ValueTask> onCompletedAsync)
- {
- if (source == null)
- throw new ArgumentNullException(nameof(source));
- if (onNextAsync == null)
- throw new ArgumentNullException(nameof(onNextAsync));
- if (onErrorAsync == null)
- throw new ArgumentNullException(nameof(onErrorAsync));
- if (onCompletedAsync == null)
- throw new ArgumentNullException(nameof(onCompletedAsync));
-
- return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, onErrorAsync, onCompletedAsync));
- }
-
- public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Action<T> onNext)
- {
- if (source == null)
- throw new ArgumentNullException(nameof(source));
- if (onNext == null)
- throw new ArgumentNullException(nameof(onNext));
-
- return source.SubscribeAsync(new AsyncObserver<T>(x => { onNext(x); return default; }, ex => new ValueTask(Task.FromException(ex)), () => default));
- }
-
- public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Action<T> onNext, Action<Exception> onError)
- {
- if (source == null)
- throw new ArgumentNullException(nameof(source));
- if (onNext == null)
- throw new ArgumentNullException(nameof(onNext));
- if (onError == null)
- throw new ArgumentNullException(nameof(onError));
-
- return source.SubscribeAsync(new AsyncObserver<T>(x => { onNext(x); return default; }, ex => { onError(ex); return default; }, () => default));
- }
-
- public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Action<T> onNext, Action onCompleted)
- {
- if (source == null)
- throw new ArgumentNullException(nameof(source));
- if (onNext == null)
- throw new ArgumentNullException(nameof(onNext));
- if (onCompleted == null)
- throw new ArgumentNullException(nameof(onCompleted));
-
- return source.SubscribeAsync(new AsyncObserver<T>(x => { onNext(x); return default; }, ex => new ValueTask(Task.FromException(ex)), () => { onCompleted(); return default; }));
- }
-
- public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Action<T> onNext, Action<Exception> onError, Action onCompleted)
- {
- if (source == null)
- throw new ArgumentNullException(nameof(source));
- if (onNext == null)
- throw new ArgumentNullException(nameof(onNext));
- if (onError == null)
- throw new ArgumentNullException(nameof(onError));
- if (onCompleted == null)
- throw new ArgumentNullException(nameof(onCompleted));
-
- return source.SubscribeAsync(new AsyncObserver<T>(x => { onNext(x); return default; }, ex => { onError(ex); return default; }, () => { onCompleted(); return default; }));
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Threading.Tasks;
-
-namespace System
-{
- public interface IAsyncObservable<out T>
- {
- ValueTask<IAsyncDisposable> SubscribeAsync(IAsyncObserver<T> observer);
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Threading.Tasks;
-
-namespace System
-{
- public interface IAsyncObserver<in T>
- {
- ValueTask OnNextAsync(T value);
- ValueTask OnErrorAsync(Exception error);
- ValueTask OnCompletedAsync();
- }
-}
\ No newline at end of file
+++ /dev/null
-<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
- <RootNamespace>System</RootNamespace>
- </PropertyGroup>
-
-</Project>
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Threading.Tasks;
-
-namespace System.Reactive
-{
- public class AsyncObservable<T> : AsyncObservableBase<T>
- {
- private readonly Func<IAsyncObserver<T>, ValueTask<IAsyncDisposable>> _subscribeAsync;
-
- public AsyncObservable(Func<IAsyncObserver<T>, ValueTask<IAsyncDisposable>> subscribeAsync)
- {
- _subscribeAsync = subscribeAsync ?? throw new ArgumentNullException(nameof(subscribeAsync));
- }
-
- protected override ValueTask<IAsyncDisposable> SubscribeAsyncCore(IAsyncObserver<T> observer)
- {
- if (observer == null)
- throw new ArgumentNullException(nameof(observer));
-
- return _subscribeAsync(observer);
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Threading.Tasks;
-
-namespace System.Reactive
-{
- public abstract class AsyncObservableBase<T> : IAsyncObservable<T>
- {
- public async ValueTask<IAsyncDisposable> SubscribeAsync(IAsyncObserver<T> observer)
- {
- if (observer == null)
- throw new ArgumentNullException(nameof(observer));
-
- var autoDetach = new AutoDetachAsyncObserver(observer);
-
- var subscription = await SubscribeAsyncCore(autoDetach).ConfigureAwait(false);
-
- await autoDetach.AssignAsync(subscription).ConfigureAwait(false);
-
- return autoDetach;
- }
-
- protected abstract ValueTask<IAsyncDisposable> SubscribeAsyncCore(IAsyncObserver<T> observer);
-
- private sealed class AutoDetachAsyncObserver : AsyncObserverBase<T>, IAsyncDisposable
- {
- private readonly IAsyncObserver<T> _observer;
- private readonly object _gate = new object();
-
- private IAsyncDisposable _subscription;
- private ValueTask _task;
- private bool _disposing;
-
- public AutoDetachAsyncObserver(IAsyncObserver<T> observer)
- {
- _observer = observer;
- }
-
- public async ValueTask AssignAsync(IAsyncDisposable subscription)
- {
- var shouldDispose = false;
-
- lock (_gate)
- {
- if (_disposing)
- {
- shouldDispose = true;
- }
- else
- {
- _subscription = subscription;
- }
- }
-
- if (shouldDispose)
- {
- await subscription.DisposeAsync().ConfigureAwait(false);
- }
- }
-
- public async ValueTask DisposeAsync()
- {
- var task = default(ValueTask);
- var subscription = default(IAsyncDisposable);
-
- lock (_gate)
- {
- //
- // NB: The postcondition of awaiting the first DisposeAsync call to complete is that all message
- // processing has ceased, i.e. no further On*AsyncCore calls will be made. This is achieved
- // here by setting _disposing to true, which is checked by the On*AsyncCore calls upon
- // entry, and by awaiting the task of any in-flight On*AsyncCore calls.
- //
- // Timing of the disposal of the subscription is less deterministic due to the intersection
- // with the AssignAsync code path. However, the auto-detach observer can only be returned
- // from the SubscribeAsync call *after* a call to AssignAsync has been made and awaited, so
- // either AssignAsync triggers the disposal and an already disposed instance is returned, or
- // the user calling DisposeAsync will either encounter a busy observer which will be stopped
- // in its tracks (as described above) or it will trigger a disposal of the subscription. In
- // both these cases the result of awaiting DisposeAsync guarantees no further message flow.
- //
-
- if (!_disposing)
- {
- _disposing = true;
-
- task = _task;
- subscription = _subscription;
- }
- }
-
- try
- {
- //
- // BUGBUG: This causes grief when an outgoing On*Async call reenters the DisposeAsync method and
- // results in the task returned from the On*Async call to be awaited to serialize the
- // call to subscription.DisposeAsync after it's done. We need to either detect reentrancy
- // and queue up the call to DisposeAsync or follow an when we trigger the disposal without
- // awaiting outstanding work (thus allowing for concurrency).
- //
- // if (task != null)
- // {
- // await task.ConfigureAwait(false);
- // }
- //
- }
- finally
- {
- if (subscription != null)
- {
- await subscription.DisposeAsync().ConfigureAwait(false);
- }
- }
- }
-
- protected override async ValueTask OnCompletedAsyncCore()
- {
- lock (_gate)
- {
- if (_disposing)
- {
- return;
- }
-
- _task = _observer.OnCompletedAsync();
- }
-
- try
- {
- await _task.ConfigureAwait(false);
- }
- finally
- {
- await FinishAsync().ConfigureAwait(false);
- }
- }
-
- protected override async ValueTask OnErrorAsyncCore(Exception error)
- {
- lock (_gate)
- {
- if (_disposing)
- {
- return;
- }
-
- _task = _observer.OnErrorAsync(error);
- }
-
- try
- {
- await _task.ConfigureAwait(false);
- }
- finally
- {
- await FinishAsync().ConfigureAwait(false);
- }
- }
-
- protected override async ValueTask OnNextAsyncCore(T value)
- {
- lock (_gate)
- {
- if (_disposing)
- {
- return;
- }
-
- _task = _observer.OnNextAsync(value);
- }
-
- try
- {
- await _task.ConfigureAwait(false);
- }
- finally
- {
- lock (_gate)
- {
- _task = default;
- }
- }
- }
-
- private async ValueTask FinishAsync()
- {
- var subscription = default(IAsyncDisposable);
-
- lock (_gate)
- {
- if (!_disposing)
- {
- _disposing = true;
-
- subscription = _subscription;
- }
-
- _task = default;
- }
-
- if (subscription != null)
- {
- await subscription.DisposeAsync().ConfigureAwait(false);
- }
- }
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Threading.Tasks;
-
-namespace System.Reactive
-{
- public class AsyncObserver<T> : AsyncObserverBase<T>
- {
- private readonly Func<T, ValueTask> _onNextAsync;
- private readonly Func<Exception, ValueTask> _onErrorAsync;
- private readonly Func<ValueTask> _onCompletedAsync;
-
- public AsyncObserver(Func<T, ValueTask> onNextAsync, Func<Exception, ValueTask> onErrorAsync, Func<ValueTask> onCompletedAsync)
- {
- _onNextAsync = onNextAsync ?? throw new ArgumentNullException(nameof(onNextAsync));
- _onErrorAsync = onErrorAsync ?? throw new ArgumentNullException(nameof(onErrorAsync));
- _onCompletedAsync = onCompletedAsync ?? throw new ArgumentNullException(nameof(onCompletedAsync));
- }
-
- protected override ValueTask OnCompletedAsyncCore() => _onCompletedAsync();
-
- protected override ValueTask OnErrorAsyncCore(Exception error) => _onErrorAsync(error ?? throw new ArgumentNullException(nameof(error)));
-
- protected override ValueTask OnNextAsyncCore(T value) => _onNextAsync(value);
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace System.Reactive
-{
- public abstract class AsyncObserverBase<T> : IAsyncObserver<T>
- {
- private const int Idle = 0;
- private const int Busy = 1;
- private const int Done = 2;
-
- private int _status = Idle;
-
- public ValueTask OnCompletedAsync()
- {
- TryEnter();
-
- try
- {
- return OnCompletedAsyncCore();
- }
- finally
- {
- Interlocked.Exchange(ref _status, Done);
- }
- }
-
- protected abstract ValueTask OnCompletedAsyncCore();
-
- public ValueTask OnErrorAsync(Exception error)
- {
- if (error == null)
- throw new ArgumentNullException(nameof(error));
-
- TryEnter();
-
- try
- {
- return OnErrorAsyncCore(error);
- }
- finally
- {
- Interlocked.Exchange(ref _status, Done);
- }
- }
-
- protected abstract ValueTask OnErrorAsyncCore(Exception error);
-
- public ValueTask OnNextAsync(T value)
- {
- TryEnter();
-
- try
- {
- return OnNextAsyncCore(value);
- }
- finally
- {
- Interlocked.Exchange(ref _status, Idle);
- }
- }
-
- protected abstract ValueTask OnNextAsyncCore(T value);
-
- private void TryEnter()
- {
- var old = Interlocked.CompareExchange(ref _status, Busy, Idle);
-
- switch (old)
- {
- case Busy:
- throw new InvalidOperationException("The observer is currently processing a notification.");
- case Done:
- throw new InvalidOperationException("The observer has already terminated.");
- }
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace System.Reactive.Disposables
-{
- public static class AsyncDisposable
- {
- public static IAsyncDisposable Nop { get; } = new NopAsyncDisposable();
-
- public static IAsyncDisposable Create(Func<ValueTask> dispose)
- {
- if (dispose == null)
- throw new ArgumentNullException(nameof(dispose));
-
- return new AnonymousAsyncDisposable(dispose);
- }
-
- private sealed class AnonymousAsyncDisposable : IAsyncDisposable
- {
- private Func<ValueTask> _dispose;
-
- public AnonymousAsyncDisposable(Func<ValueTask> dispose) => _dispose = dispose;
-
- public ValueTask DisposeAsync() => Interlocked.Exchange(ref _dispose, null)?.Invoke() ?? default;
- }
-
- private sealed class NopAsyncDisposable : IAsyncDisposable
- {
- public ValueTask DisposeAsync() => default;
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace System.Reactive.Subjects
-{
- public sealed class ConcurrentSimpleAsyncSubject<T> : SimpleAsyncSubject<T>
- {
- protected override ValueTask OnCompletedAsyncCore(IEnumerable<IAsyncObserver<T>> observers) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnCompletedAsync().AsTask())));
-
- protected override ValueTask OnErrorAsyncCore(IEnumerable<IAsyncObserver<T>> observers, Exception error) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnErrorAsync(error).AsTask())));
-
- protected override ValueTask OnNextAsyncCore(IEnumerable<IAsyncObserver<T>> observers, T value) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnNextAsync(value).AsTask())));
- }
-}
\ No newline at end of file
+++ /dev/null
-namespace System.Reactive.Subjects
-{
- public interface IAsyncSubject<in TInput, out TOutput> : IAsyncObservable<TOutput>, IAsyncObserver<TInput>
- {
- }
-
- public interface IAsyncSubject<T> : IAsyncSubject<T, T>
- {
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
-namespace System.Reactive.Subjects
-{
- public sealed class SequentialSimpleAsyncSubject<T> : SimpleAsyncSubject<T>
- {
- protected override async ValueTask OnCompletedAsyncCore(IEnumerable<IAsyncObserver<T>> observers)
- {
- foreach (var observer in observers)
- {
- await observer.OnCompletedAsync().ConfigureAwait(false);
- }
- }
-
- protected override async ValueTask OnErrorAsyncCore(IEnumerable<IAsyncObserver<T>> observers, Exception error)
- {
- foreach (var observer in observers)
- {
- await observer.OnErrorAsync(error).ConfigureAwait(false);
- }
- }
-
- protected override async ValueTask OnNextAsyncCore(IEnumerable<IAsyncObserver<T>> observers, T value)
- {
- foreach (var observer in observers)
- {
- await observer.OnNextAsync(value).ConfigureAwait(false);
- }
- }
- }
-}
\ No newline at end of file
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT License.
-// See the LICENSE file in the project root for more information.
-
-using System.Collections.Generic;
-using System.Reactive.Disposables;
-using System.Threading.Tasks;
-
-namespace System.Reactive.Subjects
-{
- public abstract class SimpleAsyncSubject<T> : IAsyncSubject<T>
- {
- private readonly object _gate = new object();
- private readonly List<IAsyncObserver<T>> _observers = new List<IAsyncObserver<T>>();
- private bool _done;
- private Exception _error;
-
- public ValueTask OnCompletedAsync()
- {
- IAsyncObserver<T>[] observers;
-
- lock (_gate)
- {
- if (_done || _error != null)
- {
- return default;
- }
-
- _done = true;
-
- observers = _observers.ToArray();
- }
-
- return OnCompletedAsyncCore(observers);
- }
-
- protected abstract ValueTask OnCompletedAsyncCore(IEnumerable<IAsyncObserver<T>> observers);
-
- public ValueTask OnErrorAsync(Exception error)
- {
- if (error == null)
- throw new ArgumentNullException(nameof(error));
-
- IAsyncObserver<T>[] observers;
-
- lock (_gate)
- {
- if (_done || _error != null)
- {
- return default;
- }
-
- _error = error;
-
- observers = _observers.ToArray();
- }
-
- return OnErrorAsyncCore(observers, error);
- }
-
- protected abstract ValueTask OnErrorAsyncCore(IEnumerable<IAsyncObserver<T>> observers, Exception error);
-
- public ValueTask OnNextAsync(T value)
- {
- IAsyncObserver<T>[] observers;
-
- lock (_gate)
- {
- if (_done || _error != null)
- {
- return default;
- }
-
- observers = _observers.ToArray();
- }
-
- return OnNextAsyncCore(observers, value);
- }
-
- protected abstract ValueTask OnNextAsyncCore(IEnumerable<IAsyncObserver<T>> observers, T value);
-
- public async ValueTask<IAsyncDisposable> SubscribeAsync(IAsyncObserver<T> observer)
- {
- if (observer == null)
- throw new ArgumentNullException(nameof(observer));
-
- bool done;
- Exception error;
-
- lock (_gate)
- {
- done = _done;
- error = _error;
-
- if (!done && error == null)
- {
- _observers.Add(observer);
- }
- }
-
- if (done)
- {
- await observer.OnCompletedAsync().ConfigureAwait(false);
-
- return AsyncDisposable.Nop;
- }
- else if (error != null)
- {
- await observer.OnErrorAsync(error).ConfigureAwait(false);
-
- return AsyncDisposable.Nop;
- }
- else
- {
- return AsyncDisposable.Create(() =>
- {
- lock (_gate)
- {
- var i = _observers.LastIndexOf(observer);
-
- if (i >= 0)
- {
- _observers.RemoveAt(i);
- }
- }
-
- return default;
- });
- }
- }
- }
-}
\ No newline at end of file
--- /dev/null
+using System;
+
+namespace Impostor.Server.Events
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ 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 };
+ }
+
+ /// <summary>
+ /// The priority of the event listener.
+ /// </summary>
+ public EventPriority Priority { get; set; }
+
+ /// <summary>
+ /// The events that the listener is listening to.
+ /// </summary>
+ public Type[] Events { get; set; }
+
+ /// <summary>
+ /// If set to true, the listener will be called regardless of the <see cref="IEventCancelable.IsCancelled"/>.
+ /// </summary>
+ public bool IgnoreCancelled { get; set; }
+
+ /// <summary>
+ /// The order of the priority.
+ /// </summary>
+ public int PriorityOrder { get; set; } = 100;
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Events
+{
+ public enum EventPriority
+ {
+ Lowest = 0,
+ Low = 1,
+ Normal = 2,
+ High = 3,
+ Highest = 4,
+ Monitor = 5
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Events
+{
+ /// <summary>
+ /// Called whenever a new <see cref="IGame"/> is created.
+ /// </summary>
+ public sealed class GameCreatedEvent : IGameEvent
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GameCreatedEvent"/> class.
+ /// </summary>
+ /// <param name="game">Instance of the game.</param>
+ public GameCreatedEvent(IGame game)
+ {
+ Game = game;
+ }
+
+ /// <inheritdoc/>
+ public IGame Game { get; }
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Events
+{
+ public interface IGameEvent : IEvent
+ {
+ IGame Game { get; }
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Events
+{
+ public interface IEvent
+ {
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Events
+{
+ public interface IEventCancelable : IEvent
+ {
+ /// <summary>
+ /// True if the event was cancelled.
+ /// </summary>
+ bool IsCancelled { get; set; }
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Events
+{
+ public interface IEventListener
+ {
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.Threading.Tasks;
+
+namespace Impostor.Server.Events.Managers
+{
+ public interface IEventManager
+ {
+ /// <summary>
+ /// Returns true if an event with the type <see cref="TEvent"/> is registered.
+ /// </summary>
+ /// <returns>True if the <see cref="TEvent"/> is registered.</returns>
+ /// <typeparam name="TEvent">Type of the event.</typeparam>
+ bool IsRegistered<TEvent>()
+ where TEvent : IEvent;
+
+ /// <summary>
+ /// Call all the event listeners for the type <see cref="TEvent"/>.
+ /// </summary>
+ /// <param name="event">The event argument.</param>
+ /// <typeparam name="TEvent">Type of the event.</typeparam>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask CallAsync<TEvent>(TEvent @event)
+ where TEvent : IEvent;
+ }
+}
\ No newline at end of file
+++ /dev/null
-namespace Impostor.Server
-{
- public class ClientVersionUnsupportedException : ImpostorException
- {
- public ClientVersionUnsupportedException(int version)
- : base($"Version {version} is not supported by Impostor")
- {
- Version = version;
- }
-
- public int Version { get; }
- }
-}
\ No newline at end of file
--- /dev/null
+using System.Linq;
+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;
-using Impostor.Shared.Innersloth;
-
-namespace Impostor.Server
-{
- public readonly struct GameCode : IEquatable<GameCode>
- {
- public GameCode(int value)
- {
- Value = value;
- Code = GameCodeParser.IntToGameName(value);
- }
-
- public GameCode(string code)
- {
- Value = GameCodeParser.GameNameToInt(code);
- Code = code;
- }
-
- public string Code { get; }
-
- public int Value { get; }
-
- public static implicit operator string(GameCode code) => code.Code;
-
- public static implicit operator int(GameCode code) => code.Value;
-
- public static implicit operator GameCode(string code) => From(code);
-
- public static implicit operator GameCode(int value) => From(value);
-
- public bool Equals(GameCode other)
- {
- return Code == other.Code && Value == other.Value;
- }
-
- public override bool Equals(object? obj)
- {
- return obj is GameCode other && Equals(other);
- }
-
- public override int GetHashCode()
- {
- return HashCode.Combine(Code, Value);
- }
-
- public static bool operator ==(GameCode left, GameCode right)
- {
- return left.Equals(right);
- }
-
- public static bool operator !=(GameCode left, GameCode right)
- {
- return !left.Equals(right);
- }
-
- public override string ToString()
- {
- return Code;
- }
-
- public static GameCode From(int value) => new GameCode(value);
-
- public static GameCode From(string value) => new GameCode(value);
-
- public static GameCode Create()
- {
- return new GameCode(GameCodeParser.GenerateCode(6));
- }
- }
-}
\ No newline at end of file
--- /dev/null
+using System;
+using Impostor.Shared.Innersloth;
+
+namespace Impostor.Server
+{
+ public readonly struct GameCode : IEquatable<GameCode>
+ {
+ public GameCode(int value)
+ {
+ Value = value;
+ Code = GameCodeParser.IntToGameName(value);
+ }
+
+ public GameCode(string code)
+ {
+ Value = GameCodeParser.GameNameToInt(code);
+ Code = code;
+ }
+
+ public string Code { get; }
+
+ public int Value { get; }
+
+ public static implicit operator string(GameCode code) => code.Code;
+
+ public static implicit operator int(GameCode code) => code.Value;
+
+ public static implicit operator GameCode(string code) => From(code);
+
+ public static implicit operator GameCode(int value) => From(value);
+
+ public static bool operator ==(GameCode left, GameCode right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(GameCode left, GameCode right)
+ {
+ return !left.Equals(right);
+ }
+
+ public static GameCode Create()
+ {
+ return new GameCode(GameCodeParser.GenerateCode(6));
+ }
+
+ public static GameCode From(int value) => new GameCode(value);
+
+ public static GameCode From(string value) => new GameCode(value);
+
+ /// <inheritdoc/>
+ public bool Equals(GameCode other)
+ {
+ return Code == other.Code && Value == other.Value;
+ }
+
+ /// <inheritdoc/>
+ public override bool Equals(object? obj)
+ {
+ return obj is GameCode other && Equals(other);
+ }
+
+ /// <inheritdoc/>
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Code, Value);
+ }
+
+ public override string ToString()
+ {
+ return Code;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Net
+{
+ public enum GameJoinError
+ {
+ /// <summary>
+ /// No error occured while joining the game.
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// The client is not registered in the client manager.
+ /// </summary>
+ InvalidClient,
+
+ /// <summary>
+ /// The client has been banned from the game.
+ /// </summary>
+ Banned,
+
+ /// <summary>
+ /// The game is full.
+ /// </summary>
+ GameFull,
+
+ /// <summary>
+ /// The limbo state of the player is incorrect.
+ /// </summary>
+ InvalidLimbo,
+
+ /// <summary>
+ /// The game is already started.
+ /// </summary>
+ GameStarted,
+
+ /// <summary>
+ /// The game has been destroyed.
+ /// </summary>
+ GameDestroyed,
+
+ /// <summary>
+ /// Custom error by a plugin.
+ /// </summary>
+ /// <remarks>
+ /// A custom message can be set in <see cref="GameJoinResult.Message"/>.
+ /// </remarks>
+ Custom,
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Impostor.Server.Net
+{
+ public readonly struct GameJoinResult
+ {
+ private GameJoinResult(GameJoinError error, string? message = null, IClientPlayer? player = null)
+ {
+ Error = error;
+ Message = message;
+ Player = player;
+ }
+
+ public GameJoinError Error { get; }
+
+ public bool IsSuccess => Error == GameJoinError.None;
+
+ public bool IsCustomError => Error == GameJoinError.Custom;
+
+ [MemberNotNullWhen(true, nameof(IsCustomError))]
+ public string? Message { get; }
+
+ [MemberNotNullWhen(true, nameof(IsSuccess))]
+ public IClientPlayer? Player { get; }
+
+ public static GameJoinResult CreateCustomError(string message)
+ {
+ return new GameJoinResult(GameJoinError.Custom, message);
+ }
+
+ public static GameJoinResult CreateSuccess(IClientPlayer player)
+ {
+ return new GameJoinResult(GameJoinError.None, player: player);
+ }
+
+ public static GameJoinResult FromError(GameJoinError error)
+ {
+ if (error == GameJoinError.Custom)
+ {
+ throw new InvalidOperationException($"Custom errors should provide a message, use {nameof(CreateCustomError)} instead.");
+ }
+
+ return new GameJoinResult(error);
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Threading.Tasks;
+using Impostor.Server.Net;
+using Impostor.Shared.Innersloth;
+using Impostor.Shared.Innersloth.Data;
+
+namespace Impostor.Server
+{
+ public interface IGame
+ {
+ GameOptionsData Options { get; }
+
+ GameCode Code { get; }
+
+ GameStates GameState { get; }
+
+ IEnumerable<IClientPlayer> Players { get; }
+
+ IPEndPoint PublicIp { get; }
+
+ int PlayerCount { get; }
+
+ IClientPlayer Host { get; }
+
+ bool IsPublic { get; }
+
+ IDictionary<object, object> Items { get; }
+
+ 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);
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Impostor.Shared.Innersloth;
+using Impostor.Shared.Innersloth.Data;
+
+namespace Impostor.Server
+{
+ public interface IGameManager
+ {
+ 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
+++ /dev/null
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using Impostor.Server.Net;
-
-namespace Impostor.Server
-{
- public interface IGame
- {
- GameCode Code { get; }
-
- IEnumerable<IClientPlayer> Players { get; }
-
- IClientPlayer Host { get; }
-
- bool IsPublic { get; }
- IDictionary<object,object> Items { get; }
-
- IGameMessageWriter CreateMessage(MessageType type);
-
- bool TryGetPlayer(int id, [NotNullWhen(true)] out IClientPlayer player);
- }
-}
\ No newline at end of file
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>Impostor.Server</RootNamespace>
<Nullable>enable</Nullable>
+ <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
- <ProjectReference Include="..\Imposter.Reactive\Imposter.Reactive.csproj" />
<ProjectReference Include="..\Impostor.Shared\Impostor.Shared.csproj" />
</ItemGroup>
<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="System.Interactive.Async" Version="4.1.1" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
</ItemGroup>
-</Project>
+</Project>
\ No newline at end of file
<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/=attributes/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cattributes/@EntryIndexedValue">True</s:Boolean>
+ <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/=net_005Cextensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
+++ /dev/null
-using System;
-
-namespace Impostor.Shared.Innersloth.Data
-{
- [Flags]
- public enum LimboStates
- {
- PreSpawn = 1,
- NotLimbo = 2,
- WaitingForHost = 4,
- All = PreSpawn | NotLimbo | WaitingForHost
- }
-}
\ No newline at end of file
public static ValueTask SendToAllExceptAsync(this IGameMessageWriter writer, LimboStates states, int? id)
{
return id.HasValue
- ? writer.SendToAllExceptAsync(states, id.Value)
+ ? writer.SendToAllExceptAsync(id.Value, states)
: writer.SendToAllAsync(states);
}
-
+
public static ValueTask SendToAllExceptAsync(this IGameMessageWriter writer, LimboStates states, IClient client)
{
- if (client == null) throw new ArgumentNullException(nameof(client));
-
- return writer.SendToAllExceptAsync(states, client.Id);
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ return writer.SendToAllExceptAsync(client.Id, states);
}
-
+
public static ValueTask SendToAsync(this IGameMessageWriter writer, IClient client)
{
- if (client == null) throw new ArgumentNullException(nameof(client));
-
+ if (client == null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
return writer.SendToAsync(client.Id);
}
+
+ public static ValueTask SendToAsync(this IGameMessageWriter writer, IClientPlayer player)
+ {
+ return SendToAsync(writer, player.Client);
+ }
}
}
\ No newline at end of file
{
public interface IClientFactory
{
- /// <summary>
- /// Get the next ID for <see cref="IClient"/>.
- /// </summary>
- int NextId();
-
/// <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>
- ValueTask<IClient> CreateAsync(IConnection connection, string name, int clientVersion);
+ IClient Create(IConnection connection, string name, int clientVersion);
}
}
\ No newline at end of file
using System.Collections.Generic;
+using System.Threading.Tasks;
namespace Impostor.Server.Net
{
+ /// <summary>
+ /// Represents a connected game client.
+ /// </summary>
public interface IClient
{
- int Id { get; }
-
+ /// <summary>
+ /// Gets or sets the unique ID of the client.
+ /// </summary>
+ /// <remarks>
+ /// This ID is generated when the client is registered in the client manager and should not be used
+ /// to store persisted data.
+ /// </remarks>
+ int Id { get; set; }
+
+ /// <summary>
+ /// Gets the name that was provided by the player in the client.
+ /// </summary>
+ /// <remarks>
+ /// The name is provided by the player and should not be used to store persisted data.
+ /// </remarks>
string Name { get; }
-
- IConnection Connection { get; }
-
- IDictionary<object,object> Items { get; }
+
+ /// <summary>
+ /// Gets the connection of the client.
+ /// </summary>
+ /// <remarks>
+ /// Null when the client was not registered by the matchmaker.
+ /// </remarks>
+ IConnection? Connection { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the client is a bot.
+ /// </summary>
+ bool IsBot { get; }
+
+ /// <summary>
+ /// Gets a key/value collection that can be used to share data between messages.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// The stored data will not be saved.
+ /// After the connection has been closed all data will be lost.
+ /// </para>
+ /// <para>
+ /// Note that the values will not be disposed after the connection has been closed.
+ /// This has to be implemented by the plugin.
+ /// </para>
+ /// </remarks>
+ IDictionary<object, object> Items { get; }
+
+ /// <summary>
+ /// Gets or sets the current game data of the <see cref="IClient"/>.
+ /// </summary>
+ IClientPlayer? Player { get; set; }
+
+ ValueTask HandleMessageAsync(IMessage message);
+
+ ValueTask HandleDisconnectAsync();
}
}
\ No newline at end of file
-using Impostor.Shared.Innersloth.Data;
+using System.Threading.Tasks;
+using Impostor.Server.Net.Manager;
+using Impostor.Shared.Innersloth.Data;
namespace Impostor.Server.Net
{
+ /// <summary>
+ /// Represents a player in <see cref="IGame"/>.
+ /// </summary>
public interface IClientPlayer
{
+ /// <summary>
+ /// Gets the client that belongs to the player.
+ /// </summary>
IClient Client { get; }
-
+
+ /// <summary>
+ /// Gets the game where the <see cref="IClientPlayer"/> belongs to.
+ /// </summary>
IGame Game { get; }
-
- LimboStates Limbo { get; }
+
+ /// <summary>
+ /// Gets or sets the current limbo state of the player.
+ /// </summary>
+ LimboStates Limbo { get; set; }
+
+ ValueTask KickAsync();
+
+ ValueTask BanAsync();
}
}
\ No newline at end of file
using System;
using System.Net;
+using System.Threading.Tasks;
namespace Impostor.Server.Net
{
+ /// <summary>
+ /// Represents the connection of the client.
+ /// </summary>
public interface IConnection
{
- IAsyncObservable<IMessage> MessageReceived { get; }
-
+ /// <summary>
+ /// Gets the IP endpoint of the client.
+ /// </summary>
IPEndPoint EndPoint { get; }
-
+
+ /// <summary>
+ /// Gets a value indicating whether the client is connected to the server.
+ /// </summary>
bool IsConnected { get; }
- IConnectionMessageWriter CreateMessage(MessageType type);
+ /// <summary>
+ /// Gets or sets the client of the connection.
+ /// </summary>
+ IClient? Client { get; set; }
+
+ /// <summary>
+ /// Create a message writer that can be send to the connection.
+ /// </summary>
+ /// <remarks>
+ /// Be aware when implementing a custom connection handler that this method is not called when a message
+ /// is being send in <see cref="IGame"/>.
+ /// </remarks>
+ /// <param name="messageType">Type of the message.</param>
+ /// <returns>Message writer for the current connection.</returns>
+ IConnectionMessageWriter CreateMessage(MessageType messageType);
+
+ /// <summary>
+ /// Start listening to the client.
+ /// </summary>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask ListenAsync();
}
}
\ No newline at end of file
--- /dev/null
+using System;
+
+namespace Impostor.Shared.Innersloth.Data
+{
+ [Flags]
+ public enum LimboStates
+ {
+ PreSpawn = 1,
+ NotLimbo = 2,
+ WaitingForHost = 4,
+ All = PreSpawn | NotLimbo | WaitingForHost
+ }
+}
\ No newline at end of file
public interface IClientManager
{
ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion);
-
+
void Register(IClient client);
void Remove(IClient client);
+
+ bool Validate(IClient client);
}
}
\ No newline at end of file
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
namespace Impostor.Server.Net
{
+ /// <summary>
+ /// Represents the message writer for <see cref="IConnection"/>.
+ /// </summary>
public interface IConnectionMessageWriter : IMessageWriter
{
+ /// <summary>
+ /// Gets the connection where the message writer belongs to.
+ /// </summary>
+ public IConnection Connection { get; }
+
+ /// <summary>
+ /// Sends the message to the <see cref="Connection"/>.
+ /// </summary>
+ /// <returns>Task.</returns>
ValueTask SendAsync();
}
}
\ No newline at end of file
namespace Impostor.Server.Net
{
+ /// <summary>
+ /// Represents the message writer for <see cref="IGame"/>.
+ /// </summary>
public interface IGameMessageWriter : IMessageWriter
{
/// <summary>
/// Send the message to all players.
/// </summary>
- /// <param name="states"></param>
- ValueTask SendToAllAsync(LimboStates states);
+ /// <param name="states">Required limbo state of the player.</param>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask SendToAllAsync(LimboStates states = LimboStates.NotLimbo);
/// <summary>
/// Send the message to all players except one.
/// </summary>
- /// <param name="states"></param>
/// <param name="senderId">The player to exclude from sending the message.</param>
- ValueTask SendToAllExceptAsync(LimboStates states, int senderId);
-
+ /// <param name="states">Required limbo state of the player.</param>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+ ValueTask SendToAllExceptAsync(int senderId, LimboStates states = LimboStates.NotLimbo);
+
/// <summary>
/// Send a message to a specific player.
/// </summary>
- /// <param name="id"></param>
+ /// <param name="id">ID of the client.</param>
+ /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
ValueTask SendToAsync(int id);
}
}
\ No newline at end of file
public interface IMessage
{
MessageType Type { get; }
-
+
IMessageReader CreateReader();
}
}
\ No newline at end of file
{
public interface IMessageReader
{
+ /// <summary>
+ /// Gets the current position of the reader.
+ /// </summary>
int Position { get; }
-
+
+ /// <summary>
+ /// Gets the buffer of the message.
+ /// </summary>
ReadOnlyMemory<byte> Buffer { get; }
-
+
+ /// <summary>
+ /// Gets the tag of the message.
+ /// </summary>
byte Tag { get; }
-
+
+ /// <summary>
+ /// Gets the length of the buffer.
+ /// </summary>
int Length { get; }
-
+
bool ReadBoolean();
sbyte ReadSByte();
int ReadPackedInt32();
uint ReadPackedUInt32();
-
+
void CopyTo(IMessageWriter writer);
}
}
\ No newline at end of file
namespace Impostor.Server.Net
{
+ /// <summary>
+ /// Base message writer.
+ /// </summary>
public interface IMessageWriter : IDisposable
{
+ /// <summary>
+ /// Writes a boolean to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(bool value);
+ /// <summary>
+ /// Writes a sbyte to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(sbyte value);
+ /// <summary>
+ /// Writes a byte to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(byte value);
+ /// <summary>
+ /// Writes a short to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(short value);
+ /// <summary>
+ /// Writes an ushort to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(ushort value);
+ /// <summary>
+ /// Writes an uint to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(uint value);
+ /// <summary>
+ /// Writes an int to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(int value);
+ /// <summary>
+ /// Writes a float to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(float value);
+ /// <summary>
+ /// Writes a string to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void Write(string value);
-
- void Write(IPAddress ipAddress);
-
+
+ /// <summary>
+ /// Writes a <see cref="IPAddress"/> to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(IPAddress value);
+
+ /// <summary>
+ /// Writes an packed int to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
void WritePacked(int value);
-
+
+ /// <summary>
+ /// Writes raw bytes to the message.
+ /// </summary>
+ /// <param name="data">Bytes to write.</param>
void Write(ReadOnlyMemory<byte> data);
-
+
+ /// <summary>
+ /// Writes a game code to the message.
+ /// </summary>
+ /// <param name="value">Value to write.</param>
+ void Write(GameCode value);
+
+ /// <summary>
+ /// Starts a new message.
+ /// </summary>
+ /// <param name="typeFlag">Message flag header.</param>
void StartMessage(byte typeFlag);
- void Write(GameCode code);
-
+ /// <summary>
+ /// Mark the end of the message.
+ /// </summary>
void EndMessage();
+ /// <summary>
+ /// Clear the message writer.
+ /// </summary>
+ /// <param name="type">New type of the message.</param>
void Clear(MessageType type);
}
}
\ No newline at end of file
namespace Impostor.Server.Net
{
+ /// <summary>
+ /// Specifies how a message should be sent between connections.
+ /// </summary>
public enum MessageType
{
+ /// <summary>
+ /// Requests unreliable delivery with no fragmentation.
+ /// </summary>
+ /// <remarks>
+ /// Sending data using unreliable delivery means that data is not guaranteed to arrive at it's destination nor is
+ /// it guaranteed to arrive only once. However, unreliable delivery can be faster than other methods and it
+ /// typically requires a smaller number of protocol bytes than other methods. There is also typically less
+ /// processing involved and less memory needed as packets are not stored once sent.
+ /// </remarks>
Unreliable,
- Reliable
+
+ /// <summary>
+ /// Requests data be sent reliably but with no fragmentation.
+ /// </summary>
+ /// <remarks>
+ /// Sending data reliably means that data is guaranteed to arrive and to arrive only once. Reliable delivery
+ /// typically requires more processing, more memory (as packets need to be stored in case they need resending),
+ /// a larger number of protocol bytes and can be slower than unreliable delivery.
+ /// </remarks>
+ Reliable,
}
}
\ No newline at end of file
--- /dev/null
+using System.Threading.Tasks;
+using Impostor.Server.Events;
+
+namespace Impostor.Server
+{
+ public interface IPlugin : IEventListener
+ {
+ ValueTask EnableAsync();
+
+ ValueTask DisableAsync();
+
+ ValueTask ReloadAsync();
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.Threading.Tasks;
+
+namespace Impostor.Server
+{
+ public class PluginBase : IPlugin
+ {
+ public virtual ValueTask EnableAsync()
+ {
+ return default;
+ }
+
+ public virtual ValueTask DisableAsync()
+ {
+ return default;
+ }
+
+ public virtual ValueTask ReloadAsync()
+ {
+ return default;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0">
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
+ <Rule Id="SA1200" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules">
+ <Rule Id="SA1101" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
+ <Rule Id="SA1309" Action="None" />
+ </Rules>
+</RuleSet>
\ No newline at end of file
using System;
+using System.Collections.Concurrent;
using System.Net;
-using System.Reactive.Subjects;
using System.Threading.Tasks;
using Hazel;
using Impostor.Server.Net;
internal class HazelConnection : IConnection
{
private readonly ILogger<HazelConnection> _logger;
- private readonly ConcurrentSimpleAsyncSubject<IMessage> _messageReceived = new ConcurrentSimpleAsyncSubject<IMessage>();
+ private readonly ConcurrentStack<DataReceivedEventArgs> _pendingMessages;
public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger)
{
_logger = logger;
+ _pendingMessages = new ConcurrentStack<DataReceivedEventArgs>();
InnerConnection = innerConnection;
innerConnection.DataReceived += ConnectionOnDataReceived;
innerConnection.Disconnected += ConnectionOnDisconnected;
}
-
- public Connection InnerConnection { get; }
- public IAsyncObservable<IMessage> MessageReceived => _messageReceived;
+ public Connection InnerConnection { get; }
public IPEndPoint EndPoint => InnerConnection.EndPoint;
public bool IsConnected => InnerConnection.State == ConnectionState.Connected;
+ public IClient Client { get; set; }
+
private void ConnectionOnDisconnected(object sender, DisconnectedEventArgs e)
{
- Task.Run(_messageReceived.OnCompletedAsync);
+ if (Client != null)
+ {
+ Task.Run(Client.HandleDisconnectAsync);
+ }
}
private void ConnectionOnDataReceived(DataReceivedEventArgs e)
private async Task HandleData(DataReceivedEventArgs e)
{
+ if (Client == null)
+ {
+ _pendingMessages.Push(e);
+ return;
+ }
+
try
{
while (true)
using var message = new HazelMessage(reader, type);
- await _messageReceived.OnNextAsync(message);
+ await Client.HandleMessageAsync(message);
}
}
catch (Exception ex)
}
}
- public IConnectionMessageWriter CreateMessage(MessageType type)
+ public IConnectionMessageWriter CreateMessage(MessageType messageType)
+ {
+ return new HazelConnectionMessageWriter(messageType, this);
+ }
+
+ public async ValueTask ListenAsync()
{
- return new HazelConnectionMessageWriter(type, InnerConnection);
+ while (_pendingMessages.TryPop(out var eventArgs))
+ {
+ await HandleData(eventArgs);
+ }
}
}
}
\ No newline at end of file
});
_connection.NewConnection += OnNewConnection;
-
+
_connection.Start();
-
+
return default;
}
private async Task HandleNewConnection(NewConnectionEventArgs e)
{
- int clientVersion;
- string name;
try
{
// Handshake.
- clientVersion = e.HandshakeData.ReadInt32();
- name = e.HandshakeData.ReadString();
+ var clientVersion = e.HandshakeData.ReadInt32();
+ var name = e.HandshakeData.ReadString();
e.HandshakeData.Recycle();
+
+ var connection = new HazelConnection(e.Connection, _connectionLogger);
+
+ // Register client
+ await _clientManager.RegisterConnectionAsync(connection, name, clientVersion);
}
catch (Exception ex)
{
_logger.LogTrace(ex, "Error in new connection.");
- return;
}
-
- var connection = new HazelConnection(e.Connection, _connectionLogger);
-
- // Register client
- await _clientManager.RegisterConnectionAsync(connection, name, clientVersion);
}
public IGameMessageWriter CreateGameMessageWriter(IGame game, MessageType messageType)
using System.Threading.Tasks;
-using Hazel;
using Impostor.Server.Net;
namespace Impostor.Server.Hazel
{
internal class HazelConnectionMessageWriter : HazelMessageWriter, IConnectionMessageWriter
{
- private readonly Connection _connection;
+ private readonly HazelConnection _connection;
- public HazelConnectionMessageWriter(MessageType type, Connection connection)
+ public HazelConnectionMessageWriter(MessageType type, HazelConnection connection)
: base(type)
{
_connection = connection;
}
-
+
+ public IConnection Connection => _connection;
+
public ValueTask SendAsync()
{
- _connection.Send(Writer);
+ _connection.InnerConnection.Send(Writer);
return default;
}
}
internal class HazelGameMessageWriter : HazelMessageWriter, IGameMessageWriter
{
private readonly IGame _game;
-
+
public HazelGameMessageWriter(MessageType type, IGame game)
: base(type)
{
{
connection.Send(Writer);
}
-
+
return default;
}
- public ValueTask SendToAllExceptAsync(LimboStates states, int senderId)
+ public ValueTask SendToAllExceptAsync(int senderId, LimboStates states)
{
- foreach (var connection in GetConnections(x =>
- x.Limbo.HasFlag(states) &&
+ foreach (var connection in GetConnections(x =>
+ x.Limbo.HasFlag(states) &&
x.Client.Id != senderId))
{
connection.Send(Writer);
public ValueTask SendToAsync(int id)
{
- if (_game.TryGetPlayer(id, out var player))
+ if (_game.TryGetPlayer(id, out var player)
+ && player.Client.Connection is HazelConnection hazelConnection)
{
- ((HazelConnection)player.Client.Connection).InnerConnection.Send(Writer);
+ hazelConnection.InnerConnection.Send(Writer);
}
-
+
return default;
}
}
Writer.Write(value);
}
- public void Write(IPAddress ipAddress)
+ public void Write(IPAddress value)
{
- Writer.Write(ipAddress.GetAddressBytes());
+ Writer.Write(value.GetAddressBytes());
}
public void WritePacked(int value)
Writer.StartMessage(typeFlag);
}
- public void Write(GameCode code)
+ public void Write(GameCode value)
{
- Write(code.Value);
+ Write(value.Value);
}
public void EndMessage()
{
internal class ServerConfig
{
- public const string Section = "Server";
-
+ public const string Section = "Server";
+
public string PublicIp { get; set; } = "127.0.0.1";
+
public ushort PublicPort { get; set; } = 22023;
+
public string ListenIp { get; set; } = "127.0.0.1";
+
public ushort ListenPort { get; set; } = 22023;
}
}
\ No newline at end of file
public const string Section = "ServerRedirector";
public bool Enabled { get; set; }
+
public bool Master { get; set; }
+
public NodeLocator Locator { get; set; }
+
public List<ServerRedirectorNode> Nodes { get; set; }
public class NodeLocator
{
public string Redis { get; set; }
+
public string UdpMasterEndpoint { get; set; }
}
}
public class ServerRedirectorNode
{
public string Ip { get; set; }
+
public ushort Port { get; set; }
}
}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Events
+{
+ internal readonly struct EventHandler
+ {
+ public EventHandler(IEventListener o, RegisteredEventListener listener)
+ {
+ Object = o;
+ Listener = listener;
+ }
+
+ public IEventListener Object { get; }
+
+ public RegisteredEventListener Listener { get; }
+
+ public void Deconstruct(out IEventListener o, out RegisteredEventListener listener)
+ {
+ o = Object;
+ listener = Listener;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Impostor.Server.Events.Managers;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Impostor.Server.Events
+{
+ internal class EventManager : IEventManager
+ {
+ private readonly IServiceProvider _serviceProvider;
+
+ public EventManager(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ /// <inheritdoc />
+ public bool IsRegistered<TEvent>()
+ where TEvent : IEvent
+ {
+ using var scope = _serviceProvider.CreateScope();
+
+ return GetHandlers<TEvent>(_serviceProvider).Any();
+ }
+
+ /// <inheritdoc />
+ public async ValueTask CallAsync<T>(T @event)
+ where T : IEvent
+ {
+ var scope = _serviceProvider.CreateScope();
+
+ try
+ {
+ foreach (var (handler, eventListener) in GetHandlers<T>(scope.ServiceProvider))
+ {
+ await eventListener.InvokeAsync(handler, @event, scope.ServiceProvider);
+ }
+ }
+ finally
+ {
+ scope.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Get all the event listeners for the given event type.
+ /// </summary>
+ /// <param name="services">Current service provider.</param>
+ /// <returns>The event listeners.</returns>
+ private static IEnumerable<EventHandler> GetHandlers<TEvent>(IServiceProvider services)
+ where TEvent : IEvent
+ {
+ foreach (var handler in services.GetServices<IEventListener>())
+ {
+ var events = RegisteredEventListener.FromType(handler.GetType());
+
+ foreach (var eventHandler in events)
+ {
+ if (eventHandler.EventType != typeof(TEvent))
+ {
+ continue;
+ }
+
+ yield return new EventHandler(handler, eventHandler);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Impostor.Server.Events
+{
+ internal class RegisteredEventListener
+ {
+ private static readonly ConcurrentDictionary<Type, RegisteredEventListener[]> Instances = new ConcurrentDictionary<Type, RegisteredEventListener[]>();
+ private readonly Func<object, object, IServiceProvider, ValueTask> _invoker;
+ private readonly Type _eventListenerType;
+
+ public RegisteredEventListener(Type eventType, MethodInfo method, EventListenerAttribute attribute, Type eventListenerType)
+ {
+ EventType = eventType;
+ _eventListenerType = eventListenerType;
+ Priority = attribute.Priority;
+ PriorityOrder = attribute.PriorityOrder;
+ IgnoreCancelled = attribute.IgnoreCancelled;
+ Method = method.GetFriendlyName(showParameters: false);
+ _invoker = CreateInvoker(method, attribute.IgnoreCancelled);
+ }
+
+ public Type EventType { get; }
+
+ public EventPriority Priority { get; }
+
+ public int PriorityOrder { get; set; }
+
+ public bool IgnoreCancelled { get; }
+
+ public string Method { get; }
+
+ public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider)
+ {
+ return _invoker(eventHandler, @event, provider);
+ }
+
+ private Func<object, object, IServiceProvider, ValueTask> CreateInvoker(MethodInfo method, bool ignoreCancelled)
+ {
+ var instance = Expression.Parameter(typeof(object), "instance");
+ var eventParameter = Expression.Parameter(typeof(object), "event");
+ var provider = Expression.Parameter(typeof(IServiceProvider), "provider");
+ var @event = Expression.Convert(eventParameter, EventType);
+
+ var getRequiredService = typeof(ServiceProviderServiceExtensions)
+ .GetMethod("GetRequiredService", new[] { typeof(IServiceProvider) });
+
+ if (getRequiredService == null)
+ {
+ throw new InvalidOperationException("The method GetRequiredService could not be found.");
+ }
+
+ var methodArguments = method.GetParameters();
+ var arguments = new Expression[methodArguments.Length];
+
+ for (var i = 0; i < methodArguments.Length; i++)
+ {
+ var methodArgument = methodArguments[i];
+
+ if (methodArgument.ParameterType == EventType)
+ {
+ arguments[i] = @event;
+ }
+ else
+ {
+ arguments[i] = Expression.Call(
+ getRequiredService.MakeGenericMethod(methodArgument.ParameterType),
+ provider);
+ }
+ }
+
+ var returnTarget = Expression.Label(typeof(ValueTask));
+ Expression invoke = Expression.Call(Expression.Convert(instance, _eventListenerType), method, arguments);
+
+ if (method.ReturnType == typeof(void))
+ {
+ if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType))
+ {
+ invoke = Expression.Block(
+ Expression.IfThenElse(
+ Expression.Property(@event, nameof(IEventCancelable.IsCancelled)),
+ Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)),
+ Expression.Block(
+ invoke,
+ Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)))),
+ Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask)));
+ }
+ else
+ {
+ invoke = Expression.Block(
+ invoke,
+ Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask)));
+ }
+ }
+ else if (method.ReturnType == typeof(ValueTask))
+ {
+ if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType))
+ {
+ invoke = Expression.Block(
+ Expression.IfThenElse(
+ Expression.Property(@event, nameof(IEventCancelable.IsCancelled)),
+ Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)),
+ Expression.Return(returnTarget, invoke)),
+ Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask)));
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException($"The method {method.GetFriendlyName()} must return void or ValueTask.");
+ }
+
+ return Expression.Lambda<Func<object, object, IServiceProvider, ValueTask>>(invoke, instance, eventParameter, provider)
+ .Compile();
+ }
+
+ public static IEnumerable<RegisteredEventListener> FromType(Type type)
+ {
+ return Instances.GetOrAdd(type, t =>
+ {
+ return t.GetMethods()
+ .Where(m => !m.IsStatic && m.GetCustomAttribute(typeof(EventListenerAttribute), false) != null)
+ .SelectMany(m => FromMethod(t, m))
+ .ToArray();
+ });
+ }
+
+ public static IEnumerable<RegisteredEventListener> FromMethod(Type listenerType, MethodInfo methodType)
+ {
+ // Get the return type.
+ var returnType = methodType.ReturnType;
+
+ if (returnType != typeof(void) && returnType != typeof(ValueTask))
+ {
+ throw new InvalidOperationException($"The method {methodType.GetFriendlyName()} does not return void or ValueTask.");
+ }
+
+ // Register the event.
+ var attribute = methodType.GetCustomAttribute<EventListenerAttribute>(false);
+
+ if (attribute == null)
+ {
+ yield break;
+ }
+
+ Type[] eventTypes;
+
+ if (attribute.Events.Length == 0)
+ {
+ 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)}.");
+ }
+
+ eventTypes = new[] { methodType.GetParameters()[0].ParameterType };
+ }
+ else
+ {
+ eventTypes = attribute.Events;
+ }
+
+ foreach (var eventType in eventTypes)
+ {
+ var listener = new RegisteredEventListener(eventType, methodType, attribute, listenerType);
+
+ yield return listener;
+ }
+ }
+ }
+}
\ No newline at end of file
{
}
- protected AmongUsException(SerializationInfo info, StreamingContext context) : base(info, context)
+ public AmongUsException(string message, Exception innerException)
+ : base(message, innerException)
{
}
- public AmongUsException(string message) : base(message)
+ public AmongUsException(string message)
+ : base(message)
{
}
- public AmongUsException(string message, Exception innerException) : base(message, innerException)
+ protected AmongUsException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
{
}
}
--- /dev/null
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+
+namespace Impostor.Server
+{
+ internal static class TypeExtensions
+ {
+ /// <summary>
+ /// Get the friendly name for the type.
+ /// </summary>
+ /// <param name="type">The type.</param>
+ /// <returns>The friendly name.</returns>
+ [SuppressMessage("ReSharper", "SA1503", Justification = "Readability")]
+ public static string GetFriendlyName(this Type type)
+ {
+ if (type == null)
+ return "null";
+ if (type == typeof(int))
+ return "int";
+ if (type == typeof(short))
+ return "short";
+ if (type == typeof(byte))
+ return "byte";
+ if (type == typeof(bool))
+ return "bool";
+ if (type == typeof(long))
+ return "long";
+ if (type == typeof(float))
+ return "float";
+ if (type == typeof(double))
+ return "double";
+ if (type == typeof(decimal))
+ return "decimal";
+ if (type == typeof(string))
+ return "string";
+ if (type.IsGenericType)
+ return type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(GetFriendlyName).ToArray()) + ">";
+ return type.Name;
+ }
+
+ /// <summary>
+ /// Get the friendly name for the method.
+ /// </summary>
+ /// <param name="method">The method.</param>
+ /// <param name="showParameters">True if the parameters should be included in the name.</param>
+ /// <returns>Friendly name of the method</returns>
+ public static string GetFriendlyName(this MethodBase method, bool showParameters = true)
+ {
+ var str = method.Name;
+
+ if (method.DeclaringType != null)
+ {
+ str = method.DeclaringType.GetFriendlyName() + '.' + str;
+ }
+
+ if (showParameters)
+ {
+ var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.GetFriendlyName()));
+ str += $"({parameters})";
+ }
+
+ return str;
+ }
+ }
+}
\ No newline at end of file
<Version>1.1.0</Version>
<ApplicationIcon>icon.ico</ApplicationIcon>
<DebugType Condition=" '$(Configuration)' == 'Release' ">None</DebugType>
+ <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<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" />
+ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
</ItemGroup>
<ItemGroup>
</Content>
</ItemGroup>
- <ItemGroup>
- <Folder Include="Net\Hazel" />
- </ItemGroup>
-
-</Project>
+</Project>
\ No newline at end of file
--- /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
using System;
using System.Threading.Tasks;
+using Impostor.Server.Data;
using Impostor.Server.Net.Manager;
using Impostor.Server.Net.Messages;
-using Impostor.Server.Net.State;
+using Impostor.Shared.Innersloth;
using Impostor.Shared.Innersloth.Data;
using Serilog;
using ILogger = Serilog.ILogger;
internal class Client : ClientBase
{
private static readonly ILogger Logger = Log.ForContext<Client>();
-
+
private readonly IClientManager _clientManager;
- private readonly GameManager _gameManager;
+ private readonly IGameManager _gameManager;
- public Client(IClientManager clientManager, GameManager gameManager, int id, string name, IConnection connection)
- : base(id, name, connection)
+ public Client(IClientManager clientManager, IGameManager gameManager, string name, IConnection connection)
+ : base(name, connection)
{
_clientManager = clientManager;
_gameManager = gameManager;
- Player = new ClientPlayer(this, _gameManager);
- }
-
- public ClientPlayer Player { get; }
-
- private bool IsPacketAllowed(IMessageReader message, bool hostOnly)
- {
- var game = Player.Game;
- if (game == null)
- {
- return false;
- }
-
- // GameCode must match code of the current game assigned to the player.
- if (message.ReadInt32() != game.Code)
- {
- return false;
- }
-
- // Some packets should only be sent by the host of the game.
- if (hostOnly)
- {
- if (game.HostId == Id)
- {
- return true;
- }
-
- Logger.Warning("[{0}] Client sent packet only allowed by the host ({1}).", Id, game.HostId);
- return false;
- }
-
- return true;
}
-
- protected override async ValueTask OnMessageReceived(IMessage message)
+ public override async ValueTask HandleMessageAsync(IMessage message)
{
var reader = message.CreateReader();
-
+
var flag = reader.Tag;
-
+
Logger.Verbose("[{0}] Server got {1}.", Id, flag);
-
+
switch (flag)
{
case MessageFlags.HostGame:
{
// Read game settings.
var gameInfo = Message00HostGame.Deserialize(reader);
-
+
// Create game.
- var game = _gameManager.Create(gameInfo);
- if (game == null)
- {
- await Player.SendDisconnectReason(DisconnectReason.ServerFull);
- return;
- }
+ var game = await _gameManager.CreateAsync(gameInfo);
// Code in the packet below will be used in JoinGame.
- using (var writer = Connection.CreateMessage(MessageType.Reliable))
- {
- Message00HostGame.Serialize(writer, game.Code);
-
- await writer.SendAsync();
- }
+ using var writer = Connection.CreateMessage(MessageType.Reliable);
+ Message00HostGame.Serialize(writer, game.Code);
+
+ await writer.SendAsync();
+
break;
}
-
+
case MessageFlags.JoinGame:
{
- Message01JoinGame.Deserialize(reader,
- out var gameCode,
- out var unknown);
-
+ Message01JoinGame.Deserialize(
+ reader,
+ out var gameCode,
+ out _);
+
var game = _gameManager.Find(gameCode);
if (game == null)
{
- await Player.SendDisconnectReason(DisconnectReason.GameMissing);
+ await SendDisconnectReason(DisconnectReason.GameMissing);
return;
}
- await game.HandleJoinGame(Player);
+ var result = await game.AddClientAsync(this);
+
+ switch (result.Error)
+ {
+ case GameJoinError.None:
+ break;
+ case GameJoinError.InvalidClient:
+ await SendDisconnectReason(DisconnectReason.Custom, "Client is in an invalid state.");
+ break;
+ case GameJoinError.Banned:
+ await SendDisconnectReason(DisconnectReason.Banned);
+ break;
+ case GameJoinError.GameFull:
+ await SendDisconnectReason(DisconnectReason.GameFull);
+ break;
+ case GameJoinError.InvalidLimbo:
+ await SendDisconnectReason(DisconnectReason.Custom, "Invalid limbo state while joining.");
+ break;
+ case GameJoinError.GameStarted:
+ await SendDisconnectReason(DisconnectReason.GameStarted);
+ break;
+ case GameJoinError.GameDestroyed:
+ await SendDisconnectReason(DisconnectReason.Custom, DisconnectMessages.Destroyed);
+ break;
+ case GameJoinError.Custom:
+ await SendDisconnectReason(DisconnectReason.Custom, result.Message);
+ break;
+ default:
+ await SendDisconnectReason(DisconnectReason.Custom, "Unknown error.");
+ break;
+ }
+
break;
}
await Player.Game.HandleStartGame(reader);
break;
}
-
+
// No idea how this flag is triggered.
case MessageFlags.RemoveGame:
break;
-
+
case MessageFlags.RemovePlayer:
{
if (!IsPacketAllowed(reader, true))
{
return;
}
-
- Message04RemovePlayer.Deserialize(reader,
- out var playerId,
+
+ Message04RemovePlayer.Deserialize(
+ reader,
+ out var playerId,
out var reason);
- await Player.Game.HandleRemovePlayer(playerId, (DisconnectReason) reason);
+ await Player.Game.HandleRemovePlayer(playerId, (DisconnectReason)reason);
break;
}
-
+
case MessageFlags.GameData:
case MessageFlags.GameDataTo:
{
// Broadcast packet to all other players.
using var writer = Player.Game.CreateMessage(message.Type);
-
+
if (flag == MessageFlags.GameDataTo)
{
var target = reader.ReadPackedInt32();
else
{
reader.CopyTo(writer);
- await writer.SendToAllExceptAsync(LimboStates.NotLimbo, Player.Client.Id);
+ await writer.SendToAllExceptAsync(Id);
}
break;
}
-
+
case MessageFlags.EndGame:
{
if (!IsPacketAllowed(reader, true))
return;
}
- Message10AlterGame.Deserialize(reader,
- out var gameTag,
+ Message10AlterGame.Deserialize(
+ reader,
+ out var gameTag,
out var value);
-
+
if (gameTag != AlterGameTags.ChangePrivacy)
{
return;
return;
}
- Message11KickPlayer.Deserialize(reader,
- out var playerId,
+ Message11KickPlayer.Deserialize(
+ reader,
+ out var playerId,
out var isBan);
await Player.Game.HandleKickPlayer(playerId, isBan);
case MessageFlags.GetGameListV2:
{
Message16GetGameListV2.Deserialize(reader, out var options);
- await Player.OnRequestGameList(options);
+ await OnRequestGameList(options);
break;
}
-
+
default:
Logger.Warning("Server received unknown flag {0}.", flag);
break;
}
-
+
#if DEBUG
if (flag != MessageFlags.GameData &&
flag != MessageFlags.GameDataTo &&
flag != MessageFlags.EndGame &&
reader.Position < reader.Length)
{
- Logger.Warning("Server did not consume all bytes from {0} ({1} < {2}).",
+ Logger.Warning(
+ "Server did not consume all bytes from {0} ({1} < {2}).",
flag,
reader.Position,
reader.Length);
}
#endif
}
-
- protected override async ValueTask OnDisconnected()
+
+ public override async ValueTask HandleDisconnectAsync()
{
try
{
- if (Player.Game != null)
+ if (Player != null)
{
await Player.Game.HandleRemovePlayer(Id, DisconnectReason.ExitGame);
}
_clientManager.Remove(this);
}
+
+ private bool IsPacketAllowed(IMessageReader message, bool hostOnly)
+ {
+ if (Player == null)
+ {
+ return false;
+ }
+
+ var game = Player.Game;
+
+ // GameCode must match code of the current game assigned to the player.
+ if (message.ReadInt32() != game.Code)
+ {
+ return false;
+ }
+
+ // Some packets should only be sent by the host of the game.
+ if (hostOnly)
+ {
+ if (game.HostId == Id)
+ {
+ return true;
+ }
+
+ Logger.Warning("[{0}] Client sent packet only allowed by the host ({1}).", Id, game.HostId);
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Triggered when the connected client requests the game listing.
+ /// </summary>
+ /// <param name="options">
+ /// All options given.
+ /// At this moment, the client can only specify the map, impostor count and chat language.
+ /// </param>
+ private async ValueTask OnRequestGameList(GameOptionsData options)
+ {
+ using var message = Connection.CreateMessage(MessageType.Reliable);
+ var games = _gameManager.FindListings((MapFlags)options.MapId, options.NumImpostors, options.Keywords);
+
+ var skeldGameCount = _gameManager.GetGameCount(MapFlags.Skeld);
+ var miraHqGameCount = _gameManager.GetGameCount(MapFlags.MiraHQ);
+ var polusGameCount = _gameManager.GetGameCount(MapFlags.Polus);
+
+ Message16GetGameListV2.Serialize(message, skeldGameCount, miraHqGameCount, polusGameCount, games);
+
+ await message.SendAsync();
+ }
+
+ private async ValueTask SendDisconnectReason(DisconnectReason reason, string message = null)
+ {
+ if (Connection == null)
+ {
+ return;
+ }
+
+ using var packet = Connection.CreateMessage(MessageType.Reliable);
+ Message01JoinGame.SerializeError(packet, false, reason, message);
+ await packet.SendAsync();
+ }
}
}
\ No newline at end of file
-using System;
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
{
public abstract class ClientBase : IClient
{
- protected ClientBase(int id, string name, IConnection connection)
+ protected ClientBase(string name, IConnection connection)
{
- Id = id;
Name = name;
Connection = connection;
Items = new ConcurrentDictionary<object, object>();
}
- public int Id { get; }
-
+ public int Id { get; set; }
+
public string Name { get; }
-
+
public IConnection Connection { get; }
-
+
+ public bool IsBot => false;
+
public IDictionary<object, object> Items { get; }
- public virtual async ValueTask InitializeAsync()
- {
- await Connection.MessageReceived.SubscribeAsync(OnMessageReceived, OnDisconnected);
- }
+ public IClientPlayer Player { get; set; }
- protected abstract ValueTask OnMessageReceived(IMessage message);
+ public abstract ValueTask HandleMessageAsync(IMessage message);
- protected abstract ValueTask OnDisconnected();
+ public abstract ValueTask HandleDisconnectAsync();
}
}
\ No newline at end of file
using System;
-using System.Threading;
using System.Threading.Tasks;
-using Impostor.Server.Net.Messages;
-using Impostor.Shared.Innersloth.Data;
using Microsoft.Extensions.DependencyInjection;
namespace Impostor.Server.Net.Factories
internal class ClientFactory<TClient> : IClientFactory
where TClient : ClientBase
{
- private int _idLast;
private readonly IServiceProvider _serviceProvider;
public ClientFactory(IServiceProvider serviceProvider)
_serviceProvider = serviceProvider;
}
- public int NextId()
+ public IClient Create(IConnection connection, string name, int clientVersion)
{
- var clientId = Interlocked.Increment(ref _idLast);
-
- if (clientId < 1)
- {
- // Super rare but reset the _idLast because of overflow.
- _idLast = 0;
-
- // And get a new id.
- clientId = Interlocked.Increment(ref _idLast);
- }
-
- return clientId;
- }
-
- public async ValueTask<IClient> CreateAsync(IConnection connection, string name, int clientVersion)
- {
- if (clientVersion != 50516550)
- {
- using var packet = connection.CreateMessage(MessageType.Reliable);
- Message01JoinGame.SerializeError(packet, false, DisconnectReason.IncorrectVersion);
- await packet.SendAsync();
-
- throw new ClientVersionUnsupportedException(clientVersion);
- }
-
- var clientId = NextId();
- var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, clientId, name, connection);
-
- await client.InitializeAsync();
-
+ var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, name, connection);
+ connection.Client = client;
return client;
}
}
-using System;
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
+using System.Threading;
using System.Threading.Tasks;
using Impostor.Server.Net.Factories;
+using Impostor.Server.Net.Messages;
+using Impostor.Shared.Innersloth.Data;
using Microsoft.Extensions.Logging;
namespace Impostor.Server.Net.Manager
private readonly ILogger<ClientManager> _logger;
private readonly ConcurrentDictionary<int, IClient> _clients;
private readonly IClientFactory _clientFactory;
-
+ private int _idLast;
+
public ClientManager(ILogger<ClientManager> logger, IClientFactory clientFactory)
{
_logger = logger;
_clients = new ConcurrentDictionary<int, IClient>();
}
- public async ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion)
+ public int NextId()
{
- try
+ var clientId = Interlocked.Increment(ref _idLast);
+
+ if (clientId < 1)
{
- var client = await _clientFactory.CreateAsync(connection, name, clientVersion);
-
- Register(client);
+ // Super rare but reset the _idLast because of overflow.
+ _idLast = 0;
+
+ // And get a new id.
+ clientId = Interlocked.Increment(ref _idLast);
}
- catch (ClientVersionUnsupportedException ex)
+
+ return clientId;
+ }
+
+ public async ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion)
+ {
+ if (clientVersion != 50516550)
{
- _logger.LogTrace("Closed connection because client version {Version} is not supported.", ex.Version);
+ using var packet = connection.CreateMessage(MessageType.Reliable);
+ Message01JoinGame.SerializeError(packet, false, DisconnectReason.IncorrectVersion);
+ await packet.SendAsync();
+ return;
}
+
+ var client = _clientFactory.Create(connection, name, clientVersion);
+
+ Register(client);
+
+ await connection.ListenAsync();
}
public void Register(IClient client)
{
+ var id = NextId();
+
+ client.Id = id;
_logger.LogInformation("Client connected.");
- _clients.TryAdd(client.Id, client);
+ _clients.TryAdd(id, client);
}
public void Remove(IClient client)
_logger.LogInformation("Client disconnected.");
_clients.TryRemove(client.Id, out _);
}
+
+ public bool Validate(IClient client)
+ {
+ return client.Id != 0
+ && _clients.TryGetValue(client.Id, out var registeredClient)
+ && ReferenceEquals(client, registeredClient);
+ }
}
}
\ No newline at end of file
using System.Collections.Generic;
using System.Linq;
using System.Net;
+using System.Threading.Tasks;
using Impostor.Server.Data;
+using Impostor.Server.Events;
+using Impostor.Server.Events.Managers;
using Impostor.Server.Net.Redirector;
using Impostor.Server.Net.State;
using Impostor.Shared.Innersloth;
namespace Impostor.Server.Net.Manager
{
- internal class GameManager
+ internal class GameManager : IGameManager
{
private readonly ILogger<GameManager> _logger;
private readonly INodeLocator _nodeLocator;
private readonly IPEndPoint _publicIp;
private readonly ConcurrentDictionary<int, Game> _games;
private readonly IServiceProvider _serviceProvider;
+ private readonly IEventManager _eventManager;
- public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeLocator nodeLocator, IServiceProvider serviceProvider)
+ public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeLocator nodeLocator, IServiceProvider serviceProvider, IEventManager eventManager)
{
_logger = logger;
_nodeLocator = nodeLocator;
_serviceProvider = serviceProvider;
+ _eventManager = eventManager;
_publicIp = new IPEndPoint(IPAddress.Parse(config.Value.PublicIp), config.Value.PublicPort);
_games = new ConcurrentDictionary<int, Game>();
}
-
- public Game Create(GameOptionsData options)
+
+ public IEnumerable<IGame> Games => _games.Select(kv => kv.Value);
+
+ public async ValueTask<IGame> CreateAsync(GameOptionsData options)
{
// TODO: Prevent duplicates when using server redirector using INodeProvider.
-
var gameCode = GameCode.Create();
var gameCodeStr = gameCode.Code;
var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options);
- if (_nodeLocator.Find(gameCodeStr) == null &&
- _games.TryAdd(gameCode, game))
+ if (_nodeLocator.Find(gameCodeStr) != null || !_games.TryAdd(gameCode, game))
{
- _nodeLocator.Save(gameCodeStr, _publicIp);
- _logger.LogDebug("Created game with code {0} ({1}).", game.Code, gameCode);
- return game;
+ throw new ImpostorException("Could not create new game"); // TODO: Fix generic exception.
}
- _logger.LogWarning("Failed to create game.");
- return null;
+ _nodeLocator.Save(gameCodeStr, _publicIp);
+ _logger.LogDebug("Created game with code {0} ({1}).", game.Code, gameCode);
+
+ await _eventManager.CallAsync(new GameCreatedEvent(game));
+
+ return game;
}
- public Game Find(int gameCode)
+ public IGame Find(GameCode code)
{
- _games.TryGetValue(gameCode, out var game);
+ _games.TryGetValue(code, out var game);
return game;
}
- public IEnumerable<Game> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10)
+ public IEnumerable<IGame> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10)
{
var results = 0;
-
+
// Find games that have not started yet.
- foreach (var (code, game) in _games.Where(x =>
+ foreach (var (_, game) in _games.Where(x =>
x.Value.IsPublic &&
- x.Value.GameState == GameStates.NotStarted &&
+ x.Value.GameState == GameStates.NotStarted &&
x.Value.PlayerCount < x.Value.Options.MaxPlayers))
{
// Check for options.
- if (!map.HasFlag((MapFlags) (1 << game.Options.MapId)))
+ if (!map.HasFlag((MapFlags)(1 << game.Options.MapId)))
{
continue;
}
{
continue;
}
-
+
if (impostorCount != 0 && game.Options.NumImpostors != impostorCount)
{
continue;
}
-
+
// Add to result.
yield return game;
}
}
- public int GetGameCount(MapFlags map)
+ public async ValueTask RemoveAsync(GameCode gameCode)
{
- var count = 0;
-
- foreach (var (code, game) in _games) {
- if (!map.HasFlag((MapFlags)(1 << game.Options.MapId)))
+ if (_games.TryGetValue(gameCode, out var game) && game.PlayerCount > 0)
+ {
+ foreach (var player in game.Players)
{
- continue;
+ await player.KickAsync();
}
- count++;
+
+ return;
+ }
+
+ if (!_games.TryRemove(gameCode, out _))
+ {
+ return;
}
- return count;
- }
- public void Remove(int gameCode)
- {
_logger.LogDebug("Remove game with code {0} ({1}).", GameCodeParser.IntToGameName(gameCode), gameCode);
_nodeLocator.Remove(GameCodeParser.IntToGameName(gameCode));
- _games.TryRemove(gameCode, out _);
}
}
}
\ No newline at end of file
{
writer.Clear(MessageType.Reliable);
}
-
+
writer.StartMessage(MessageFlags.JoinGame);
writer.Write(gameCode);
writer.Write(playerId);
writer.Write(hostId);
writer.EndMessage();
}
-
+
public static void SerializeError(IMessageWriter writer, bool clear, DisconnectReason reason, string message = null)
{
if (clear)
{
writer.Clear(MessageType.Reliable);
}
-
+
writer.StartMessage(MessageFlags.JoinGame);
writer.Write((int) reason);
{
throw new ArgumentNullException(nameof(message));
}
-
+
writer.Write(message);
}
-
+
writer.EndMessage();
}
{
writer.Clear(MessageType.Reliable);
}
-
+
writer.StartMessage(MessageFlags.RemovePlayer);
writer.Write(gameCode);
writer.Write(playerId);
writer.Write(hostId);
- writer.Write((byte) reason);
+ writer.Write((byte)reason);
writer.EndMessage();
}
{
writer.Clear(MessageType.Reliable);
}
-
+
writer.StartMessage(MessageFlags.JoinedGame);
writer.Write(gameCode);
writer.Write(playerId);
{
writer.WritePacked(id);
}
-
+
writer.EndMessage();
}
}
{
writer.Clear(MessageType.Reliable);
}
-
+
writer.StartMessage(MessageFlags.HostGame);
writer.Write(gameCode);
writer.EndMessage();
{
writer.Clear(MessageType.Reliable);
}
-
+
writer.StartMessage(MessageFlags.KickPlayer);
writer.Write(gameCode);
writer.WritePacked(playerId);
{
writer.Clear(MessageType.Reliable);
}
-
+
writer.StartMessage(MessageFlags.WaitForHost);
writer.Write(gameCode);
writer.Write(playerId);
{
writer.Clear(MessageType.Reliable);
}
-
+
writer.StartMessage(MessageFlags.Redirect);
writer.Write(ipEndPoint.Address);
- writer.Write((ushort) ipEndPoint.Port);
+ writer.Write((ushort)ipEndPoint.Port);
writer.EndMessage();
}
}
using System.Collections.Generic;
-using Impostor.Server.Net.State;
using Impostor.Shared.Innersloth;
namespace Impostor.Server.Net.Messages
options = GameOptionsData.Deserialize(reader.ReadBytesAndSize());
}
- public static void Serialize(IMessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable<Game> games)
+ public static void Serialize(IMessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable<IGame> games)
{
writer.StartMessage(MessageFlags.GetGameListV2);
-
+
// Count
writer.StartMessage(1);
writer.Write(skeldGameCount); // The Skeld
writer.Write(miraHqGameCount); // Mira HQ
writer.Write(polusGameCount); // Polus
writer.EndMessage();
-
+
// Listing
writer.StartMessage(0);
+
foreach (var game in games)
{
writer.StartMessage(0);
writer.Write((byte) game.Options.MaxPlayers);
writer.EndMessage();
}
+
writer.EndMessage();
-
writer.EndMessage();
}
}
private readonly INodeProvider _nodeProvider;
private readonly INodeLocator _nodeLocator;
- public ClientRedirector(int id, string name, IConnection connection, IClientManager clientManager, INodeProvider nodeProvider, INodeLocator nodeLocator)
- : base(id, name, connection)
+ public ClientRedirector(
+ string name,
+ IConnection connection,
+ IClientManager clientManager,
+ INodeProvider nodeProvider,
+ INodeLocator nodeLocator)
+ : base(name, connection)
{
_clientManager = clientManager;
_nodeProvider = nodeProvider;
_nodeLocator = nodeLocator;
}
- protected override async ValueTask OnMessageReceived(IMessage message)
+ public override async ValueTask HandleMessageAsync(IMessage message)
{
var reader = message.CreateReader();
var flag = reader.Tag;
-
+
Logger.Verbose("Server got {0}.", flag);
switch (flag)
case MessageFlags.JoinGame:
{
- Message01JoinGame.Deserialize(reader,
- out var gameCode,
- out var unknown);
+ Message01JoinGame.Deserialize(
+ reader,
+ out var gameCode,
+ out _);
- using (var packet = Connection.CreateMessage(MessageType.Reliable))
+ using var packet = Connection.CreateMessage(MessageType.Reliable);
+ var endpoint = _nodeLocator.Find(GameCodeParser.IntToGameName(gameCode));
+ if (endpoint == null)
{
- var endpoint = _nodeLocator.Find(GameCodeParser.IntToGameName(gameCode));
- if (endpoint == null)
- {
- Message01JoinGame.SerializeError(packet, false, DisconnectReason.GameMissing);
- }
- else
- {
- Message13Redirect.Serialize(packet, false, endpoint);
- }
-
- await packet.SendAsync();
+ Message01JoinGame.SerializeError(packet, false, DisconnectReason.GameMissing);
}
+ else
+ {
+ Message13Redirect.Serialize(packet, false, endpoint);
+ }
+
+ await packet.SendAsync();
break;
}
case MessageFlags.GetGameListV2:
{
// TODO: Implement.
- using (var packet = Connection.CreateMessage(MessageType.Reliable))
- {
- Message01JoinGame.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.NotImplemented);
- await packet.SendAsync();
- }
+ using var packet = Connection.CreateMessage(MessageType.Reliable);
+ Message01JoinGame.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.NotImplemented);
+ await packet.SendAsync();
break;
}
}
}
- protected override ValueTask OnDisconnected()
+ public override ValueTask HandleDisconnectAsync()
{
_clientManager.Remove(this);
return default;
public interface INodeLocator
{
IPEndPoint Find(string gameCode);
+
void Save(string gameCode, IPEndPoint endPoint);
+
void Remove(string gameCode);
}
}
\ No newline at end of file
public class NodeLocatorRedis : INodeLocator
{
private readonly IDistributedCache _cache;
-
+
public NodeLocatorRedis(ILogger<NodeLocatorRedis> logger, IDistributedCache cache)
{
logger.LogWarning("Using the redis NodeLocator.");
{
return null;
}
-
+
return IPEndPoint.Parse(entry);
}
{
_cache.SetString(gameCode, endPoint.ToString(), new DistributedCacheEntryOptions
{
- SlidingExpiration = TimeSpan.FromHours(1)
+ SlidingExpiration = TimeSpan.FromHours(1),
});
}
namespace Impostor.Server.Net.Redirector
{
- public class NodeLocatorUDP : INodeLocator, IDisposable
+ public class NodeLocatorUdp : INodeLocator, IDisposable
{
- private readonly ILogger<NodeLocatorUDP> _logger;
+ private readonly ILogger<NodeLocatorUdp> _logger;
private readonly bool _isMaster;
private readonly IPEndPoint _server;
private readonly UdpClient _client;
private readonly ConcurrentDictionary<string, AvailableNode> _availableNodes;
-
- public NodeLocatorUDP(ILogger<NodeLocatorUDP> logger, IOptions<ServerRedirectorConfig> config)
+
+ public NodeLocatorUdp(ILogger<NodeLocatorUdp> logger, IOptions<ServerRedirectorConfig> config)
{
_logger = logger;
-
+
if (config.Value.Master)
{
_isMaster = true;
else
{
_isMaster = false;
-
+
if (!IPEndPoint.TryParse(config.Value.Locator.UdpMasterEndpoint, out var endpoint))
{
throw new ArgumentException("UdpMasterEndpoint should be in the ip:port format.");
}
-
+
_logger.LogWarning("Node server will send updates to {0}.", endpoint);
_server = endpoint;
_client = new UdpClient
public void Update(IPEndPoint ip, string gameCode)
{
_logger.LogDebug("Received update {0} -> {1}", gameCode, ip);
-
- _availableNodes.AddOrUpdate(gameCode, s => new AvailableNode
- {
- Endpoint = ip,
- LastUpdated = DateTimeOffset.UtcNow
- }, (s, node) =>
- {
- node.Endpoint = ip;
- node.LastUpdated = DateTimeOffset.UtcNow;
-
- return node;
- });
+
+ _availableNodes.AddOrUpdate(
+ gameCode,
+ s => new AvailableNode
+ {
+ Endpoint = ip,
+ LastUpdated = DateTimeOffset.UtcNow,
+ },
+ (s, node) =>
+ {
+ node.Endpoint = ip;
+ node.LastUpdated = DateTimeOffset.UtcNow;
+
+ return node;
+ });
foreach (var (key, value) in _availableNodes)
{
{
return null;
}
-
+
if (_availableNodes.TryGetValue(gameCode, out var node))
{
if (node.Expired)
return node.Endpoint;
}
-
+
return null;
}
{
return;
}
-
+
_availableNodes.TryRemove(gameCode, out _);
}
_client?.Dispose();
}
- private class AvailableNode {
+ private class AvailableNode
+ {
public IPEndPoint Endpoint { get; set; }
+
public DateTimeOffset LastUpdated { get; set; }
+
public bool Expired => LastUpdated < DateTimeOffset.UtcNow.AddHours(-1);
}
}
-}
+}
\ No newline at end of file
namespace Impostor.Server.Net.Redirector
{
- public class NodeLocatorUDPService : BackgroundService
+ public class NodeLocatorUdpService : BackgroundService
{
- private readonly NodeLocatorUDP _nodeLocator;
- private readonly ILogger<NodeLocatorUDPService> _logger;
+ private readonly NodeLocatorUdp _nodeLocator;
+ private readonly ILogger<NodeLocatorUdpService> _logger;
private readonly UdpClient _client;
- public NodeLocatorUDPService(
- INodeLocator nodeLocator,
- ILogger<NodeLocatorUDPService> logger,
+ public NodeLocatorUdpService(
+ INodeLocator nodeLocator,
+ ILogger<NodeLocatorUdpService> logger,
IOptions<ServerRedirectorConfig> options)
{
- _nodeLocator = (NodeLocatorUDP) nodeLocator;
+ _nodeLocator = (NodeLocatorUdp)nodeLocator;
_logger = logger;
if (!IPEndPoint.TryParse(options.Value.Locator.UdpMasterEndpoint, out var endpoint))
_client = new UdpClient(endpoint)
{
- DontFragment = true
+ DontFragment = true,
};
}
-
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogWarning("Master server is listening for node updates on {0}.", _client.Client.LocalEndPoint);
-
+
stoppingToken.Register(() =>
{
_client.Close();
{
// Receive data from a node.
UdpReceiveResult data;
-
+
try
{
data = await _client.ReceiveAsync();
{
break;
}
-
+
// Check if data is valid.
if (data.Buffer.Length == 0)
{
{
_logger.LogError(e, "Error in NodeLocatorUDPService.");
}
-
+
_logger.LogWarning("Master server node update listener is stopping.");
}
}
private readonly List<IPEndPoint> _nodes;
private readonly object _lock;
private int _currentIndex;
-
+
public NodeProviderConfig(IOptions<ServerRedirectorConfig> redirectorConfig)
{
_nodes = new List<IPEndPoint>();
{
_currentIndex = 0;
}
-
+
return node;
}
}
+++ /dev/null
-using System.Threading.Tasks;
-using Impostor.Server.Net.Messages;
-using Impostor.Shared.Innersloth;
-using Impostor.Shared.Innersloth.Data;
-
-namespace Impostor.Server.Net.State
-{
- internal partial class ClientPlayer
- {
- /// <summary>
- /// Triggered when the connected client requests the game listing.
- /// </summary>
- /// <param name="options">
- /// All options given.
- /// At this moment, the client can only specify the map, impostor count and chat language.
- /// </param>
- public async ValueTask OnRequestGameList(GameOptionsData options)
- {
- using (var message = Client.Connection.CreateMessage(MessageType.Reliable))
- {
- var games = _gameManager.FindListings((MapFlags) options.MapId, options.NumImpostors, options.Keywords);
-
- var skeldGameCount = _gameManager.GetGameCount(MapFlags.Skeld);
- var miraHqGameCount = _gameManager.GetGameCount(MapFlags.MiraHQ);
- var polusGameCount = _gameManager.GetGameCount(MapFlags.Polus);
-
- Message16GetGameListV2.Serialize(message, skeldGameCount, miraHqGameCount, polusGameCount, games);
-
- await message.SendAsync();
- }
- }
- }
-}
\ No newline at end of file
using System.Threading.Tasks;
-using Impostor.Server.Net.Manager;
-using Impostor.Server.Net.Messages;
using Impostor.Shared.Innersloth.Data;
namespace Impostor.Server.Net.State
{
- internal partial class ClientPlayer : IClientPlayer
+ internal class ClientPlayer : IClientPlayer
{
- private readonly GameManager _gameManager;
-
- public ClientPlayer(Client client, GameManager gameManager)
+ public ClientPlayer(IClient client, Game game)
{
- _gameManager = gameManager;
-
+ Game = game;
Client = client;
Limbo = LimboStates.PreSpawn;
}
-
- public Client Client { get; }
- public Game Game { get; set; }
- public LimboStates Limbo { get; set; }
- public async ValueTask SendDisconnectReason(DisconnectReason reason, string message = null)
- {
- using (var packet = Client.Connection.CreateMessage(MessageType.Reliable))
- {
- Message01JoinGame.SerializeError(packet, false, reason, message);
- await packet.SendAsync();
- }
- }
+ public IClient 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()
+ {
+ return Game.HandleKickPlayer(Client.Id, false);
+ }
+
+ /// <inheritdoc />
+ public ValueTask BanAsync()
+ {
+ return Game.HandleKickPlayer(Client.Id, true);
+ }
}
}
\ No newline at end of file
-using System;
-using System.Threading.Tasks;
-using Impostor.Server.Data;
+using System.Threading.Tasks;
using Impostor.Shared.Innersloth.Data;
namespace Impostor.Server.Net.State
using var packet = CreateMessage(MessageType.Reliable);
message.CopyTo(packet);
- await packet.SendToAllAsync(LimboStates.NotLimbo);
+ await packet.SendToAllAsync();
}
- public async ValueTask HandleJoinGame(ClientPlayer sender)
+ public async ValueTask<GameJoinResult> AddClientAsync(IClient client)
{
// Check if the IP of the player is banned.
- if (_bannedIps.Contains(sender.Client.Connection.EndPoint.Address))
+ if (client.Connection != null && _bannedIps.Contains(client.Connection.EndPoint.Address))
{
- await sender.SendDisconnectReason(DisconnectReason.Banned);
- return;
+ return GameJoinResult.FromError(GameJoinError.Banned);
}
-
+
+ var player = client.Player;
+
// Check if;
// - The player is already in this game.
// - The game is full.
- if (sender.Game != this && _players.Count >= Options.MaxPlayers)
+ if (player?.Game != this && _players.Count >= Options.MaxPlayers)
{
- await sender.SendDisconnectReason(DisconnectReason.GameFull);
- return;
+ return GameJoinResult.FromError(GameJoinError.GameFull);
+ }
+
+ if (GameState == GameStates.Started)
+ {
+ return GameJoinResult.FromError(GameJoinError.GameStarted);
+ }
+
+ if (GameState == GameStates.Destroyed)
+ {
+ return GameJoinResult.FromError(GameJoinError.GameDestroyed);
+ }
+
+ var isNew = false;
+
+ if (player == null || player.Game != this)
+ {
+ var clientPlayer = new ClientPlayer(client, this);
+
+ if (!_clientManager.Validate(client))
+ {
+ return GameJoinResult.FromError(GameJoinError.InvalidClient);
+ }
+
+ isNew = true;
+ player = clientPlayer;
+ client.Player = clientPlayer;
}
-
+
// Check current player state.
- if (sender.Limbo == LimboStates.NotLimbo)
+ if (player.Limbo == LimboStates.NotLimbo)
{
- await sender.SendDisconnectReason(DisconnectReason.Custom, "Invalid limbo state while joining.");
- return;
+ return GameJoinResult.FromError(GameJoinError.InvalidLimbo);
}
-
- switch (GameState)
+
+ if (GameState == GameStates.Ended)
{
- case GameStates.NotStarted:
- await HandleJoinGameNew(sender);
- break;
- case GameStates.Ended:
- await HandleJoinGameNext(sender);
- break;
- case GameStates.Started:
- await sender.SendDisconnectReason(DisconnectReason.GameStarted);
- return;
- case GameStates.Destroyed:
- await sender.SendDisconnectReason(DisconnectReason.Custom, DisconnectMessages.Destroyed);
- return;
- default:
- throw new ArgumentOutOfRangeException();
+ await HandleJoinGameNext(player, isNew);
+ return GameJoinResult.CreateSuccess(player);
}
+
+ await HandleJoinGameNew(player, isNew);
+ return GameJoinResult.CreateSuccess(player);
}
public async ValueTask HandleEndGame(IMessageReader message)
{
GameState = GameStates.Ended;
-
+
// Broadcast end of the game.
using (var packet = CreateMessage(MessageType.Reliable))
{
message.CopyTo(packet);
- await packet.SendToAllAsync(LimboStates.NotLimbo);
+ await packet.SendToAllAsync();
}
-
+
// Put all players in the correct limbo state.
foreach (var player in _players)
{
}
}
- public async ValueTask HandleAlterGame(IMessageReader message, ClientPlayer sender, bool isPublic)
+ public async ValueTask HandleAlterGame(IMessageReader message, IClientPlayer sender, bool isPublic)
{
IsPublic = isPublic;
using var packet = CreateMessage(MessageType.Reliable);
message.CopyTo(packet);
- await packet.SendToAllExceptAsync(LimboStates.NotLimbo, sender.Client.Id);
+ await packet.SendToAllExceptAsync(sender.Client.Id);
}
-
+
public async ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason)
{
await PlayerRemove(playerId);
using var packet = CreateMessage(MessageType.Reliable);
WriteRemovePlayerMessage(packet, false, playerId, reason);
- await packet.SendToAllExceptAsync(LimboStates.NotLimbo, playerId);
+ await packet.SendToAllExceptAsync(playerId);
}
public async ValueTask HandleKickPlayer(int playerId, bool isBan)
{
Logger.Information("{0} - Player {1} has left.", Code, playerId);
-
- using (var message = CreateMessage(MessageType.Reliable))
- {
- // Send message to everyone that this player was kicked.
- WriteKickPlayerMessage(message, false, playerId, isBan);
- await message.SendToAllAsync(LimboStates.NotLimbo);
-
- await PlayerRemove(playerId, isBan);
-
- // Rmeove the player from everyone's game.
- WriteRemovePlayerMessage(message, true, playerId, isBan
- ? DisconnectReason.Banned
- : DisconnectReason.Kicked);
- await message.SendToAllExceptAsync(LimboStates.NotLimbo, playerId);
- }
+
+ using var message = CreateMessage(MessageType.Reliable);
+
+ // Send message to everyone that this player was kicked.
+ WriteKickPlayerMessage(message, false, playerId, isBan);
+ await message.SendToAllAsync();
+
+ await PlayerRemove(playerId, isBan);
+
+ // Remove the player from everyone's game.
+ WriteRemovePlayerMessage(
+ message,
+ true,
+ playerId,
+ isBan ? DisconnectReason.Banned : DisconnectReason.Kicked);
+ await message.SendToAllExceptAsync(playerId);
}
-
- private async ValueTask HandleJoinGameNew(ClientPlayer sender)
+
+ private async ValueTask HandleJoinGameNew(IClientPlayer sender, bool isNew)
{
Logger.Information("{0} - Player {1} ({2}) is joining.", Code, sender.Client.Name, sender.Client.Id);
-
+
// Add player to the game.
- if (sender.Game == null)
+ if (isNew)
{
PlayerAdd(sender);
}
{
WriteJoinedGameMessage(message, false, sender);
WriteAlterGameMessage(message, false);
-
+
sender.Limbo = LimboStates.NotLimbo;
- await message.SendToAsync(sender.Client);
+ await message.SendToAsync(sender);
await BroadcastJoinMessage(message, true, sender);
}
}
- private async ValueTask HandleJoinGameNext(ClientPlayer sender)
+ private async ValueTask HandleJoinGameNext(IClientPlayer sender, bool isNew)
{
Logger.Information("{0} - Player {1} ({2}) is rejoining.", Code, sender.Client.Name, sender.Client.Id);
-
+
// Add player to the game.
- if (sender.Game == null)
+ if (isNew)
{
PlayerAdd(sender);
}
-
+
// Check if the host joined and let everyone join.
if (sender.Client.Id == HostId)
{
GameState = GameStates.NotStarted;
-
+
// Spawn the host.
- await HandleJoinGameNew(sender);
-
+ await HandleJoinGameNew(sender, false);
+
// Pull players out of limbo.
await CheckLimboPlayers();
return;
sender.Limbo = LimboStates.WaitingForHost;
using var packet = CreateMessage(MessageType.Reliable);
-
+
WriteWaitForHostMessage(packet, false, sender);
await packet.SendToAsync(sender.Client);
{
Message04RemovePlayer.Serialize(message, clear, Code, playerId, HostId, reason);
}
-
- private void WriteJoinedGameMessage(IMessageWriter message, bool clear, ClientPlayer player)
+
+ private void WriteJoinedGameMessage(IMessageWriter message, bool clear, IClientPlayer player)
{
var playerIds = _players
.Where(x => x.Value != player)
.Select(x => x.Key)
.ToArray();
-
+
Message07JoinedGame.Serialize(message, clear, Code, player.Client.Id, HostId, playerIds);
}
{
Message11KickPlayer.Serialize(message, clear, Code, playerId, isBan);
}
-
- private void WriteWaitForHostMessage(IMessageWriter message, bool clear, ClientPlayer player)
+
+ private void WriteWaitForHostMessage(IMessageWriter message, bool clear, IClientPlayer player)
{
Message12WaitForHost.Serialize(message, clear, Code, player.Client.Id);
}
-using System.Collections.Generic;
-using System.Linq;
+using System.Linq;
using System.Threading.Tasks;
using Impostor.Server.Exceptions;
using Impostor.Shared.Innersloth.Data;
{
internal partial class Game
{
- private void PlayerAdd(ClientPlayer player)
+ private void PlayerAdd(IClientPlayer player)
{
// Store player.
if (!_players.TryAdd(player.Client.Id, player))
{
throw new AmongUsException("Failed to add player to game.");
}
-
- // Assign player to this game for future packets.
- player.Game = this;
// Assign hostId if none is set.
if (HostId == -1)
return false;
}
- player.Limbo = LimboStates.PreSpawn;
- player.Game = null;
-
Logger.Information("{0} - Player {1} ({2}) has left.", Code, player.Client.Name, playerId);
-
+
+ player.Client.Player = null;
+
// Game is empty, remove it.
- if (_players.Count == 0)
+ if (_players.IsEmpty)
{
GameState = GameStates.Destroyed;
// Remove instance reference.
- _gameManager.Remove(Code);
+ await _gameManager.RemoveAsync(Code);
return true;
}
await MigrateHost();
}
- if (isBan)
+ if (isBan && player.Client.Connection != null)
{
_bannedIps.Add(player.Client.Connection.EndPoint.Address);
}
private async ValueTask MigrateHost()
{
// Pick the first player as new host.
- var host = _players.First().Value;
-
+ var host = _players
+ .Select(p => p.Value)
+ .FirstOrDefault(p => !p.Client.IsBot);
+
+ if (host == null)
+ {
+ await EndAsync();
+ return;
+ }
+
HostId = host.Client.Id;
Logger.Information("{0} - Assigned {1} ({2}) as new host.", Code, host.Client.Name, host.Client.Id);
-
+
// Check our current game state.
if (GameState == GameStates.Ended && host.Limbo == LimboStates.WaitingForHost)
{
GameState = GameStates.NotStarted;
-
+
// Spawn the host.
- await HandleJoinGameNew(host);
-
+ await HandleJoinGameNew(host, false);
+
// Pull players out of limbo.
await CheckLimboPlayers();
}
private async ValueTask CheckLimboPlayers()
{
using var message = CreateMessage(MessageType.Reliable);
-
+
foreach (var (_, player) in _players.Where(x => x.Value.Limbo == LimboStates.WaitingForHost))
{
WriteJoinedGameMessage(message, true, player);
WriteAlterGameMessage(message, false);
-
+
player.Limbo = LimboStates.NotLimbo;
await message.SendToAsync(player.Client);
}
internal partial class Game : IGame
{
private static readonly ILogger Logger = Log.ForContext<Game>();
-
- private readonly GameManager _gameManager;
- private readonly INodeLocator _nodeLocator;
- private readonly IMatchmaker matchmaker;
- private readonly ConcurrentDictionary<int, ClientPlayer> _players;
+
+ private readonly IGameManager _gameManager;
+ private readonly IClientManager _clientManager;
+ private readonly IMatchmaker _matchmaker;
+ private readonly ConcurrentDictionary<int, IClientPlayer> _players;
private readonly HashSet<IPAddress> _bannedIps;
public Game(
- GameManager gameManager,
+ IGameManager gameManager,
INodeLocator nodeLocator,
IPEndPoint publicIp,
GameCode code,
GameOptionsData options,
- IMatchmaker matchmaker)
+ IMatchmaker matchmaker,
+ IClientManager clientManager)
{
_gameManager = gameManager;
- _nodeLocator = nodeLocator;
- _players = new ConcurrentDictionary<int, ClientPlayer>();
+ _players = new ConcurrentDictionary<int, IClientPlayer>();
_bannedIps = new HashSet<IPAddress>();
PublicIp = publicIp;
HostId = -1;
GameState = GameStates.NotStarted;
Options = options;
- this.matchmaker = matchmaker;
+ _matchmaker = matchmaker;
+ _clientManager = clientManager;
Items = new ConcurrentDictionary<object, object>();
}
public IPEndPoint PublicIp { get; }
+
public GameCode Code { get; }
+
public bool IsPublic { get; private set; }
+
public int HostId { get; private set; }
+
public GameStates GameState { get; private set; }
+
public GameOptionsData Options { get; }
+
public IDictionary<object, object> Items { get; }
-
+
public int PlayerCount => _players.Count;
-
+
public IClientPlayer Host => _players[HostId];
-
- private ValueTask BroadcastJoinMessage(IGameMessageWriter message, bool clear, ClientPlayer player)
- {
- Message01JoinGame.SerializeJoin(message, clear, Code, player.Client.Id, HostId);
-
- return message.SendToAllExceptAsync(LimboStates.NotLimbo, player.Client.Id);
- }
public IEnumerable<IClientPlayer> Players => _players.Select(p => p.Value);
public IGameMessageWriter CreateMessage(MessageType type)
{
- return matchmaker.CreateGameMessageWriter(this, type);
+ return _matchmaker.CreateGameMessageWriter(this, type);
}
public bool TryGetPlayer(int id, out IClientPlayer player)
player = default;
return false;
}
+
+ public ValueTask EndAsync()
+ {
+ return _gameManager.RemoveAsync(Code);
+ }
+
+ private ValueTask BroadcastJoinMessage(IGameMessageWriter message, bool clear, IClientPlayer player)
+ {
+ Message01JoinGame.SerializeJoin(message, clear, Code, player.Client.Id, HostId);
+
+ return message.SendToAllExceptAsync(player.Client.Id);
+ }
}
}
\ No newline at end of file
using System;
using Impostor.Server.Data;
+using Impostor.Server.Events;
+using Impostor.Server.Events.Managers;
using Impostor.Server.Hazel;
using Impostor.Server.Net;
using Impostor.Server.Net.Factories;
Log.CloseAndFlush();
}
}
-
+
private static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
#if DEBUG
var redirector = host.Configuration
.GetSection(ServerRedirectorConfig.Section)
.Get<ServerRedirectorConfig>() ?? new ServerRedirectorConfig();
-
+
services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
}
else if (!string.IsNullOrEmpty(redirector.Locator.UdpMasterEndpoint))
{
- services.AddSingleton<INodeLocator, NodeLocatorUDP>();
+ services.AddSingleton<INodeLocator, NodeLocatorUdp>();
if (redirector.Master)
{
- services.AddHostedService<NodeLocatorUDPService>();
+ services.AddHostedService<NodeLocatorUdpService>();
}
}
else
{
throw new Exception("Missing a valid NodeLocator config.");
}
-
+
// Use the configuration as source for the list of nodes to provide
// when creating a game.
services.AddSingleton<INodeProvider, NodeProviderConfig>();
// So we provide one that ignores all calls.
services.AddSingleton<INodeLocator, NodeLocatorNoOp>();
}
-
+
services.AddSingleton<IClientManager, ClientManager>();
-
+
if (redirector.Enabled && redirector.Master)
{
services.AddSingleton<IClientFactory, ClientFactory<ClientRedirector>>();
+
// For a master server, we don't need a GameManager.
}
else
{
services.AddSingleton<IClientFactory, ClientFactory<Client>>();
- services.AddSingleton<GameManager>();
+ services.AddSingleton<IGameManager, GameManager>();
}
+ services.AddSingleton<IEventManager, EventManager>();
services.UseHazelMatchmaking();
services.AddHostedService<MatchmakerService>();
})
--- /dev/null
+<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0">
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
+ <Rule Id="SA1200" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules">
+ <Rule Id="SA1600" Action="None" />
+ <Rule Id="SA1601" Action="None" />
+ <Rule Id="SA1615" Action="None" />
+ <Rule Id="SA1633" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules">
+ <Rule Id="SA1101" Action="None" />
+ </Rules>
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
+ <Rule Id="SA1309" Action="None" />
+ </Rules>
+</RuleSet>
\ No newline at end of file
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}") = "Imposter.Reactive", "Imposter.Reactive\Imposter.Reactive.csproj", "{7D0541DF-5BD8-4175-BAF1-14278B7894F1}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Server.Hazel", "Impostor.Server.Hazel\Impostor.Server.Hazel.csproj", "{C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}"
EndProject
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
- {7D0541DF-5BD8-4175-BAF1-14278B7894F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7D0541DF-5BD8-4175-BAF1-14278B7894F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7D0541DF-5BD8-4175-BAF1-14278B7894F1}.Debug|x86.ActiveCfg = Debug|Any CPU
- {7D0541DF-5BD8-4175-BAF1-14278B7894F1}.Debug|x86.Build.0 = Debug|Any CPU
- {7D0541DF-5BD8-4175-BAF1-14278B7894F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7D0541DF-5BD8-4175-BAF1-14278B7894F1}.Release|Any CPU.Build.0 = Release|Any CPU
- {7D0541DF-5BD8-4175-BAF1-14278B7894F1}.Release|x86.ActiveCfg = Release|Any CPU
- {7D0541DF-5BD8-4175-BAF1-14278B7894F1}.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
-Subproject commit 0c61aa426a2e47be9ac82c601c2f19b9345992a1
+Subproject commit 7a7caf89e0c49ed59b3442987089a03df7de6c7a