]> git.deb.at Git - rhonda/impostor.git/commitdiff
More abstractions, event manager and docs
authorGerard Smit <gsmit1996@gmail.com>
Sat, 10 Oct 2020 22:45:38 +0000 (00:45 +0200)
committerGerard Smit <gsmit1996@gmail.com>
Sat, 10 Oct 2020 22:45:38 +0000 (00:45 +0200)
94 files changed:
src/Imposter.Reactive/AsyncObservableExtensions.cs [deleted file]
src/Imposter.Reactive/IAsyncObservable.cs [deleted file]
src/Imposter.Reactive/IAsyncObserver.cs [deleted file]
src/Imposter.Reactive/Imposter.Reactive.csproj [deleted file]
src/Imposter.Reactive/Reactive/AsyncObservable.cs [deleted file]
src/Imposter.Reactive/Reactive/AsyncObservableBase.cs [deleted file]
src/Imposter.Reactive/Reactive/AsyncObserver.cs [deleted file]
src/Imposter.Reactive/Reactive/AsyncObserverBase.cs [deleted file]
src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs [deleted file]
src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs [deleted file]
src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs [deleted file]
src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs [deleted file]
src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs [deleted file]
src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs [new file with mode: 0644]
src/Impostor.Server.Api/Events/EventPriority.cs [new file with mode: 0644]
src/Impostor.Server.Api/Events/Game/GameCreatedEvent.cs [new file with mode: 0644]
src/Impostor.Server.Api/Events/Game/IGameEvent.cs [new file with mode: 0644]
src/Impostor.Server.Api/Events/IEvent.cs [new file with mode: 0644]
src/Impostor.Server.Api/Events/IEventCancelable.cs [new file with mode: 0644]
src/Impostor.Server.Api/Events/IEventListener.cs [new file with mode: 0644]
src/Impostor.Server.Api/Events/Managers/IEventManager.cs [new file with mode: 0644]
src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs [deleted file]
src/Impostor.Server.Api/Extensions/GameManagerExtensions.cs [new file with mode: 0644]
src/Impostor.Server.Api/GameCode.cs [deleted file]
src/Impostor.Server.Api/Games/GameCode.cs [new file with mode: 0644]
src/Impostor.Server.Api/Games/GameJoinError.cs [new file with mode: 0644]
src/Impostor.Server.Api/Games/GameJoinResult.cs [new file with mode: 0644]
src/Impostor.Server.Api/Games/IGame.cs [new file with mode: 0644]
src/Impostor.Server.Api/Games/Managers/IGameManager.cs [new file with mode: 0644]
src/Impostor.Server.Api/IGame.cs [deleted file]
src/Impostor.Server.Api/Impostor.Server.Api.csproj
src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings
src/Impostor.Server.Api/LimboStates.cs [deleted file]
src/Impostor.Server.Api/Net/Extensions/GameMessageWriterExtensions.cs
src/Impostor.Server.Api/Net/Factories/IClientFactory.cs
src/Impostor.Server.Api/Net/IClient.cs
src/Impostor.Server.Api/Net/IClientPlayer.cs
src/Impostor.Server.Api/Net/IConnection.cs
src/Impostor.Server.Api/Net/LimboStates.cs [new file with mode: 0644]
src/Impostor.Server.Api/Net/Manager/IClientManager.cs
src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs
src/Impostor.Server.Api/Net/Messages/IConnectionMessageWriter.cs
src/Impostor.Server.Api/Net/Messages/IGameMessageWriter.cs
src/Impostor.Server.Api/Net/Messages/IMessage.cs
src/Impostor.Server.Api/Net/Messages/IMessageReader.cs
src/Impostor.Server.Api/Net/Messages/IMessageWriter.cs
src/Impostor.Server.Api/Net/Messages/MessageType.cs
src/Impostor.Server.Api/Plugins/IPlugin.cs [new file with mode: 0644]
src/Impostor.Server.Api/Plugins/PluginBase.cs [new file with mode: 0644]
src/Impostor.Server.Api/ProjectRules.ruleset [new file with mode: 0644]
src/Impostor.Server.Hazel/HazelConnection.cs
src/Impostor.Server.Hazel/HazelMatchmaker.cs
src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs
src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs
src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs
src/Impostor.Server/Data/ServerConfig.cs
src/Impostor.Server/Data/ServerRedirectorConfig.cs
src/Impostor.Server/Data/ServerRedirectorNode.cs
src/Impostor.Server/Events/EventHandler.cs [new file with mode: 0644]
src/Impostor.Server/Events/EventManager.cs [new file with mode: 0644]
src/Impostor.Server/Events/RegisteredEventListener.cs [new file with mode: 0644]
src/Impostor.Server/Exceptions/AmongUsException.cs
src/Impostor.Server/Extensions/TypeExtensions.cs [new file with mode: 0644]
src/Impostor.Server/Impostor.Server.csproj
src/Impostor.Server/Impostor.Server.csproj.DotSettings [new file with mode: 0644]
src/Impostor.Server/Net/Client.cs
src/Impostor.Server/Net/ClientBase.cs
src/Impostor.Server/Net/Factories/ClientFactory.cs
src/Impostor.Server/Net/Manager/ClientManager.cs
src/Impostor.Server/Net/Manager/GameManager.cs
src/Impostor.Server/Net/Messages/Message01JoinGame.cs
src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs
src/Impostor.Server/Net/Messages/Message07JoinedGame.cs
src/Impostor.Server/Net/Messages/Message10AlterGame.cs
src/Impostor.Server/Net/Messages/Message11KickPlayer.cs
src/Impostor.Server/Net/Messages/Message12WaitForHost.cs
src/Impostor.Server/Net/Messages/Message13Redirect.cs
src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs
src/Impostor.Server/Net/Redirector/ClientRedirector.cs
src/Impostor.Server/Net/Redirector/INodeLocator.cs
src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs
src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs
src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs
src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs
src/Impostor.Server/Net/State/ClientPlayer.Events.cs [deleted file]
src/Impostor.Server/Net/State/ClientPlayer.cs
src/Impostor.Server/Net/State/Game.Incoming.cs
src/Impostor.Server/Net/State/Game.Outgoing.cs
src/Impostor.Server/Net/State/Game.State.cs
src/Impostor.Server/Net/State/Game.cs
src/Impostor.Server/Program.cs
src/Impostor.Server/ProjectRules.ruleset [new file with mode: 0644]
src/Impostor.sln
submodules/Hazel-Networking

