From 21e7aca3531eabf35d71376f1d220a5df6ace749 Mon Sep 17 00:00:00 2001 From: Gerard Smit Date: Sun, 11 Oct 2020 00:45:38 +0200 Subject: [PATCH] More abstractions, event manager and docs --- .../AsyncObservableExtensions.cs | 108 --------- src/Imposter.Reactive/IAsyncObservable.cs | 13 - src/Imposter.Reactive/IAsyncObserver.cs | 15 -- .../Imposter.Reactive.csproj | 8 - .../Reactive/AsyncObservable.cs | 26 -- .../Reactive/AsyncObservableBase.cs | 210 ---------------- .../Reactive/AsyncObserver.cs | 28 --- .../Reactive/AsyncObserverBase.cs | 82 ------- .../Reactive/Disposables/AsyncDisposable.cs | 36 --- .../Subjects/ConcurrentSimpleAsyncSubject.cs | 19 -- .../Reactive/Subjects/IAsyncSubject.cs | 10 - .../Subjects/SequentialSimpleAsyncSubject.cs | 36 --- .../Reactive/Subjects/SimpleAsyncSubject.cs | 132 ---------- .../Attributes/EventListenerAttribute.cs | 40 +++ .../Events/EventPriority.cs | 12 + .../Events/Game/GameCreatedEvent.cs | 20 ++ .../Events/Game/IGameEvent.cs | 7 + src/Impostor.Server.Api/Events/IEvent.cs | 6 + .../Events/IEventCancelable.cs | 10 + .../Events/IEventListener.cs | 6 + .../Events/Managers/IEventManager.cs | 24 ++ .../ClientVersionUnsupportedException.cs | 13 - .../Extensions/GameManagerExtensions.cs | 13 + .../{ => Games}/GameCode.cs | 43 ++-- .../Games/GameJoinError.cs | 48 ++++ .../Games/GameJoinResult.cs | 47 ++++ src/Impostor.Server.Api/Games/IGame.cs | 60 +++++ .../Games/Managers/IGameManager.cs | 20 ++ src/Impostor.Server.Api/IGame.cs | 22 -- .../Impostor.Server.Api.csproj | 9 +- .../Impostor.Server.Api.csproj.DotSettings | 4 + .../Extensions/GameMessageWriterExtensions.cs | 27 ++- .../Net/Factories/IClientFactory.cs | 7 +- src/Impostor.Server.Api/Net/IClient.cs | 62 ++++- src/Impostor.Server.Api/Net/IClientPlayer.cs | 26 +- src/Impostor.Server.Api/Net/IConnection.cs | 36 ++- .../{ => Net}/LimboStates.cs | 0 .../Net/Manager/IClientManager.cs | 4 +- .../Net/Manager/IMatchmaker.cs | 18 ++ .../Net/Messages/IConnectionMessageWriter.cs | 12 + .../Net/Messages/IGameMessageWriter.cs | 18 +- .../Net/Messages/IMessage.cs | 2 +- .../Net/Messages/IMessageReader.cs | 22 +- .../Net/Messages/IMessageWriter.cs | 80 +++++- .../Net/Messages/MessageType.cs | 23 +- src/Impostor.Server.Api/Plugins/IPlugin.cs | 14 ++ src/Impostor.Server.Api/Plugins/PluginBase.cs | 22 ++ src/Impostor.Server.Api/ProjectRules.ruleset | 11 + src/Impostor.Server.Hazel/HazelConnection.cs | 36 ++- src/Impostor.Server.Hazel/HazelMatchmaker.cs | 21 +- .../Messages/HazelConnectionMessageWriter.cs | 11 +- .../Messages/HazelGameMessageWriter.cs | 17 +- .../Messages/HazelMessageWriter.cs | 8 +- src/Impostor.Server/Data/ServerConfig.cs | 7 +- .../Data/ServerRedirectorConfig.cs | 4 + .../Data/ServerRedirectorNode.cs | 1 + src/Impostor.Server/Events/EventHandler.cs | 21 ++ src/Impostor.Server/Events/EventManager.cs | 71 ++++++ .../Events/RegisteredEventListener.cs | 175 +++++++++++++ .../Exceptions/AmongUsException.cs | 9 +- .../Extensions/TypeExtensions.cs | 67 +++++ src/Impostor.Server/Impostor.Server.csproj | 11 +- .../Impostor.Server.csproj.DotSettings | 2 + src/Impostor.Server/Net/Client.cs | 229 +++++++++++------- src/Impostor.Server/Net/ClientBase.cs | 25 +- .../Net/Factories/ClientFactory.cs | 38 +-- .../Net/Manager/ClientManager.cs | 54 ++++- .../Net/Manager/GameManager.cs | 73 +++--- .../Net/Messages/Message01JoinGame.cs | 10 +- .../Net/Messages/Message04RemovePlayer.cs | 4 +- .../Net/Messages/Message07JoinedGame.cs | 4 +- .../Net/Messages/Message10AlterGame.cs | 2 +- .../Net/Messages/Message11KickPlayer.cs | 2 +- .../Net/Messages/Message12WaitForHost.cs | 2 +- .../Net/Messages/Message13Redirect.cs | 4 +- .../Net/Messages/Message16GetGameListV2.cs | 10 +- .../Net/Redirector/ClientRedirector.cs | 52 ++-- .../Net/Redirector/INodeLocator.cs | 2 + .../Net/Redirector/NodeLocatorRedis.cs | 6 +- .../Net/Redirector/NodeLocatorUDP.cs | 54 +++-- .../Net/Redirector/NodeLocatorUDPService.cs | 26 +- .../Net/Redirector/NodeProviderConfig.cs | 4 +- .../Net/State/ClientPlayer.Events.cs | 33 --- src/Impostor.Server/Net/State/ClientPlayer.cs | 43 ++-- .../Net/State/Game.Incoming.cs | 151 ++++++------ .../Net/State/Game.Outgoing.cs | 10 +- src/Impostor.Server/Net/State/Game.State.cs | 45 ++-- src/Impostor.Server/Net/State/Game.cs | 52 ++-- src/Impostor.Server/Program.cs | 20 +- src/Impostor.Server/ProjectRules.ruleset | 17 ++ src/Impostor.sln | 10 - submodules/Hazel-Networking | 2 +- 92 files changed, 1629 insertions(+), 1327 deletions(-) delete mode 100644 src/Imposter.Reactive/AsyncObservableExtensions.cs delete mode 100644 src/Imposter.Reactive/IAsyncObservable.cs delete mode 100644 src/Imposter.Reactive/IAsyncObserver.cs delete mode 100644 src/Imposter.Reactive/Imposter.Reactive.csproj delete mode 100644 src/Imposter.Reactive/Reactive/AsyncObservable.cs delete mode 100644 src/Imposter.Reactive/Reactive/AsyncObservableBase.cs delete mode 100644 src/Imposter.Reactive/Reactive/AsyncObserver.cs delete mode 100644 src/Imposter.Reactive/Reactive/AsyncObserverBase.cs delete mode 100644 src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs delete mode 100644 src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs delete mode 100644 src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs delete mode 100644 src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs delete mode 100644 src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs create mode 100644 src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs create mode 100644 src/Impostor.Server.Api/Events/EventPriority.cs create mode 100644 src/Impostor.Server.Api/Events/Game/GameCreatedEvent.cs create mode 100644 src/Impostor.Server.Api/Events/Game/IGameEvent.cs create mode 100644 src/Impostor.Server.Api/Events/IEvent.cs create mode 100644 src/Impostor.Server.Api/Events/IEventCancelable.cs create mode 100644 src/Impostor.Server.Api/Events/IEventListener.cs create mode 100644 src/Impostor.Server.Api/Events/Managers/IEventManager.cs delete mode 100644 src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs create mode 100644 src/Impostor.Server.Api/Extensions/GameManagerExtensions.cs rename src/Impostor.Server.Api/{ => Games}/GameCode.cs (95%) create mode 100644 src/Impostor.Server.Api/Games/GameJoinError.cs create mode 100644 src/Impostor.Server.Api/Games/GameJoinResult.cs create mode 100644 src/Impostor.Server.Api/Games/IGame.cs create mode 100644 src/Impostor.Server.Api/Games/Managers/IGameManager.cs delete mode 100644 src/Impostor.Server.Api/IGame.cs rename src/Impostor.Server.Api/{ => Net}/LimboStates.cs (100%) create mode 100644 src/Impostor.Server.Api/Plugins/IPlugin.cs create mode 100644 src/Impostor.Server.Api/Plugins/PluginBase.cs create mode 100644 src/Impostor.Server.Api/ProjectRules.ruleset create mode 100644 src/Impostor.Server/Events/EventHandler.cs create mode 100644 src/Impostor.Server/Events/EventManager.cs create mode 100644 src/Impostor.Server/Events/RegisteredEventListener.cs create mode 100644 src/Impostor.Server/Extensions/TypeExtensions.cs create mode 100644 src/Impostor.Server/Impostor.Server.csproj.DotSettings delete mode 100644 src/Impostor.Server/Net/State/ClientPlayer.Events.cs create mode 100644 src/Impostor.Server/ProjectRules.ruleset diff --git a/src/Imposter.Reactive/AsyncObservableExtensions.cs b/src/Imposter.Reactive/AsyncObservableExtensions.cs deleted file mode 100644 index 56f5043..0000000 --- a/src/Imposter.Reactive/AsyncObservableExtensions.cs +++ /dev/null @@ -1,108 +0,0 @@ -// 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 SubscribeAsync(this IAsyncObservable source, Func onNextAsync) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - if (onNextAsync == null) - throw new ArgumentNullException(nameof(onNextAsync)); - - return source.SubscribeAsync(new AsyncObserver(onNextAsync, ex => new ValueTask(Task.FromException(ex)), () => default)); - } - - public static ValueTask SubscribeAsync(this IAsyncObservable source, Func onNextAsync, Func 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(onNextAsync, onErrorAsync, () => default)); - } - - public static ValueTask SubscribeAsync(this IAsyncObservable source, Func onNextAsync, Func 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(onNextAsync, ex => new ValueTask(Task.FromException(ex)), onCompletedAsync)); - } - - public static ValueTask SubscribeAsync(this IAsyncObservable source, Func onNextAsync, Func onErrorAsync, Func 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(onNextAsync, onErrorAsync, onCompletedAsync)); - } - - public static ValueTask SubscribeAsync(this IAsyncObservable source, Action onNext) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - if (onNext == null) - throw new ArgumentNullException(nameof(onNext)); - - return source.SubscribeAsync(new AsyncObserver(x => { onNext(x); return default; }, ex => new ValueTask(Task.FromException(ex)), () => default)); - } - - public static ValueTask SubscribeAsync(this IAsyncObservable source, Action onNext, Action 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(x => { onNext(x); return default; }, ex => { onError(ex); return default; }, () => default)); - } - - public static ValueTask SubscribeAsync(this IAsyncObservable source, Action 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(x => { onNext(x); return default; }, ex => new ValueTask(Task.FromException(ex)), () => { onCompleted(); return default; })); - } - - public static ValueTask SubscribeAsync(this IAsyncObservable source, Action onNext, Action 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(x => { onNext(x); return default; }, ex => { onError(ex); return default; }, () => { onCompleted(); return default; })); - } - } -} \ No newline at end of file diff --git a/src/Imposter.Reactive/IAsyncObservable.cs b/src/Imposter.Reactive/IAsyncObservable.cs deleted file mode 100644 index e0dcce3..0000000 --- a/src/Imposter.Reactive/IAsyncObservable.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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 - { - ValueTask SubscribeAsync(IAsyncObserver observer); - } -} \ No newline at end of file diff --git a/src/Imposter.Reactive/IAsyncObserver.cs b/src/Imposter.Reactive/IAsyncObserver.cs deleted file mode 100644 index eaaf3d5..0000000 --- a/src/Imposter.Reactive/IAsyncObserver.cs +++ /dev/null @@ -1,15 +0,0 @@ -// 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 - { - ValueTask OnNextAsync(T value); - ValueTask OnErrorAsync(Exception error); - ValueTask OnCompletedAsync(); - } -} \ No newline at end of file diff --git a/src/Imposter.Reactive/Imposter.Reactive.csproj b/src/Imposter.Reactive/Imposter.Reactive.csproj deleted file mode 100644 index 63eb9ec..0000000 --- a/src/Imposter.Reactive/Imposter.Reactive.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - net5.0 - System - - - diff --git a/src/Imposter.Reactive/Reactive/AsyncObservable.cs b/src/Imposter.Reactive/Reactive/AsyncObservable.cs deleted file mode 100644 index f6b8045..0000000 --- a/src/Imposter.Reactive/Reactive/AsyncObservable.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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 : AsyncObservableBase - { - private readonly Func, ValueTask> _subscribeAsync; - - public AsyncObservable(Func, ValueTask> subscribeAsync) - { - _subscribeAsync = subscribeAsync ?? throw new ArgumentNullException(nameof(subscribeAsync)); - } - - protected override ValueTask SubscribeAsyncCore(IAsyncObserver observer) - { - if (observer == null) - throw new ArgumentNullException(nameof(observer)); - - return _subscribeAsync(observer); - } - } -} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/AsyncObservableBase.cs b/src/Imposter.Reactive/Reactive/AsyncObservableBase.cs deleted file mode 100644 index a4f86ff..0000000 --- a/src/Imposter.Reactive/Reactive/AsyncObservableBase.cs +++ /dev/null @@ -1,210 +0,0 @@ -// 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 : IAsyncObservable - { - public async ValueTask SubscribeAsync(IAsyncObserver 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 SubscribeAsyncCore(IAsyncObserver observer); - - private sealed class AutoDetachAsyncObserver : AsyncObserverBase, IAsyncDisposable - { - private readonly IAsyncObserver _observer; - private readonly object _gate = new object(); - - private IAsyncDisposable _subscription; - private ValueTask _task; - private bool _disposing; - - public AutoDetachAsyncObserver(IAsyncObserver 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 diff --git a/src/Imposter.Reactive/Reactive/AsyncObserver.cs b/src/Imposter.Reactive/Reactive/AsyncObserver.cs deleted file mode 100644 index bfe0066..0000000 --- a/src/Imposter.Reactive/Reactive/AsyncObserver.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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 : AsyncObserverBase - { - private readonly Func _onNextAsync; - private readonly Func _onErrorAsync; - private readonly Func _onCompletedAsync; - - public AsyncObserver(Func onNextAsync, Func onErrorAsync, Func 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 diff --git a/src/Imposter.Reactive/Reactive/AsyncObserverBase.cs b/src/Imposter.Reactive/Reactive/AsyncObserverBase.cs deleted file mode 100644 index 75564e6..0000000 --- a/src/Imposter.Reactive/Reactive/AsyncObserverBase.cs +++ /dev/null @@ -1,82 +0,0 @@ -// 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 : IAsyncObserver - { - 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 diff --git a/src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs b/src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs deleted file mode 100644 index b5aaaf2..0000000 --- a/src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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 dispose) - { - if (dispose == null) - throw new ArgumentNullException(nameof(dispose)); - - return new AnonymousAsyncDisposable(dispose); - } - - private sealed class AnonymousAsyncDisposable : IAsyncDisposable - { - private Func _dispose; - - public AnonymousAsyncDisposable(Func 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 diff --git a/src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs deleted file mode 100644 index b2f3dae..0000000 --- a/src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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 : SimpleAsyncSubject - { - protected override ValueTask OnCompletedAsyncCore(IEnumerable> observers) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnCompletedAsync().AsTask()))); - - protected override ValueTask OnErrorAsyncCore(IEnumerable> observers, Exception error) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnErrorAsync(error).AsTask()))); - - protected override ValueTask OnNextAsyncCore(IEnumerable> observers, T value) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnNextAsync(value).AsTask()))); - } -} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs deleted file mode 100644 index 8a3342e..0000000 --- a/src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace System.Reactive.Subjects -{ - public interface IAsyncSubject : IAsyncObservable, IAsyncObserver - { - } - - public interface IAsyncSubject : IAsyncSubject - { - } -} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs deleted file mode 100644 index 28a64fd..0000000 --- a/src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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 : SimpleAsyncSubject - { - protected override async ValueTask OnCompletedAsyncCore(IEnumerable> observers) - { - foreach (var observer in observers) - { - await observer.OnCompletedAsync().ConfigureAwait(false); - } - } - - protected override async ValueTask OnErrorAsyncCore(IEnumerable> observers, Exception error) - { - foreach (var observer in observers) - { - await observer.OnErrorAsync(error).ConfigureAwait(false); - } - } - - protected override async ValueTask OnNextAsyncCore(IEnumerable> observers, T value) - { - foreach (var observer in observers) - { - await observer.OnNextAsync(value).ConfigureAwait(false); - } - } - } -} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs deleted file mode 100644 index b922056..0000000 --- a/src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs +++ /dev/null @@ -1,132 +0,0 @@ -// 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 : IAsyncSubject - { - private readonly object _gate = new object(); - private readonly List> _observers = new List>(); - private bool _done; - private Exception _error; - - public ValueTask OnCompletedAsync() - { - IAsyncObserver[] observers; - - lock (_gate) - { - if (_done || _error != null) - { - return default; - } - - _done = true; - - observers = _observers.ToArray(); - } - - return OnCompletedAsyncCore(observers); - } - - protected abstract ValueTask OnCompletedAsyncCore(IEnumerable> observers); - - public ValueTask OnErrorAsync(Exception error) - { - if (error == null) - throw new ArgumentNullException(nameof(error)); - - IAsyncObserver[] observers; - - lock (_gate) - { - if (_done || _error != null) - { - return default; - } - - _error = error; - - observers = _observers.ToArray(); - } - - return OnErrorAsyncCore(observers, error); - } - - protected abstract ValueTask OnErrorAsyncCore(IEnumerable> observers, Exception error); - - public ValueTask OnNextAsync(T value) - { - IAsyncObserver[] observers; - - lock (_gate) - { - if (_done || _error != null) - { - return default; - } - - observers = _observers.ToArray(); - } - - return OnNextAsyncCore(observers, value); - } - - protected abstract ValueTask OnNextAsyncCore(IEnumerable> observers, T value); - - public async ValueTask SubscribeAsync(IAsyncObserver 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 diff --git a/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs b/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs new file mode 100644 index 0000000..bea45c1 --- /dev/null +++ b/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs @@ -0,0 +1,40 @@ +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 }; + } + + /// + /// The priority of the event listener. + /// + public EventPriority Priority { get; set; } + + /// + /// The events that the listener is listening to. + /// + public Type[] Events { get; set; } + + /// + /// If set to true, the listener will be called regardless of the . + /// + public bool IgnoreCancelled { get; set; } + + /// + /// The order of the priority. + /// + public int PriorityOrder { get; set; } = 100; + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/EventPriority.cs b/src/Impostor.Server.Api/Events/EventPriority.cs new file mode 100644 index 0000000..dbf506e --- /dev/null +++ b/src/Impostor.Server.Api/Events/EventPriority.cs @@ -0,0 +1,12 @@ +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 diff --git a/src/Impostor.Server.Api/Events/Game/GameCreatedEvent.cs b/src/Impostor.Server.Api/Events/Game/GameCreatedEvent.cs new file mode 100644 index 0000000..27c8b69 --- /dev/null +++ b/src/Impostor.Server.Api/Events/Game/GameCreatedEvent.cs @@ -0,0 +1,20 @@ +namespace Impostor.Server.Events +{ + /// + /// Called whenever a new is created. + /// + public sealed class GameCreatedEvent : IGameEvent + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the game. + public GameCreatedEvent(IGame game) + { + Game = game; + } + + /// + public IGame Game { get; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/Game/IGameEvent.cs b/src/Impostor.Server.Api/Events/Game/IGameEvent.cs new file mode 100644 index 0000000..a3771bc --- /dev/null +++ b/src/Impostor.Server.Api/Events/Game/IGameEvent.cs @@ -0,0 +1,7 @@ +namespace Impostor.Server.Events +{ + public interface IGameEvent : IEvent + { + IGame Game { get; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/IEvent.cs b/src/Impostor.Server.Api/Events/IEvent.cs new file mode 100644 index 0000000..e9b3505 --- /dev/null +++ b/src/Impostor.Server.Api/Events/IEvent.cs @@ -0,0 +1,6 @@ +namespace Impostor.Server.Events +{ + public interface IEvent + { + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/IEventCancelable.cs b/src/Impostor.Server.Api/Events/IEventCancelable.cs new file mode 100644 index 0000000..75e2364 --- /dev/null +++ b/src/Impostor.Server.Api/Events/IEventCancelable.cs @@ -0,0 +1,10 @@ +namespace Impostor.Server.Events +{ + public interface IEventCancelable : IEvent + { + /// + /// True if the event was cancelled. + /// + bool IsCancelled { get; set; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/IEventListener.cs b/src/Impostor.Server.Api/Events/IEventListener.cs new file mode 100644 index 0000000..3e08c36 --- /dev/null +++ b/src/Impostor.Server.Api/Events/IEventListener.cs @@ -0,0 +1,6 @@ +namespace Impostor.Server.Events +{ + public interface IEventListener + { + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Events/Managers/IEventManager.cs b/src/Impostor.Server.Api/Events/Managers/IEventManager.cs new file mode 100644 index 0000000..488e38c --- /dev/null +++ b/src/Impostor.Server.Api/Events/Managers/IEventManager.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Impostor.Server.Events.Managers +{ + public interface IEventManager + { + /// + /// Returns true if an event with the type is registered. + /// + /// True if the is registered. + /// Type of the event. + bool IsRegistered() + where TEvent : IEvent; + + /// + /// Call all the event listeners for the type . + /// + /// The event argument. + /// Type of the event. + /// A representing the asynchronous operation. + ValueTask CallAsync(TEvent @event) + where TEvent : IEvent; + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs b/src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs deleted file mode 100644 index 73e5407..0000000 --- a/src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 diff --git a/src/Impostor.Server.Api/Extensions/GameManagerExtensions.cs b/src/Impostor.Server.Api/Extensions/GameManagerExtensions.cs new file mode 100644 index 0000000..9d41e28 --- /dev/null +++ b/src/Impostor.Server.Api/Extensions/GameManagerExtensions.cs @@ -0,0 +1,13 @@ +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 diff --git a/src/Impostor.Server.Api/GameCode.cs b/src/Impostor.Server.Api/Games/GameCode.cs similarity index 95% rename from src/Impostor.Server.Api/GameCode.cs rename to src/Impostor.Server.Api/Games/GameCode.cs index 6178732..b97b1e2 100644 --- a/src/Impostor.Server.Api/GameCode.cs +++ b/src/Impostor.Server.Api/Games/GameCode.cs @@ -16,9 +16,9 @@ namespace Impostor.Server Value = GameCodeParser.GameNameToInt(code); Code = code; } - + public string Code { get; } - + public int Value { get; } public static implicit operator string(GameCode code) => code.Code; @@ -29,43 +29,46 @@ namespace Impostor.Server public static implicit operator GameCode(int value) => From(value); - public bool Equals(GameCode other) + public static bool operator ==(GameCode left, GameCode right) { - return Code == other.Code && Value == other.Value; + return left.Equals(right); } - public override bool Equals(object? obj) + public static bool operator !=(GameCode left, GameCode right) { - return obj is GameCode other && Equals(other); + return !left.Equals(right); } - public override int GetHashCode() + public static GameCode Create() { - return HashCode.Combine(Code, Value); + return new GameCode(GameCodeParser.GenerateCode(6)); } - public static bool operator ==(GameCode left, GameCode right) + public static GameCode From(int value) => new GameCode(value); + + public static GameCode From(string value) => new GameCode(value); + + /// + public bool Equals(GameCode other) { - return left.Equals(right); + return Code == other.Code && Value == other.Value; } - public static bool operator !=(GameCode left, GameCode right) + /// + public override bool Equals(object? obj) { - return !left.Equals(right); + return obj is GameCode other && Equals(other); } - public override string ToString() + /// + public override int GetHashCode() { - return Code; + return HashCode.Combine(Code, Value); } - public static GameCode From(int value) => new GameCode(value); - - public static GameCode From(string value) => new GameCode(value); - - public static GameCode Create() + public override string ToString() { - return new GameCode(GameCodeParser.GenerateCode(6)); + return Code; } } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Games/GameJoinError.cs b/src/Impostor.Server.Api/Games/GameJoinError.cs new file mode 100644 index 0000000..c30c27d --- /dev/null +++ b/src/Impostor.Server.Api/Games/GameJoinError.cs @@ -0,0 +1,48 @@ +namespace Impostor.Server.Net +{ + public enum GameJoinError + { + /// + /// No error occured while joining the game. + /// + None, + + /// + /// The client is not registered in the client manager. + /// + InvalidClient, + + /// + /// The client has been banned from the game. + /// + Banned, + + /// + /// The game is full. + /// + GameFull, + + /// + /// The limbo state of the player is incorrect. + /// + InvalidLimbo, + + /// + /// The game is already started. + /// + GameStarted, + + /// + /// The game has been destroyed. + /// + GameDestroyed, + + /// + /// Custom error by a plugin. + /// + /// + /// A custom message can be set in . + /// + Custom, + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Games/GameJoinResult.cs b/src/Impostor.Server.Api/Games/GameJoinResult.cs new file mode 100644 index 0000000..96e39e3 --- /dev/null +++ b/src/Impostor.Server.Api/Games/GameJoinResult.cs @@ -0,0 +1,47 @@ +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 diff --git a/src/Impostor.Server.Api/Games/IGame.cs b/src/Impostor.Server.Api/Games/IGame.cs new file mode 100644 index 0000000..e130342 --- /dev/null +++ b/src/Impostor.Server.Api/Games/IGame.cs @@ -0,0 +1,60 @@ +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 Players { get; } + + IPEndPoint PublicIp { get; } + + int PlayerCount { get; } + + IClientPlayer Host { get; } + + bool IsPublic { get; } + + IDictionary Items { get; } + + int HostId { get; } + + IGameMessageWriter CreateMessage(MessageType type); + + bool TryGetPlayer(int id, [NotNullWhen(true)] out IClientPlayer player); + + /// + /// Register a new client to the game. + /// + /// Client to register. + /// Join result. + ValueTask AddClientAsync(IClient client); + + /// + /// Kicks all the players from the game to end the game. + /// + /// A representing the asynchronous operation. + ValueTask EndAsync(); + + ValueTask HandleStartGame(IMessageReader reader); + + ValueTask HandleEndGame(IMessageReader reader); + + ValueTask HandleKickPlayer(int playerId, bool isBan); + + ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason); + + ValueTask HandleAlterGame(IMessageReader message, IClientPlayer sender, bool isPublic); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Games/Managers/IGameManager.cs b/src/Impostor.Server.Api/Games/Managers/IGameManager.cs new file mode 100644 index 0000000..4f8d33e --- /dev/null +++ b/src/Impostor.Server.Api/Games/Managers/IGameManager.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Shared.Innersloth; +using Impostor.Shared.Innersloth.Data; + +namespace Impostor.Server +{ + public interface IGameManager + { + IEnumerable Games { get; } + + ValueTask CreateAsync(GameOptionsData options); + + IGame? Find(GameCode code); + + IEnumerable FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10); + + ValueTask RemoveAsync(GameCode code); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/IGame.cs b/src/Impostor.Server.Api/IGame.cs deleted file mode 100644 index b37c5ea..0000000 --- a/src/Impostor.Server.Api/IGame.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Impostor.Server.Net; - -namespace Impostor.Server -{ - public interface IGame - { - GameCode Code { get; } - - IEnumerable Players { get; } - - IClientPlayer Host { get; } - - bool IsPublic { get; } - IDictionary Items { get; } - - IGameMessageWriter CreateMessage(MessageType type); - - bool TryGetPlayer(int id, [NotNullWhen(true)] out IClientPlayer player); - } -} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Impostor.Server.Api.csproj b/src/Impostor.Server.Api/Impostor.Server.Api.csproj index 95ebef0..8a568af 100644 --- a/src/Impostor.Server.Api/Impostor.Server.Api.csproj +++ b/src/Impostor.Server.Api/Impostor.Server.Api.csproj @@ -4,17 +4,20 @@ net5.0 Impostor.Server enable + ProjectRules.ruleset - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + \ No newline at end of file diff --git a/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings b/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings index ec2baa7..c8f8b80 100644 --- a/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings +++ b/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings @@ -1,3 +1,7 @@  + True + True + True True + True True \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Extensions/GameMessageWriterExtensions.cs b/src/Impostor.Server.Api/Net/Extensions/GameMessageWriterExtensions.cs index 52808c9..5bb1d5a 100644 --- a/src/Impostor.Server.Api/Net/Extensions/GameMessageWriterExtensions.cs +++ b/src/Impostor.Server.Api/Net/Extensions/GameMessageWriterExtensions.cs @@ -9,22 +9,33 @@ namespace Impostor.Server.Net 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 diff --git a/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs b/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs index 77bc72e..ac0faec 100644 --- a/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs +++ b/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs @@ -4,17 +4,12 @@ namespace Impostor.Server.Net.Factories { public interface IClientFactory { - /// - /// Get the next ID for . - /// - int NextId(); - /// /// Creates a client for the Hazel . /// /// Hazel connection. /// /// - ValueTask CreateAsync(IConnection connection, string name, int clientVersion); + IClient Create(IConnection connection, string name, int clientVersion); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/IClient.cs b/src/Impostor.Server.Api/Net/IClient.cs index 3228e0b..e83ab90 100644 --- a/src/Impostor.Server.Api/Net/IClient.cs +++ b/src/Impostor.Server.Api/Net/IClient.cs @@ -1,15 +1,65 @@ using System.Collections.Generic; +using System.Threading.Tasks; namespace Impostor.Server.Net { + /// + /// Represents a connected game client. + /// public interface IClient { - int Id { get; } - + /// + /// Gets or sets the unique ID of the client. + /// + /// + /// This ID is generated when the client is registered in the client manager and should not be used + /// to store persisted data. + /// + int Id { get; set; } + + /// + /// Gets the name that was provided by the player in the client. + /// + /// + /// The name is provided by the player and should not be used to store persisted data. + /// string Name { get; } - - IConnection Connection { get; } - - IDictionary Items { get; } + + /// + /// Gets the connection of the client. + /// + /// + /// Null when the client was not registered by the matchmaker. + /// + IConnection? Connection { get; } + + /// + /// Gets a value indicating whether the client is a bot. + /// + bool IsBot { get; } + + /// + /// Gets a key/value collection that can be used to share data between messages. + /// + /// + /// + /// The stored data will not be saved. + /// After the connection has been closed all data will be lost. + /// + /// + /// Note that the values will not be disposed after the connection has been closed. + /// This has to be implemented by the plugin. + /// + /// + IDictionary Items { get; } + + /// + /// Gets or sets the current game data of the . + /// + IClientPlayer? Player { get; set; } + + ValueTask HandleMessageAsync(IMessage message); + + ValueTask HandleDisconnectAsync(); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/IClientPlayer.cs b/src/Impostor.Server.Api/Net/IClientPlayer.cs index 4034875..0805e74 100644 --- a/src/Impostor.Server.Api/Net/IClientPlayer.cs +++ b/src/Impostor.Server.Api/Net/IClientPlayer.cs @@ -1,13 +1,31 @@ -using Impostor.Shared.Innersloth.Data; +using System.Threading.Tasks; +using Impostor.Server.Net.Manager; +using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net { + /// + /// Represents a player in . + /// public interface IClientPlayer { + /// + /// Gets the client that belongs to the player. + /// IClient Client { get; } - + + /// + /// Gets the game where the belongs to. + /// IGame Game { get; } - - LimboStates Limbo { get; } + + /// + /// Gets or sets the current limbo state of the player. + /// + LimboStates Limbo { get; set; } + + ValueTask KickAsync(); + + ValueTask BanAsync(); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/IConnection.cs b/src/Impostor.Server.Api/Net/IConnection.cs index 9aa3203..a1d1269 100644 --- a/src/Impostor.Server.Api/Net/IConnection.cs +++ b/src/Impostor.Server.Api/Net/IConnection.cs @@ -1,16 +1,44 @@ using System; using System.Net; +using System.Threading.Tasks; namespace Impostor.Server.Net { + /// + /// Represents the connection of the client. + /// public interface IConnection { - IAsyncObservable MessageReceived { get; } - + /// + /// Gets the IP endpoint of the client. + /// IPEndPoint EndPoint { get; } - + + /// + /// Gets a value indicating whether the client is connected to the server. + /// bool IsConnected { get; } - IConnectionMessageWriter CreateMessage(MessageType type); + /// + /// Gets or sets the client of the connection. + /// + IClient? Client { get; set; } + + /// + /// Create a message writer that can be send to the connection. + /// + /// + /// Be aware when implementing a custom connection handler that this method is not called when a message + /// is being send in . + /// + /// Type of the message. + /// Message writer for the current connection. + IConnectionMessageWriter CreateMessage(MessageType messageType); + + /// + /// Start listening to the client. + /// + /// A representing the asynchronous operation. + ValueTask ListenAsync(); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/LimboStates.cs b/src/Impostor.Server.Api/Net/LimboStates.cs similarity index 100% rename from src/Impostor.Server.Api/LimboStates.cs rename to src/Impostor.Server.Api/Net/LimboStates.cs diff --git a/src/Impostor.Server.Api/Net/Manager/IClientManager.cs b/src/Impostor.Server.Api/Net/Manager/IClientManager.cs index 82bdbe0..46da848 100644 --- a/src/Impostor.Server.Api/Net/Manager/IClientManager.cs +++ b/src/Impostor.Server.Api/Net/Manager/IClientManager.cs @@ -5,9 +5,11 @@ namespace Impostor.Server.Net.Manager public interface IClientManager { ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion); - + void Register(IClient client); void Remove(IClient client); + + bool Validate(IClient client); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs b/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs index 561c426..244d8ec 100644 --- a/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs +++ b/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs @@ -3,12 +3,30 @@ using System.Threading.Tasks; namespace Impostor.Server.Net.Manager { + /// + /// Represents the matchmaker which will listen for incoming connections. + /// public interface IMatchmaker { + /// + /// Starts the matchmaker on the given endpoint. + /// + /// Endpoint where the matchmaker should listen to. + /// A representing the asynchronous operation. ValueTask StartAsync(IPEndPoint ipEndPoint); + /// + /// Stop the matchmaker. + /// + /// A representing the asynchronous operation. ValueTask StopAsync(); + /// + /// Create a message writer that can be send to players in the game. + /// + /// The game. + /// Type of the message. + /// Message writer for the given game. IGameMessageWriter CreateGameMessageWriter(IGame game, MessageType messageType); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IConnectionMessageWriter.cs b/src/Impostor.Server.Api/Net/Messages/IConnectionMessageWriter.cs index 0e37c41..24206e6 100644 --- a/src/Impostor.Server.Api/Net/Messages/IConnectionMessageWriter.cs +++ b/src/Impostor.Server.Api/Net/Messages/IConnectionMessageWriter.cs @@ -2,8 +2,20 @@ namespace Impostor.Server.Net { + /// + /// Represents the message writer for . + /// public interface IConnectionMessageWriter : IMessageWriter { + /// + /// Gets the connection where the message writer belongs to. + /// + public IConnection Connection { get; } + + /// + /// Sends the message to the . + /// + /// Task. ValueTask SendAsync(); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IGameMessageWriter.cs b/src/Impostor.Server.Api/Net/Messages/IGameMessageWriter.cs index 2d947e9..4b8267d 100644 --- a/src/Impostor.Server.Api/Net/Messages/IGameMessageWriter.cs +++ b/src/Impostor.Server.Api/Net/Messages/IGameMessageWriter.cs @@ -3,25 +3,31 @@ using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net { + /// + /// Represents the message writer for . + /// public interface IGameMessageWriter : IMessageWriter { /// /// Send the message to all players. /// - /// - ValueTask SendToAllAsync(LimboStates states); + /// Required limbo state of the player. + /// A representing the asynchronous operation. + ValueTask SendToAllAsync(LimboStates states = LimboStates.NotLimbo); /// /// Send the message to all players except one. /// - /// /// The player to exclude from sending the message. - ValueTask SendToAllExceptAsync(LimboStates states, int senderId); - + /// Required limbo state of the player. + /// A representing the asynchronous operation. + ValueTask SendToAllExceptAsync(int senderId, LimboStates states = LimboStates.NotLimbo); + /// /// Send a message to a specific player. /// - /// + /// ID of the client. + /// A representing the asynchronous operation. ValueTask SendToAsync(int id); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IMessage.cs b/src/Impostor.Server.Api/Net/Messages/IMessage.cs index b8364ff..e829d1b 100644 --- a/src/Impostor.Server.Api/Net/Messages/IMessage.cs +++ b/src/Impostor.Server.Api/Net/Messages/IMessage.cs @@ -3,7 +3,7 @@ public interface IMessage { MessageType Type { get; } - + IMessageReader CreateReader(); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IMessageReader.cs b/src/Impostor.Server.Api/Net/Messages/IMessageReader.cs index a7fb66b..c7c288c 100644 --- a/src/Impostor.Server.Api/Net/Messages/IMessageReader.cs +++ b/src/Impostor.Server.Api/Net/Messages/IMessageReader.cs @@ -4,14 +4,26 @@ namespace Impostor.Server.Net { public interface IMessageReader { + /// + /// Gets the current position of the reader. + /// int Position { get; } - + + /// + /// Gets the buffer of the message. + /// ReadOnlyMemory Buffer { get; } - + + /// + /// Gets the tag of the message. + /// byte Tag { get; } - + + /// + /// Gets the length of the buffer. + /// int Length { get; } - + bool ReadBoolean(); sbyte ReadSByte(); @@ -37,7 +49,7 @@ namespace Impostor.Server.Net int ReadPackedInt32(); uint ReadPackedUInt32(); - + void CopyTo(IMessageWriter writer); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IMessageWriter.cs b/src/Impostor.Server.Api/Net/Messages/IMessageWriter.cs index 007baf8..a1f0632 100644 --- a/src/Impostor.Server.Api/Net/Messages/IMessageWriter.cs +++ b/src/Impostor.Server.Api/Net/Messages/IMessageWriter.cs @@ -3,38 +3,104 @@ using System.Net; namespace Impostor.Server.Net { + /// + /// Base message writer. + /// public interface IMessageWriter : IDisposable { + /// + /// Writes a boolean to the message. + /// + /// Value to write. void Write(bool value); + /// + /// Writes a sbyte to the message. + /// + /// Value to write. void Write(sbyte value); + /// + /// Writes a byte to the message. + /// + /// Value to write. void Write(byte value); + /// + /// Writes a short to the message. + /// + /// Value to write. void Write(short value); + /// + /// Writes an ushort to the message. + /// + /// Value to write. void Write(ushort value); + /// + /// Writes an uint to the message. + /// + /// Value to write. void Write(uint value); + /// + /// Writes an int to the message. + /// + /// Value to write. void Write(int value); + /// + /// Writes a float to the message. + /// + /// Value to write. void Write(float value); + /// + /// Writes a string to the message. + /// + /// Value to write. void Write(string value); - - void Write(IPAddress ipAddress); - + + /// + /// Writes a to the message. + /// + /// Value to write. + void Write(IPAddress value); + + /// + /// Writes an packed int to the message. + /// + /// Value to write. void WritePacked(int value); - + + /// + /// Writes raw bytes to the message. + /// + /// Bytes to write. void Write(ReadOnlyMemory data); - + + /// + /// Writes a game code to the message. + /// + /// Value to write. + void Write(GameCode value); + + /// + /// Starts a new message. + /// + /// Message flag header. void StartMessage(byte typeFlag); - void Write(GameCode code); - + /// + /// Mark the end of the message. + /// void EndMessage(); + /// + /// Clear the message writer. + /// + /// New type of the message. void Clear(MessageType type); } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/MessageType.cs b/src/Impostor.Server.Api/Net/Messages/MessageType.cs index 00f030a..2e9dcb2 100644 --- a/src/Impostor.Server.Api/Net/Messages/MessageType.cs +++ b/src/Impostor.Server.Api/Net/Messages/MessageType.cs @@ -1,8 +1,29 @@ namespace Impostor.Server.Net { + /// + /// Specifies how a message should be sent between connections. + /// public enum MessageType { + /// + /// Requests unreliable delivery with no fragmentation. + /// + /// + /// 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. + /// Unreliable, - Reliable + + /// + /// Requests data be sent reliably but with no fragmentation. + /// + /// + /// 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. + /// + Reliable, } } \ No newline at end of file diff --git a/src/Impostor.Server.Api/Plugins/IPlugin.cs b/src/Impostor.Server.Api/Plugins/IPlugin.cs new file mode 100644 index 0000000..129cf15 --- /dev/null +++ b/src/Impostor.Server.Api/Plugins/IPlugin.cs @@ -0,0 +1,14 @@ +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 diff --git a/src/Impostor.Server.Api/Plugins/PluginBase.cs b/src/Impostor.Server.Api/Plugins/PluginBase.cs new file mode 100644 index 0000000..eb09de8 --- /dev/null +++ b/src/Impostor.Server.Api/Plugins/PluginBase.cs @@ -0,0 +1,22 @@ +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 diff --git a/src/Impostor.Server.Api/ProjectRules.ruleset b/src/Impostor.Server.Api/ProjectRules.ruleset new file mode 100644 index 0000000..c2f447c --- /dev/null +++ b/src/Impostor.Server.Api/ProjectRules.ruleset @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/HazelConnection.cs b/src/Impostor.Server.Hazel/HazelConnection.cs index af96605..ce87dac 100644 --- a/src/Impostor.Server.Hazel/HazelConnection.cs +++ b/src/Impostor.Server.Hazel/HazelConnection.cs @@ -1,6 +1,6 @@ using System; +using System.Collections.Concurrent; using System.Net; -using System.Reactive.Subjects; using System.Threading.Tasks; using Hazel; using Impostor.Server.Net; @@ -11,27 +11,31 @@ namespace Impostor.Server.Hazel internal class HazelConnection : IConnection { private readonly ILogger _logger; - private readonly ConcurrentSimpleAsyncSubject _messageReceived = new ConcurrentSimpleAsyncSubject(); + private readonly ConcurrentStack _pendingMessages; public HazelConnection(Connection innerConnection, ILogger logger) { _logger = logger; + _pendingMessages = new ConcurrentStack(); InnerConnection = innerConnection; innerConnection.DataReceived += ConnectionOnDataReceived; innerConnection.Disconnected += ConnectionOnDisconnected; } - - public Connection InnerConnection { get; } - public IAsyncObservable 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) @@ -41,6 +45,12 @@ namespace Impostor.Server.Hazel private async Task HandleData(DataReceivedEventArgs e) { + if (Client == null) + { + _pendingMessages.Push(e); + return; + } + try { while (true) @@ -60,7 +70,7 @@ namespace Impostor.Server.Hazel using var message = new HazelMessage(reader, type); - await _messageReceived.OnNextAsync(message); + await Client.HandleMessageAsync(message); } } catch (Exception ex) @@ -73,9 +83,17 @@ namespace Impostor.Server.Hazel } } - 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 diff --git a/src/Impostor.Server.Hazel/HazelMatchmaker.cs b/src/Impostor.Server.Hazel/HazelMatchmaker.cs index bfdfaee..439a773 100644 --- a/src/Impostor.Server.Hazel/HazelMatchmaker.cs +++ b/src/Impostor.Server.Hazel/HazelMatchmaker.cs @@ -42,9 +42,9 @@ namespace Impostor.Server.Hazel }); _connection.NewConnection += OnNewConnection; - + _connection.Start(); - + return default; } @@ -62,26 +62,23 @@ namespace Impostor.Server.Hazel 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) diff --git a/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs index c0d27d5..b12629c 100644 --- a/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs +++ b/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs @@ -1,22 +1,23 @@ 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; } } diff --git a/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs index 8af291f..e1e063e 100644 --- a/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs +++ b/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs @@ -11,7 +11,7 @@ namespace Impostor.Server.Hazel internal class HazelGameMessageWriter : HazelMessageWriter, IGameMessageWriter { private readonly IGame _game; - + public HazelGameMessageWriter(MessageType type, IGame game) : base(type) { @@ -33,14 +33,14 @@ namespace Impostor.Server.Hazel { 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); @@ -50,11 +50,12 @@ namespace Impostor.Server.Hazel 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; } } diff --git a/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs index e63df4c..b16a192 100644 --- a/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs +++ b/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs @@ -83,9 +83,9 @@ namespace Impostor.Server.Hazel 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) @@ -103,9 +103,9 @@ namespace Impostor.Server.Hazel Writer.StartMessage(typeFlag); } - public void Write(GameCode code) + public void Write(GameCode value) { - Write(code.Value); + Write(value.Value); } public void EndMessage() diff --git a/src/Impostor.Server/Data/ServerConfig.cs b/src/Impostor.Server/Data/ServerConfig.cs index 5bcf68d..da5f5a7 100644 --- a/src/Impostor.Server/Data/ServerConfig.cs +++ b/src/Impostor.Server/Data/ServerConfig.cs @@ -2,11 +2,14 @@ { 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 diff --git a/src/Impostor.Server/Data/ServerRedirectorConfig.cs b/src/Impostor.Server/Data/ServerRedirectorConfig.cs index 3233167..5b14ef1 100644 --- a/src/Impostor.Server/Data/ServerRedirectorConfig.cs +++ b/src/Impostor.Server/Data/ServerRedirectorConfig.cs @@ -7,13 +7,17 @@ namespace Impostor.Server.Data public const string Section = "ServerRedirector"; public bool Enabled { get; set; } + public bool Master { get; set; } + public NodeLocator Locator { get; set; } + public List Nodes { get; set; } public class NodeLocator { public string Redis { get; set; } + public string UdpMasterEndpoint { get; set; } } } diff --git a/src/Impostor.Server/Data/ServerRedirectorNode.cs b/src/Impostor.Server/Data/ServerRedirectorNode.cs index 328ad65..2e6a0b8 100644 --- a/src/Impostor.Server/Data/ServerRedirectorNode.cs +++ b/src/Impostor.Server/Data/ServerRedirectorNode.cs @@ -3,6 +3,7 @@ public class ServerRedirectorNode { public string Ip { get; set; } + public ushort Port { get; set; } } } \ No newline at end of file diff --git a/src/Impostor.Server/Events/EventHandler.cs b/src/Impostor.Server/Events/EventHandler.cs new file mode 100644 index 0000000..61ac4d8 --- /dev/null +++ b/src/Impostor.Server/Events/EventHandler.cs @@ -0,0 +1,21 @@ +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 diff --git a/src/Impostor.Server/Events/EventManager.cs b/src/Impostor.Server/Events/EventManager.cs new file mode 100644 index 0000000..f3d33ca --- /dev/null +++ b/src/Impostor.Server/Events/EventManager.cs @@ -0,0 +1,71 @@ +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; + } + + /// + public bool IsRegistered() + where TEvent : IEvent + { + using var scope = _serviceProvider.CreateScope(); + + return GetHandlers(_serviceProvider).Any(); + } + + /// + public async ValueTask CallAsync(T @event) + where T : IEvent + { + var scope = _serviceProvider.CreateScope(); + + try + { + foreach (var (handler, eventListener) in GetHandlers(scope.ServiceProvider)) + { + await eventListener.InvokeAsync(handler, @event, scope.ServiceProvider); + } + } + finally + { + scope.Dispose(); + } + } + + /// + /// Get all the event listeners for the given event type. + /// + /// Current service provider. + /// The event listeners. + private static IEnumerable GetHandlers(IServiceProvider services) + where TEvent : IEvent + { + foreach (var handler in services.GetServices()) + { + 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 diff --git a/src/Impostor.Server/Events/RegisteredEventListener.cs b/src/Impostor.Server/Events/RegisteredEventListener.cs new file mode 100644 index 0000000..058f099 --- /dev/null +++ b/src/Impostor.Server/Events/RegisteredEventListener.cs @@ -0,0 +1,175 @@ +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 Instances = new ConcurrentDictionary(); + private readonly Func _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 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>(invoke, instance, eventParameter, provider) + .Compile(); + } + + public static IEnumerable 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 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(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 diff --git a/src/Impostor.Server/Exceptions/AmongUsException.cs b/src/Impostor.Server/Exceptions/AmongUsException.cs index 2e622f2..d6ace6c 100644 --- a/src/Impostor.Server/Exceptions/AmongUsException.cs +++ b/src/Impostor.Server/Exceptions/AmongUsException.cs @@ -9,15 +9,18 @@ namespace Impostor.Server.Exceptions { } - 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) { } } diff --git a/src/Impostor.Server/Extensions/TypeExtensions.cs b/src/Impostor.Server/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..55d42fb --- /dev/null +++ b/src/Impostor.Server/Extensions/TypeExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + +namespace Impostor.Server +{ + internal static class TypeExtensions + { + /// + /// Get the friendly name for the type. + /// + /// The type. + /// The friendly name. + [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; + } + + /// + /// Get the friendly name for the method. + /// + /// The method. + /// True if the parameters should be included in the name. + /// Friendly name of the method + 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 diff --git a/src/Impostor.Server/Impostor.Server.csproj b/src/Impostor.Server/Impostor.Server.csproj index b70cb08..017b18c 100644 --- a/src/Impostor.Server/Impostor.Server.csproj +++ b/src/Impostor.Server/Impostor.Server.csproj @@ -13,6 +13,7 @@ 1.1.0 icon.ico None + ProjectRules.ruleset @@ -26,6 +27,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -46,8 +51,4 @@ - - - - - + \ No newline at end of file diff --git a/src/Impostor.Server/Impostor.Server.csproj.DotSettings b/src/Impostor.Server/Impostor.Server.csproj.DotSettings new file mode 100644 index 0000000..17962b1 --- /dev/null +++ b/src/Impostor.Server/Impostor.Server.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Impostor.Server/Net/Client.cs b/src/Impostor.Server/Net/Client.cs index fd4c581..b81a606 100644 --- a/src/Impostor.Server/Net/Client.cs +++ b/src/Impostor.Server/Net/Client.cs @@ -1,8 +1,9 @@ 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; @@ -12,97 +13,90 @@ namespace Impostor.Server.Net internal class Client : ClientBase { private static readonly ILogger Logger = Log.ForContext(); - + 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; } @@ -116,26 +110,27 @@ namespace Impostor.Server.Net 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: { @@ -146,7 +141,7 @@ namespace Impostor.Server.Net // Broadcast packet to all other players. using var writer = Player.Game.CreateMessage(message.Type); - + if (flag == MessageFlags.GameDataTo) { var target = reader.ReadPackedInt32(); @@ -156,12 +151,12 @@ namespace Impostor.Server.Net else { reader.CopyTo(writer); - await writer.SendToAllExceptAsync(LimboStates.NotLimbo, Player.Client.Id); + await writer.SendToAllExceptAsync(Id); } break; } - + case MessageFlags.EndGame: { if (!IsPacketAllowed(reader, true)) @@ -180,10 +175,11 @@ namespace Impostor.Server.Net return; } - Message10AlterGame.Deserialize(reader, - out var gameTag, + Message10AlterGame.Deserialize( + reader, + out var gameTag, out var value); - + if (gameTag != AlterGameTags.ChangePrivacy) { return; @@ -200,8 +196,9 @@ namespace Impostor.Server.Net return; } - Message11KickPlayer.Deserialize(reader, - out var playerId, + Message11KickPlayer.Deserialize( + reader, + out var playerId, out var isBan); await Player.Game.HandleKickPlayer(playerId, isBan); @@ -211,34 +208,35 @@ namespace Impostor.Server.Net 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); } @@ -250,5 +248,68 @@ namespace Impostor.Server.Net _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; + } + + /// + /// Triggered when the connected client requests the game listing. + /// + /// + /// All options given. + /// At this moment, the client can only specify the map, impostor count and chat language. + /// + 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 diff --git a/src/Impostor.Server/Net/ClientBase.cs b/src/Impostor.Server/Net/ClientBase.cs index 03c6414..591d359 100644 --- a/src/Impostor.Server/Net/ClientBase.cs +++ b/src/Impostor.Server/Net/ClientBase.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; @@ -7,29 +6,27 @@ namespace Impostor.Server.Net { 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(); } - public int Id { get; } - + public int Id { get; set; } + public string Name { get; } - + public IConnection Connection { get; } - + + public bool IsBot => false; + public IDictionary 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 diff --git a/src/Impostor.Server/Net/Factories/ClientFactory.cs b/src/Impostor.Server/Net/Factories/ClientFactory.cs index 3a6f3c9..653eeda 100644 --- a/src/Impostor.Server/Net/Factories/ClientFactory.cs +++ b/src/Impostor.Server/Net/Factories/ClientFactory.cs @@ -1,8 +1,5 @@ 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 @@ -10,7 +7,6 @@ namespace Impostor.Server.Net.Factories internal class ClientFactory : IClientFactory where TClient : ClientBase { - private int _idLast; private readonly IServiceProvider _serviceProvider; public ClientFactory(IServiceProvider serviceProvider) @@ -18,38 +14,10 @@ namespace Impostor.Server.Net.Factories _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 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(_serviceProvider, clientId, name, connection); - - await client.InitializeAsync(); - + var client = ActivatorUtilities.CreateInstance(_serviceProvider, name, connection); + connection.Client = client; return client; } } diff --git a/src/Impostor.Server/Net/Manager/ClientManager.cs b/src/Impostor.Server/Net/Manager/ClientManager.cs index e0ea68e..38e29cb 100644 --- a/src/Impostor.Server/Net/Manager/ClientManager.cs +++ b/src/Impostor.Server/Net/Manager/ClientManager.cs @@ -1,7 +1,9 @@ -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 @@ -11,7 +13,8 @@ namespace Impostor.Server.Net.Manager private readonly ILogger _logger; private readonly ConcurrentDictionary _clients; private readonly IClientFactory _clientFactory; - + private int _idLast; + public ClientManager(ILogger logger, IClientFactory clientFactory) { _logger = logger; @@ -19,24 +22,46 @@ namespace Impostor.Server.Net.Manager _clients = new ConcurrentDictionary(); } - 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) @@ -44,5 +69,12 @@ namespace Impostor.Server.Net.Manager _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 diff --git a/src/Impostor.Server/Net/Manager/GameManager.cs b/src/Impostor.Server/Net/Manager/GameManager.cs index 2b39ab6..a2ca481 100644 --- a/src/Impostor.Server/Net/Manager/GameManager.cs +++ b/src/Impostor.Server/Net/Manager/GameManager.cs @@ -3,7 +3,10 @@ using System.Collections.Concurrent; 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; @@ -14,61 +17,65 @@ using Microsoft.Extensions.Options; namespace Impostor.Server.Net.Manager { - internal class GameManager + internal class GameManager : IGameManager { private readonly ILogger _logger; private readonly INodeLocator _nodeLocator; private readonly IPEndPoint _publicIp; private readonly ConcurrentDictionary _games; private readonly IServiceProvider _serviceProvider; + private readonly IEventManager _eventManager; - public GameManager(ILogger logger, IOptions config, INodeLocator nodeLocator, IServiceProvider serviceProvider) + public GameManager(ILogger logger, IOptions 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(); } - - public Game Create(GameOptionsData options) + + public IEnumerable Games => _games.Select(kv => kv.Value); + + public async ValueTask CreateAsync(GameOptionsData options) { // TODO: Prevent duplicates when using server redirector using INodeProvider. - var gameCode = GameCode.Create(); var gameCodeStr = gameCode.Code; var game = ActivatorUtilities.CreateInstance(_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 FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10) + public IEnumerable FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10) { var results = 0; - + // 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; } @@ -77,12 +84,12 @@ namespace Impostor.Server.Net.Manager { continue; } - + if (impostorCount != 0 && game.Options.NumImpostors != impostorCount) { continue; } - + // Add to result. yield return game; @@ -94,25 +101,25 @@ namespace Impostor.Server.Net.Manager } } - 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 diff --git a/src/Impostor.Server/Net/Messages/Message01JoinGame.cs b/src/Impostor.Server/Net/Messages/Message01JoinGame.cs index f98f7ef..745d9f3 100644 --- a/src/Impostor.Server/Net/Messages/Message01JoinGame.cs +++ b/src/Impostor.Server/Net/Messages/Message01JoinGame.cs @@ -11,21 +11,21 @@ namespace Impostor.Server.Net.Messages { 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); @@ -35,10 +35,10 @@ namespace Impostor.Server.Net.Messages { throw new ArgumentNullException(nameof(message)); } - + writer.Write(message); } - + writer.EndMessage(); } diff --git a/src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs b/src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs index 6335a18..bb06b63 100644 --- a/src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs +++ b/src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs @@ -12,12 +12,12 @@ namespace Impostor.Server.Net.Messages { 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(); } diff --git a/src/Impostor.Server/Net/Messages/Message07JoinedGame.cs b/src/Impostor.Server/Net/Messages/Message07JoinedGame.cs index 6a4494d..7fba0d7 100644 --- a/src/Impostor.Server/Net/Messages/Message07JoinedGame.cs +++ b/src/Impostor.Server/Net/Messages/Message07JoinedGame.cs @@ -8,7 +8,7 @@ { writer.Clear(MessageType.Reliable); } - + writer.StartMessage(MessageFlags.JoinedGame); writer.Write(gameCode); writer.Write(playerId); @@ -19,7 +19,7 @@ { writer.WritePacked(id); } - + writer.EndMessage(); } } diff --git a/src/Impostor.Server/Net/Messages/Message10AlterGame.cs b/src/Impostor.Server/Net/Messages/Message10AlterGame.cs index fad8e33..772c4c4 100644 --- a/src/Impostor.Server/Net/Messages/Message10AlterGame.cs +++ b/src/Impostor.Server/Net/Messages/Message10AlterGame.cs @@ -10,7 +10,7 @@ namespace Impostor.Server.Net.Messages { writer.Clear(MessageType.Reliable); } - + writer.StartMessage(MessageFlags.HostGame); writer.Write(gameCode); writer.EndMessage(); diff --git a/src/Impostor.Server/Net/Messages/Message11KickPlayer.cs b/src/Impostor.Server/Net/Messages/Message11KickPlayer.cs index 8b899ab..9a9c209 100644 --- a/src/Impostor.Server/Net/Messages/Message11KickPlayer.cs +++ b/src/Impostor.Server/Net/Messages/Message11KickPlayer.cs @@ -8,7 +8,7 @@ { writer.Clear(MessageType.Reliable); } - + writer.StartMessage(MessageFlags.KickPlayer); writer.Write(gameCode); writer.WritePacked(playerId); diff --git a/src/Impostor.Server/Net/Messages/Message12WaitForHost.cs b/src/Impostor.Server/Net/Messages/Message12WaitForHost.cs index 65d4cfa..2f1cd03 100644 --- a/src/Impostor.Server/Net/Messages/Message12WaitForHost.cs +++ b/src/Impostor.Server/Net/Messages/Message12WaitForHost.cs @@ -8,7 +8,7 @@ { writer.Clear(MessageType.Reliable); } - + writer.StartMessage(MessageFlags.WaitForHost); writer.Write(gameCode); writer.Write(playerId); diff --git a/src/Impostor.Server/Net/Messages/Message13Redirect.cs b/src/Impostor.Server/Net/Messages/Message13Redirect.cs index 5219564..17ba12f 100644 --- a/src/Impostor.Server/Net/Messages/Message13Redirect.cs +++ b/src/Impostor.Server/Net/Messages/Message13Redirect.cs @@ -10,10 +10,10 @@ namespace Impostor.Server.Net.Messages { writer.Clear(MessageType.Reliable); } - + writer.StartMessage(MessageFlags.Redirect); writer.Write(ipEndPoint.Address); - writer.Write((ushort) ipEndPoint.Port); + writer.Write((ushort)ipEndPoint.Port); writer.EndMessage(); } } diff --git a/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs b/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs index 038d168..4b71065 100644 --- a/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs +++ b/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Impostor.Server.Net.State; using Impostor.Shared.Innersloth; namespace Impostor.Server.Net.Messages @@ -12,19 +11,20 @@ namespace Impostor.Server.Net.Messages options = GameOptionsData.Deserialize(reader.ReadBytesAndSize()); } - public static void Serialize(IMessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable games) + public static void Serialize(IMessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable 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); @@ -39,8 +39,8 @@ namespace Impostor.Server.Net.Messages writer.Write((byte) game.Options.MaxPlayers); writer.EndMessage(); } + writer.EndMessage(); - writer.EndMessage(); } } diff --git a/src/Impostor.Server/Net/Redirector/ClientRedirector.cs b/src/Impostor.Server/Net/Redirector/ClientRedirector.cs index 50951ca..ac5da50 100644 --- a/src/Impostor.Server/Net/Redirector/ClientRedirector.cs +++ b/src/Impostor.Server/Net/Redirector/ClientRedirector.cs @@ -17,19 +17,24 @@ namespace Impostor.Server.Net.Redirector 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) @@ -44,35 +49,32 @@ namespace Impostor.Server.Net.Redirector 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; } @@ -84,7 +86,7 @@ namespace Impostor.Server.Net.Redirector } } - protected override ValueTask OnDisconnected() + public override ValueTask HandleDisconnectAsync() { _clientManager.Remove(this); return default; diff --git a/src/Impostor.Server/Net/Redirector/INodeLocator.cs b/src/Impostor.Server/Net/Redirector/INodeLocator.cs index f7dcf57..2dd2f07 100644 --- a/src/Impostor.Server/Net/Redirector/INodeLocator.cs +++ b/src/Impostor.Server/Net/Redirector/INodeLocator.cs @@ -5,7 +5,9 @@ namespace Impostor.Server.Net.Redirector public interface INodeLocator { IPEndPoint Find(string gameCode); + void Save(string gameCode, IPEndPoint endPoint); + void Remove(string gameCode); } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs b/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs index e611887..36632a7 100644 --- a/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs +++ b/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs @@ -8,7 +8,7 @@ namespace Impostor.Server.Net.Redirector public class NodeLocatorRedis : INodeLocator { private readonly IDistributedCache _cache; - + public NodeLocatorRedis(ILogger logger, IDistributedCache cache) { logger.LogWarning("Using the redis NodeLocator."); @@ -22,7 +22,7 @@ namespace Impostor.Server.Net.Redirector { return null; } - + return IPEndPoint.Parse(entry); } @@ -30,7 +30,7 @@ namespace Impostor.Server.Net.Redirector { _cache.SetString(gameCode, endPoint.ToString(), new DistributedCacheEntryOptions { - SlidingExpiration = TimeSpan.FromHours(1) + SlidingExpiration = TimeSpan.FromHours(1), }); } diff --git a/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs b/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs index aad8149..8fe843b 100644 --- a/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs +++ b/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs @@ -9,18 +9,18 @@ using Microsoft.Extensions.Logging; namespace Impostor.Server.Net.Redirector { - public class NodeLocatorUDP : INodeLocator, IDisposable + public class NodeLocatorUdp : INodeLocator, IDisposable { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly bool _isMaster; private readonly IPEndPoint _server; private readonly UdpClient _client; private readonly ConcurrentDictionary _availableNodes; - - public NodeLocatorUDP(ILogger logger, IOptions config) + + public NodeLocatorUdp(ILogger logger, IOptions config) { _logger = logger; - + if (config.Value.Master) { _isMaster = true; @@ -29,12 +29,12 @@ namespace Impostor.Server.Net.Redirector 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 @@ -47,18 +47,21 @@ namespace Impostor.Server.Net.Redirector 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) { @@ -75,7 +78,7 @@ namespace Impostor.Server.Net.Redirector { return null; } - + if (_availableNodes.TryGetValue(gameCode, out var node)) { if (node.Expired) @@ -86,7 +89,7 @@ namespace Impostor.Server.Net.Redirector return node.Endpoint; } - + return null; } @@ -96,7 +99,7 @@ namespace Impostor.Server.Net.Redirector { return; } - + _availableNodes.TryRemove(gameCode, out _); } @@ -111,10 +114,13 @@ namespace Impostor.Server.Net.Redirector _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 diff --git a/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs b/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs index 46d27f7..8258198 100644 --- a/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs +++ b/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs @@ -11,18 +11,18 @@ using Microsoft.Extensions.Options; namespace Impostor.Server.Net.Redirector { - public class NodeLocatorUDPService : BackgroundService + public class NodeLocatorUdpService : BackgroundService { - private readonly NodeLocatorUDP _nodeLocator; - private readonly ILogger _logger; + private readonly NodeLocatorUdp _nodeLocator; + private readonly ILogger _logger; private readonly UdpClient _client; - public NodeLocatorUDPService( - INodeLocator nodeLocator, - ILogger logger, + public NodeLocatorUdpService( + INodeLocator nodeLocator, + ILogger logger, IOptions options) { - _nodeLocator = (NodeLocatorUDP) nodeLocator; + _nodeLocator = (NodeLocatorUdp)nodeLocator; _logger = logger; if (!IPEndPoint.TryParse(options.Value.Locator.UdpMasterEndpoint, out var endpoint)) @@ -32,14 +32,14 @@ namespace Impostor.Server.Net.Redirector _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(); @@ -52,7 +52,7 @@ namespace Impostor.Server.Net.Redirector { // Receive data from a node. UdpReceiveResult data; - + try { data = await _client.ReceiveAsync(); @@ -61,7 +61,7 @@ namespace Impostor.Server.Net.Redirector { break; } - + // Check if data is valid. if (data.Buffer.Length == 0) { @@ -89,7 +89,7 @@ namespace Impostor.Server.Net.Redirector { _logger.LogError(e, "Error in NodeLocatorUDPService."); } - + _logger.LogWarning("Master server node update listener is stopping."); } } diff --git a/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs b/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs index 864a540..158090c 100644 --- a/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs +++ b/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs @@ -10,7 +10,7 @@ namespace Impostor.Server.Net.Redirector private readonly List _nodes; private readonly object _lock; private int _currentIndex; - + public NodeProviderConfig(IOptions redirectorConfig) { _nodes = new List(); @@ -35,7 +35,7 @@ namespace Impostor.Server.Net.Redirector { _currentIndex = 0; } - + return node; } } diff --git a/src/Impostor.Server/Net/State/ClientPlayer.Events.cs b/src/Impostor.Server/Net/State/ClientPlayer.Events.cs deleted file mode 100644 index 0468618..0000000 --- a/src/Impostor.Server/Net/State/ClientPlayer.Events.cs +++ /dev/null @@ -1,33 +0,0 @@ -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 - { - /// - /// Triggered when the connected client requests the game listing. - /// - /// - /// All options given. - /// At this moment, the client can only specify the map, impostor count and chat language. - /// - 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 diff --git a/src/Impostor.Server/Net/State/ClientPlayer.cs b/src/Impostor.Server/Net/State/ClientPlayer.cs index f186e17..1f3d3da 100644 --- a/src/Impostor.Server/Net/State/ClientPlayer.cs +++ b/src/Impostor.Server/Net/State/ClientPlayer.cs @@ -1,37 +1,40 @@ 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; } + + /// + public LimboStates Limbo { get; set; } + /// IClient IClientPlayer.Client => Client; + /// IGame IClientPlayer.Game => Game; + + /// + public ValueTask KickAsync() + { + return Game.HandleKickPlayer(Client.Id, false); + } + + /// + public ValueTask BanAsync() + { + return Game.HandleKickPlayer(Client.Id, true); + } } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/State/Game.Incoming.cs b/src/Impostor.Server/Net/State/Game.Incoming.cs index 8b7db28..fa8380a 100644 --- a/src/Impostor.Server/Net/State/Game.Incoming.cs +++ b/src/Impostor.Server/Net/State/Game.Incoming.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using Impostor.Server.Data; +using System.Threading.Tasks; using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net.State @@ -13,64 +11,80 @@ 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 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) { @@ -78,15 +92,15 @@ namespace Impostor.Server.Net.State } } - 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); @@ -99,35 +113,36 @@ namespace Impostor.Server.Net.State 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); } @@ -136,32 +151,32 @@ namespace Impostor.Server.Net.State { 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; @@ -170,7 +185,7 @@ namespace Impostor.Server.Net.State sender.Limbo = LimboStates.WaitingForHost; using var packet = CreateMessage(MessageType.Reliable); - + WriteWaitForHostMessage(packet, false, sender); await packet.SendToAsync(sender.Client); diff --git a/src/Impostor.Server/Net/State/Game.Outgoing.cs b/src/Impostor.Server/Net/State/Game.Outgoing.cs index a26cfb3..c5538aa 100644 --- a/src/Impostor.Server/Net/State/Game.Outgoing.cs +++ b/src/Impostor.Server/Net/State/Game.Outgoing.cs @@ -10,14 +10,14 @@ namespace Impostor.Server.Net.State { 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); } @@ -30,8 +30,8 @@ namespace Impostor.Server.Net.State { 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); } diff --git a/src/Impostor.Server/Net/State/Game.State.cs b/src/Impostor.Server/Net/State/Game.State.cs index 6731f2c..e581fa6 100644 --- a/src/Impostor.Server/Net/State/Game.State.cs +++ b/src/Impostor.Server/Net/State/Game.State.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using Impostor.Server.Exceptions; using Impostor.Shared.Innersloth.Data; @@ -8,16 +7,13 @@ namespace Impostor.Server.Net.State { 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) @@ -33,18 +29,17 @@ namespace Impostor.Server.Net.State 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; } @@ -54,7 +49,7 @@ namespace Impostor.Server.Net.State await MigrateHost(); } - if (isBan) + if (isBan && player.Client.Connection != null) { _bannedIps.Add(player.Client.Connection.EndPoint.Address); } @@ -65,19 +60,27 @@ namespace Impostor.Server.Net.State 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(); } @@ -86,12 +89,12 @@ namespace Impostor.Server.Net.State 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); } diff --git a/src/Impostor.Server/Net/State/Game.cs b/src/Impostor.Server/Net/State/Game.cs index 10e9a7a..d42e2b1 100644 --- a/src/Impostor.Server/Net/State/Game.cs +++ b/src/Impostor.Server/Net/State/Game.cs @@ -16,24 +16,24 @@ namespace Impostor.Server.Net.State internal partial class Game : IGame { private static readonly ILogger Logger = Log.ForContext(); - - private readonly GameManager _gameManager; - private readonly INodeLocator _nodeLocator; - private readonly IMatchmaker matchmaker; - private readonly ConcurrentDictionary _players; + + private readonly IGameManager _gameManager; + private readonly IClientManager _clientManager; + private readonly IMatchmaker _matchmaker; + private readonly ConcurrentDictionary _players; private readonly HashSet _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(); + _players = new ConcurrentDictionary(); _bannedIps = new HashSet(); PublicIp = publicIp; @@ -41,34 +41,34 @@ namespace Impostor.Server.Net.State HostId = -1; GameState = GameStates.NotStarted; Options = options; - this.matchmaker = matchmaker; + _matchmaker = matchmaker; + _clientManager = clientManager; Items = new ConcurrentDictionary(); } 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 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 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) @@ -82,5 +82,17 @@ namespace Impostor.Server.Net.State 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 diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index a9b8219..113f018 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -1,5 +1,7 @@ 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; @@ -45,7 +47,7 @@ namespace Impostor.Server Log.CloseAndFlush(); } } - + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) #if DEBUG @@ -65,7 +67,7 @@ namespace Impostor.Server var redirector = host.Configuration .GetSection(ServerRedirectorConfig.Section) .Get() ?? new ServerRedirectorConfig(); - + services.Configure(host.Configuration.GetSection(ServerConfig.Section)); services.Configure(host.Configuration.GetSection(ServerRedirectorConfig.Section)); @@ -86,18 +88,18 @@ namespace Impostor.Server } else if (!string.IsNullOrEmpty(redirector.Locator.UdpMasterEndpoint)) { - services.AddSingleton(); + services.AddSingleton(); if (redirector.Master) { - services.AddHostedService(); + services.AddHostedService(); } } 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(); @@ -108,20 +110,22 @@ namespace Impostor.Server // So we provide one that ignores all calls. services.AddSingleton(); } - + services.AddSingleton(); - + if (redirector.Enabled && redirector.Master) { services.AddSingleton>(); + // For a master server, we don't need a GameManager. } else { services.AddSingleton>(); - services.AddSingleton(); + services.AddSingleton(); } + services.AddSingleton(); services.UseHazelMatchmaking(); services.AddHostedService(); }) diff --git a/src/Impostor.Server/ProjectRules.ruleset b/src/Impostor.Server/ProjectRules.ruleset new file mode 100644 index 0000000..d2b2f2f --- /dev/null +++ b/src/Impostor.Server/ProjectRules.ruleset @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Impostor.sln b/src/Impostor.sln index a81e19c..ba1a81a 100644 --- a/src/Impostor.sln +++ b/src/Impostor.sln @@ -21,8 +21,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client", "Impostor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Server.Api", "Impostor.Server.Api\Impostor.Server.Api.csproj", "{E096A7D7-D693-4A13-A526-38CC574D84F8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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 @@ -89,14 +87,6 @@ Global {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.Build.0 = Release|Any CPU {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.ActiveCfg = Release|Any CPU {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.Build.0 = Release|Any CPU - {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 diff --git a/submodules/Hazel-Networking b/submodules/Hazel-Networking index 0c61aa4..7a7caf8 160000 --- a/submodules/Hazel-Networking +++ b/submodules/Hazel-Networking @@ -1 +1 @@ -Subproject commit 0c61aa426a2e47be9ac82c601c2f19b9345992a1 +Subproject commit 7a7caf89e0c49ed59b3442987089a03df7de6c7a -- 2.39.5