diff --git a/src/Imposter.Reactive/AsyncObservableExtensions.cs b/src/Imposter.Reactive/AsyncObservableExtensions.cs
deleted file mode 100644 (file)
index 56f5043..0000000
+++ /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<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync)
-        {
-            if (source == null)
-                throw new ArgumentNullException(nameof(source));
-            if (onNextAsync == null)
-                throw new ArgumentNullException(nameof(onNextAsync));
-
-            return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, ex => new ValueTask(Task.FromException(ex)), () => default));
-        }
-
-        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync, Func<Exception, ValueTask> onErrorAsync)
-        {
-            if (source == null)
-                throw new ArgumentNullException(nameof(source));
-            if (onNextAsync == null)
-                throw new ArgumentNullException(nameof(onNextAsync));
-            if (onErrorAsync == null)
-                throw new ArgumentNullException(nameof(onErrorAsync));
-
-            return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, onErrorAsync, () => default));
-        }
-
-        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync, Func<ValueTask> onCompletedAsync)
-        {
-            if (source == null)
-                throw new ArgumentNullException(nameof(source));
-            if (onNextAsync == null)
-                throw new ArgumentNullException(nameof(onNextAsync));
-            if (onCompletedAsync == null)
-                throw new ArgumentNullException(nameof(onCompletedAsync));
-
-            return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, ex => new ValueTask(Task.FromException(ex)), onCompletedAsync));
-        }
-
-        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Func<T, ValueTask> onNextAsync, Func<Exception, ValueTask> onErrorAsync, Func<ValueTask> onCompletedAsync)
-        {
-            if (source == null)
-                throw new ArgumentNullException(nameof(source));
-            if (onNextAsync == null)
-                throw new ArgumentNullException(nameof(onNextAsync));
-            if (onErrorAsync == null)
-                throw new ArgumentNullException(nameof(onErrorAsync));
-            if (onCompletedAsync == null)
-                throw new ArgumentNullException(nameof(onCompletedAsync));
-
-            return source.SubscribeAsync(new AsyncObserver<T>(onNextAsync, onErrorAsync, onCompletedAsync));
-        }
-
-        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Action<T> onNext)
-        {
-            if (source == null)
-                throw new ArgumentNullException(nameof(source));
-            if (onNext == null)
-                throw new ArgumentNullException(nameof(onNext));
-
-            return source.SubscribeAsync(new AsyncObserver<T>(x => { onNext(x); return default; }, ex => new ValueTask(Task.FromException(ex)), () => default));
-        }
-
-        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Action<T> onNext, Action<Exception> onError)
-        {
-            if (source == null)
-                throw new ArgumentNullException(nameof(source));
-            if (onNext == null)
-                throw new ArgumentNullException(nameof(onNext));
-            if (onError == null)
-                throw new ArgumentNullException(nameof(onError));
-
-            return source.SubscribeAsync(new AsyncObserver<T>(x => { onNext(x); return default; }, ex => { onError(ex); return default; }, () => default));
-        }
-
-        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Action<T> onNext, Action onCompleted)
-        {
-            if (source == null)
-                throw new ArgumentNullException(nameof(source));
-            if (onNext == null)
-                throw new ArgumentNullException(nameof(onNext));
-            if (onCompleted == null)
-                throw new ArgumentNullException(nameof(onCompleted));
-
-            return source.SubscribeAsync(new AsyncObserver<T>(x => { onNext(x); return default; }, ex => new ValueTask(Task.FromException(ex)), () => { onCompleted(); return default; }));
-        }
-
-        public static ValueTask<IAsyncDisposable> SubscribeAsync<T>(this IAsyncObservable<T> source, Action<T> onNext, Action<Exception> onError, Action onCompleted)
-        {
-            if (source == null)
-                throw new ArgumentNullException(nameof(source));
-            if (onNext == null)
-                throw new ArgumentNullException(nameof(onNext));
-            if (onError == null)
-                throw new ArgumentNullException(nameof(onError));
-            if (onCompleted == null)
-                throw new ArgumentNullException(nameof(onCompleted));
-
-            return source.SubscribeAsync(new AsyncObserver<T>(x => { onNext(x); return default; }, ex => { onError(ex); return default; }, () => { onCompleted(); return default; }));
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/IAsyncObservable.cs b/src/Imposter.Reactive/IAsyncObservable.cs
deleted file mode 100644 (file)
index e0dcce3..0000000
+++ /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<out T>
-    {
-        ValueTask<IAsyncDisposable> SubscribeAsync(IAsyncObserver<T> 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 (file)
index eaaf3d5..0000000
+++ /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<in T>
-    {
-        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 (file)
index 63eb9ec..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-    <PropertyGroup>
-        <TargetFramework>net5.0</TargetFramework>
-        <RootNamespace>System</RootNamespace>
-    </PropertyGroup>
-
-</Project>
diff --git a/src/Imposter.Reactive/Reactive/AsyncObservable.cs b/src/Imposter.Reactive/Reactive/AsyncObservable.cs
deleted file mode 100644 (file)
index f6b8045..0000000
+++ /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<T> : AsyncObservableBase<T>
-    {
-        private readonly Func<IAsyncObserver<T>, ValueTask<IAsyncDisposable>> _subscribeAsync;
-
-        public AsyncObservable(Func<IAsyncObserver<T>, ValueTask<IAsyncDisposable>> subscribeAsync)
-        {
-            _subscribeAsync = subscribeAsync ?? throw new ArgumentNullException(nameof(subscribeAsync));
-        }
-
-        protected override ValueTask<IAsyncDisposable> SubscribeAsyncCore(IAsyncObserver<T> observer)
-        {
-            if (observer == null)
-                throw new ArgumentNullException(nameof(observer));
-
-            return _subscribeAsync(observer);
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/Reactive/AsyncObservableBase.cs b/src/Imposter.Reactive/Reactive/AsyncObservableBase.cs
deleted file mode 100644 (file)
index a4f86ff..0000000
+++ /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<T> : IAsyncObservable<T>
-    {
-        public async ValueTask<IAsyncDisposable> SubscribeAsync(IAsyncObserver<T> observer)
-        {
-            if (observer == null)
-                throw new ArgumentNullException(nameof(observer));
-
-            var autoDetach = new AutoDetachAsyncObserver(observer);
-
-            var subscription = await SubscribeAsyncCore(autoDetach).ConfigureAwait(false);
-
-            await autoDetach.AssignAsync(subscription).ConfigureAwait(false);
-
-            return autoDetach;
-        }
-
-        protected abstract ValueTask<IAsyncDisposable> SubscribeAsyncCore(IAsyncObserver<T> observer);
-
-        private sealed class AutoDetachAsyncObserver : AsyncObserverBase<T>, IAsyncDisposable
-        {
-            private readonly IAsyncObserver<T> _observer;
-            private readonly object _gate = new object();
-
-            private IAsyncDisposable _subscription;
-            private ValueTask _task;
-            private bool _disposing;
-
-            public AutoDetachAsyncObserver(IAsyncObserver<T> observer)
-            {
-                _observer = observer;
-            }
-
-            public async ValueTask AssignAsync(IAsyncDisposable subscription)
-            {
-                var shouldDispose = false;
-
-                lock (_gate)
-                {
-                    if (_disposing)
-                    {
-                        shouldDispose = true;
-                    }
-                    else
-                    {
-                        _subscription = subscription;
-                    }
-                }
-
-                if (shouldDispose)
-                {
-                    await subscription.DisposeAsync().ConfigureAwait(false);
-                }
-            }
-
-            public async ValueTask DisposeAsync()
-            {
-                var task = default(ValueTask);
-                var subscription = default(IAsyncDisposable);
-
-                lock (_gate)
-                {
-                    //
-                    // NB: The postcondition of awaiting the first DisposeAsync call to complete is that all message
-                    //     processing has ceased, i.e. no further On*AsyncCore calls will be made. This is achieved
-                    //     here by setting _disposing to true, which is checked by the On*AsyncCore calls upon
-                    //     entry, and by awaiting the task of any in-flight On*AsyncCore calls.
-                    //
-                    //     Timing of the disposal of the subscription is less deterministic due to the intersection
-                    //     with the AssignAsync code path. However, the auto-detach observer can only be returned
-                    //     from the SubscribeAsync call *after* a call to AssignAsync has been made and awaited, so
-                    //     either AssignAsync triggers the disposal and an already disposed instance is returned, or
-                    //     the user calling DisposeAsync will either encounter a busy observer which will be stopped
-                    //     in its tracks (as described above) or it will trigger a disposal of the subscription. In
-                    //     both these cases the result of awaiting DisposeAsync guarantees no further message flow.
-                    //
-
-                    if (!_disposing)
-                    {
-                        _disposing = true;
-
-                        task = _task;
-                        subscription = _subscription;
-                    }
-                }
-
-                try
-                {
-                    //
-                    // BUGBUG: This causes grief when an outgoing On*Async call reenters the DisposeAsync method and
-                    //         results in the task returned from the On*Async call to be awaited to serialize the
-                    //         call to subscription.DisposeAsync after it's done. We need to either detect reentrancy
-                    //         and queue up the call to DisposeAsync or follow an when we trigger the disposal without
-                    //         awaiting outstanding work (thus allowing for concurrency).
-                    //
-                    // if (task != null)
-                    // {
-                    //     await task.ConfigureAwait(false);
-                    // }
-                    //
-                }
-                finally
-                {
-                    if (subscription != null)
-                    {
-                        await subscription.DisposeAsync().ConfigureAwait(false);
-                    }
-                }
-            }
-
-            protected override async ValueTask OnCompletedAsyncCore()
-            {
-                lock (_gate)
-                {
-                    if (_disposing)
-                    {
-                        return;
-                    }
-
-                    _task = _observer.OnCompletedAsync();
-                }
-
-                try
-                {
-                    await _task.ConfigureAwait(false);
-                }
-                finally
-                {
-                    await FinishAsync().ConfigureAwait(false);
-                }
-            }
-
-            protected override async ValueTask OnErrorAsyncCore(Exception error)
-            {
-                lock (_gate)
-                {
-                    if (_disposing)
-                    {
-                        return;
-                    }
-
-                    _task = _observer.OnErrorAsync(error);
-                }
-
-                try
-                {
-                    await _task.ConfigureAwait(false);
-                }
-                finally
-                {
-                    await FinishAsync().ConfigureAwait(false);
-                }
-            }
-
-            protected override async ValueTask OnNextAsyncCore(T value)
-            {
-                lock (_gate)
-                {
-                    if (_disposing)
-                    {
-                        return;
-                    }
-
-                    _task = _observer.OnNextAsync(value);
-                }
-
-                try
-                {
-                    await _task.ConfigureAwait(false);
-                }
-                finally
-                {
-                    lock (_gate)
-                    {
-                        _task = default;
-                    }
-                }
-            }
-
-            private async ValueTask FinishAsync()
-            {
-                var subscription = default(IAsyncDisposable);
-
-                lock (_gate)
-                {
-                    if (!_disposing)
-                    {
-                        _disposing = true;
-
-                        subscription = _subscription;
-                    }
-
-                    _task = default;
-                }
-
-                if (subscription != null)
-                {
-                    await subscription.DisposeAsync().ConfigureAwait(false);
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/Reactive/AsyncObserver.cs b/src/Imposter.Reactive/Reactive/AsyncObserver.cs
deleted file mode 100644 (file)
index bfe0066..0000000
+++ /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<T> : AsyncObserverBase<T>
-    {
-        private readonly Func<T, ValueTask> _onNextAsync;
-        private readonly Func<Exception, ValueTask> _onErrorAsync;
-        private readonly Func<ValueTask> _onCompletedAsync;
-
-        public AsyncObserver(Func<T, ValueTask> onNextAsync, Func<Exception, ValueTask> onErrorAsync, Func<ValueTask> onCompletedAsync)
-        {
-            _onNextAsync = onNextAsync ?? throw new ArgumentNullException(nameof(onNextAsync));
-            _onErrorAsync = onErrorAsync ?? throw new ArgumentNullException(nameof(onErrorAsync));
-            _onCompletedAsync = onCompletedAsync ?? throw new ArgumentNullException(nameof(onCompletedAsync));
-        }
-
-        protected override ValueTask OnCompletedAsyncCore() => _onCompletedAsync();
-
-        protected override ValueTask OnErrorAsyncCore(Exception error) => _onErrorAsync(error ?? throw new ArgumentNullException(nameof(error)));
-
-        protected override ValueTask OnNextAsyncCore(T value) => _onNextAsync(value);
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/Reactive/AsyncObserverBase.cs b/src/Imposter.Reactive/Reactive/AsyncObserverBase.cs
deleted file mode 100644 (file)
index 75564e6..0000000
+++ /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<T> : IAsyncObserver<T>
-    {
-        private const int Idle = 0;
-        private const int Busy = 1;
-        private const int Done = 2;
-
-        private int _status = Idle;
-
-        public ValueTask OnCompletedAsync()
-        {
-            TryEnter();
-
-            try
-            {
-                return OnCompletedAsyncCore();
-            }
-            finally
-            {
-                Interlocked.Exchange(ref _status, Done);
-            }
-        }
-
-        protected abstract ValueTask OnCompletedAsyncCore();
-
-        public ValueTask OnErrorAsync(Exception error)
-        {
-            if (error == null)
-                throw new ArgumentNullException(nameof(error));
-
-            TryEnter();
-
-            try
-            {
-                return OnErrorAsyncCore(error);
-            }
-            finally
-            {
-                Interlocked.Exchange(ref _status, Done);
-            }
-        }
-
-        protected abstract ValueTask OnErrorAsyncCore(Exception error);
-
-        public ValueTask OnNextAsync(T value)
-        {
-            TryEnter();
-
-            try
-            {
-                return OnNextAsyncCore(value);
-            }
-            finally
-            {
-                Interlocked.Exchange(ref _status, Idle);
-            }
-        }
-
-        protected abstract ValueTask OnNextAsyncCore(T value);
-
-        private void TryEnter()
-        {
-            var old = Interlocked.CompareExchange(ref _status, Busy, Idle);
-
-            switch (old)
-            {
-                case Busy:
-                    throw new InvalidOperationException("The observer is currently processing a notification.");
-                case Done:
-                    throw new InvalidOperationException("The observer has already terminated.");
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs b/src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs
deleted file mode 100644 (file)
index b5aaaf2..0000000
+++ /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<ValueTask> dispose)
-        {
-            if (dispose == null)
-                throw new ArgumentNullException(nameof(dispose));
-
-            return new AnonymousAsyncDisposable(dispose);
-        }
-
-        private sealed class AnonymousAsyncDisposable : IAsyncDisposable
-        {
-            private Func<ValueTask> _dispose;
-
-            public AnonymousAsyncDisposable(Func<ValueTask> dispose) => _dispose = dispose;
-
-            public ValueTask DisposeAsync() => Interlocked.Exchange(ref _dispose, null)?.Invoke() ?? default;
-        }
-
-        private sealed class NopAsyncDisposable : IAsyncDisposable
-        {
-            public ValueTask DisposeAsync() => default;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs
deleted file mode 100644 (file)
index b2f3dae..0000000
+++ /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<T> : SimpleAsyncSubject<T>
-    {
-        protected override ValueTask OnCompletedAsyncCore(IEnumerable<IAsyncObserver<T>> observers) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnCompletedAsync().AsTask())));
-
-        protected override ValueTask OnErrorAsyncCore(IEnumerable<IAsyncObserver<T>> observers, Exception error) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnErrorAsync(error).AsTask())));
-
-        protected override ValueTask OnNextAsyncCore(IEnumerable<IAsyncObserver<T>> observers, T value) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnNextAsync(value).AsTask())));
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs
deleted file mode 100644 (file)
index 8a3342e..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace System.Reactive.Subjects
-{
-    public interface IAsyncSubject<in TInput, out TOutput> : IAsyncObservable<TOutput>, IAsyncObserver<TInput>
-    {
-    }
-
-    public interface IAsyncSubject<T> : IAsyncSubject<T, T>
-    {
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs
deleted file mode 100644 (file)
index 28a64fd..0000000
+++ /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<T> : SimpleAsyncSubject<T>
-    {
-        protected override async ValueTask OnCompletedAsyncCore(IEnumerable<IAsyncObserver<T>> observers)
-        {
-            foreach (var observer in observers)
-            {
-                await observer.OnCompletedAsync().ConfigureAwait(false);
-            }
-        }
-
-        protected override async ValueTask OnErrorAsyncCore(IEnumerable<IAsyncObserver<T>> observers, Exception error)
-        {
-            foreach (var observer in observers)
-            {
-                await observer.OnErrorAsync(error).ConfigureAwait(false);
-            }
-        }
-
-        protected override async ValueTask OnNextAsyncCore(IEnumerable<IAsyncObserver<T>> observers, T value)
-        {
-            foreach (var observer in observers)
-            {
-                await observer.OnNextAsync(value).ConfigureAwait(false);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs
deleted file mode 100644 (file)
index b922056..0000000
+++ /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<T> : IAsyncSubject<T>
-    {
-        private readonly object _gate = new object();
-        private readonly List<IAsyncObserver<T>> _observers = new List<IAsyncObserver<T>>();
-        private bool _done;
-        private Exception _error;
-
-        public ValueTask OnCompletedAsync()
-        {
-            IAsyncObserver<T>[] observers;
-
-            lock (_gate)
-            {
-                if (_done || _error != null)
-                {
-                    return default;
-                }
-
-                _done = true;
-
-                observers = _observers.ToArray();
-            }
-
-            return OnCompletedAsyncCore(observers);
-        }
-
-        protected abstract ValueTask OnCompletedAsyncCore(IEnumerable<IAsyncObserver<T>> observers);
-
-        public ValueTask OnErrorAsync(Exception error)
-        {
-            if (error == null)
-                throw new ArgumentNullException(nameof(error));
-
-            IAsyncObserver<T>[] observers;
-
-            lock (_gate)
-            {
-                if (_done || _error != null)
-                {
-                    return default;
-                }
-
-                _error = error;
-
-                observers = _observers.ToArray();
-            }
-
-            return OnErrorAsyncCore(observers, error);
-        }
-
-        protected abstract ValueTask OnErrorAsyncCore(IEnumerable<IAsyncObserver<T>> observers, Exception error);
-
-        public ValueTask OnNextAsync(T value)
-        {
-            IAsyncObserver<T>[] observers;
-
-            lock (_gate)
-            {
-                if (_done || _error != null)
-                {
-                    return default;
-                }
-
-                observers = _observers.ToArray();
-            }
-
-            return OnNextAsyncCore(observers, value);
-        }
-
-        protected abstract ValueTask OnNextAsyncCore(IEnumerable<IAsyncObserver<T>> observers, T value);
-
-        public async ValueTask<IAsyncDisposable> SubscribeAsync(IAsyncObserver<T> observer)
-        {
-            if (observer == null)
-                throw new ArgumentNullException(nameof(observer));
-
-            bool done;
-            Exception error;
-
-            lock (_gate)
-            {
-                done = _done;
-                error = _error;
-
-                if (!done && error == null)
-                {
-                    _observers.Add(observer);
-                }
-            }
-
-            if (done)
-            {
-                await observer.OnCompletedAsync().ConfigureAwait(false);
-
-                return AsyncDisposable.Nop;
-            }
-            else if (error != null)
-            {
-                await observer.OnErrorAsync(error).ConfigureAwait(false);
-
-                return AsyncDisposable.Nop;
-            }
-            else
-            {
-                return AsyncDisposable.Create(() =>
-                {
-                    lock (_gate)
-                    {
-                        var i = _observers.LastIndexOf(observer);
-
-                        if (i >= 0)
-                        {
-                            _observers.RemoveAt(i);
-                        }
-                    }
-
-                    return default;
-                });
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs b/src/Impostor.Server.Api/Events/Attributes/EventListenerAttribute.cs
new file mode 100644 (file)
index 0000000..bea45c1
--- /dev/null
@@ -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 };
+        }
+
+        /// <summary>
+        ///     The priority of the event listener.
+        /// </summary>
+        public EventPriority Priority { get; set; }
+
+        /// <summary>
+        ///     The events that the listener is listening to.
+        /// </summary>
+        public Type[] Events { get; set; }
+
+        /// <summary>
+        ///     If set to true, the listener will be called regardless of the <see cref="IEventCancelable.IsCancelled"/>.
+        /// </summary>
+        public bool IgnoreCancelled { get; set; }
+
+        /// <summary>
+        ///     The order of the priority.
+        /// </summary>
+        public int PriorityOrder { get; set; } = 100;
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Events/EventPriority.cs b/src/Impostor.Server.Api/Events/EventPriority.cs
new file mode 100644 (file)
index 0000000..dbf506e
--- /dev/null
@@ -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 (file)
index 0000000..27c8b69
--- /dev/null
@@ -0,0 +1,20 @@
+namespace Impostor.Server.Events
+{
+    /// <summary>
+    ///     Called whenever a new <see cref="IGame"/> is created.
+    /// </summary>
+    public sealed class GameCreatedEvent : IGameEvent
+    {
+        /// <summary>
+        ///     Initializes a new instance of the <see cref="GameCreatedEvent"/> class.
+        /// </summary>
+        /// <param name="game">Instance of the game.</param>
+        public GameCreatedEvent(IGame game)
+        {
+            Game = game;
+        }
+
+        /// <inheritdoc/>
+        public IGame Game { get; }
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Events/Game/IGameEvent.cs b/src/Impostor.Server.Api/Events/Game/IGameEvent.cs
new file mode 100644 (file)
index 0000000..a3771bc
--- /dev/null
@@ -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 (file)
index 0000000..e9b3505
--- /dev/null
@@ -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 (file)
index 0000000..75e2364
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Impostor.Server.Events
+{
+    public interface IEventCancelable : IEvent
+    {
+        /// <summary>
+        ///     True if the event was cancelled.
+        /// </summary>
+        bool IsCancelled { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Events/IEventListener.cs b/src/Impostor.Server.Api/Events/IEventListener.cs
new file mode 100644 (file)
index 0000000..3e08c36
--- /dev/null
@@ -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 (file)
index 0000000..488e38c
--- /dev/null
@@ -0,0 +1,24 @@
+using System.Threading.Tasks;
+
+namespace Impostor.Server.Events.Managers
+{
+    public interface IEventManager
+    {
+        /// <summary>
+        ///     Returns true if an event with the type <see cref="TEvent"/> is registered.
+        /// </summary>
+        /// <returns>True if the <see cref="TEvent"/> is registered.</returns>
+        /// <typeparam name="TEvent">Type of the event.</typeparam>
+        bool IsRegistered<TEvent>()
+            where TEvent : IEvent;
+
+        /// <summary>
+        ///     Call all the event listeners for the type <see cref="TEvent"/>.
+        /// </summary>
+        /// <param name="event">The event argument.</param>
+        /// <typeparam name="TEvent">Type of the event.</typeparam>
+        /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+        ValueTask CallAsync<TEvent>(TEvent @event)
+            where TEvent : IEvent;
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs b/src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs
deleted file mode 100644 (file)
index 73e5407..0000000
+++ /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 (file)
index 0000000..9d41e28
--- /dev/null
@@ -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/GameCode.cs
deleted file mode 100644 (file)
index 6178732..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using Impostor.Shared.Innersloth;
-
-namespace Impostor.Server
-{
-    public readonly struct GameCode : IEquatable<GameCode>
-    {
-        public GameCode(int value)
-        {
-            Value = value;
-            Code = GameCodeParser.IntToGameName(value);
-        }
-
-        public GameCode(string code)
-        {
-            Value = GameCodeParser.GameNameToInt(code);
-            Code = code;
-        }
-        
-        public string Code { get; }
-        
-        public int Value { get; }
-
-        public static implicit operator string(GameCode code) => code.Code;
-
-        public static implicit operator int(GameCode code) => code.Value;
-
-        public static implicit operator GameCode(string code) => From(code);
-
-        public static implicit operator GameCode(int value) => From(value);
-
-        public bool Equals(GameCode other)
-        {
-            return Code == other.Code && Value == other.Value;
-        }
-
-        public override bool Equals(object? obj)
-        {
-            return obj is GameCode other && Equals(other);
-        }
-
-        public override int GetHashCode()
-        {
-            return HashCode.Combine(Code, Value);
-        }
-
-        public static bool operator ==(GameCode left, GameCode right)
-        {
-            return left.Equals(right);
-        }
-
-        public static bool operator !=(GameCode left, GameCode right)
-        {
-            return !left.Equals(right);
-        }
-
-        public override string ToString()
-        {
-            return Code;
-        }
-
-        public static GameCode From(int value) => new GameCode(value);
-
-        public static GameCode From(string value) => new GameCode(value);
-
-        public static GameCode Create()
-        {
-            return new GameCode(GameCodeParser.GenerateCode(6));
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Games/GameCode.cs b/src/Impostor.Server.Api/Games/GameCode.cs
new file mode 100644 (file)
index 0000000..b97b1e2
--- /dev/null
@@ -0,0 +1,74 @@
+using System;
+using Impostor.Shared.Innersloth;
+
+namespace Impostor.Server
+{
+    public readonly struct GameCode : IEquatable<GameCode>
+    {
+        public GameCode(int value)
+        {
+            Value = value;
+            Code = GameCodeParser.IntToGameName(value);
+        }
+
+        public GameCode(string code)
+        {
+            Value = GameCodeParser.GameNameToInt(code);
+            Code = code;
+        }
+
+        public string Code { get; }
+
+        public int Value { get; }
+
+        public static implicit operator string(GameCode code) => code.Code;
+
+        public static implicit operator int(GameCode code) => code.Value;
+
+        public static implicit operator GameCode(string code) => From(code);
+
+        public static implicit operator GameCode(int value) => From(value);
+
+        public static bool operator ==(GameCode left, GameCode right)
+        {
+            return left.Equals(right);
+        }
+
+        public static bool operator !=(GameCode left, GameCode right)
+        {
+            return !left.Equals(right);
+        }
+
+        public static GameCode Create()
+        {
+            return new GameCode(GameCodeParser.GenerateCode(6));
+        }
+
+        public static GameCode From(int value) => new GameCode(value);
+
+        public static GameCode From(string value) => new GameCode(value);
+
+        /// <inheritdoc/>
+        public bool Equals(GameCode other)
+        {
+            return Code == other.Code && Value == other.Value;
+        }
+
+        /// <inheritdoc/>
+        public override bool Equals(object? obj)
+        {
+            return obj is GameCode other && Equals(other);
+        }
+
+        /// <inheritdoc/>
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(Code, Value);
+        }
+
+        public override string ToString()
+        {
+            return Code;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Games/GameJoinError.cs b/src/Impostor.Server.Api/Games/GameJoinError.cs
new file mode 100644 (file)
index 0000000..c30c27d
--- /dev/null
@@ -0,0 +1,48 @@
+namespace Impostor.Server.Net
+{
+    public enum GameJoinError
+    {
+        /// <summary>
+        ///     No error occured while joining the game.
+        /// </summary>
+        None,
+
+        /// <summary>
+        ///     The client is not registered in the client manager.
+        /// </summary>
+        InvalidClient,
+
+        /// <summary>
+        ///     The client has been banned from the game.
+        /// </summary>
+        Banned,
+
+        /// <summary>
+        ///     The game is full.
+        /// </summary>
+        GameFull,
+
+        /// <summary>
+        ///     The limbo state of the player is incorrect.
+        /// </summary>
+        InvalidLimbo,
+
+        /// <summary>
+        ///     The game is already started.
+        /// </summary>
+        GameStarted,
+
+        /// <summary>
+        ///     The game has been destroyed.
+        /// </summary>
+        GameDestroyed,
+
+        /// <summary>
+        ///     Custom error by a plugin.
+        /// </summary>
+        /// <remarks>
+        ///     A custom message can be set in <see cref="GameJoinResult.Message"/>.
+        /// </remarks>
+        Custom,
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Games/GameJoinResult.cs b/src/Impostor.Server.Api/Games/GameJoinResult.cs
new file mode 100644 (file)
index 0000000..96e39e3
--- /dev/null
@@ -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 (file)
index 0000000..e130342
--- /dev/null
@@ -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<IClientPlayer> Players { get; }
+
+        IPEndPoint PublicIp { get; }
+
+        int PlayerCount { get; }
+
+        IClientPlayer Host { get; }
+
+        bool IsPublic { get; }
+
+        IDictionary<object, object> Items { get; }
+
+        int HostId { get; }
+
+        IGameMessageWriter CreateMessage(MessageType type);
+
+        bool TryGetPlayer(int id, [NotNullWhen(true)] out IClientPlayer player);
+
+        /// <summary>
+        ///     Register a new client to the game.
+        /// </summary>
+        /// <param name="client">Client to register.</param>
+        /// <returns>Join result.</returns>
+        ValueTask<GameJoinResult> AddClientAsync(IClient client);
+
+        /// <summary>
+        ///     Kicks all the players from the game to end the game.
+        /// </summary>
+        /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+        ValueTask EndAsync();
+
+        ValueTask HandleStartGame(IMessageReader reader);
+
+        ValueTask HandleEndGame(IMessageReader reader);
+
+        ValueTask HandleKickPlayer(int playerId, bool isBan);
+
+        ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason);
+
+        ValueTask HandleAlterGame(IMessageReader message, IClientPlayer sender, bool isPublic);
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Games/Managers/IGameManager.cs b/src/Impostor.Server.Api/Games/Managers/IGameManager.cs
new file mode 100644 (file)
index 0000000..4f8d33e
--- /dev/null
@@ -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<IGame> Games { get; }
+
+        ValueTask<IGame> CreateAsync(GameOptionsData options);
+
+        IGame? Find(GameCode code);
+
+        IEnumerable<IGame> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10);
+
+        ValueTask RemoveAsync(GameCode code);
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/IGame.cs b/src/Impostor.Server.Api/IGame.cs
deleted file mode 100644 (file)
index b37c5ea..0000000
+++ /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<IClientPlayer> Players { get; }
-        
-        IClientPlayer Host { get; }
-        
-        bool IsPublic { get; }
-        IDictionary<object,object> Items { get; }
-
-        IGameMessageWriter CreateMessage(MessageType type);
-
-        bool TryGetPlayer(int id, [NotNullWhen(true)] out IClientPlayer player);
-    }
-}
\ No newline at end of file
index 95ebef010a5f576b374b04584255093ed2be4ed3..8a568afd54d991658e9f5b90fc422202193087a9 100644 (file)
@@ -4,17 +4,20 @@
         <TargetFramework>net5.0</TargetFramework>
         <RootNamespace>Impostor.Server</RootNamespace>
         <Nullable>enable</Nullable>
+        <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet>
     </PropertyGroup>
 
     <ItemGroup>
-      <ProjectReference Include="..\Imposter.Reactive\Imposter.Reactive.csproj" />
       <ProjectReference Include="..\Impostor.Shared\Impostor.Shared.csproj" />
     </ItemGroup>
 
     <ItemGroup>
       <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0-rc.1.20451.14" />
       <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0-rc.1.20451.14" />
-      <PackageReference Include="System.Interactive.Async" Version="4.1.1" />
+      <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
+        <PrivateAssets>all</PrivateAssets>
+        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      </PackageReference>
     </ItemGroup>
 
-</Project>
+</Project>
\ No newline at end of file
index ec2baa740fa7c49ebb69bd73b41f5411453ea972..c8f8b80d12c6cdc4137ecb7ced1dff8ac6c2ae82 100644 (file)
@@ -1,3 +1,7 @@
 <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+       <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=attributes/@EntryIndexedValue">True</s:Boolean>
+       <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cattributes/@EntryIndexedValue">True</s:Boolean>
+       <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cgame/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean>
+       <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=net_005Cextensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/LimboStates.cs b/src/Impostor.Server.Api/LimboStates.cs
deleted file mode 100644 (file)
index de7f629..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-
-namespace Impostor.Shared.Innersloth.Data
-{
-    [Flags]
-    public enum LimboStates
-    {
-        PreSpawn = 1,
-        NotLimbo = 2,
-        WaitingForHost = 4,
-        All = PreSpawn | NotLimbo | WaitingForHost
-    }
-}
\ No newline at end of file
index 52808c9b78b5757099e90a658d4eee26f3084a33..5bb1d5a9dbc245cb222172bc1ff5090d9fc79059 100644 (file)
@@ -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
index 77bc72e0586b55f104e0a63009a49278338521e5..ac0faecff862c61f0bfbf86403767eaf3575be82 100644 (file)
@@ -4,17 +4,12 @@ namespace Impostor.Server.Net.Factories
 {
     public interface IClientFactory
     {
-        /// <summary>
-        ///     Get the next ID for <see cref="IClient"/>.
-        /// </summary>
-        int NextId();
-
         /// <summary>
         ///     Creates a client for the Hazel <see cref="connection"/>.
         /// </summary>
         /// <param name="connection">Hazel connection.</param>
         /// <param name="name"></param>
         /// <param name="clientVersion"></param>
-        ValueTask<IClient> CreateAsync(IConnection connection, string name, int clientVersion);
+        IClient Create(IConnection connection, string name, int clientVersion);
     }
 }
\ No newline at end of file
index 3228e0bad489589ee979911de86f2f613bce21dd..e83ab90ee84dbc2bde071374a34a7da12b210a2b 100644 (file)
@@ -1,15 +1,65 @@
 using System.Collections.Generic;
+using System.Threading.Tasks;
 
 namespace Impostor.Server.Net
 {
+    /// <summary>
+    ///     Represents a connected game client.
+    /// </summary>
     public interface IClient
     {
-        int Id { get; }
-        
+        /// <summary>
+        ///     Gets or sets the unique ID of the client.
+        /// </summary>
+        /// <remarks>
+        ///     This ID is generated when the client is registered in the client manager and should not be used
+        ///     to store persisted data.
+        /// </remarks>
+        int Id { get; set; }
+
+        /// <summary>
+        ///     Gets the name that was provided by the player in the client.
+        /// </summary>
+        /// <remarks>
+        ///     The name is provided by the player and should not be used to store persisted data.
+        /// </remarks>
         string Name { get; }
-        
-        IConnection Connection { get; }
-        
-        IDictionary<object,object> Items { get; }
+
+        /// <summary>
+        ///     Gets the connection of the client.
+        /// </summary>
+        /// <remarks>
+        ///     Null when the client was not registered by the matchmaker.
+        /// </remarks>
+        IConnection? Connection { get; }
+
+        /// <summary>
+        ///     Gets a value indicating whether the client is a bot.
+        /// </summary>
+        bool IsBot { get; }
+
+        /// <summary>
+        ///     Gets a key/value collection that can be used to share data between messages.
+        /// </summary>
+        /// <remarks>
+        ///     <para>
+        ///         The stored data will not be saved.
+        ///         After the connection has been closed all data will be lost.
+        ///     </para>
+        ///     <para>
+        ///         Note that the values will not be disposed after the connection has been closed.
+        ///         This has to be implemented by the plugin.
+        ///     </para>
+        /// </remarks>
+        IDictionary<object, object> Items { get; }
+
+        /// <summary>
+        ///     Gets or sets the current game data of the <see cref="IClient"/>.
+        /// </summary>
+        IClientPlayer? Player { get; set; }
+
+        ValueTask HandleMessageAsync(IMessage message);
+
+        ValueTask HandleDisconnectAsync();
     }
 }
\ No newline at end of file
index 4034875bde692445e35ab9c2d1556723c08016ae..0805e74b049a006bc916b4a1593798cc6e02409b 100644 (file)
@@ -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
 {
+    /// <summary>
+    ///     Represents a player in <see cref="IGame"/>.
+    /// </summary>
     public interface IClientPlayer
     {
+        /// <summary>
+        ///     Gets the client that belongs to the player.
+        /// </summary>
         IClient Client { get; }
-        
+
+        /// <summary>
+        ///     Gets the game where the <see cref="IClientPlayer"/> belongs to.
+        /// </summary>
         IGame Game { get; }
-        
-        LimboStates Limbo { get; }
+
+        /// <summary>
+        ///     Gets or sets the current limbo state of the player.
+        /// </summary>
+        LimboStates Limbo { get; set; }
+
+        ValueTask KickAsync();
+
+        ValueTask BanAsync();
     }
 }
\ No newline at end of file
index 9aa3203a54e7c1f65e3f96691ba69c8fc443fedf..a1d12695610344387007cf54a915bf75d29b8998 100644 (file)
@@ -1,16 +1,44 @@
 using System;
 using System.Net;
+using System.Threading.Tasks;
 
 namespace Impostor.Server.Net
 {
+    /// <summary>
+    ///     Represents the connection of the client.
+    /// </summary>
     public interface IConnection
     {
-        IAsyncObservable<IMessage> MessageReceived { get; }
-        
+        /// <summary>
+        ///     Gets the IP endpoint of the client.
+        /// </summary>
         IPEndPoint EndPoint { get; }
-        
+
+        /// <summary>
+        ///     Gets a value indicating whether the client is connected to the server.
+        /// </summary>
         bool IsConnected { get; }
 
-        IConnectionMessageWriter CreateMessage(MessageType type);
+        /// <summary>
+        ///     Gets or sets the client of the connection.
+        /// </summary>
+        IClient? Client { get; set; }
+
+        /// <summary>
+        ///     Create a message writer that can be send to the connection.
+        /// </summary>
+        /// <remarks>
+        ///     Be aware when implementing a custom connection handler that this method is not called when a message
+        ///     is being send in <see cref="IGame"/>.
+        /// </remarks>
+        /// <param name="messageType">Type of the message.</param>
+        /// <returns>Message writer for the current connection.</returns>
+        IConnectionMessageWriter CreateMessage(MessageType messageType);
+
+        /// <summary>
+        ///     Start listening to the client.
+        /// </summary>
+        /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+        ValueTask ListenAsync();
     }
 }
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Net/LimboStates.cs b/src/Impostor.Server.Api/Net/LimboStates.cs
new file mode 100644 (file)
index 0000000..de7f629
--- /dev/null
@@ -0,0 +1,13 @@
+using System;
+
+namespace Impostor.Shared.Innersloth.Data
+{
+    [Flags]
+    public enum LimboStates
+    {
+        PreSpawn = 1,
+        NotLimbo = 2,
+        WaitingForHost = 4,
+        All = PreSpawn | NotLimbo | WaitingForHost
+    }
+}
\ No newline at end of file
index 82bdbe01b72c75518ea630d84fa73cd4ef6d5fc6..46da8484d5736a1da080403e7a258f83c8d9542d 100644 (file)
@@ -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
index 561c42648942a57840b151194c80bc570df084c6..244d8eca8477605fafe2aaf40b73bf89e63b7524 100644 (file)
@@ -3,12 +3,30 @@ using System.Threading.Tasks;
 
 namespace Impostor.Server.Net.Manager
 {
+    /// <summary>
+    ///     Represents the matchmaker which will listen for incoming connections.
+    /// </summary>
     public interface IMatchmaker
     {
+        /// <summary>
+        ///     Starts the matchmaker on the given endpoint.
+        /// </summary>
+        /// <param name="ipEndPoint">Endpoint where the matchmaker should listen to.</param>
+        /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
         ValueTask StartAsync(IPEndPoint ipEndPoint);
 
+        /// <summary>
+        ///     Stop the matchmaker.
+        /// </summary>
+        /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
         ValueTask StopAsync();
 
+        /// <summary>
+        ///     Create a message writer that can be send to players in the game.
+        /// </summary>
+        /// <param name="game">The game.</param>
+        /// <param name="messageType">Type of the message.</param>
+        /// <returns>Message writer for the given game.</returns>
         IGameMessageWriter CreateGameMessageWriter(IGame game, MessageType messageType);
     }
 }
\ No newline at end of file
index 0e37c41aaf5f47c06cafe908c3430e8f61ea4483..24206e66593a8af52e2254302081545daab20faa 100644 (file)
@@ -2,8 +2,20 @@
 
 namespace Impostor.Server.Net
 {
+    /// <summary>
+    ///     Represents the message writer for <see cref="IConnection"/>.
+    /// </summary>
     public interface IConnectionMessageWriter : IMessageWriter
     {
+        /// <summary>
+        ///     Gets the connection where the message writer belongs to.
+        /// </summary>
+        public IConnection Connection { get; }
+
+        /// <summary>
+        ///     Sends the message to the <see cref="Connection"/>.
+        /// </summary>
+        /// <returns>Task.</returns>
         ValueTask SendAsync();
     }
 }
\ No newline at end of file
index 2d947e9b6cde9ee939edff8d7844b46d80e10082..4b8267d38b2190165415a292a927d5a3d95e64b1 100644 (file)
@@ -3,25 +3,31 @@ using Impostor.Shared.Innersloth.Data;
 
 namespace Impostor.Server.Net
 {
+    /// <summary>
+    ///     Represents the message writer for <see cref="IGame"/>.
+    /// </summary>
     public interface IGameMessageWriter : IMessageWriter
     {
         /// <summary>
         ///     Send the message to all players.
         /// </summary>
-        /// <param name="states"></param>
-        ValueTask SendToAllAsync(LimboStates states);
+        /// <param name="states">Required limbo state of the player.</param>
+        /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+        ValueTask SendToAllAsync(LimboStates states = LimboStates.NotLimbo);
 
         /// <summary>
         ///     Send the message to all players except one.
         /// </summary>
-        /// <param name="states"></param>
         /// <param name="senderId">The player to exclude from sending the message.</param>
-        ValueTask SendToAllExceptAsync(LimboStates states, int senderId);
-        
+        /// <param name="states">Required limbo state of the player.</param>
+        /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
+        ValueTask SendToAllExceptAsync(int senderId, LimboStates states = LimboStates.NotLimbo);
+
         /// <summary>
         ///     Send a message to a specific player.
         /// </summary>
-        /// <param name="id"></param>
+        /// <param name="id">ID of the client.</param>
+        /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns>
         ValueTask SendToAsync(int id);
     }
 }
\ No newline at end of file
index b8364ff5372882b493e9ddca7fe434b4da83f9f0..e829d1b8833ebfa85dff9074180f427eee9175d6 100644 (file)
@@ -3,7 +3,7 @@
     public interface IMessage
     {
         MessageType Type { get; }
-        
+
         IMessageReader CreateReader();
     }
 }
\ No newline at end of file
index a7fb66b8ae3b775393516003d3e8cc9103570269..c7c288cb2fabe41737ae77cc8b7df4a07463281c 100644 (file)
@@ -4,14 +4,26 @@ namespace Impostor.Server.Net
 {
     public interface IMessageReader
     {
+        /// <summary>
+        ///     Gets the current position of the reader.
+        /// </summary>
         int Position { get; }
-        
+
+        /// <summary>
+        ///     Gets the buffer of the message.
+        /// </summary>
         ReadOnlyMemory<byte> Buffer { get; }
-        
+
+        /// <summary>
+        ///     Gets the tag of the message.
+        /// </summary>
         byte Tag { get; }
-        
+
+        /// <summary>
+        ///     Gets the length of the buffer.
+        /// </summary>
         int Length { get; }
-        
+
         bool ReadBoolean();
 
         sbyte ReadSByte();
@@ -37,7 +49,7 @@ namespace Impostor.Server.Net
         int ReadPackedInt32();
 
         uint ReadPackedUInt32();
-        
+
         void CopyTo(IMessageWriter writer);
     }
 }
\ No newline at end of file
index 007baf89107a4676fa4d570f6d48af2b247629be..a1f0632d974ed7a221eb79fe2f8fd2f5799f92b0 100644 (file)
@@ -3,38 +3,104 @@ using System.Net;
 
 namespace Impostor.Server.Net
 {
+    /// <summary>
+    ///     Base message writer.
+    /// </summary>
     public interface IMessageWriter : IDisposable
     {
+        /// <summary>
+        ///     Writes a boolean to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(bool value);
 
+        /// <summary>
+        ///     Writes a sbyte to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(sbyte value);
 
+        /// <summary>
+        ///     Writes a byte to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(byte value);
 
+        /// <summary>
+        ///     Writes a short to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(short value);
 
+        /// <summary>
+        ///     Writes an ushort to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(ushort value);
 
+        /// <summary>
+        ///     Writes an uint to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(uint value);
 
+        /// <summary>
+        ///     Writes an int to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(int value);
 
+        /// <summary>
+        ///     Writes a float to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(float value);
 
+        /// <summary>
+        ///     Writes a string to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void Write(string value);
-        
-        void Write(IPAddress ipAddress);
-        
+
+        /// <summary>
+        ///     Writes a <see cref="IPAddress"/> to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
+        void Write(IPAddress value);
+
+        /// <summary>
+        ///     Writes an packed int to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
         void WritePacked(int value);
-        
+
+        /// <summary>
+        ///     Writes raw bytes to the message.
+        /// </summary>
+        /// <param name="data">Bytes to write.</param>
         void Write(ReadOnlyMemory<byte> data);
-        
+
+        /// <summary>
+        ///     Writes a game code to the message.
+        /// </summary>
+        /// <param name="value">Value to write.</param>
+        void Write(GameCode value);
+
+        /// <summary>
+        ///     Starts a new message.
+        /// </summary>
+        /// <param name="typeFlag">Message flag header.</param>
         void StartMessage(byte typeFlag);
 
-        void Write(GameCode code);
-        
+        /// <summary>
+        ///     Mark the end of the message.
+        /// </summary>
         void EndMessage();
 
+        /// <summary>
+        ///     Clear the message writer.
+        /// </summary>
+        /// <param name="type">New type of the message.</param>
         void Clear(MessageType type);
     }
 }
\ No newline at end of file
index 00f030af85f8adde67669c7e368e6aa82bc98d65..2e9dcb27d4705adc2f80f484c8f2e3f089f5e369 100644 (file)
@@ -1,8 +1,29 @@
 namespace Impostor.Server.Net
 {
+    /// <summary>
+    ///     Specifies how a message should be sent between connections.
+    /// </summary>
     public enum MessageType
     {
+        /// <summary>
+        ///     Requests unreliable delivery with no fragmentation.
+        /// </summary>
+        /// <remarks>
+        ///     Sending data using unreliable delivery means that data is not guaranteed to arrive at it's destination nor is
+        ///     it guaranteed to arrive only once. However, unreliable delivery can be faster than other methods and it
+        ///     typically requires a smaller number of protocol bytes than other methods. There is also typically less
+        ///     processing involved and less memory needed as packets are not stored once sent.
+        /// </remarks>
         Unreliable,
-        Reliable
+
+        /// <summary>
+        ///     Requests data be sent reliably but with no fragmentation.
+        /// </summary>
+        /// <remarks>
+        ///     Sending data reliably means that data is guaranteed to arrive and to arrive only once. Reliable delivery
+        ///     typically requires more processing, more memory (as packets need to be stored in case they need resending),
+        ///     a larger number of protocol bytes and can be slower than unreliable delivery.
+        /// </remarks>
+        Reliable,
     }
 }
\ No newline at end of file
diff --git a/src/Impostor.Server.Api/Plugins/IPlugin.cs b/src/Impostor.Server.Api/Plugins/IPlugin.cs
new file mode 100644 (file)
index 0000000..129cf15
--- /dev/null
@@ -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 (file)
index 0000000..eb09de8
--- /dev/null
@@ -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 (file)
index 0000000..c2f447c
--- /dev/null
@@ -0,0 +1,11 @@
+<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0">
+    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
+        <Rule Id="SA1200" Action="None" />
+    </Rules>
+    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules">
+        <Rule Id="SA1101" Action="None" />
+    </Rules>
+    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
+        <Rule Id="SA1309" Action="None" />
+    </Rules>
+</RuleSet>
\ No newline at end of file
index af966057519220b4bc50fa36e8fefd6e26ddf9b8..ce87dacebd9fed3ed34b3027ec28a57bc285aecc 100644 (file)
@@ -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<HazelConnection> _logger;
-        private readonly ConcurrentSimpleAsyncSubject<IMessage> _messageReceived = new ConcurrentSimpleAsyncSubject<IMessage>();
+        private readonly ConcurrentStack<DataReceivedEventArgs> _pendingMessages;
 
         public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger)
         {
             _logger = logger;
+            _pendingMessages = new ConcurrentStack<DataReceivedEventArgs>();
             InnerConnection = innerConnection;
             innerConnection.DataReceived += ConnectionOnDataReceived;
             innerConnection.Disconnected += ConnectionOnDisconnected;
         }
-        
-        public Connection InnerConnection { get; }
 
-        public IAsyncObservable<IMessage> MessageReceived => _messageReceived;
+        public Connection InnerConnection { get; }
 
         public IPEndPoint EndPoint => InnerConnection.EndPoint;
 
         public bool IsConnected => InnerConnection.State == ConnectionState.Connected;
 
+        public IClient Client { get; set; }
+
         private void ConnectionOnDisconnected(object sender, DisconnectedEventArgs e)
         {
-            Task.Run(_messageReceived.OnCompletedAsync);
+            if (Client != null)
+            {
+                Task.Run(Client.HandleDisconnectAsync);
+            }
         }
 
         private void ConnectionOnDataReceived(DataReceivedEventArgs e)
@@ -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
index bfdfaeedc348918aacfd18594eda3394c9ce3160..439a7736ac1985caf6d82e08611176d3a6a1c1cf 100644 (file)
@@ -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)
index c0d27d52cde17139c67a7690a2ec296b3aecee64..b12629ceba38fb14c8e02c674693e775083dcbea 100644 (file)
@@ -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;
         }
     }
index 8af291f77e2babce501db9481ac15247cad65a36..e1e063e53f08eb2655df4844188037b236470622 100644 (file)
@@ -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;
         }
     }
index e63df4cf5c10da8dd1ac7cfec108a0c9e156ed7b..b16a192ae791d4111d28e0f4090c36d37a281ba3 100644 (file)
@@ -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()
index 5bcf68df3b2d523532758ab1ca62fffbb9d2e46b..da5f5a7ac1ff1e4691b424a201f2480b755accbe 100644 (file)
@@ -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
index 3233167f887197a0797d48c274c5f58699fe5247..5b14ef109f409bcb6319c7c4c36d381aebf266f5 100644 (file)
@@ -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<ServerRedirectorNode> Nodes { get; set; }
 
         public class NodeLocator
         {
             public string Redis { get; set; }
+
             public string UdpMasterEndpoint { get; set; }
         }
     }
index 328ad65f4b64afaa0f7033972beb342c6fd21fa3..2e6a0b8db3a1c76eea8ce5a90d88e8d26ce216ce 100644 (file)
@@ -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 (file)
index 0000000..61ac4d8
--- /dev/null
@@ -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 (file)
index 0000000..f3d33ca
--- /dev/null
@@ -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;
+        }
+
+        /// <inheritdoc />
+        public bool IsRegistered<TEvent>()
+            where TEvent : IEvent
+        {
+            using var scope = _serviceProvider.CreateScope();
+
+            return GetHandlers<TEvent>(_serviceProvider).Any();
+        }
+
+        /// <inheritdoc />
+        public async ValueTask CallAsync<T>(T @event)
+            where T : IEvent
+        {
+            var scope = _serviceProvider.CreateScope();
+
+            try
+            {
+                foreach (var (handler, eventListener) in GetHandlers<T>(scope.ServiceProvider))
+                {
+                    await eventListener.InvokeAsync(handler, @event, scope.ServiceProvider);
+                }
+            }
+            finally
+            {
+                scope.Dispose();
+            }
+        }
+
+        /// <summary>
+        ///     Get all the event listeners for the given event type.
+        /// </summary>
+        /// <param name="services">Current service provider.</param>
+        /// <returns>The event listeners.</returns>
+        private static IEnumerable<EventHandler> GetHandlers<TEvent>(IServiceProvider services)
+            where TEvent : IEvent
+        {
+            foreach (var handler in services.GetServices<IEventListener>())
+            {
+                var events = RegisteredEventListener.FromType(handler.GetType());
+
+                foreach (var eventHandler in events)
+                {
+                    if (eventHandler.EventType != typeof(TEvent))
+                    {
+                        continue;
+                    }
+
+                    yield return new EventHandler(handler, eventHandler);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Impostor.Server/Events/RegisteredEventListener.cs b/src/Impostor.Server/Events/RegisteredEventListener.cs
new file mode 100644 (file)
index 0000000..058f099
--- /dev/null
@@ -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<Type, RegisteredEventListener[]> Instances = new ConcurrentDictionary<Type, RegisteredEventListener[]>();
+        private readonly Func<object, object, IServiceProvider, ValueTask> _invoker;
+        private readonly Type _eventListenerType;
+
+        public RegisteredEventListener(Type eventType, MethodInfo method, EventListenerAttribute attribute, Type eventListenerType)
+        {
+            EventType = eventType;
+            _eventListenerType = eventListenerType;
+            Priority = attribute.Priority;
+            PriorityOrder = attribute.PriorityOrder;
+            IgnoreCancelled = attribute.IgnoreCancelled;
+            Method = method.GetFriendlyName(showParameters: false);
+            _invoker = CreateInvoker(method, attribute.IgnoreCancelled);
+        }
+
+        public Type EventType { get; }
+
+        public EventPriority Priority { get; }
+
+        public int PriorityOrder { get; set; }
+
+        public bool IgnoreCancelled { get; }
+
+        public string Method { get; }
+
+        public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider)
+        {
+            return _invoker(eventHandler, @event, provider);
+        }
+
+        private Func<object, object, IServiceProvider, ValueTask> CreateInvoker(MethodInfo method, bool ignoreCancelled)
+        {
+            var instance = Expression.Parameter(typeof(object), "instance");
+            var eventParameter = Expression.Parameter(typeof(object), "event");
+            var provider = Expression.Parameter(typeof(IServiceProvider), "provider");
+            var @event = Expression.Convert(eventParameter, EventType);
+
+            var getRequiredService = typeof(ServiceProviderServiceExtensions)
+                .GetMethod("GetRequiredService", new[] { typeof(IServiceProvider) });
+
+            if (getRequiredService == null)
+            {
+                throw new InvalidOperationException("The method GetRequiredService could not be found.");
+            }
+
+            var methodArguments = method.GetParameters();
+            var arguments = new Expression[methodArguments.Length];
+
+            for (var i = 0; i < methodArguments.Length; i++)
+            {
+                var methodArgument = methodArguments[i];
+
+                if (methodArgument.ParameterType == EventType)
+                {
+                    arguments[i] = @event;
+                }
+                else
+                {
+                    arguments[i] = Expression.Call(
+                        getRequiredService.MakeGenericMethod(methodArgument.ParameterType),
+                        provider);
+                }
+            }
+
+            var returnTarget = Expression.Label(typeof(ValueTask));
+            Expression invoke = Expression.Call(Expression.Convert(instance, _eventListenerType), method, arguments);
+
+            if (method.ReturnType == typeof(void))
+            {
+                if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType))
+                {
+                    invoke = Expression.Block(
+                        Expression.IfThenElse(
+                            Expression.Property(@event, nameof(IEventCancelable.IsCancelled)),
+                            Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)),
+                            Expression.Block(
+                                invoke,
+                                Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)))),
+                        Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask)));
+                }
+                else
+                {
+                    invoke = Expression.Block(
+                        invoke,
+                        Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask)));
+                }
+            }
+            else if (method.ReturnType == typeof(ValueTask))
+            {
+                if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType))
+                {
+                    invoke = Expression.Block(
+                        Expression.IfThenElse(
+                            Expression.Property(@event, nameof(IEventCancelable.IsCancelled)),
+                            Expression.Return(returnTarget, Expression.Constant(Task.CompletedTask)),
+                            Expression.Return(returnTarget, invoke)),
+                        Expression.Label(returnTarget, Expression.Constant(Task.CompletedTask)));
+                }
+            }
+            else
+            {
+                throw new InvalidOperationException($"The method {method.GetFriendlyName()} must return void or ValueTask.");
+            }
+
+            return Expression.Lambda<Func<object, object, IServiceProvider, ValueTask>>(invoke,  instance, eventParameter, provider)
+                .Compile();
+        }
+
+        public static IEnumerable<RegisteredEventListener> FromType(Type type)
+        {
+            return Instances.GetOrAdd(type, t =>
+            {
+                return t.GetMethods()
+                    .Where(m => !m.IsStatic && m.GetCustomAttribute(typeof(EventListenerAttribute), false) != null)
+                    .SelectMany(m => FromMethod(t, m))
+                    .ToArray();
+            });
+        }
+
+        public static IEnumerable<RegisteredEventListener> FromMethod(Type listenerType, MethodInfo methodType)
+        {
+            // Get the return type.
+            var returnType = methodType.ReturnType;
+
+            if (returnType != typeof(void) && returnType != typeof(ValueTask))
+            {
+                throw new InvalidOperationException($"The method {methodType.GetFriendlyName()} does not return void or ValueTask.");
+            }
+
+            // Register the event.
+            var attribute = methodType.GetCustomAttribute<EventListenerAttribute>(false);
+
+            if (attribute == null)
+            {
+                yield break;
+            }
+
+            Type[] eventTypes;
+
+            if (attribute.Events.Length == 0)
+            {
+                if (methodType.GetParameters().Length == 0 || !typeof(IEvent).IsAssignableFrom(methodType.GetParameters()[0].ParameterType))
+                {
+                    throw new InvalidOperationException($"The first parameter of the method {methodType.GetFriendlyName()} should be the type {nameof(IEvent)}.");
+                }
+
+                eventTypes = new[] { methodType.GetParameters()[0].ParameterType };
+            }
+            else
+            {
+                eventTypes = attribute.Events;
+            }
+
+            foreach (var eventType in eventTypes)
+            {
+                var listener = new RegisteredEventListener(eventType, methodType, attribute, listenerType);
+
+                yield return listener;
+            }
+        }
+    }
+}
\ No newline at end of file
index 2e622f2f18298b521b919e1b9dc3bddce2a4807b..d6ace6ca7ac7f75fa1c510c022dd514bedab1fc6 100644 (file)
@@ -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 (file)
index 0000000..55d42fb
--- /dev/null
@@ -0,0 +1,67 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+
+namespace Impostor.Server
+{
+    internal static class TypeExtensions
+    {
+        /// <summary>
+        ///     Get the friendly name for the type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>The friendly name.</returns>
+        [SuppressMessage("ReSharper", "SA1503", Justification = "Readability")]
+        public static string GetFriendlyName(this Type type)
+        {
+            if (type == null)
+                return "null";
+            if (type == typeof(int))
+                return "int";
+            if (type == typeof(short))
+                return "short";
+            if (type == typeof(byte))
+                return "byte";
+            if (type == typeof(bool))
+                return "bool";
+            if (type == typeof(long))
+                return "long";
+            if (type == typeof(float))
+                return "float";
+            if (type == typeof(double))
+                return "double";
+            if (type == typeof(decimal))
+                return "decimal";
+            if (type == typeof(string))
+                return "string";
+            if (type.IsGenericType)
+                return type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(GetFriendlyName).ToArray()) + ">";
+            return type.Name;
+        }
+
+        /// <summary>
+        ///     Get the friendly name for the method.
+        /// </summary>
+        /// <param name="method">The method.</param>
+        /// <param name="showParameters">True if the parameters should be included in the name.</param>
+        /// <returns>Friendly name of the method</returns>
+        public static string GetFriendlyName(this MethodBase method, bool showParameters = true)
+        {
+            var str = method.Name;
+
+            if (method.DeclaringType != null)
+            {
+                str = method.DeclaringType.GetFriendlyName() + '.' + str;
+            }
+
+            if (showParameters)
+            {
+                var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.GetFriendlyName()));
+                str += $"({parameters})";
+            }
+
+            return str;
+        }
+    }
+}
\ No newline at end of file
index b70cb0816b9eef43fa0aaec5a608a2cdc9191421..017b18c60a02c1bd292db74bf66dc857f3dcc6f7 100644 (file)
@@ -13,6 +13,7 @@
       <Version>1.1.0</Version>
       <ApplicationIcon>icon.ico</ApplicationIcon>
       <DebugType Condition=" '$(Configuration)' == 'Release' ">None</DebugType>
+      <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet>
     </PropertyGroup>
 
     <ItemGroup>
       <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0-rc.1.20451.14" />
       <PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
       <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
+      <PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
+        <PrivateAssets>all</PrivateAssets>
+        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      </PackageReference>
     </ItemGroup>
 
     <ItemGroup>
@@ -46,8 +51,4 @@
       </Content>
     </ItemGroup>
 
-    <ItemGroup>
-      <Folder Include="Net\Hazel" />
-    </ItemGroup>
-
-</Project>
+</Project>
\ 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 (file)
index 0000000..17962b1
--- /dev/null
@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+       <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
index fd4c581eda29f5278753e0896c0e8d24d3a875b2..b81a6061b185e04bb2894dfc9b6d557dfed36f5a 100644 (file)
@@ -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<Client>();
-        
+
         private readonly IClientManager _clientManager;
-        private readonly GameManager _gameManager;
+        private readonly IGameManager _gameManager;
 
-        public Client(IClientManager clientManager, GameManager gameManager, int id, string name, IConnection connection)
-            : base(id, name, connection)
+        public Client(IClientManager clientManager, IGameManager gameManager, string name, IConnection connection)
+            : base(name, connection)
         {
             _clientManager = clientManager;
             _gameManager = gameManager;
-            Player = new ClientPlayer(this, _gameManager);
-        }
-
-        public ClientPlayer Player { get; }
-        
-        private bool IsPacketAllowed(IMessageReader message, bool hostOnly)
-        {
-            var game = Player.Game;
-            if (game == null)
-            {
-                return false;
-            }
-
-            // GameCode must match code of the current game assigned to the player.
-            if (message.ReadInt32() != game.Code)
-            {
-                return false;
-            }
-            
-            // Some packets should only be sent by the host of the game.
-            if (hostOnly)
-            {
-                if (game.HostId == Id)
-                {
-                    return true;
-                }
-                
-                Logger.Warning("[{0}] Client sent packet only allowed by the host ({1}).", Id, game.HostId);
-                return false;
-            }
-
-            return true;
         }
 
-        
-        protected override async ValueTask OnMessageReceived(IMessage message)
+        public override async ValueTask HandleMessageAsync(IMessage message)
         {
             var reader = message.CreateReader();
-            
+
             var flag = reader.Tag;
-            
+
             Logger.Verbose("[{0}] Server got {1}.", Id, flag);
-            
+
             switch (flag)
             {
                 case MessageFlags.HostGame:
                 {
                     // Read game settings.
                     var gameInfo = Message00HostGame.Deserialize(reader);
-                    
+
                     // Create game.
-                    var game = _gameManager.Create(gameInfo);
-                    if (game == null)
-                    {
-                        await Player.SendDisconnectReason(DisconnectReason.ServerFull);
-                        return;
-                    }
+                    var game = await _gameManager.CreateAsync(gameInfo);
 
                     // Code in the packet below will be used in JoinGame.
-                    using (var writer = Connection.CreateMessage(MessageType.Reliable))
-                    {
-                        Message00HostGame.Serialize(writer, game.Code);
-                
-                        await writer.SendAsync();
-                    }
+                    using var writer = Connection.CreateMessage(MessageType.Reliable);
+                    Message00HostGame.Serialize(writer, game.Code);
+
+                    await writer.SendAsync();
+
                     break;
                 }
-                
+
                 case MessageFlags.JoinGame:
                 {
-                    Message01JoinGame.Deserialize(reader, 
-                        out var gameCode, 
-                        out var unknown);
-                    
+                    Message01JoinGame.Deserialize(
+                        reader,
+                        out var gameCode,
+                        out _);
+
                     var game = _gameManager.Find(gameCode);
                     if (game == null)
                     {
-                        await Player.SendDisconnectReason(DisconnectReason.GameMissing);
+                        await SendDisconnectReason(DisconnectReason.GameMissing);
                         return;
                     }
 
-                    await game.HandleJoinGame(Player);
+                    var result = await game.AddClientAsync(this);
+
+                    switch (result.Error)
+                    {
+                        case GameJoinError.None:
+                            break;
+                        case GameJoinError.InvalidClient:
+                            await SendDisconnectReason(DisconnectReason.Custom, "Client is in an invalid state.");
+                            break;
+                        case GameJoinError.Banned:
+                            await SendDisconnectReason(DisconnectReason.Banned);
+                            break;
+                        case GameJoinError.GameFull:
+                            await SendDisconnectReason(DisconnectReason.GameFull);
+                            break;
+                        case GameJoinError.InvalidLimbo:
+                            await SendDisconnectReason(DisconnectReason.Custom, "Invalid limbo state while joining.");
+                            break;
+                        case GameJoinError.GameStarted:
+                            await SendDisconnectReason(DisconnectReason.GameStarted);
+                            break;
+                        case GameJoinError.GameDestroyed:
+                            await SendDisconnectReason(DisconnectReason.Custom, DisconnectMessages.Destroyed);
+                            break;
+                        case GameJoinError.Custom:
+                            await SendDisconnectReason(DisconnectReason.Custom, result.Message);
+                            break;
+                        default:
+                            await SendDisconnectReason(DisconnectReason.Custom, "Unknown error.");
+                            break;
+                    }
+
                     break;
                 }
 
@@ -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;
+        }
+
+        /// <summary>
+        ///     Triggered when the connected client requests the game listing.
+        /// </summary>
+        /// <param name="options">
+        ///     All options given.
+        ///     At this moment, the client can only specify the map, impostor count and chat language.
+        /// </param>
+        private async ValueTask OnRequestGameList(GameOptionsData options)
+        {
+            using var message = Connection.CreateMessage(MessageType.Reliable);
+            var games = _gameManager.FindListings((MapFlags)options.MapId, options.NumImpostors, options.Keywords);
+
+            var skeldGameCount = _gameManager.GetGameCount(MapFlags.Skeld);
+            var miraHqGameCount = _gameManager.GetGameCount(MapFlags.MiraHQ);
+            var polusGameCount = _gameManager.GetGameCount(MapFlags.Polus);
+
+            Message16GetGameListV2.Serialize(message, skeldGameCount, miraHqGameCount, polusGameCount, games);
+
+            await message.SendAsync();
+        }
+
+        private async ValueTask SendDisconnectReason(DisconnectReason reason, string message = null)
+        {
+            if (Connection == null)
+            {
+                return;
+            }
+
+            using var packet = Connection.CreateMessage(MessageType.Reliable);
+            Message01JoinGame.SerializeError(packet, false, reason, message);
+            await packet.SendAsync();
+        }
     }
 }
\ No newline at end of file
index 03c64144e43c2cf82904f24142b2830b6458bd3b..591d359f27d50ce4f621a5b474bcb83dc5d79ba3 100644 (file)
@@ -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<object, object>();
         }
 
-        public int Id { get; }
-        
+        public int Id { get; set; }
+
         public string Name { get; }
-        
+
         public IConnection Connection { get; }
-        
+
+        public bool IsBot => false;
+
         public IDictionary<object, object> Items { get; }
 
-        public virtual async ValueTask InitializeAsync()
-        {
-            await Connection.MessageReceived.SubscribeAsync(OnMessageReceived, OnDisconnected);
-        }
+        public IClientPlayer Player { get; set; }
 
-        protected abstract ValueTask OnMessageReceived(IMessage message);
+        public abstract ValueTask HandleMessageAsync(IMessage message);
 
-        protected abstract ValueTask OnDisconnected();
+        public abstract ValueTask HandleDisconnectAsync();
     }
 }
\ No newline at end of file
index 3a6f3c966339a1eeb430bcf5d5c9cac20a7ae9bf..653eeda5d432c68d9ba932f4b47049161899e00b 100644 (file)
@@ -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<TClient> : 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<IClient> CreateAsync(IConnection connection, string name, int clientVersion)
-        {
-            if (clientVersion != 50516550)
-            {
-                using var packet = connection.CreateMessage(MessageType.Reliable);
-                Message01JoinGame.SerializeError(packet, false, DisconnectReason.IncorrectVersion);
-                await packet.SendAsync();
-
-                throw new ClientVersionUnsupportedException(clientVersion);
-            }
-            
-            var clientId = NextId();
-            var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, clientId, name, connection);
-
-            await client.InitializeAsync();
-            
+            var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, name, connection);
+            connection.Client = client;
             return client;
         }
     }
index e0ea68e32b00fef7c9b2d79b41d3ef56499feb5c..38e29cbac193cc6d7bdc8507d63895f856d4a07d 100644 (file)
@@ -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<ClientManager> _logger;
         private readonly ConcurrentDictionary<int, IClient> _clients;
         private readonly IClientFactory _clientFactory;
-        
+        private int _idLast;
+
         public ClientManager(ILogger<ClientManager> logger, IClientFactory clientFactory)
         {
             _logger = logger;
@@ -19,24 +22,46 @@ namespace Impostor.Server.Net.Manager
             _clients = new ConcurrentDictionary<int, IClient>();
         }
 
-        public async ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion)
+        public int NextId()
         {
-            try
+            var clientId = Interlocked.Increment(ref _idLast);
+
+            if (clientId < 1)
             {
-                var client = await _clientFactory.CreateAsync(connection, name, clientVersion);
-                
-                Register(client);
+                // Super rare but reset the _idLast because of overflow.
+                _idLast = 0;
+
+                // And get a new id.
+                clientId = Interlocked.Increment(ref _idLast);
             }
-            catch (ClientVersionUnsupportedException ex)
+
+            return clientId;
+        }
+
+        public async ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion)
+        {
+            if (clientVersion != 50516550)
             {
-                _logger.LogTrace("Closed connection because client version {Version} is not supported.", ex.Version);
+                using var packet = connection.CreateMessage(MessageType.Reliable);
+                Message01JoinGame.SerializeError(packet, false, DisconnectReason.IncorrectVersion);
+                await packet.SendAsync();
+                return;
             }
+
+            var client = _clientFactory.Create(connection, name, clientVersion);
+
+            Register(client);
+
+            await connection.ListenAsync();
         }
 
         public void Register(IClient client)
         {
+            var id = NextId();
+
+            client.Id = id;
             _logger.LogInformation("Client connected.");
-            _clients.TryAdd(client.Id, client);
+            _clients.TryAdd(id, client);
         }
 
         public void Remove(IClient client)
@@ -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
index 2b39ab6f15f9a65a088f5f82af1b19612836bf52..a2ca4814874c17b9f3ea22406eb3e16fd8f6d4f5 100644 (file)
@@ -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<GameManager> _logger;
         private readonly INodeLocator _nodeLocator;
         private readonly IPEndPoint _publicIp;
         private readonly ConcurrentDictionary<int, Game> _games;
         private readonly IServiceProvider _serviceProvider;
+        private readonly IEventManager _eventManager;
 
-        public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeLocator nodeLocator, IServiceProvider serviceProvider)
+        public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeLocator nodeLocator, IServiceProvider serviceProvider, IEventManager eventManager)
         {
             _logger = logger;
             _nodeLocator = nodeLocator;
             _serviceProvider = serviceProvider;
+            _eventManager = eventManager;
             _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.PublicIp), config.Value.PublicPort);
             _games = new ConcurrentDictionary<int, Game>();
         }
-        
-        public Game Create(GameOptionsData options)
+
+        public IEnumerable<IGame> Games => _games.Select(kv => kv.Value);
+
+        public async ValueTask<IGame> CreateAsync(GameOptionsData options)
         {
             // TODO: Prevent duplicates when using server redirector using INodeProvider.
-            
             var gameCode = GameCode.Create();
             var gameCodeStr = gameCode.Code;
             var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options);
 
-            if (_nodeLocator.Find(gameCodeStr) == null && 
-                _games.TryAdd(gameCode, game))
+            if (_nodeLocator.Find(gameCodeStr) != null || !_games.TryAdd(gameCode, game))
             {
-                _nodeLocator.Save(gameCodeStr, _publicIp);
-                _logger.LogDebug("Created game with code {0} ({1}).", game.Code, gameCode);        
-                return game;
+                throw new ImpostorException("Could not create new game"); // TODO: Fix generic exception.
             }
 
-            _logger.LogWarning("Failed to create game.");
-            return null;
+            _nodeLocator.Save(gameCodeStr, _publicIp);
+            _logger.LogDebug("Created game with code {0} ({1}).", game.Code, gameCode);
+
+            await _eventManager.CallAsync(new GameCreatedEvent(game));
+
+            return game;
         }
 
-        public Game Find(int gameCode)
+        public IGame Find(GameCode code)
         {
-            _games.TryGetValue(gameCode, out var game);
+            _games.TryGetValue(code, out var game);
             return game;
         }
 
-        public IEnumerable<Game> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10)
+        public IEnumerable<IGame> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10)
         {
             var results = 0;
-            
+
             // Find games that have not started yet.
-            foreach (var (code, game) in _games.Where(x => 
+            foreach (var (_, game) in _games.Where(x =>
                 x.Value.IsPublic &&
-                x.Value.GameState == GameStates.NotStarted && 
+                x.Value.GameState == GameStates.NotStarted &&
                 x.Value.PlayerCount < x.Value.Options.MaxPlayers))
             {
                 // Check for options.
-                if (!map.HasFlag((MapFlags) (1 << game.Options.MapId)))
+                if (!map.HasFlag((MapFlags)(1 << game.Options.MapId)))
                 {
                     continue;
                 }
@@ -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
index f98f7ef862cdf89b202c000e2468f78f716e66c2..745d9f3c63ff80a1bfd3600f9f4e4e91679d3965 100644 (file)
@@ -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();
         }
 
index 6335a18476cc7cf9557c839fae891503db831f7b..bb06b6354e86725fbfeb1d6473dd4ad6b7ae9af8 100644 (file)
@@ -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();
         }
 
index 6a4494df4382ec3789eb9fa81730af9cefe68382..7fba0d7b4489c9f922c9a1a643d3e8db1614ddde 100644 (file)
@@ -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();
         }
     }
index fad8e33d115cfe01e4e78939f27cb6b96455b969..772c4c4227aa47c0c0c28fb7a244651d9cab1624 100644 (file)
@@ -10,7 +10,7 @@ namespace Impostor.Server.Net.Messages
             {
                 writer.Clear(MessageType.Reliable);
             }
-            
+
             writer.StartMessage(MessageFlags.HostGame);
             writer.Write(gameCode);
             writer.EndMessage();
index 8b899abd33ed2f536895b24406ec816ea8f209bd..9a9c2093e892e8a11eea09b801abe6d352f4a780 100644 (file)
@@ -8,7 +8,7 @@
             {
                 writer.Clear(MessageType.Reliable);
             }
-            
+
             writer.StartMessage(MessageFlags.KickPlayer);
             writer.Write(gameCode);
             writer.WritePacked(playerId);
index 65d4cfa022c516382db9e279bcdb1a68ea5c5bc4..2f1cd0326e04f224fe6093b61afda648e2095ecd 100644 (file)
@@ -8,7 +8,7 @@
             {
                 writer.Clear(MessageType.Reliable);
             }
-            
+
             writer.StartMessage(MessageFlags.WaitForHost);
             writer.Write(gameCode);
             writer.Write(playerId);
index 5219564a30f72fdc0ef425ca0d0b7d86d1b72dd7..17ba12f21abcc9a44de327d7dfbb4ca73440f386 100644 (file)
@@ -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();
         }
     }
index 038d1689f4eea44109b3391c8d31ae8e9a4f9038..4b71065558233c7823a1a212c840162a677b456b 100644 (file)
@@ -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<Game> games)
+        public static void Serialize(IMessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable<IGame> games)
         {
             writer.StartMessage(MessageFlags.GetGameListV2);
-                
+
             // Count
             writer.StartMessage(1);
             writer.Write(skeldGameCount); // The Skeld
             writer.Write(miraHqGameCount); // Mira HQ
             writer.Write(polusGameCount); // Polus
             writer.EndMessage();
-                
+
             // Listing
             writer.StartMessage(0);
+
             foreach (var game in games)
             {
                 writer.StartMessage(0);
@@ -39,8 +39,8 @@ namespace Impostor.Server.Net.Messages
                 writer.Write((byte) game.Options.MaxPlayers);
                 writer.EndMessage();
             }
+
             writer.EndMessage();
-                
             writer.EndMessage();
         }
     }
index 50951ca19ff2626631c1a461855755233b75a363..ac5da50a6ca3c1c2b39e99d3e7b997a7fc00b9e1 100644 (file)
@@ -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;
index f7dcf5709c802a3f90614acd865753e0abdc0ed5..2dd2f07848da3f1e233906384830a504dcba97ef 100644 (file)
@@ -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
index e611887879883cf4a85eb05a25814dd3095330c6..36632a7106a347bde492b319fd4e7063cfceca09 100644 (file)
@@ -8,7 +8,7 @@ namespace Impostor.Server.Net.Redirector
     public class NodeLocatorRedis : INodeLocator
     {
         private readonly IDistributedCache _cache;
-        
+
         public NodeLocatorRedis(ILogger<NodeLocatorRedis> 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),
             });
         }
 
index aad8149056b50176e15f6b8d6e80226a85697c37..8fe843bb67d4eca19bd7793bf7addfd902c910e9 100644 (file)
@@ -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<NodeLocatorUDP> _logger;
+        private readonly ILogger<NodeLocatorUdp> _logger;
         private readonly bool _isMaster;
         private readonly IPEndPoint _server;
         private readonly UdpClient _client;
         private readonly ConcurrentDictionary<string, AvailableNode> _availableNodes;
-        
-        public NodeLocatorUDP(ILogger<NodeLocatorUDP> logger, IOptions<ServerRedirectorConfig> config)
+
+        public NodeLocatorUdp(ILogger<NodeLocatorUdp> logger, IOptions<ServerRedirectorConfig> config)
         {
             _logger = logger;
-            
+
             if (config.Value.Master)
             {
                 _isMaster = true;
@@ -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
index 46d27f78c4b3e905aac06907160b1daa7d6c401b..825819883d32b9035691015736464f1b280a362f 100644 (file)
@@ -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<NodeLocatorUDPService> _logger;
+        private readonly NodeLocatorUdp _nodeLocator;
+        private readonly ILogger<NodeLocatorUdpService> _logger;
         private readonly UdpClient _client;
 
-        public NodeLocatorUDPService(
-            INodeLocator nodeLocator, 
-            ILogger<NodeLocatorUDPService> logger, 
+        public NodeLocatorUdpService(
+            INodeLocator nodeLocator,
+            ILogger<NodeLocatorUdpService> logger,
             IOptions<ServerRedirectorConfig> options)
         {
-            _nodeLocator = (NodeLocatorUDP) nodeLocator;
+            _nodeLocator = (NodeLocatorUdp)nodeLocator;
             _logger = logger;
 
             if (!IPEndPoint.TryParse(options.Value.Locator.UdpMasterEndpoint, out var endpoint))
@@ -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.");
         }
     }
index 864a540f73efc0532ed91606b9dcce6664c88e22..158090c9544b87c47416c3eb34c448d429f77023 100644 (file)
@@ -10,7 +10,7 @@ namespace Impostor.Server.Net.Redirector
         private readonly List<IPEndPoint> _nodes;
         private readonly object _lock;
         private int _currentIndex;
-        
+
         public NodeProviderConfig(IOptions<ServerRedirectorConfig> redirectorConfig)
         {
             _nodes = new List<IPEndPoint>();
@@ -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 (file)
index 0468618..0000000
+++ /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
-    {
-        /// <summary>
-        ///     Triggered when the connected client requests the game listing.
-        /// </summary>
-        /// <param name="options">
-        ///     All options given.
-        ///     At this moment, the client can only specify the map, impostor count and chat language.
-        /// </param>
-        public async ValueTask OnRequestGameList(GameOptionsData options)
-        {
-            using (var message = Client.Connection.CreateMessage(MessageType.Reliable))
-            {
-                var games = _gameManager.FindListings((MapFlags) options.MapId, options.NumImpostors, options.Keywords);
-
-                var skeldGameCount = _gameManager.GetGameCount(MapFlags.Skeld);
-                var miraHqGameCount = _gameManager.GetGameCount(MapFlags.MiraHQ);
-                var polusGameCount = _gameManager.GetGameCount(MapFlags.Polus);
-
-                Message16GetGameListV2.Serialize(message, skeldGameCount, miraHqGameCount, polusGameCount, games);
-
-                await message.SendAsync();
-            }
-        }
-    }
-}
\ No newline at end of file
index f186e179cb7b1c34367da6a68ef73e0f1b03c34c..1f3d3da569ea40800416b007004d36c539a79b05 100644 (file)
@@ -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; }
+
+        /// <inheritdoc />
+        public LimboStates Limbo { get; set; }
 
+        /// <inheritdoc />
         IClient IClientPlayer.Client => Client;
 
+        /// <inheritdoc />
         IGame IClientPlayer.Game => Game;
+
+        /// <inheritdoc />
+        public ValueTask KickAsync()
+        {
+            return Game.HandleKickPlayer(Client.Id, false);
+        }
+
+        /// <inheritdoc />
+        public ValueTask BanAsync()
+        {
+            return Game.HandleKickPlayer(Client.Id, true);
+        }
     }
 }
\ No newline at end of file
index 8b7db28a595b4719c1226fa412eabedbbeef3133..fa8380a73cc8c2017de4231d5036b4cb8e9122e1 100644 (file)
@@ -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<GameJoinResult> AddClientAsync(IClient client)
         {
             // Check if the IP of the player is banned.
-            if (_bannedIps.Contains(sender.Client.Connection.EndPoint.Address))
+            if (client.Connection != null && _bannedIps.Contains(client.Connection.EndPoint.Address))
             {
-                await sender.SendDisconnectReason(DisconnectReason.Banned);
-                return;
+                return GameJoinResult.FromError(GameJoinError.Banned);
             }
-            
+
+            var player = client.Player;
+
             // Check if;
             // - The player is already in this game.
             // - The game is full.
-            if (sender.Game != this && _players.Count >= Options.MaxPlayers)
+            if (player?.Game != this && _players.Count >= Options.MaxPlayers)
             {
-                await sender.SendDisconnectReason(DisconnectReason.GameFull);
-                return;
+                return GameJoinResult.FromError(GameJoinError.GameFull);
+            }
+
+            if (GameState == GameStates.Started)
+            {
+                return GameJoinResult.FromError(GameJoinError.GameStarted);
+            }
+
+            if (GameState == GameStates.Destroyed)
+            {
+                return GameJoinResult.FromError(GameJoinError.GameDestroyed);
+            }
+
+            var isNew = false;
+
+            if (player == null || player.Game != this)
+            {
+                var clientPlayer = new ClientPlayer(client, this);
+
+                if (!_clientManager.Validate(client))
+                {
+                    return GameJoinResult.FromError(GameJoinError.InvalidClient);
+                }
+
+                isNew = true;
+                player = clientPlayer;
+                client.Player = clientPlayer;
             }
-            
+
             // Check current player state.
-            if (sender.Limbo == LimboStates.NotLimbo)
+            if (player.Limbo == LimboStates.NotLimbo)
             {
-                await sender.SendDisconnectReason(DisconnectReason.Custom, "Invalid limbo state while joining.");
-                return;
+                return GameJoinResult.FromError(GameJoinError.InvalidLimbo);
             }
-            
-            switch (GameState)
+
+            if (GameState == GameStates.Ended)
             {
-                case GameStates.NotStarted:
-                    await HandleJoinGameNew(sender);
-                    break;
-                case GameStates.Ended:
-                    await HandleJoinGameNext(sender);
-                    break;
-                case GameStates.Started:
-                    await sender.SendDisconnectReason(DisconnectReason.GameStarted);
-                    return;
-                case GameStates.Destroyed:
-                    await sender.SendDisconnectReason(DisconnectReason.Custom, DisconnectMessages.Destroyed);
-                    return;
-                default:
-                    throw new ArgumentOutOfRangeException();
+                await HandleJoinGameNext(player, isNew);
+                return GameJoinResult.CreateSuccess(player);
             }
+
+            await HandleJoinGameNew(player, isNew);
+            return GameJoinResult.CreateSuccess(player);
         }
 
         public async ValueTask HandleEndGame(IMessageReader message)
         {
             GameState = GameStates.Ended;
-            
+
             // Broadcast end of the game.
             using (var packet = CreateMessage(MessageType.Reliable))
             {
                 message.CopyTo(packet);
-                await packet.SendToAllAsync(LimboStates.NotLimbo);
+                await packet.SendToAllAsync();
             }
-            
+
             // Put all players in the correct limbo state.
             foreach (var player in _players)
             {
@@ -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);
 
index a26cfb320b4da6ed740be8bce614cc74867825b4..c5538aaa894aacf2ddac5cfb4b39d4cc01f3d0b9 100644 (file)
@@ -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);
         }
index 6731f2c812c098d1cd9e63f88f3ca728647e528c..e581fa67200249332390615b841183e184561749 100644 (file)
@@ -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);
             }
index 10e9a7a8342afe09a4fe0ff64453d0a83699a0d1..d42e2b1ae794f7a5f4cded2bda995d0528492649 100644 (file)
@@ -16,24 +16,24 @@ namespace Impostor.Server.Net.State
     internal partial class Game : IGame
     {
         private static readonly ILogger Logger = Log.ForContext<Game>();
-        
-        private readonly GameManager _gameManager;
-        private readonly INodeLocator _nodeLocator;
-        private readonly IMatchmaker matchmaker;
-        private readonly ConcurrentDictionary<int, ClientPlayer> _players;
+
+        private readonly IGameManager _gameManager;
+        private readonly IClientManager _clientManager;
+        private readonly IMatchmaker _matchmaker;
+        private readonly ConcurrentDictionary<int, IClientPlayer> _players;
         private readonly HashSet<IPAddress> _bannedIps;
 
         public Game(
-            GameManager gameManager,
+            IGameManager gameManager,
             INodeLocator nodeLocator,
             IPEndPoint publicIp,
             GameCode code,
             GameOptionsData options,
-            IMatchmaker matchmaker)
+            IMatchmaker matchmaker,
+            IClientManager clientManager)
         {
             _gameManager = gameManager;
-            _nodeLocator = nodeLocator;
-            _players = new ConcurrentDictionary<int, ClientPlayer>();
+            _players = new ConcurrentDictionary<int, IClientPlayer>();
             _bannedIps = new HashSet<IPAddress>();
 
             PublicIp = publicIp;
@@ -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<object, object>();
         }
 
         public IPEndPoint PublicIp { get; }
+
         public GameCode Code { get; }
+
         public bool IsPublic { get; private set; }
+
         public int HostId { get; private set; }
+
         public GameStates GameState { get; private set; }
+
         public GameOptionsData Options { get; }
+
         public IDictionary<object, object> Items { get; }
-        
+
         public int PlayerCount => _players.Count;
-        
+
         public IClientPlayer Host => _players[HostId];
-        
-        private ValueTask BroadcastJoinMessage(IGameMessageWriter message, bool clear, ClientPlayer player)
-        {
-            Message01JoinGame.SerializeJoin(message, clear, Code, player.Client.Id, HostId);
-            
-            return message.SendToAllExceptAsync(LimboStates.NotLimbo, player.Client.Id);
-        }
 
         public IEnumerable<IClientPlayer> Players => _players.Select(p => p.Value);
 
         public IGameMessageWriter CreateMessage(MessageType type)
         {
-            return matchmaker.CreateGameMessageWriter(this, type);
+            return _matchmaker.CreateGameMessageWriter(this, type);
         }
 
         public bool TryGetPlayer(int id, out IClientPlayer player)
@@ -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
index a9b82197584cd2ad8188a2833811b926a222c45f..113f0181b8f41b9745a066d0df08b23e9544fde9 100644 (file)
@@ -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<ServerRedirectorConfig>() ?? new ServerRedirectorConfig();
-                    
+
                     services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
                     services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
 
@@ -86,18 +88,18 @@ namespace Impostor.Server
                         }
                         else if (!string.IsNullOrEmpty(redirector.Locator.UdpMasterEndpoint))
                         {
-                            services.AddSingleton<INodeLocator, NodeLocatorUDP>();
+                            services.AddSingleton<INodeLocator, NodeLocatorUdp>();
 
                             if (redirector.Master)
                             {
-                                services.AddHostedService<NodeLocatorUDPService>();
+                                services.AddHostedService<NodeLocatorUdpService>();
                             }
                         }
                         else
                         {
                             throw new Exception("Missing a valid NodeLocator config.");
                         }
-                        
+
                         // Use the configuration as source for the list of nodes to provide
                         // when creating a game.
                         services.AddSingleton<INodeProvider, NodeProviderConfig>();
@@ -108,20 +110,22 @@ namespace Impostor.Server
                         // So we provide one that ignores all calls.
                         services.AddSingleton<INodeLocator, NodeLocatorNoOp>();
                     }
-                    
+
                     services.AddSingleton<IClientManager, ClientManager>();
-                    
+
                     if (redirector.Enabled && redirector.Master)
                     {
                         services.AddSingleton<IClientFactory, ClientFactory<ClientRedirector>>();
+
                         // For a master server, we don't need a GameManager.
                     }
                     else
                     {
                         services.AddSingleton<IClientFactory, ClientFactory<Client>>();
-                        services.AddSingleton<GameManager>();
+                        services.AddSingleton<IGameManager, GameManager>();
                     }
 
+                    services.AddSingleton<IEventManager, EventManager>();
                     services.UseHazelMatchmaking();
                     services.AddHostedService<MatchmakerService>();
                 })
diff --git a/src/Impostor.Server/ProjectRules.ruleset b/src/Impostor.Server/ProjectRules.ruleset
new file mode 100644 (file)
index 0000000..d2b2f2f
--- /dev/null
@@ -0,0 +1,17 @@
+<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0">
+    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
+        <Rule Id="SA1200" Action="None" />
+    </Rules>
+    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules">
+        <Rule Id="SA1600" Action="None" />
+        <Rule Id="SA1601" Action="None" />
+        <Rule Id="SA1615" Action="None" />
+        <Rule Id="SA1633" Action="None" />
+    </Rules>
+    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules">
+        <Rule Id="SA1101" Action="None" />
+    </Rules>
+    <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
+        <Rule Id="SA1309" Action="None" />
+    </Rules>
+</RuleSet>
\ No newline at end of file
index a81e19c1dc52865fe55b15c036789688fb33666b..ba1a81adb18df2f57235c01857a0f1f2b58b1d83 100644 (file)
@@ -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
index 0c61aa426a2e47be9ac82c601c2f19b9345992a1..7a7caf89e0c49ed59b3442987089a03df7de6c7a 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 0c61aa426a2e47be9ac82c601c2f19b9345992a1
+Subproject commit 7a7caf89e0c49ed59b3442987089a03df7de6c7a