From: Gerard Smit Date: Fri, 9 Oct 2020 00:00:08 +0000 (+0200) Subject: Created Impostor.Server.Api X-Git-Tag: v1.2.2~117^2~5 X-Git-Url: https://git.deb.at/?a=commitdiff_plain;h=47e2e3a06a212893974e4b42654bb9d5949cf178;p=rhonda%2Fimpostor.git Created Impostor.Server.Api --- diff --git a/src/Imposter.Reactive/AsyncObservableExtensions.cs b/src/Imposter.Reactive/AsyncObservableExtensions.cs new file mode 100644 index 0000000..56f5043 --- /dev/null +++ b/src/Imposter.Reactive/AsyncObservableExtensions.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Reactive; +using System.Threading.Tasks; + +namespace System +{ + public static class AsyncObservableExtensions + { + public static ValueTask SubscribeAsync(this IAsyncObservable source, Func onNextAsync) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (onNextAsync == null) + throw new ArgumentNullException(nameof(onNextAsync)); + + return source.SubscribeAsync(new AsyncObserver(onNextAsync, ex => new ValueTask(Task.FromException(ex)), () => default)); + } + + public static ValueTask SubscribeAsync(this IAsyncObservable source, Func onNextAsync, Func onErrorAsync) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (onNextAsync == null) + throw new ArgumentNullException(nameof(onNextAsync)); + if (onErrorAsync == null) + throw new ArgumentNullException(nameof(onErrorAsync)); + + return source.SubscribeAsync(new AsyncObserver(onNextAsync, onErrorAsync, () => default)); + } + + public static ValueTask SubscribeAsync(this IAsyncObservable source, Func onNextAsync, Func onCompletedAsync) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (onNextAsync == null) + throw new ArgumentNullException(nameof(onNextAsync)); + if (onCompletedAsync == null) + throw new ArgumentNullException(nameof(onCompletedAsync)); + + return source.SubscribeAsync(new AsyncObserver(onNextAsync, ex => new ValueTask(Task.FromException(ex)), onCompletedAsync)); + } + + public static ValueTask SubscribeAsync(this IAsyncObservable source, Func onNextAsync, Func onErrorAsync, Func onCompletedAsync) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (onNextAsync == null) + throw new ArgumentNullException(nameof(onNextAsync)); + if (onErrorAsync == null) + throw new ArgumentNullException(nameof(onErrorAsync)); + if (onCompletedAsync == null) + throw new ArgumentNullException(nameof(onCompletedAsync)); + + return source.SubscribeAsync(new AsyncObserver(onNextAsync, onErrorAsync, onCompletedAsync)); + } + + public static ValueTask SubscribeAsync(this IAsyncObservable source, Action onNext) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (onNext == null) + throw new ArgumentNullException(nameof(onNext)); + + return source.SubscribeAsync(new AsyncObserver(x => { onNext(x); return default; }, ex => new ValueTask(Task.FromException(ex)), () => default)); + } + + public static ValueTask SubscribeAsync(this IAsyncObservable source, Action onNext, Action onError) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (onNext == null) + throw new ArgumentNullException(nameof(onNext)); + if (onError == null) + throw new ArgumentNullException(nameof(onError)); + + return source.SubscribeAsync(new AsyncObserver(x => { onNext(x); return default; }, ex => { onError(ex); return default; }, () => default)); + } + + public static ValueTask SubscribeAsync(this IAsyncObservable source, Action onNext, Action onCompleted) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (onNext == null) + throw new ArgumentNullException(nameof(onNext)); + if (onCompleted == null) + throw new ArgumentNullException(nameof(onCompleted)); + + return source.SubscribeAsync(new AsyncObserver(x => { onNext(x); return default; }, ex => new ValueTask(Task.FromException(ex)), () => { onCompleted(); return default; })); + } + + public static ValueTask SubscribeAsync(this IAsyncObservable source, Action onNext, Action onError, Action onCompleted) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (onNext == null) + throw new ArgumentNullException(nameof(onNext)); + if (onError == null) + throw new ArgumentNullException(nameof(onError)); + if (onCompleted == null) + throw new ArgumentNullException(nameof(onCompleted)); + + return source.SubscribeAsync(new AsyncObserver(x => { onNext(x); return default; }, ex => { onError(ex); return default; }, () => { onCompleted(); return default; })); + } + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/IAsyncObservable.cs b/src/Imposter.Reactive/IAsyncObservable.cs new file mode 100644 index 0000000..e0dcce3 --- /dev/null +++ b/src/Imposter.Reactive/IAsyncObservable.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace System +{ + public interface IAsyncObservable + { + ValueTask SubscribeAsync(IAsyncObserver observer); + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/IAsyncObserver.cs b/src/Imposter.Reactive/IAsyncObserver.cs new file mode 100644 index 0000000..eaaf3d5 --- /dev/null +++ b/src/Imposter.Reactive/IAsyncObserver.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace System +{ + public interface IAsyncObserver + { + ValueTask OnNextAsync(T value); + ValueTask OnErrorAsync(Exception error); + ValueTask OnCompletedAsync(); + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Imposter.Reactive.csproj b/src/Imposter.Reactive/Imposter.Reactive.csproj new file mode 100644 index 0000000..63eb9ec --- /dev/null +++ b/src/Imposter.Reactive/Imposter.Reactive.csproj @@ -0,0 +1,8 @@ + + + + net5.0 + System + + + diff --git a/src/Imposter.Reactive/Reactive/AsyncObservable.cs b/src/Imposter.Reactive/Reactive/AsyncObservable.cs new file mode 100644 index 0000000..f6b8045 --- /dev/null +++ b/src/Imposter.Reactive/Reactive/AsyncObservable.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace System.Reactive +{ + public class AsyncObservable : AsyncObservableBase + { + private readonly Func, ValueTask> _subscribeAsync; + + public AsyncObservable(Func, ValueTask> subscribeAsync) + { + _subscribeAsync = subscribeAsync ?? throw new ArgumentNullException(nameof(subscribeAsync)); + } + + protected override ValueTask SubscribeAsyncCore(IAsyncObserver observer) + { + if (observer == null) + throw new ArgumentNullException(nameof(observer)); + + return _subscribeAsync(observer); + } + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/AsyncObservableBase.cs b/src/Imposter.Reactive/Reactive/AsyncObservableBase.cs new file mode 100644 index 0000000..a4f86ff --- /dev/null +++ b/src/Imposter.Reactive/Reactive/AsyncObservableBase.cs @@ -0,0 +1,210 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace System.Reactive +{ + public abstract class AsyncObservableBase : IAsyncObservable + { + public async ValueTask SubscribeAsync(IAsyncObserver observer) + { + if (observer == null) + throw new ArgumentNullException(nameof(observer)); + + var autoDetach = new AutoDetachAsyncObserver(observer); + + var subscription = await SubscribeAsyncCore(autoDetach).ConfigureAwait(false); + + await autoDetach.AssignAsync(subscription).ConfigureAwait(false); + + return autoDetach; + } + + protected abstract ValueTask SubscribeAsyncCore(IAsyncObserver observer); + + private sealed class AutoDetachAsyncObserver : AsyncObserverBase, IAsyncDisposable + { + private readonly IAsyncObserver _observer; + private readonly object _gate = new object(); + + private IAsyncDisposable _subscription; + private ValueTask _task; + private bool _disposing; + + public AutoDetachAsyncObserver(IAsyncObserver observer) + { + _observer = observer; + } + + public async ValueTask AssignAsync(IAsyncDisposable subscription) + { + var shouldDispose = false; + + lock (_gate) + { + if (_disposing) + { + shouldDispose = true; + } + else + { + _subscription = subscription; + } + } + + if (shouldDispose) + { + await subscription.DisposeAsync().ConfigureAwait(false); + } + } + + public async ValueTask DisposeAsync() + { + var task = default(ValueTask); + var subscription = default(IAsyncDisposable); + + lock (_gate) + { + // + // NB: The postcondition of awaiting the first DisposeAsync call to complete is that all message + // processing has ceased, i.e. no further On*AsyncCore calls will be made. This is achieved + // here by setting _disposing to true, which is checked by the On*AsyncCore calls upon + // entry, and by awaiting the task of any in-flight On*AsyncCore calls. + // + // Timing of the disposal of the subscription is less deterministic due to the intersection + // with the AssignAsync code path. However, the auto-detach observer can only be returned + // from the SubscribeAsync call *after* a call to AssignAsync has been made and awaited, so + // either AssignAsync triggers the disposal and an already disposed instance is returned, or + // the user calling DisposeAsync will either encounter a busy observer which will be stopped + // in its tracks (as described above) or it will trigger a disposal of the subscription. In + // both these cases the result of awaiting DisposeAsync guarantees no further message flow. + // + + if (!_disposing) + { + _disposing = true; + + task = _task; + subscription = _subscription; + } + } + + try + { + // + // BUGBUG: This causes grief when an outgoing On*Async call reenters the DisposeAsync method and + // results in the task returned from the On*Async call to be awaited to serialize the + // call to subscription.DisposeAsync after it's done. We need to either detect reentrancy + // and queue up the call to DisposeAsync or follow an when we trigger the disposal without + // awaiting outstanding work (thus allowing for concurrency). + // + // if (task != null) + // { + // await task.ConfigureAwait(false); + // } + // + } + finally + { + if (subscription != null) + { + await subscription.DisposeAsync().ConfigureAwait(false); + } + } + } + + protected override async ValueTask OnCompletedAsyncCore() + { + lock (_gate) + { + if (_disposing) + { + return; + } + + _task = _observer.OnCompletedAsync(); + } + + try + { + await _task.ConfigureAwait(false); + } + finally + { + await FinishAsync().ConfigureAwait(false); + } + } + + protected override async ValueTask OnErrorAsyncCore(Exception error) + { + lock (_gate) + { + if (_disposing) + { + return; + } + + _task = _observer.OnErrorAsync(error); + } + + try + { + await _task.ConfigureAwait(false); + } + finally + { + await FinishAsync().ConfigureAwait(false); + } + } + + protected override async ValueTask OnNextAsyncCore(T value) + { + lock (_gate) + { + if (_disposing) + { + return; + } + + _task = _observer.OnNextAsync(value); + } + + try + { + await _task.ConfigureAwait(false); + } + finally + { + lock (_gate) + { + _task = default; + } + } + } + + private async ValueTask FinishAsync() + { + var subscription = default(IAsyncDisposable); + + lock (_gate) + { + if (!_disposing) + { + _disposing = true; + + subscription = _subscription; + } + + _task = default; + } + + if (subscription != null) + { + await subscription.DisposeAsync().ConfigureAwait(false); + } + } + } + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/AsyncObserver.cs b/src/Imposter.Reactive/Reactive/AsyncObserver.cs new file mode 100644 index 0000000..bfe0066 --- /dev/null +++ b/src/Imposter.Reactive/Reactive/AsyncObserver.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace System.Reactive +{ + public class AsyncObserver : AsyncObserverBase + { + private readonly Func _onNextAsync; + private readonly Func _onErrorAsync; + private readonly Func _onCompletedAsync; + + public AsyncObserver(Func onNextAsync, Func onErrorAsync, Func onCompletedAsync) + { + _onNextAsync = onNextAsync ?? throw new ArgumentNullException(nameof(onNextAsync)); + _onErrorAsync = onErrorAsync ?? throw new ArgumentNullException(nameof(onErrorAsync)); + _onCompletedAsync = onCompletedAsync ?? throw new ArgumentNullException(nameof(onCompletedAsync)); + } + + protected override ValueTask OnCompletedAsyncCore() => _onCompletedAsync(); + + protected override ValueTask OnErrorAsyncCore(Exception error) => _onErrorAsync(error ?? throw new ArgumentNullException(nameof(error))); + + protected override ValueTask OnNextAsyncCore(T value) => _onNextAsync(value); + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/AsyncObserverBase.cs b/src/Imposter.Reactive/Reactive/AsyncObserverBase.cs new file mode 100644 index 0000000..75564e6 --- /dev/null +++ b/src/Imposter.Reactive/Reactive/AsyncObserverBase.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Reactive +{ + public abstract class AsyncObserverBase : IAsyncObserver + { + private const int Idle = 0; + private const int Busy = 1; + private const int Done = 2; + + private int _status = Idle; + + public ValueTask OnCompletedAsync() + { + TryEnter(); + + try + { + return OnCompletedAsyncCore(); + } + finally + { + Interlocked.Exchange(ref _status, Done); + } + } + + protected abstract ValueTask OnCompletedAsyncCore(); + + public ValueTask OnErrorAsync(Exception error) + { + if (error == null) + throw new ArgumentNullException(nameof(error)); + + TryEnter(); + + try + { + return OnErrorAsyncCore(error); + } + finally + { + Interlocked.Exchange(ref _status, Done); + } + } + + protected abstract ValueTask OnErrorAsyncCore(Exception error); + + public ValueTask OnNextAsync(T value) + { + TryEnter(); + + try + { + return OnNextAsyncCore(value); + } + finally + { + Interlocked.Exchange(ref _status, Idle); + } + } + + protected abstract ValueTask OnNextAsyncCore(T value); + + private void TryEnter() + { + var old = Interlocked.CompareExchange(ref _status, Busy, Idle); + + switch (old) + { + case Busy: + throw new InvalidOperationException("The observer is currently processing a notification."); + case Done: + throw new InvalidOperationException("The observer has already terminated."); + } + } + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs b/src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs new file mode 100644 index 0000000..b5aaaf2 --- /dev/null +++ b/src/Imposter.Reactive/Reactive/Disposables/AsyncDisposable.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Reactive.Disposables +{ + public static class AsyncDisposable + { + public static IAsyncDisposable Nop { get; } = new NopAsyncDisposable(); + + public static IAsyncDisposable Create(Func dispose) + { + if (dispose == null) + throw new ArgumentNullException(nameof(dispose)); + + return new AnonymousAsyncDisposable(dispose); + } + + private sealed class AnonymousAsyncDisposable : IAsyncDisposable + { + private Func _dispose; + + public AnonymousAsyncDisposable(Func dispose) => _dispose = dispose; + + public ValueTask DisposeAsync() => Interlocked.Exchange(ref _dispose, null)?.Invoke() ?? default; + } + + private sealed class NopAsyncDisposable : IAsyncDisposable + { + public ValueTask DisposeAsync() => default; + } + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs new file mode 100644 index 0000000..b2f3dae --- /dev/null +++ b/src/Imposter.Reactive/Reactive/Subjects/ConcurrentSimpleAsyncSubject.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace System.Reactive.Subjects +{ + public sealed class ConcurrentSimpleAsyncSubject : SimpleAsyncSubject + { + protected override ValueTask OnCompletedAsyncCore(IEnumerable> observers) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnCompletedAsync().AsTask()))); + + protected override ValueTask OnErrorAsyncCore(IEnumerable> observers, Exception error) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnErrorAsync(error).AsTask()))); + + protected override ValueTask OnNextAsyncCore(IEnumerable> observers, T value) => new ValueTask(Task.WhenAll(observers.Select(observer => observer.OnNextAsync(value).AsTask()))); + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs new file mode 100644 index 0000000..8a3342e --- /dev/null +++ b/src/Imposter.Reactive/Reactive/Subjects/IAsyncSubject.cs @@ -0,0 +1,10 @@ +namespace System.Reactive.Subjects +{ + public interface IAsyncSubject : IAsyncObservable, IAsyncObserver + { + } + + public interface IAsyncSubject : IAsyncSubject + { + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs new file mode 100644 index 0000000..28a64fd --- /dev/null +++ b/src/Imposter.Reactive/Reactive/Subjects/SequentialSimpleAsyncSubject.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace System.Reactive.Subjects +{ + public sealed class SequentialSimpleAsyncSubject : SimpleAsyncSubject + { + protected override async ValueTask OnCompletedAsyncCore(IEnumerable> observers) + { + foreach (var observer in observers) + { + await observer.OnCompletedAsync().ConfigureAwait(false); + } + } + + protected override async ValueTask OnErrorAsyncCore(IEnumerable> observers, Exception error) + { + foreach (var observer in observers) + { + await observer.OnErrorAsync(error).ConfigureAwait(false); + } + } + + protected override async ValueTask OnNextAsyncCore(IEnumerable> observers, T value) + { + foreach (var observer in observers) + { + await observer.OnNextAsync(value).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs b/src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs new file mode 100644 index 0000000..b922056 --- /dev/null +++ b/src/Imposter.Reactive/Reactive/Subjects/SimpleAsyncSubject.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT License. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading.Tasks; + +namespace System.Reactive.Subjects +{ + public abstract class SimpleAsyncSubject : IAsyncSubject + { + private readonly object _gate = new object(); + private readonly List> _observers = new List>(); + private bool _done; + private Exception _error; + + public ValueTask OnCompletedAsync() + { + IAsyncObserver[] observers; + + lock (_gate) + { + if (_done || _error != null) + { + return default; + } + + _done = true; + + observers = _observers.ToArray(); + } + + return OnCompletedAsyncCore(observers); + } + + protected abstract ValueTask OnCompletedAsyncCore(IEnumerable> observers); + + public ValueTask OnErrorAsync(Exception error) + { + if (error == null) + throw new ArgumentNullException(nameof(error)); + + IAsyncObserver[] observers; + + lock (_gate) + { + if (_done || _error != null) + { + return default; + } + + _error = error; + + observers = _observers.ToArray(); + } + + return OnErrorAsyncCore(observers, error); + } + + protected abstract ValueTask OnErrorAsyncCore(IEnumerable> observers, Exception error); + + public ValueTask OnNextAsync(T value) + { + IAsyncObserver[] observers; + + lock (_gate) + { + if (_done || _error != null) + { + return default; + } + + observers = _observers.ToArray(); + } + + return OnNextAsyncCore(observers, value); + } + + protected abstract ValueTask OnNextAsyncCore(IEnumerable> observers, T value); + + public async ValueTask SubscribeAsync(IAsyncObserver observer) + { + if (observer == null) + throw new ArgumentNullException(nameof(observer)); + + bool done; + Exception error; + + lock (_gate) + { + done = _done; + error = _error; + + if (!done && error == null) + { + _observers.Add(observer); + } + } + + if (done) + { + await observer.OnCompletedAsync().ConfigureAwait(false); + + return AsyncDisposable.Nop; + } + else if (error != null) + { + await observer.OnErrorAsync(error).ConfigureAwait(false); + + return AsyncDisposable.Nop; + } + else + { + return AsyncDisposable.Create(() => + { + lock (_gate) + { + var i = _observers.LastIndexOf(observer); + + if (i >= 0) + { + _observers.RemoveAt(i); + } + } + + return default; + }); + } + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs b/src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs new file mode 100644 index 0000000..73e5407 --- /dev/null +++ b/src/Impostor.Server.Api/Exceptions/ClientVersionUnsupportedException.cs @@ -0,0 +1,13 @@ +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/Exceptions/ImpostorException.cs b/src/Impostor.Server.Api/Exceptions/ImpostorException.cs new file mode 100644 index 0000000..9e9b8f7 --- /dev/null +++ b/src/Impostor.Server.Api/Exceptions/ImpostorException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace Impostor.Server +{ + public class ImpostorException : Exception + { + public ImpostorException() + { + } + + protected ImpostorException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public ImpostorException(string? message) : base(message) + { + } + + public ImpostorException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/GameCode.cs b/src/Impostor.Server.Api/GameCode.cs new file mode 100644 index 0000000..6178732 --- /dev/null +++ b/src/Impostor.Server.Api/GameCode.cs @@ -0,0 +1,71 @@ +using System; +using Impostor.Shared.Innersloth; + +namespace Impostor.Server +{ + public readonly struct GameCode : IEquatable + { + 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/IGame.cs b/src/Impostor.Server.Api/IGame.cs new file mode 100644 index 0000000..b37c5ea --- /dev/null +++ b/src/Impostor.Server.Api/IGame.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Impostor.Server.Net; + +namespace Impostor.Server +{ + public interface IGame + { + GameCode Code { get; } + + IEnumerable Players { get; } + + IClientPlayer Host { get; } + + bool IsPublic { get; } + IDictionary Items { get; } + + IGameMessageWriter CreateMessage(MessageType type); + + bool TryGetPlayer(int id, [NotNullWhen(true)] out IClientPlayer player); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Impostor.Server.Api.csproj b/src/Impostor.Server.Api/Impostor.Server.Api.csproj new file mode 100644 index 0000000..95ebef0 --- /dev/null +++ b/src/Impostor.Server.Api/Impostor.Server.Api.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + Impostor.Server + enable + + + + + + + + + + + + + + diff --git a/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings b/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings new file mode 100644 index 0000000..ec2baa7 --- /dev/null +++ b/src/Impostor.Server.Api/Impostor.Server.Api.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/src/Impostor.Server.Api/LimboStates.cs b/src/Impostor.Server.Api/LimboStates.cs new file mode 100644 index 0000000..de7f629 --- /dev/null +++ b/src/Impostor.Server.Api/LimboStates.cs @@ -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 diff --git a/src/Impostor.Server.Api/Net/Extensions/GameMessageWriterExtensions.cs b/src/Impostor.Server.Api/Net/Extensions/GameMessageWriterExtensions.cs new file mode 100644 index 0000000..52808c9 --- /dev/null +++ b/src/Impostor.Server.Api/Net/Extensions/GameMessageWriterExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using Impostor.Shared.Innersloth.Data; + +namespace Impostor.Server.Net +{ + public static class GameMessageWriterExtensions + { + public static ValueTask SendToAllExceptAsync(this IGameMessageWriter writer, LimboStates states, int? id) + { + return id.HasValue + ? writer.SendToAllExceptAsync(states, id.Value) + : 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); + } + + public static ValueTask SendToAsync(this IGameMessageWriter writer, IClient client) + { + if (client == null) throw new ArgumentNullException(nameof(client)); + + return writer.SendToAsync(client.Id); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs b/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs new file mode 100644 index 0000000..77bc72e --- /dev/null +++ b/src/Impostor.Server.Api/Net/Factories/IClientFactory.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Impostor.Server.Net.Factories +{ + public interface IClientFactory + { + /// + /// Get the next ID for . + /// + int NextId(); + + /// + /// Creates a client for the Hazel . + /// + /// Hazel connection. + /// + /// + ValueTask CreateAsync(IConnection connection, string name, int clientVersion); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/IClient.cs b/src/Impostor.Server.Api/Net/IClient.cs new file mode 100644 index 0000000..3228e0b --- /dev/null +++ b/src/Impostor.Server.Api/Net/IClient.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Impostor.Server.Net +{ + public interface IClient + { + int Id { get; } + + string Name { get; } + + IConnection Connection { get; } + + IDictionary Items { get; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/IClientPlayer.cs b/src/Impostor.Server.Api/Net/IClientPlayer.cs new file mode 100644 index 0000000..4034875 --- /dev/null +++ b/src/Impostor.Server.Api/Net/IClientPlayer.cs @@ -0,0 +1,13 @@ +using Impostor.Shared.Innersloth.Data; + +namespace Impostor.Server.Net +{ + public interface IClientPlayer + { + IClient Client { get; } + + IGame Game { get; } + + LimboStates Limbo { get; } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/IConnection.cs b/src/Impostor.Server.Api/Net/IConnection.cs new file mode 100644 index 0000000..9aa3203 --- /dev/null +++ b/src/Impostor.Server.Api/Net/IConnection.cs @@ -0,0 +1,16 @@ +using System; +using System.Net; + +namespace Impostor.Server.Net +{ + public interface IConnection + { + IAsyncObservable MessageReceived { get; } + + IPEndPoint EndPoint { get; } + + bool IsConnected { get; } + + IConnectionMessageWriter CreateMessage(MessageType type); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Manager/IClientManager.cs b/src/Impostor.Server.Api/Net/Manager/IClientManager.cs new file mode 100644 index 0000000..82bdbe0 --- /dev/null +++ b/src/Impostor.Server.Api/Net/Manager/IClientManager.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Impostor.Server.Net.Manager +{ + public interface IClientManager + { + ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion); + + void Register(IClient client); + + void Remove(IClient client); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs b/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs new file mode 100644 index 0000000..561c426 --- /dev/null +++ b/src/Impostor.Server.Api/Net/Manager/IMatchmaker.cs @@ -0,0 +1,14 @@ +using System.Net; +using System.Threading.Tasks; + +namespace Impostor.Server.Net.Manager +{ + public interface IMatchmaker + { + ValueTask StartAsync(IPEndPoint ipEndPoint); + + ValueTask StopAsync(); + + IGameMessageWriter CreateGameMessageWriter(IGame game, MessageType messageType); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IConnectionMessageWriter.cs b/src/Impostor.Server.Api/Net/Messages/IConnectionMessageWriter.cs new file mode 100644 index 0000000..0e37c41 --- /dev/null +++ b/src/Impostor.Server.Api/Net/Messages/IConnectionMessageWriter.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Impostor.Server.Net +{ + public interface IConnectionMessageWriter : IMessageWriter + { + ValueTask SendAsync(); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IGameMessageWriter.cs b/src/Impostor.Server.Api/Net/Messages/IGameMessageWriter.cs new file mode 100644 index 0000000..2d947e9 --- /dev/null +++ b/src/Impostor.Server.Api/Net/Messages/IGameMessageWriter.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Impostor.Shared.Innersloth.Data; + +namespace Impostor.Server.Net +{ + public interface IGameMessageWriter : IMessageWriter + { + /// + /// Send the message to all players. + /// + /// + ValueTask SendToAllAsync(LimboStates states); + + /// + /// Send the message to all players except one. + /// + /// + /// The player to exclude from sending the message. + ValueTask SendToAllExceptAsync(LimboStates states, int senderId); + + /// + /// Send a message to a specific player. + /// + /// + ValueTask SendToAsync(int id); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IMessage.cs b/src/Impostor.Server.Api/Net/Messages/IMessage.cs new file mode 100644 index 0000000..b8364ff --- /dev/null +++ b/src/Impostor.Server.Api/Net/Messages/IMessage.cs @@ -0,0 +1,9 @@ +namespace Impostor.Server.Net +{ + public interface IMessage + { + MessageType Type { get; } + + IMessageReader CreateReader(); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IMessageReader.cs b/src/Impostor.Server.Api/Net/Messages/IMessageReader.cs new file mode 100644 index 0000000..a7fb66b --- /dev/null +++ b/src/Impostor.Server.Api/Net/Messages/IMessageReader.cs @@ -0,0 +1,43 @@ +using System; + +namespace Impostor.Server.Net +{ + public interface IMessageReader + { + int Position { get; } + + ReadOnlyMemory Buffer { get; } + + byte Tag { get; } + + int Length { get; } + + bool ReadBoolean(); + + sbyte ReadSByte(); + + byte ReadByte(); + + ushort ReadUInt16(); + + short ReadInt16(); + + uint ReadUInt32(); + + int ReadInt32(); + + float ReadSingle(); + + string ReadString(); + + ReadOnlyMemory ReadBytesAndSize(); + + ReadOnlyMemory ReadBytes(int length); + + int ReadPackedInt32(); + + uint ReadPackedUInt32(); + + void CopyTo(IMessageWriter writer); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/IMessageWriter.cs b/src/Impostor.Server.Api/Net/Messages/IMessageWriter.cs new file mode 100644 index 0000000..007baf8 --- /dev/null +++ b/src/Impostor.Server.Api/Net/Messages/IMessageWriter.cs @@ -0,0 +1,40 @@ +using System; +using System.Net; + +namespace Impostor.Server.Net +{ + public interface IMessageWriter : IDisposable + { + void Write(bool value); + + void Write(sbyte value); + + void Write(byte value); + + void Write(short value); + + void Write(ushort value); + + void Write(uint value); + + void Write(int value); + + void Write(float value); + + void Write(string value); + + void Write(IPAddress ipAddress); + + void WritePacked(int value); + + void Write(ReadOnlyMemory data); + + void StartMessage(byte typeFlag); + + void Write(GameCode code); + + void EndMessage(); + + void Clear(MessageType type); + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Api/Net/Messages/MessageType.cs b/src/Impostor.Server.Api/Net/Messages/MessageType.cs new file mode 100644 index 0000000..00f030a --- /dev/null +++ b/src/Impostor.Server.Api/Net/Messages/MessageType.cs @@ -0,0 +1,8 @@ +namespace Impostor.Server.Net +{ + public enum MessageType + { + Unreliable, + Reliable + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Extensions/ServiceExtensions.cs b/src/Impostor.Server.Hazel/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..b97a65f --- /dev/null +++ b/src/Impostor.Server.Hazel/Extensions/ServiceExtensions.cs @@ -0,0 +1,14 @@ +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.DependencyInjection; + +namespace Impostor.Server.Hazel +{ + public static class ServiceExtensions + { + public static IServiceCollection UseHazelMatchmaking(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/HazelConnection.cs b/src/Impostor.Server.Hazel/HazelConnection.cs new file mode 100644 index 0000000..af96605 --- /dev/null +++ b/src/Impostor.Server.Hazel/HazelConnection.cs @@ -0,0 +1,81 @@ +using System; +using System.Net; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using Hazel; +using Impostor.Server.Net; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Hazel +{ + internal class HazelConnection : IConnection + { + private readonly ILogger _logger; + private readonly ConcurrentSimpleAsyncSubject _messageReceived = new ConcurrentSimpleAsyncSubject(); + + public HazelConnection(Connection innerConnection, ILogger logger) + { + _logger = logger; + InnerConnection = innerConnection; + innerConnection.DataReceived += ConnectionOnDataReceived; + innerConnection.Disconnected += ConnectionOnDisconnected; + } + + public Connection InnerConnection { get; } + + public IAsyncObservable MessageReceived => _messageReceived; + + public IPEndPoint EndPoint => InnerConnection.EndPoint; + + public bool IsConnected => InnerConnection.State == ConnectionState.Connected; + + private void ConnectionOnDisconnected(object sender, DisconnectedEventArgs e) + { + Task.Run(_messageReceived.OnCompletedAsync); + } + + private void ConnectionOnDataReceived(DataReceivedEventArgs e) + { + Task.Run(() => HandleData(e)); + } + + private async Task HandleData(DataReceivedEventArgs e) + { + try + { + while (true) + { + if (e.Message.Position >= e.Message.Length) + { + break; + } + + var reader = e.Message.ReadMessage(); + var type = e.SendOption switch + { + SendOption.None => MessageType.Unreliable, + SendOption.Reliable => MessageType.Reliable, + _ => throw new NotSupportedException() + }; + + using var message = new HazelMessage(reader, type); + + await _messageReceived.OnNextAsync(message); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception caught in client data handler."); + } + finally + { + e.Message.Recycle(); + } + } + + public IConnectionMessageWriter CreateMessage(MessageType type) + { + return new HazelConnectionMessageWriter(type, InnerConnection); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/HazelMatchmaker.cs b/src/Impostor.Server.Hazel/HazelMatchmaker.cs new file mode 100644 index 0000000..bfdfaee --- /dev/null +++ b/src/Impostor.Server.Hazel/HazelMatchmaker.cs @@ -0,0 +1,92 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Hazel; +using Hazel.Udp; +using Impostor.Server.Net; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Hazel +{ + internal class HazelMatchmaker : IMatchmaker + { + private readonly IClientManager _clientManager; + private readonly ILogger _logger; + private readonly ILogger _connectionLogger; + private UdpConnectionListener _connection; + + public HazelMatchmaker( + ILogger logger, + IClientManager clientManager, + ILogger connectionLogger) + { + _logger = logger; + _clientManager = clientManager; + _connectionLogger = connectionLogger; + } + + public ValueTask StartAsync(IPEndPoint ipEndPoint) + { + var mode = ipEndPoint.AddressFamily switch + { + AddressFamily.InterNetwork => IPMode.IPv4, + AddressFamily.InterNetworkV6 => IPMode.IPv6, + _ => throw new InvalidOperationException() + }; + + _connection = new UdpConnectionListener(ipEndPoint, mode, s => + { + _logger.LogWarning("Log from Hazel: {0}", s); + }); + + _connection.NewConnection += OnNewConnection; + + _connection.Start(); + + return default; + } + + public ValueTask StopAsync() + { + _connection.Dispose(); + + return default; + } + + private void OnNewConnection(NewConnectionEventArgs e) + { + Task.Run(() => HandleNewConnection(e)); + } + + private async Task HandleNewConnection(NewConnectionEventArgs e) + { + int clientVersion; + string name; + try + { + // Handshake. + clientVersion = e.HandshakeData.ReadInt32(); + name = e.HandshakeData.ReadString(); + + e.HandshakeData.Recycle(); + } + 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) + { + return new HazelGameMessageWriter(messageType, game); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj b/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj new file mode 100644 index 0000000..7d37edf --- /dev/null +++ b/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + true + + + + + + + + + + + + diff --git a/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj.DotSettings b/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj.DotSettings new file mode 100644 index 0000000..17962b1 --- /dev/null +++ b/src/Impostor.Server.Hazel/Impostor.Server.Hazel.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/BufferMessageReader.cs b/src/Impostor.Server.Hazel/Messages/BufferMessageReader.cs new file mode 100644 index 0000000..dbf85e3 --- /dev/null +++ b/src/Impostor.Server.Hazel/Messages/BufferMessageReader.cs @@ -0,0 +1,194 @@ +using System; +using System.Runtime.CompilerServices; +using System.Text; +using Impostor.Server.Net; + +namespace Impostor.Server.Hazel +{ + public class BufferMessageReader : IMessageReader + { + // TODO: Remove _offset, we can slice the buffer. + private readonly int _offset; + private int _position; + private int readHead; + + public ReadOnlyMemory Buffer { get; } + + public byte Tag { get; } + + public int Length { get; } + + public int Position + { + get { return _position; } + set + { + _position = value; + readHead = value + _offset; + } + } + + public BufferMessageReader(byte tag, ReadOnlyMemory buffer, int offset, int length) + { + Tag = tag; + Buffer = buffer; + Length = length; + _offset = offset; + readHead = offset; + } + + public bool ReadBoolean() + { + byte val = FastByte(); + return val != 0; + } + + public sbyte ReadSByte() + { + return (sbyte)FastByte(); + } + + public byte ReadByte() + { + return FastByte(); + } + + public ushort ReadUInt16() + { + // TODO: Refactor to System.Buffers.Binary.BinaryPrimitives + + ushort output = + (ushort)(FastByte() + | FastByte() << 8); + return output; + } + + public short ReadInt16() + { + // TODO: Refactor to System.Buffers.Binary.BinaryPrimitives + + short output = + (short)(FastByte() | FastByte() << 8); + return output; + } + + public uint ReadUInt32() + { + // TODO: Refactor to System.Buffers.Binary.BinaryPrimitives + + uint output = FastByte() + | (uint)FastByte() << 8 + | (uint)FastByte() << 16 + | (uint)FastByte() << 24; + + return output; + } + + public int ReadInt32() + { + // TODO: Refactor to System.Buffers.Binary.BinaryPrimitives + + int output = FastByte() + | FastByte() << 8 + | FastByte() << 16 + | FastByte() << 24; + + return output; + } + + public unsafe float ReadSingle() + { + // TODO: Refactor to System.Buffers.Binary.BinaryPrimitives + + float output = 0; + fixed (byte* bufPtr = &Buffer.Span[readHead]) + { + byte* outPtr = (byte*)&output; + + *outPtr = *bufPtr; + *(outPtr + 1) = *(bufPtr + 1); + *(outPtr + 2) = *(bufPtr + 2); + *(outPtr + 3) = *(bufPtr + 3); + } + + Position += 4; + return output; + } + + public string ReadString() + { + var len = ReadPackedInt32(); + var output = Encoding.UTF8.GetString(Buffer.Span.Slice(readHead, len)); + Position += len; + return output; + } + + public ReadOnlyMemory ReadBytesAndSize() + { + var len = ReadPackedInt32(); + return ReadBytes(len); + } + + public ReadOnlyMemory ReadBytes(int length) + { + var output = Buffer.Slice(readHead, length); + Position += length; + return output; + } + + public int ReadPackedInt32() + { + return (int)ReadPackedUInt32(); + } + + public uint ReadPackedUInt32() + { + bool readMore = true; + int shift = 0; + uint output = 0; + + while (readMore) + { + byte b = ReadByte(); + if (b >= 0x80) + { + readMore = true; + b ^= 0x80; + } + else + { + readMore = false; + } + + output |= (uint)(b << shift); + shift += 7; + } + + return output; + } + + public void CopyTo(IMessageWriter writer) + { + int offset, length; + if (Tag == byte.MaxValue) + { + offset = _offset; + length = Length; + } + else + { + offset = _offset - 3; + length = Length + 3; + } + + writer.Write(Buffer.Slice(offset, length)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte FastByte() + { + _position++; + return Buffer.Span[readHead++]; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs new file mode 100644 index 0000000..c0d27d5 --- /dev/null +++ b/src/Impostor.Server.Hazel/Messages/HazelConnectionMessageWriter.cs @@ -0,0 +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; + + public HazelConnectionMessageWriter(MessageType type, Connection connection) + : base(type) + { + _connection = connection; + } + + public ValueTask SendAsync() + { + _connection.Send(Writer); + return default; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs new file mode 100644 index 0000000..8af291f --- /dev/null +++ b/src/Impostor.Server.Hazel/Messages/HazelGameMessageWriter.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Hazel; +using Impostor.Server.Net; +using Impostor.Shared.Innersloth.Data; + +namespace Impostor.Server.Hazel +{ + internal class HazelGameMessageWriter : HazelMessageWriter, IGameMessageWriter + { + private readonly IGame _game; + + public HazelGameMessageWriter(MessageType type, IGame game) + : base(type) + { + _game = game; + } + + private IEnumerable GetConnections(Func filter) + { + return _game.Players + .Where(filter) + .Select(p => p.Client.Connection) + .OfType() + .Select(c => c.InnerConnection); + } + + public ValueTask SendToAllAsync(LimboStates states) + { + foreach (var connection in GetConnections(x => x.Limbo.HasFlag(states))) + { + connection.Send(Writer); + } + + return default; + } + + public ValueTask SendToAllExceptAsync(LimboStates states, int senderId) + { + foreach (var connection in GetConnections(x => + x.Limbo.HasFlag(states) && + x.Client.Id != senderId)) + { + connection.Send(Writer); + } + return default; + } + + public ValueTask SendToAsync(int id) + { + if (_game.TryGetPlayer(id, out var player)) + { + ((HazelConnection)player.Client.Connection).InnerConnection.Send(Writer); + } + + return default; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/HazelMessage.cs b/src/Impostor.Server.Hazel/Messages/HazelMessage.cs new file mode 100644 index 0000000..d61695e --- /dev/null +++ b/src/Impostor.Server.Hazel/Messages/HazelMessage.cs @@ -0,0 +1,49 @@ +using System; +using Hazel; +using Impostor.Server.Net; + +namespace Impostor.Server.Hazel +{ + internal class HazelMessage : IMessage, IDisposable + { + private bool _isDisposed; + private readonly MessageReader _reader; + + public HazelMessage(MessageReader reader, MessageType type) + { + _reader = reader; + Type = type; + } + + public MessageType Type { get; } + + public IMessageReader CreateReader() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(_reader)); + } + + return new BufferMessageReader(_reader.Tag, _reader.Buffer, _reader.Offset, _reader.Length); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~HazelMessage() + { + Dispose(false); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs b/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs new file mode 100644 index 0000000..e63df4c --- /dev/null +++ b/src/Impostor.Server.Hazel/Messages/HazelMessageWriter.cs @@ -0,0 +1,121 @@ +using System; +using System.Net; +using Hazel; +using Impostor.Server.Net; + +namespace Impostor.Server.Hazel +{ + internal abstract class HazelMessageWriter : IMessageWriter + { + protected readonly MessageWriter Writer; + + protected HazelMessageWriter(MessageType type) + { + Writer = MessageWriter.Get(ToSendOption(type)); + } + + private static SendOption ToSendOption(MessageType type) + { + return type switch + { + MessageType.Unreliable => SendOption.None, + MessageType.Reliable => SendOption.Reliable, + _ => throw new NotSupportedException($"Message type {type} is not supported") + }; + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Writer.Recycle(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Write(bool value) + { + Writer.Write(value); + } + + public void Write(sbyte value) + { + Writer.Write(value); + } + + public void Write(byte value) + { + Writer.Write(value); + } + + public void Write(short value) + { + Writer.Write(value); + } + + public void Write(ushort value) + { + Writer.Write(value); + } + + public void Write(uint value) + { + Writer.Write(value); + } + + public void Write(int value) + { + Writer.Write(value); + } + + public void Write(float value) + { + Writer.Write(value); + } + + public void Write(string value) + { + Writer.Write(value); + } + + public void Write(IPAddress ipAddress) + { + Writer.Write(ipAddress.GetAddressBytes()); + } + + public void WritePacked(int value) + { + Writer.WritePacked(value); + } + + public void Write(ReadOnlyMemory data) + { + Writer.Write(data.ToArray()); // TODO: Fix memory allocation. + } + + public void StartMessage(byte typeFlag) + { + Writer.StartMessage(typeFlag); + } + + public void Write(GameCode code) + { + Write(code.Value); + } + + public void EndMessage() + { + Writer.EndMessage(); + } + + public void Clear(MessageType type) + { + Writer.Clear(ToSendOption(type)); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Impostor.Server.csproj b/src/Impostor.Server/Impostor.Server.csproj index 24907d0..b70cb08 100644 --- a/src/Impostor.Server/Impostor.Server.csproj +++ b/src/Impostor.Server/Impostor.Server.csproj @@ -16,7 +16,8 @@ - + + @@ -45,4 +46,8 @@ + + + + diff --git a/src/Impostor.Server/Net/Client.cs b/src/Impostor.Server/Net/Client.cs index 2dbf257..fd4c581 100644 --- a/src/Impostor.Server/Net/Client.cs +++ b/src/Impostor.Server/Net/Client.cs @@ -1,6 +1,5 @@ using System; -using Hazel; -using Impostor.Server.Data; +using System.Threading.Tasks; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Messages; using Impostor.Server.Net.State; @@ -10,36 +9,24 @@ using ILogger = Serilog.ILogger; namespace Impostor.Server.Net { - internal class Client + internal class Client : ClientBase { private static readonly ILogger Logger = Log.ForContext(); - private readonly ClientManager _clientManager; + private readonly IClientManager _clientManager; private readonly GameManager _gameManager; - public Client(ClientManager clientManager, GameManager gameManager, int id, string name, Connection connection) + public Client(IClientManager clientManager, GameManager gameManager, int id, string name, IConnection connection) + : base(id, name, connection) { _clientManager = clientManager; _gameManager = gameManager; - Id = id; - Name = name; - Connection = connection; - Connection.DataReceived += OnDataReceived; - Connection.Disconnected += OnDisconnected; Player = new ClientPlayer(this, _gameManager); } - public int Id { get; } - public string Name { get; } - public Connection Connection { get; } public ClientPlayer Player { get; } - - public void Send(MessageWriter writer) - { - Connection.Send(writer); - } - private bool IsPacketAllowed(MessageReader message, bool hostOnly) + private bool IsPacketAllowed(IMessageReader message, bool hostOnly) { var game = Player.Game; if (game == null) @@ -68,34 +55,12 @@ namespace Impostor.Server.Net return true; } - private void OnDataReceived(DataReceivedEventArgs e) - { - try - { - while (true) - { - if (e.Message.Position >= e.Message.Length) - { - break; - } - - OnMessageReceived(e.Message.ReadMessage(), e.SendOption); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Exception caught in client data handler."); - Player.SendDisconnectReason(DisconnectReason.Custom, DisconnectMessages.Error); - } - finally - { - e.Message.Recycle(); - } - } - - private void OnMessageReceived(MessageReader message, SendOption sendOption) + + protected override async ValueTask OnMessageReceived(IMessage message) { - var flag = message.Tag; + var reader = message.CreateReader(); + + var flag = reader.Tag; Logger.Verbose("[{0}] Server got {1}.", Id, flag); @@ -104,51 +69,51 @@ namespace Impostor.Server.Net case MessageFlags.HostGame: { // Read game settings. - var gameInfo = Message00HostGame.Deserialize(message); + var gameInfo = Message00HostGame.Deserialize(reader); // Create game. var game = _gameManager.Create(gameInfo); if (game == null) { - Player.SendDisconnectReason(DisconnectReason.ServerFull); + await Player.SendDisconnectReason(DisconnectReason.ServerFull); return; } // Code in the packet below will be used in JoinGame. - using (var writer = MessageWriter.Get(SendOption.Reliable)) + using (var writer = Connection.CreateMessage(MessageType.Reliable)) { Message00HostGame.Serialize(writer, game.Code); - Connection.Send(writer); + await writer.SendAsync(); } break; } case MessageFlags.JoinGame: { - Message01JoinGame.Deserialize(message, + Message01JoinGame.Deserialize(reader, out var gameCode, out var unknown); var game = _gameManager.Find(gameCode); if (game == null) { - Player.SendDisconnectReason(DisconnectReason.GameMissing); + await Player.SendDisconnectReason(DisconnectReason.GameMissing); return; } - game.HandleJoinGame(Player); + await game.HandleJoinGame(Player); break; } case MessageFlags.StartGame: { - if (!IsPacketAllowed(message, true)) + if (!IsPacketAllowed(reader, true)) { return; } - Player.Game.HandleStartGame(message); + await Player.Game.HandleStartGame(reader); break; } @@ -158,64 +123,64 @@ namespace Impostor.Server.Net case MessageFlags.RemovePlayer: { - if (!IsPacketAllowed(message, true)) + if (!IsPacketAllowed(reader, true)) { return; } - Message04RemovePlayer.Deserialize(message, + Message04RemovePlayer.Deserialize(reader, out var playerId, out var reason); - Player.Game.HandleRemovePlayer(playerId, (DisconnectReason) reason); + await Player.Game.HandleRemovePlayer(playerId, (DisconnectReason) reason); break; } case MessageFlags.GameData: case MessageFlags.GameDataTo: { - if (!IsPacketAllowed(message, false)) + if (!IsPacketAllowed(reader, false)) { return; } // Broadcast packet to all other players. - using (var writer = MessageWriter.Get(sendOption)) + using var writer = Player.Game.CreateMessage(message.Type); + + if (flag == MessageFlags.GameDataTo) { - if (flag == MessageFlags.GameDataTo) - { - var target = message.ReadPackedInt32(); - writer.CopyFrom(message); - Player.Game.SendTo(writer, target); - } - else - { - writer.CopyFrom(message); - Player.Game.SendToAllExcept(writer, Player.Client.Id); - } + var target = reader.ReadPackedInt32(); + reader.CopyTo(writer); + await writer.SendToAsync(target); } + else + { + reader.CopyTo(writer); + await writer.SendToAllExceptAsync(LimboStates.NotLimbo, Player.Client.Id); + } + break; } case MessageFlags.EndGame: { - if (!IsPacketAllowed(message, true)) + if (!IsPacketAllowed(reader, true)) { return; } - Player.Game.HandleEndGame(message); + await Player.Game.HandleEndGame(reader); break; } case MessageFlags.AlterGame: { - if (!IsPacketAllowed(message, true)) + if (!IsPacketAllowed(reader, true)) { return; } - Message10AlterGame.Deserialize(message, + Message10AlterGame.Deserialize(reader, out var gameTag, out var value); @@ -224,29 +189,29 @@ namespace Impostor.Server.Net return; } - Player.Game.HandleAlterGame(message, Player, value); + await Player.Game.HandleAlterGame(reader, Player, value); break; } case MessageFlags.KickPlayer: { - if (!IsPacketAllowed(message, true)) + if (!IsPacketAllowed(reader, true)) { return; } - Message11KickPlayer.Deserialize(message, + Message11KickPlayer.Deserialize(reader, out var playerId, out var isBan); - Player.Game.HandleKickPlayer(playerId, isBan); + await Player.Game.HandleKickPlayer(playerId, isBan); break; } case MessageFlags.GetGameListV2: { - Message16GetGameListV2.Deserialize(message, out var options); - Player.OnRequestGameList(options); + Message16GetGameListV2.Deserialize(reader, out var options); + await Player.OnRequestGameList(options); break; } @@ -259,23 +224,23 @@ namespace Impostor.Server.Net if (flag != MessageFlags.GameData && flag != MessageFlags.GameDataTo && flag != MessageFlags.EndGame && - message.Position < message.Length) + reader.Position < reader.Length) { Logger.Warning("Server did not consume all bytes from {0} ({1} < {2}).", flag, - message.Position, - message.Length); + reader.Position, + reader.Length); } #endif } - private void OnDisconnected(object sender, DisconnectedEventArgs e) + protected override async ValueTask OnDisconnected() { try { if (Player.Game != null) { - Player.Game.HandleRemovePlayer(Id, DisconnectReason.ExitGame); + await Player.Game.HandleRemovePlayer(Id, DisconnectReason.ExitGame); } } catch (Exception ex) diff --git a/src/Impostor.Server/Net/ClientBase.cs b/src/Impostor.Server/Net/ClientBase.cs new file mode 100644 index 0000000..03c6414 --- /dev/null +++ b/src/Impostor.Server/Net/ClientBase.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Impostor.Server.Net +{ + public abstract class ClientBase : IClient + { + protected ClientBase(int id, string name, IConnection connection) + { + Id = id; + Name = name; + Connection = connection; + Items = new ConcurrentDictionary(); + } + + public int Id { get; } + + public string Name { get; } + + public IConnection Connection { get; } + + public IDictionary Items { get; } + + public virtual async ValueTask InitializeAsync() + { + await Connection.MessageReceived.SubscribeAsync(OnMessageReceived, OnDisconnected); + } + + protected abstract ValueTask OnMessageReceived(IMessage message); + + protected abstract ValueTask OnDisconnected(); + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Factories/ClientFactory.cs b/src/Impostor.Server/Net/Factories/ClientFactory.cs new file mode 100644 index 0000000..3a6f3c9 --- /dev/null +++ b/src/Impostor.Server/Net/Factories/ClientFactory.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Server.Net.Messages; +using Impostor.Shared.Innersloth.Data; +using Microsoft.Extensions.DependencyInjection; + +namespace Impostor.Server.Net.Factories +{ + internal class ClientFactory : IClientFactory + where TClient : ClientBase + { + private int _idLast; + private readonly IServiceProvider _serviceProvider; + + public ClientFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public int NextId() + { + var clientId = Interlocked.Increment(ref _idLast); + + if (clientId < 1) + { + // Super rare but reset the _idLast because of overflow. + _idLast = 0; + + // And get a new id. + clientId = Interlocked.Increment(ref _idLast); + } + + return clientId; + } + + public async ValueTask CreateAsync(IConnection connection, string name, int clientVersion) + { + if (clientVersion != 50516550) + { + using var packet = connection.CreateMessage(MessageType.Reliable); + Message01JoinGame.SerializeError(packet, false, DisconnectReason.IncorrectVersion); + await packet.SendAsync(); + + throw new ClientVersionUnsupportedException(clientVersion); + } + + var clientId = NextId(); + var client = ActivatorUtilities.CreateInstance(_serviceProvider, clientId, name, connection); + + await client.InitializeAsync(); + + return client; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Manager/ClientManager.cs b/src/Impostor.Server/Net/Manager/ClientManager.cs index 7e4b64e..e0ea68e 100644 --- a/src/Impostor.Server/Net/Manager/ClientManager.cs +++ b/src/Impostor.Server/Net/Manager/ClientManager.cs @@ -1,54 +1,47 @@ -using System.Collections.Concurrent; -using System.Threading; -using Hazel; -using Impostor.Server.Exceptions; +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Impostor.Server.Net.Factories; using Microsoft.Extensions.Logging; namespace Impostor.Server.Net.Manager { internal class ClientManager : IClientManager { - private readonly ILogger _clientManager; - private readonly GameManager _gameManager; - private readonly ConcurrentDictionary _clients; - private readonly object _idLock; - private int _idLast; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _clients; + private readonly IClientFactory _clientFactory; - public ClientManager(ILogger clientManager, GameManager gameManager) + public ClientManager(ILogger logger, IClientFactory clientFactory) { - _clientManager = clientManager; - _gameManager = gameManager; - _clients = new ConcurrentDictionary(); - _idLock = new object(); - _idLast = 0; + _logger = logger; + _clientFactory = clientFactory; + _clients = new ConcurrentDictionary(); } - private int NextId() + public async ValueTask RegisterConnectionAsync(IConnection connection, string name, int clientVersion) { - var clientId = Interlocked.Increment(ref _idLast); - if (clientId < 1) + try { - // Super rare but reset the _idLast because of overflow. - _idLast = 0; + var client = await _clientFactory.CreateAsync(connection, name, clientVersion); - // And get a new id. - clientId = Interlocked.Increment(ref _idLast); + Register(client); + } + catch (ClientVersionUnsupportedException ex) + { + _logger.LogTrace("Closed connection because client version {Version} is not supported.", ex.Version); } - - return clientId; } - - public void Create(string name, Connection connection) + + public void Register(IClient client) { - var clientId = NextId(); - - _clientManager.LogInformation("Client connected."); - _clients.TryAdd(clientId, new Client(this, _gameManager, clientId, name, connection)); + _logger.LogInformation("Client connected."); + _clients.TryAdd(client.Id, client); } - public void Remove(Client client) + public void Remove(IClient client) { - _clientManager.LogInformation("Client disconnected."); + _logger.LogInformation("Client disconnected."); _clients.TryRemove(client.Id, out _); } } diff --git a/src/Impostor.Server/Net/Manager/GameManager.cs b/src/Impostor.Server/Net/Manager/GameManager.cs index 0a1b59a..2b39ab6 100644 --- a/src/Impostor.Server/Net/Manager/GameManager.cs +++ b/src/Impostor.Server/Net/Manager/GameManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; @@ -7,9 +8,9 @@ using Impostor.Server.Net.Redirector; using Impostor.Server.Net.State; using Impostor.Shared.Innersloth; using Impostor.Shared.Innersloth.Data; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Serilog; namespace Impostor.Server.Net.Manager { @@ -19,11 +20,13 @@ namespace Impostor.Server.Net.Manager private readonly INodeLocator _nodeLocator; private readonly IPEndPoint _publicIp; private readonly ConcurrentDictionary _games; + private readonly IServiceProvider _serviceProvider; - public GameManager(ILogger logger, IOptions config, INodeLocator nodeLocator) + public GameManager(ILogger logger, IOptions config, INodeLocator nodeLocator, IServiceProvider serviceProvider) { _logger = logger; _nodeLocator = nodeLocator; + _serviceProvider = serviceProvider; _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.PublicIp), config.Value.PublicPort); _games = new ConcurrentDictionary(); } @@ -32,15 +35,15 @@ namespace Impostor.Server.Net.Manager { // TODO: Prevent duplicates when using server redirector using INodeProvider. - var gameCode = GameCode.GenerateCode(6); - var gameCodeStr = GameCode.IntToGameName(gameCode); - var game = new Game(this, _nodeLocator, _publicIp, gameCode, options); + var gameCode = GameCode.Create(); + var gameCodeStr = gameCode.Code; + var game = ActivatorUtilities.CreateInstance(_serviceProvider, _publicIp, gameCode, options); if (_nodeLocator.Find(gameCodeStr) == null && _games.TryAdd(gameCode, game)) { _nodeLocator.Save(gameCodeStr, _publicIp); - _logger.LogDebug("Created game with code {0} ({1}).", game.CodeStr, gameCode); + _logger.LogDebug("Created game with code {0} ({1}).", game.Code, gameCode); return game; } @@ -107,8 +110,8 @@ namespace Impostor.Server.Net.Manager public void Remove(int gameCode) { - _logger.LogDebug("Remove game with code {0} ({1}).", GameCode.IntToGameName(gameCode), gameCode); - _nodeLocator.Remove(GameCode.IntToGameName(gameCode)); + _logger.LogDebug("Remove game with code {0} ({1}).", GameCodeParser.IntToGameName(gameCode), gameCode); + _nodeLocator.Remove(GameCodeParser.IntToGameName(gameCode)); _games.TryRemove(gameCode, out _); } } diff --git a/src/Impostor.Server/Net/Manager/IClientManager.cs b/src/Impostor.Server/Net/Manager/IClientManager.cs deleted file mode 100644 index 4449e2d..0000000 --- a/src/Impostor.Server/Net/Manager/IClientManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Hazel; - -namespace Impostor.Server.Net.Manager -{ - internal interface IClientManager - { - void Create(string name, Connection connection); - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Matchmaker.cs b/src/Impostor.Server/Net/Matchmaker.cs deleted file mode 100644 index ff2e242..0000000 --- a/src/Impostor.Server/Net/Matchmaker.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Net; -using Hazel; -using Hazel.Udp; -using Impostor.Server.Data; -using Impostor.Server.Net.Manager; -using Impostor.Server.Net.Messages; -using Impostor.Shared.Innersloth.Data; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Impostor.Server.Net -{ - internal class Matchmaker - { - private readonly ILogger _logger; - private readonly ServerConfig _serverConfig; - private readonly IClientManager _clientManager; - private readonly UdpConnectionListener _connection; - - public Matchmaker( - ILogger logger, - IOptions serverConfig, - IClientManager clientManager) - { - _logger = logger; - _serverConfig = serverConfig.Value; - _clientManager = clientManager; - _connection = new UdpConnectionListener(new IPEndPoint(IPAddress.Parse(_serverConfig.ListenIp), _serverConfig.ListenPort), IPMode.IPv4, s => - { - _logger.LogWarning("Log from Hazel: {0}", s); - }); - - _connection.NewConnection += OnNewConnection; - } - - public IPEndPoint EndPoint => _connection.EndPoint; - - private void OnNewConnection(NewConnectionEventArgs e) - { - try - { - // Handshake. - var clientVersion = e.HandshakeData.ReadInt32(); - var clientName = e.HandshakeData.ReadString(); - - e.HandshakeData.Recycle(); - - if (clientVersion != 50516550) - { - using (var packet = MessageWriter.Get(SendOption.Reliable)) - { - Message01JoinGame.SerializeError(packet, false, DisconnectReason.IncorrectVersion); - e.Connection.Send(packet); - } - return; - } - - // Create client. - _clientManager.Create(clientName, e.Connection); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in new connection."); - } - } - - public void Start() - { - _connection.Start(); - } - - public void Stop() - { - _connection.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Net/MatchmakerService.cs b/src/Impostor.Server/Net/MatchmakerService.cs index 08d0f1e..db2b38e 100644 --- a/src/Impostor.Server/Net/MatchmakerService.cs +++ b/src/Impostor.Server/Net/MatchmakerService.cs @@ -1,6 +1,8 @@ -using System.Threading; +using System.Net; +using System.Threading; using System.Threading.Tasks; using Impostor.Server.Data; +using Impostor.Server.Net.Manager; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,13 +14,13 @@ namespace Impostor.Server.Net private readonly ILogger _logger; private readonly ServerConfig _serverConfig; private readonly ServerRedirectorConfig _redirectorConfig; - private readonly Matchmaker _matchmaker; + private readonly IMatchmaker _matchmaker; public MatchmakerService( ILogger logger, IOptions serverConfig, IOptions redirectorConfig, - Matchmaker matchmaker) + IMatchmaker matchmaker) { _logger = logger; _serverConfig = serverConfig.Value; @@ -26,13 +28,15 @@ namespace Impostor.Server.Net _matchmaker = matchmaker; } - public Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken) { - _matchmaker.Start(); + var endpoint = new IPEndPoint(IPAddress.Parse(_serverConfig.ListenIp), _serverConfig.ListenPort); + + await _matchmaker.StartAsync(endpoint); _logger.LogInformation("Matchmaker is listening on {0}:{1}, the public server ip is {2}:{3}.", - _matchmaker.EndPoint.Address, - _matchmaker.EndPoint.Port, + endpoint.Address, + endpoint.Port, _serverConfig.PublicIp, _serverConfig.PublicPort); @@ -42,16 +46,12 @@ namespace Impostor.Server.Net ? "Server redirection is enabled as master, this instance will redirect clients to other nodes." : "Server redirection is enabled as node, this instance will accept clients."); } - - return Task.CompletedTask; } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) { _logger.LogWarning("Matchmaker is shutting down!"); - _matchmaker.Stop(); - - return Task.CompletedTask; + await _matchmaker.StopAsync(); } } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/Messages/Message00HostGame.cs b/src/Impostor.Server/Net/Messages/Message00HostGame.cs index 60a5c64..c1462e2 100644 --- a/src/Impostor.Server/Net/Messages/Message00HostGame.cs +++ b/src/Impostor.Server/Net/Messages/Message00HostGame.cs @@ -1,18 +1,17 @@ -using Hazel; -using Impostor.Shared.Innersloth; +using Impostor.Shared.Innersloth; namespace Impostor.Server.Net.Messages { internal static class Message00HostGame { - public static void Serialize(MessageWriter writer, int gameCode) + public static void Serialize(IMessageWriter writer, int gameCode) { writer.StartMessage(MessageFlags.HostGame); writer.Write(gameCode); writer.EndMessage(); } - public static GameOptionsData Deserialize(MessageReader reader) + public static GameOptionsData Deserialize(IMessageReader reader) { return GameOptionsData.Deserialize(reader.ReadBytesAndSize()); } diff --git a/src/Impostor.Server/Net/Messages/Message01JoinGame.cs b/src/Impostor.Server/Net/Messages/Message01JoinGame.cs index e478e83..f98f7ef 100644 --- a/src/Impostor.Server/Net/Messages/Message01JoinGame.cs +++ b/src/Impostor.Server/Net/Messages/Message01JoinGame.cs @@ -1,16 +1,15 @@ using System; -using Hazel; using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net.Messages { internal static class Message01JoinGame { - public static void SerializeJoin(MessageWriter writer, bool clear, int gameCode, int playerId, int hostId) + public static void SerializeJoin(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId) { if (clear) { - writer.Clear(SendOption.Reliable); + writer.Clear(MessageType.Reliable); } writer.StartMessage(MessageFlags.JoinGame); @@ -20,11 +19,11 @@ namespace Impostor.Server.Net.Messages writer.EndMessage(); } - public static void SerializeError(MessageWriter writer, bool clear, DisconnectReason reason, string message = null) + public static void SerializeError(IMessageWriter writer, bool clear, DisconnectReason reason, string message = null) { if (clear) { - writer.Clear(SendOption.Reliable); + writer.Clear(MessageType.Reliable); } writer.StartMessage(MessageFlags.JoinGame); @@ -43,7 +42,7 @@ namespace Impostor.Server.Net.Messages writer.EndMessage(); } - public static void Deserialize(MessageReader reader, out int gameCode, out byte unknown) + public static void Deserialize(IMessageReader reader, out int gameCode, out byte unknown) { gameCode = reader.ReadInt32(); unknown = reader.ReadByte(); diff --git a/src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs b/src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs index 3c82c1b..6335a18 100644 --- a/src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs +++ b/src/Impostor.Server/Net/Messages/Message04RemovePlayer.cs @@ -1,17 +1,16 @@ -using Hazel; -using Impostor.Shared.Innersloth.Data; +using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net.Messages { internal static class Message04RemovePlayer { - public static void Serialize(MessageWriter writer, bool clear, int gameCode, int playerId, int hostId, DisconnectReason reason) + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId, DisconnectReason reason) { // Only a subset of DisconnectReason shows an unique message. // ExitGame, Banned and Kicked. if (clear) { - writer.Clear(SendOption.Reliable); + writer.Clear(MessageType.Reliable); } writer.StartMessage(MessageFlags.RemovePlayer); @@ -22,7 +21,7 @@ namespace Impostor.Server.Net.Messages writer.EndMessage(); } - public static void Deserialize(MessageReader reader, out int playerId, out byte reason) + public static void Deserialize(IMessageReader reader, out int playerId, out byte reason) { playerId = reader.ReadPackedInt32(); reason = reader.ReadByte(); diff --git a/src/Impostor.Server/Net/Messages/Message07JoinedGame.cs b/src/Impostor.Server/Net/Messages/Message07JoinedGame.cs index 9fa08e7..6a4494d 100644 --- a/src/Impostor.Server/Net/Messages/Message07JoinedGame.cs +++ b/src/Impostor.Server/Net/Messages/Message07JoinedGame.cs @@ -1,14 +1,12 @@ -using Hazel; - -namespace Impostor.Server.Net.Messages +namespace Impostor.Server.Net.Messages { internal static class Message07JoinedGame { - public static void Serialize(MessageWriter writer, bool clear, int gameCode, int playerId, int hostId, int[] otherPlayerIds) + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId, int[] otherPlayerIds) { if (clear) { - writer.Clear(SendOption.Reliable); + writer.Clear(MessageType.Reliable); } writer.StartMessage(MessageFlags.JoinedGame); diff --git a/src/Impostor.Server/Net/Messages/Message10AlterGame.cs b/src/Impostor.Server/Net/Messages/Message10AlterGame.cs index f4f08a8..fad8e33 100644 --- a/src/Impostor.Server/Net/Messages/Message10AlterGame.cs +++ b/src/Impostor.Server/Net/Messages/Message10AlterGame.cs @@ -1,15 +1,14 @@ -using Hazel; -using Impostor.Shared.Innersloth.Data; +using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net.Messages { internal static class Message10AlterGame { - public static void Serialize(MessageWriter writer, bool clear, int gameCode) + public static void Serialize(IMessageWriter writer, bool clear, int gameCode) { if (clear) { - writer.Clear(SendOption.Reliable); + writer.Clear(MessageType.Reliable); } writer.StartMessage(MessageFlags.HostGame); @@ -17,7 +16,7 @@ namespace Impostor.Server.Net.Messages writer.EndMessage(); } - public static void Deserialize(MessageReader reader, out AlterGameTags gameTag, out bool value) + public static void Deserialize(IMessageReader reader, out AlterGameTags gameTag, out bool value) { gameTag = (AlterGameTags) reader.ReadByte(); value = reader.ReadBoolean(); diff --git a/src/Impostor.Server/Net/Messages/Message11KickPlayer.cs b/src/Impostor.Server/Net/Messages/Message11KickPlayer.cs index e8af2bb..8b899ab 100644 --- a/src/Impostor.Server/Net/Messages/Message11KickPlayer.cs +++ b/src/Impostor.Server/Net/Messages/Message11KickPlayer.cs @@ -1,14 +1,12 @@ -using Hazel; - -namespace Impostor.Server.Net.Messages +namespace Impostor.Server.Net.Messages { internal static class Message11KickPlayer { - public static void Serialize(MessageWriter writer, bool clear, int gameCode, int playerId, bool isBan) + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, bool isBan) { if (clear) { - writer.Clear(SendOption.Reliable); + writer.Clear(MessageType.Reliable); } writer.StartMessage(MessageFlags.KickPlayer); @@ -18,7 +16,7 @@ namespace Impostor.Server.Net.Messages writer.EndMessage(); } - public static void Deserialize(MessageReader reader, out int playerId, out bool isBan) + public static void Deserialize(IMessageReader reader, out int playerId, out bool isBan) { playerId = reader.ReadPackedInt32(); isBan = reader.ReadBoolean(); diff --git a/src/Impostor.Server/Net/Messages/Message12WaitForHost.cs b/src/Impostor.Server/Net/Messages/Message12WaitForHost.cs index e09f4b4..65d4cfa 100644 --- a/src/Impostor.Server/Net/Messages/Message12WaitForHost.cs +++ b/src/Impostor.Server/Net/Messages/Message12WaitForHost.cs @@ -1,14 +1,12 @@ -using Hazel; - -namespace Impostor.Server.Net.Messages +namespace Impostor.Server.Net.Messages { internal static class Message12WaitForHost { - public static void Serialize(MessageWriter writer, bool clear, int gameCode, int playerId) + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId) { if (clear) { - writer.Clear(SendOption.Reliable); + writer.Clear(MessageType.Reliable); } writer.StartMessage(MessageFlags.WaitForHost); diff --git a/src/Impostor.Server/Net/Messages/Message13Redirect.cs b/src/Impostor.Server/Net/Messages/Message13Redirect.cs index fe49c2d..5219564 100644 --- a/src/Impostor.Server/Net/Messages/Message13Redirect.cs +++ b/src/Impostor.Server/Net/Messages/Message13Redirect.cs @@ -1,19 +1,18 @@ using System.Net; -using Hazel; namespace Impostor.Server.Net.Messages { internal static class Message13Redirect { - public static void Serialize(MessageWriter writer, bool clear, IPEndPoint ipEndPoint) + public static void Serialize(IMessageWriter writer, bool clear, IPEndPoint ipEndPoint) { if (clear) { - writer.Clear(SendOption.Reliable); + writer.Clear(MessageType.Reliable); } writer.StartMessage(MessageFlags.Redirect); - writer.Write(ipEndPoint.Address.GetAddressBytes()); + writer.Write(ipEndPoint.Address); writer.Write((ushort) ipEndPoint.Port); writer.EndMessage(); } diff --git a/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs b/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs index f174892..038d168 100644 --- a/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs +++ b/src/Impostor.Server/Net/Messages/Message16GetGameListV2.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Hazel; using Impostor.Server.Net.State; using Impostor.Shared.Innersloth; @@ -7,13 +6,13 @@ namespace Impostor.Server.Net.Messages { internal static class Message16GetGameListV2 { - public static void Deserialize(MessageReader reader, out GameOptionsData options) + public static void Deserialize(IMessageReader reader, out GameOptionsData options) { reader.ReadPackedInt32(); // Hardcoded 0. options = GameOptionsData.Deserialize(reader.ReadBytesAndSize()); } - public static void Serialize(MessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable games) + public static void Serialize(IMessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable games) { writer.StartMessage(MessageFlags.GetGameListV2); @@ -29,7 +28,7 @@ namespace Impostor.Server.Net.Messages foreach (var game in games) { writer.StartMessage(0); - writer.Write(game.PublicIp.Address.GetAddressBytes()); + writer.Write(game.PublicIp.Address); writer.Write((ushort) game.PublicIp.Port); writer.Write(game.Code); writer.Write(game.Host.Client.Name); diff --git a/src/Impostor.Server/Net/Redirector/ClientManagerRedirector.cs b/src/Impostor.Server/Net/Redirector/ClientManagerRedirector.cs deleted file mode 100644 index 78631dc..0000000 --- a/src/Impostor.Server/Net/Redirector/ClientManagerRedirector.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Collections.Generic; -using Hazel; -using Impostor.Server.Net.Manager; -using Microsoft.Extensions.Logging; - -namespace Impostor.Server.Net.Redirector -{ - internal class ClientManagerRedirector : IClientManager - { - private readonly ILogger _logger; - private readonly INodeProvider _nodeProvider; - private readonly INodeLocator _nodeLocator; - private readonly HashSet _clients; - - public ClientManagerRedirector(ILogger logger, INodeProvider nodeProvider, INodeLocator nodeLocator) - { - _logger = logger; - _nodeProvider = nodeProvider; - _nodeLocator = nodeLocator; - _clients = new HashSet(); - } - - public void Create(string name, Connection connection) - { - _logger.LogInformation("Client connected."); - _clients.Add(new ClientRedirector(name, connection, this, _nodeProvider, _nodeLocator)); - } - - public void Remove(ClientRedirector client) - { - _logger.LogInformation("Client disconnected."); - _clients.Remove(client); - } - } -} \ No newline at end of file diff --git a/src/Impostor.Server/Net/Redirector/ClientRedirector.cs b/src/Impostor.Server/Net/Redirector/ClientRedirector.cs index 8bbe605..50951ca 100644 --- a/src/Impostor.Server/Net/Redirector/ClientRedirector.cs +++ b/src/Impostor.Server/Net/Redirector/ClientRedirector.cs @@ -1,6 +1,6 @@ -using System; -using Hazel; +using System.Threading.Tasks; using Impostor.Server.Data; +using Impostor.Server.Net.Manager; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth; using Impostor.Shared.Innersloth.Data; @@ -9,50 +9,26 @@ using ILogger = Serilog.ILogger; namespace Impostor.Server.Net.Redirector { - internal class ClientRedirector + internal class ClientRedirector : ClientBase { private static readonly ILogger Logger = Log.ForContext(); - private readonly string _name; - private readonly Connection _connection; - private readonly ClientManagerRedirector _clientManager; + private readonly IClientManager _clientManager; private readonly INodeProvider _nodeProvider; private readonly INodeLocator _nodeLocator; - public ClientRedirector(string name, Connection connection, ClientManagerRedirector clientManager, INodeProvider nodeProvider, INodeLocator nodeLocator) + public ClientRedirector(int id, string name, IConnection connection, IClientManager clientManager, INodeProvider nodeProvider, INodeLocator nodeLocator) + : base(id, name, connection) { - _name = name; - _connection = connection; - _connection.DataReceived += OnDataReceived; - _connection.Disconnected += OnDisconnected; _clientManager = clientManager; _nodeProvider = nodeProvider; _nodeLocator = nodeLocator; } - private void OnDataReceived(DataReceivedEventArgs e) + protected override async ValueTask OnMessageReceived(IMessage message) { - try - { - while (true) - { - if (e.Message.Position >= e.Message.Length) - { - break; - } - - OnMessageReceived(e.Message.ReadMessage()); - } - } - catch (Exception ex) - { - Logger.Error(ex, "Exception caught in client data handler."); - } - } - - private void OnMessageReceived(MessageReader message) - { - var flag = message.Tag; + var reader = message.CreateReader(); + var flag = reader.Tag; Logger.Verbose("Server got {0}.", flag); @@ -60,23 +36,21 @@ namespace Impostor.Server.Net.Redirector { case MessageFlags.HostGame: { - using (var packet = MessageWriter.Get(SendOption.Reliable)) - { - Message13Redirect.Serialize(packet, false, _nodeProvider.Get()); - _connection.Send(packet); - } + using var packet = Connection.CreateMessage(MessageType.Reliable); + Message13Redirect.Serialize(packet, false, _nodeProvider.Get()); + await packet.SendAsync(); break; } case MessageFlags.JoinGame: { - Message01JoinGame.Deserialize(message, + Message01JoinGame.Deserialize(reader, out var gameCode, out var unknown); - using (var packet = MessageWriter.Get(SendOption.Reliable)) + using (var packet = Connection.CreateMessage(MessageType.Reliable)) { - var endpoint = _nodeLocator.Find(GameCode.IntToGameName(gameCode)); + var endpoint = _nodeLocator.Find(GameCodeParser.IntToGameName(gameCode)); if (endpoint == null) { Message01JoinGame.SerializeError(packet, false, DisconnectReason.GameMissing); @@ -85,8 +59,8 @@ namespace Impostor.Server.Net.Redirector { Message13Redirect.Serialize(packet, false, endpoint); } - - _connection.Send(packet); + + await packet.SendAsync(); } break; } @@ -94,10 +68,10 @@ namespace Impostor.Server.Net.Redirector case MessageFlags.GetGameListV2: { // TODO: Implement. - using (var packet = MessageWriter.Get(SendOption.Reliable)) + using (var packet = Connection.CreateMessage(MessageType.Reliable)) { Message01JoinGame.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.NotImplemented); - _connection.Send(packet); + await packet.SendAsync(); } break; } @@ -110,9 +84,10 @@ namespace Impostor.Server.Net.Redirector } } - private void OnDisconnected(object sender, DisconnectedEventArgs e) + protected override ValueTask OnDisconnected() { _clientManager.Remove(this); + return default; } } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/State/ClientPlayer.Events.cs b/src/Impostor.Server/Net/State/ClientPlayer.Events.cs index b3468f9..0468618 100644 --- a/src/Impostor.Server/Net/State/ClientPlayer.Events.cs +++ b/src/Impostor.Server/Net/State/ClientPlayer.Events.cs @@ -1,4 +1,4 @@ -using Hazel; +using System.Threading.Tasks; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth; using Impostor.Shared.Innersloth.Data; @@ -14,9 +14,9 @@ namespace Impostor.Server.Net.State /// All options given. /// At this moment, the client can only specify the map, impostor count and chat language. /// - public void OnRequestGameList(GameOptionsData options) + public async ValueTask OnRequestGameList(GameOptionsData options) { - using (var message = MessageWriter.Get(SendOption.Reliable)) + using (var message = Client.Connection.CreateMessage(MessageType.Reliable)) { var games = _gameManager.FindListings((MapFlags) options.MapId, options.NumImpostors, options.Keywords); @@ -25,8 +25,8 @@ namespace Impostor.Server.Net.State var polusGameCount = _gameManager.GetGameCount(MapFlags.Polus); Message16GetGameListV2.Serialize(message, skeldGameCount, miraHqGameCount, polusGameCount, games); - - Client.Send(message); + + await message.SendAsync(); } } } diff --git a/src/Impostor.Server/Net/State/ClientPlayer.cs b/src/Impostor.Server/Net/State/ClientPlayer.cs index cce8a1b..f186e17 100644 --- a/src/Impostor.Server/Net/State/ClientPlayer.cs +++ b/src/Impostor.Server/Net/State/ClientPlayer.cs @@ -1,11 +1,11 @@ -using Hazel; +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 + internal partial class ClientPlayer : IClientPlayer { private readonly GameManager _gameManager; @@ -21,13 +21,17 @@ namespace Impostor.Server.Net.State public Game Game { get; set; } public LimboStates Limbo { get; set; } - public void SendDisconnectReason(DisconnectReason reason, string message = null) + public async ValueTask SendDisconnectReason(DisconnectReason reason, string message = null) { - using (var packet = MessageWriter.Get(SendOption.Reliable)) + using (var packet = Client.Connection.CreateMessage(MessageType.Reliable)) { Message01JoinGame.SerializeError(packet, false, reason, message); - Client.Connection.Send(packet); + await packet.SendAsync(); } } + + IClient IClientPlayer.Client => Client; + + IGame IClientPlayer.Game => Game; } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/State/Game.Incoming.cs b/src/Impostor.Server/Net/State/Game.Incoming.cs index 168429c..8b7db28 100644 --- a/src/Impostor.Server/Net/State/Game.Incoming.cs +++ b/src/Impostor.Server/Net/State/Game.Incoming.cs @@ -1,31 +1,27 @@ using System; -using System.Linq; -using Hazel; +using System.Threading.Tasks; using Impostor.Server.Data; -using Impostor.Server.Exceptions; using Impostor.Shared.Innersloth.Data; namespace Impostor.Server.Net.State { internal partial class Game { - public void HandleStartGame(MessageReader message) + public async ValueTask HandleStartGame(IMessageReader message) { GameState = GameStates.Started; - - using (var packet = MessageWriter.Get(SendOption.Reliable)) - { - packet.CopyFrom(message); - SendToAllExcept(packet, null); - } + + using var packet = CreateMessage(MessageType.Reliable); + message.CopyTo(packet); + await packet.SendToAllAsync(LimboStates.NotLimbo); } - public void HandleJoinGame(ClientPlayer sender) + public async ValueTask HandleJoinGame(ClientPlayer sender) { // Check if the IP of the player is banned. if (_bannedIps.Contains(sender.Client.Connection.EndPoint.Address)) { - sender.SendDisconnectReason(DisconnectReason.Banned); + await sender.SendDisconnectReason(DisconnectReason.Banned); return; } @@ -34,45 +30,45 @@ namespace Impostor.Server.Net.State // - The game is full. if (sender.Game != this && _players.Count >= Options.MaxPlayers) { - sender.SendDisconnectReason(DisconnectReason.GameFull); + await sender.SendDisconnectReason(DisconnectReason.GameFull); return; } // Check current player state. if (sender.Limbo == LimboStates.NotLimbo) { - sender.SendDisconnectReason(DisconnectReason.Custom, "Invalid limbo state while joining."); + await sender.SendDisconnectReason(DisconnectReason.Custom, "Invalid limbo state while joining."); return; } switch (GameState) { case GameStates.NotStarted: - HandleJoinGameNew(sender); + await HandleJoinGameNew(sender); break; case GameStates.Ended: - HandleJoinGameNext(sender); + await HandleJoinGameNext(sender); break; case GameStates.Started: - sender.SendDisconnectReason(DisconnectReason.GameStarted); + await sender.SendDisconnectReason(DisconnectReason.GameStarted); return; case GameStates.Destroyed: - sender.SendDisconnectReason(DisconnectReason.Custom, DisconnectMessages.Destroyed); + await sender.SendDisconnectReason(DisconnectReason.Custom, DisconnectMessages.Destroyed); return; default: throw new ArgumentOutOfRangeException(); } } - public void HandleEndGame(MessageReader message) + public async ValueTask HandleEndGame(IMessageReader message) { GameState = GameStates.Ended; // Broadcast end of the game. - using (var packet = MessageWriter.Get(SendOption.Reliable)) + using (var packet = CreateMessage(MessageType.Reliable)) { - packet.CopyFrom(message); - SendToAllExcept(packet, null); + message.CopyTo(packet); + await packet.SendToAllAsync(LimboStates.NotLimbo); } // Put all players in the correct limbo state. @@ -82,60 +78,53 @@ namespace Impostor.Server.Net.State } } - public void HandleAlterGame(MessageReader message, ClientPlayer sender, bool isPublic) + public async ValueTask HandleAlterGame(IMessageReader message, ClientPlayer sender, bool isPublic) { IsPublic = isPublic; - - using (var packet = MessageWriter.Get(SendOption.Reliable)) - { - packet.CopyFrom(message); - SendToAllExcept(packet, sender.Client.Id); - } + + using var packet = CreateMessage(MessageType.Reliable); + message.CopyTo(packet); + await packet.SendToAllExceptAsync(LimboStates.NotLimbo, sender.Client.Id); } - public void HandleRemovePlayer(int playerId, DisconnectReason reason) + public async ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason) { - PlayerRemove(playerId, out _); + await PlayerRemove(playerId); // It's possible that the last player was removed, so check if the game is still around. if (GameState == GameStates.Destroyed) { return; } - - using (var packet = MessageWriter.Get(SendOption.Reliable)) - { - WriteRemovePlayerMessage(packet, false, playerId, reason); - SendToAllExcept(packet, playerId); - } + + using var packet = CreateMessage(MessageType.Reliable); + WriteRemovePlayerMessage(packet, false, playerId, reason); + await packet.SendToAllExceptAsync(LimboStates.NotLimbo, playerId); } - public void HandleKickPlayer(int playerId, bool isBan) + public async ValueTask HandleKickPlayer(int playerId, bool isBan) { - Logger.Information("{0} - Player {1} has left.", CodeStr, playerId); + Logger.Information("{0} - Player {1} has left.", Code, playerId); - using (var message = MessageWriter.Get(SendOption.Reliable)) + using (var message = CreateMessage(MessageType.Reliable)) { // Send message to everyone that this player was kicked. WriteKickPlayerMessage(message, false, playerId, isBan); - SendToAllExcept(message, null); - - if (PlayerRemove(playerId, out var player) && isBan) - { - _bannedIps.Add(player.Client.Connection.EndPoint.Address); - } + 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); - SendToAllExcept(message, player?.Client.Id); + await message.SendToAllExceptAsync(LimboStates.NotLimbo, playerId); } } - private void HandleJoinGameNew(ClientPlayer sender) + private async ValueTask HandleJoinGameNew(ClientPlayer sender) { - Logger.Information("{0} - Player {1} ({2}) is joining.", CodeStr, sender.Client.Name, sender.Client.Id); + Logger.Information("{0} - Player {1} ({2}) is joining.", Code, sender.Client.Name, sender.Client.Id); // Add player to the game. if (sender.Game == null) @@ -143,21 +132,21 @@ namespace Impostor.Server.Net.State PlayerAdd(sender); } - using (var message = MessageWriter.Get(SendOption.Reliable)) + using (var message = CreateMessage(MessageType.Reliable)) { WriteJoinedGameMessage(message, false, sender); WriteAlterGameMessage(message, false); sender.Limbo = LimboStates.NotLimbo; - sender.Client.Send(message); + await message.SendToAsync(sender.Client); - BroadcastJoinMessage(message, true, sender); + await BroadcastJoinMessage(message, true, sender); } } - private void HandleJoinGameNext(ClientPlayer sender) + private async ValueTask HandleJoinGameNext(ClientPlayer sender) { - Logger.Information("{0} - Player {1} ({2}) is rejoining.", CodeStr, sender.Client.Name, sender.Client.Id); + Logger.Information("{0} - Player {1} ({2}) is rejoining.", Code, sender.Client.Name, sender.Client.Id); // Add player to the game. if (sender.Game == null) @@ -171,22 +160,21 @@ namespace Impostor.Server.Net.State GameState = GameStates.NotStarted; // Spawn the host. - HandleJoinGameNew(sender); + await HandleJoinGameNew(sender); // Pull players out of limbo. - CheckLimboPlayers(); + await CheckLimboPlayers(); return; } sender.Limbo = LimboStates.WaitingForHost; - using (var packet = MessageWriter.Get(SendOption.Reliable)) - { - WriteWaitForHostMessage(packet, false, sender); - sender.Client.Send(packet); + using var packet = CreateMessage(MessageType.Reliable); + + WriteWaitForHostMessage(packet, false, sender); + await packet.SendToAsync(sender.Client); - BroadcastJoinMessage(packet, true, sender); - } + await BroadcastJoinMessage(packet, true, sender); } } } \ No newline at end of file diff --git a/src/Impostor.Server/Net/State/Game.Outgoing.cs b/src/Impostor.Server/Net/State/Game.Outgoing.cs index 0938ee1..a26cfb3 100644 --- a/src/Impostor.Server/Net/State/Game.Outgoing.cs +++ b/src/Impostor.Server/Net/State/Game.Outgoing.cs @@ -1,5 +1,4 @@ using System.Linq; -using Hazel; using Impostor.Server.Net.Messages; using Impostor.Shared.Innersloth.Data; @@ -7,12 +6,12 @@ namespace Impostor.Server.Net.State { internal partial class Game { - private void WriteRemovePlayerMessage(MessageWriter message, bool clear, int playerId, DisconnectReason reason) + private void WriteRemovePlayerMessage(IMessageWriter message, bool clear, int playerId, DisconnectReason reason) { Message04RemovePlayer.Serialize(message, clear, Code, playerId, HostId, reason); } - private void WriteJoinedGameMessage(MessageWriter message, bool clear, ClientPlayer player) + private void WriteJoinedGameMessage(IMessageWriter message, bool clear, ClientPlayer player) { var playerIds = _players .Where(x => x.Value != player) @@ -22,17 +21,17 @@ namespace Impostor.Server.Net.State Message07JoinedGame.Serialize(message, clear, Code, player.Client.Id, HostId, playerIds); } - private void WriteAlterGameMessage(MessageWriter message, bool clear) + private void WriteAlterGameMessage(IMessageWriter message, bool clear) { Message10AlterGame.Serialize(message, clear, Code); } - private void WriteKickPlayerMessage(MessageWriter message, bool clear, int playerId, bool isBan) + private void WriteKickPlayerMessage(IMessageWriter message, bool clear, int playerId, bool isBan) { Message11KickPlayer.Serialize(message, clear, Code, playerId, isBan); } - private void WriteWaitForHostMessage(MessageWriter message, bool clear, ClientPlayer player) + private void WriteWaitForHostMessage(IMessageWriter message, bool clear, ClientPlayer player) { Message12WaitForHost.Serialize(message, clear, Code, player.Client.Id); } diff --git a/src/Impostor.Server/Net/State/Game.State.cs b/src/Impostor.Server/Net/State/Game.State.cs index 057526e..6731f2c 100644 --- a/src/Impostor.Server/Net/State/Game.State.cs +++ b/src/Impostor.Server/Net/State/Game.State.cs @@ -1,5 +1,6 @@ -using System.Linq; -using Hazel; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Impostor.Server.Exceptions; using Impostor.Shared.Innersloth.Data; @@ -25,9 +26,9 @@ namespace Impostor.Server.Net.State } } - private bool PlayerRemove(int playerId, out ClientPlayer player) + private async ValueTask PlayerRemove(int playerId, bool isBan = false) { - if (!_players.TryRemove(playerId, out player)) + if (!_players.TryRemove(playerId, out var player)) { return false; } @@ -35,7 +36,7 @@ namespace Impostor.Server.Net.State player.Limbo = LimboStates.PreSpawn; player.Game = null; - Logger.Information("{0} - Player {1} ({2}) has left.", CodeStr, player.Client.Name, playerId); + Logger.Information("{0} - Player {1} ({2}) has left.", Code, player.Client.Name, playerId); // Game is empty, remove it. if (_players.Count == 0) @@ -50,19 +51,24 @@ namespace Impostor.Server.Net.State // Host migration. if (HostId == playerId) { - MigrateHost(); + await MigrateHost(); + } + + if (isBan) + { + _bannedIps.Add(player.Client.Connection.EndPoint.Address); } return true; } - private void MigrateHost() + private async ValueTask MigrateHost() { // Pick the first player as new host. var host = _players.First().Value; HostId = host.Client.Id; - Logger.Information("{0} - Assigned {1} ({2}) as new host.", CodeStr, host.Client.Name, 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) @@ -70,25 +76,24 @@ namespace Impostor.Server.Net.State GameState = GameStates.NotStarted; // Spawn the host. - HandleJoinGameNew(host); + await HandleJoinGameNew(host); // Pull players out of limbo. - CheckLimboPlayers(); + await CheckLimboPlayers(); } } - private void CheckLimboPlayers() + private async ValueTask CheckLimboPlayers() { - using (var message = MessageWriter.Get(SendOption.Reliable)) + using var message = CreateMessage(MessageType.Reliable); + + foreach (var (_, player) in _players.Where(x => x.Value.Limbo == LimboStates.WaitingForHost)) { - foreach (var (_, player) in _players.Where(x => x.Value.Limbo == LimboStates.WaitingForHost)) - { - WriteJoinedGameMessage(message, true, player); - WriteAlterGameMessage(message, false); + WriteJoinedGameMessage(message, true, player); + WriteAlterGameMessage(message, false); - player.Limbo = LimboStates.NotLimbo; - player.Client.Send(message); - } + player.Limbo = LimboStates.NotLimbo; + await message.SendToAsync(player.Client); } } } diff --git a/src/Impostor.Server/Net/State/Game.cs b/src/Impostor.Server/Net/State/Game.cs index 4d0031f..10e9a7a 100644 --- a/src/Impostor.Server/Net/State/Game.cs +++ b/src/Impostor.Server/Net/State/Game.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using Hazel; +using System.Threading.Tasks; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Messages; using Impostor.Server.Net.Redirector; @@ -13,16 +13,23 @@ using ILogger = Serilog.ILogger; namespace Impostor.Server.Net.State { - internal partial class Game + internal partial class Game : IGame { private static readonly ILogger Logger = Log.ForContext(); private readonly GameManager _gameManager; private readonly INodeLocator _nodeLocator; + private readonly IMatchmaker matchmaker; private readonly ConcurrentDictionary _players; private readonly HashSet _bannedIps; - public Game(GameManager gameManager, INodeLocator nodeLocator, IPEndPoint publicIp, int code, GameOptionsData options) + public Game( + GameManager gameManager, + INodeLocator nodeLocator, + IPEndPoint publicIp, + GameCode code, + GameOptionsData options, + IMatchmaker matchmaker) { _gameManager = gameManager; _nodeLocator = nodeLocator; @@ -31,75 +38,49 @@ namespace Impostor.Server.Net.State PublicIp = publicIp; Code = code; - CodeStr = GameCode.IntToGameName(code); HostId = -1; GameState = GameStates.NotStarted; Options = options; + this.matchmaker = matchmaker; + Items = new ConcurrentDictionary(); } public IPEndPoint PublicIp { get; } - public int Code { get; } - public string CodeStr { get; } + public GameCode Code { get; } public bool IsPublic { get; private set; } public int HostId { get; private set; } public GameStates GameState { get; private set; } public GameOptionsData Options { get; } + public IDictionary Items { get; } public int PlayerCount => _players.Count; - public ClientPlayer Host => _players[HostId]; - /// - /// Send a message to all players except one. - /// - /// The message to send. - /// - /// The player to exclude from sending the message. - /// Set to null to send a message to everyone. - /// - public void SendToAllExcept(MessageWriter message, int? senderId) + public IClientPlayer Host => _players[HostId]; + + private ValueTask BroadcastJoinMessage(IGameMessageWriter message, bool clear, ClientPlayer player) { - foreach (var (_, player) in _players.Where(x => - x.Value.Limbo == LimboStates.NotLimbo && - x.Value.Client.Id != senderId)) - { - if (player.Client.Connection.State != ConnectionState.Connected) - { - Logger.Warning("[{0}] Tried to send data to a disconnected player ({1}).", senderId, player.Client.Id); - continue; - } - - player.Client.Send(message); - } + Message01JoinGame.SerializeJoin(message, clear, Code, player.Client.Id, HostId); + + return message.SendToAllExceptAsync(LimboStates.NotLimbo, player.Client.Id); } - /// - /// Send a message to a specific player. - /// - /// The message to send. - /// - public void SendTo(MessageWriter message, int playerId) + public IEnumerable Players => _players.Select(p => p.Value); + + public IGameMessageWriter CreateMessage(MessageType type) { - if (_players.TryGetValue(playerId, out var player)) - { - if (player.Client.Connection.State != ConnectionState.Connected) - { - Logger.Warning("[{0}] Sending data to {1} failed, player is not connected.", CodeStr, player.Client.Id); - return; - } - - player.Client.Send(message); - } - else - { - Logger.Warning("[{0}] Sending data to {1} failed, player does not exist.", CodeStr, playerId); - } + return matchmaker.CreateGameMessageWriter(this, type); } - - private void BroadcastJoinMessage(MessageWriter message, bool clear, ClientPlayer player) + + public bool TryGetPlayer(int id, out IClientPlayer player) { - Message01JoinGame.SerializeJoin(message, clear, Code, player.Client.Id, HostId); - - SendToAllExcept(message, player.Client.Id); + if (_players.TryGetValue(id, out var result)) + { + player = result; + return true; + } + + player = default; + return false; } } } \ No newline at end of file diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index 2dfded5..a9b8219 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -1,7 +1,8 @@ using System; -using System.IO; using Impostor.Server.Data; +using Impostor.Server.Hazel; using Impostor.Server.Net; +using Impostor.Server.Net.Factories; using Impostor.Server.Net.Manager; using Impostor.Server.Net.Redirector; using Microsoft.Extensions.Configuration; @@ -108,18 +109,20 @@ namespace Impostor.Server services.AddSingleton(); } + services.AddSingleton(); + if (redirector.Enabled && redirector.Master) { - services.AddSingleton(); + services.AddSingleton>(); // For a master server, we don't need a GameManager. } else { - services.AddSingleton(); + services.AddSingleton>(); services.AddSingleton(); } - - services.AddSingleton(); + + services.UseHazelMatchmaking(); services.AddHostedService(); }) .UseConsoleLifetime() diff --git a/src/Impostor.Shared/Innersloth/Data/LimboStates.cs b/src/Impostor.Shared/Innersloth/Data/LimboStates.cs deleted file mode 100644 index e501614..0000000 --- a/src/Impostor.Shared/Innersloth/Data/LimboStates.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Impostor.Shared.Innersloth.Data -{ - public enum LimboStates - { - PreSpawn, - NotLimbo, - WaitingForHost, - } -} \ No newline at end of file diff --git a/src/Impostor.Shared/Innersloth/GameCode.cs b/src/Impostor.Shared/Innersloth/GameCode.cs deleted file mode 100644 index 3bcc662..0000000 --- a/src/Impostor.Shared/Innersloth/GameCode.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Buffers.Binary; -using System.Linq; -using System.Security.Cryptography; -using System.Text; - -namespace Impostor.Shared.Innersloth -{ - public static class GameCode - { - private const string V2 = "QWXRTYLPESDFGHUJKZOCVBINMA"; - private static readonly int[] V2Map = { - 25, - 21, - 19, - 10, - 8, - 11, - 12, - 13, - 22, - 15, - 16, - 6, - 24, - 23, - 18, - 7, - 0, - 3, - 9, - 4, - 14, - 20, - 1, - 2, - 5, - 17 - }; - private static readonly RNGCryptoServiceProvider Random = new RNGCryptoServiceProvider(); - - public static string IntToGameName(int input) - { - // V2. - if (input < -1) - { - return IntToGameNameV2(input); - } - - // V1. - Span code = stackalloc byte[4]; - BinaryPrimitives.WriteInt32LittleEndian(code, input); -#if NET452 - return Encoding.UTF8.GetString(code.Slice(0, 4).ToArray()); -#else - return Encoding.UTF8.GetString(code.Slice(0, 4)); -#endif - } - - private static string IntToGameNameV2(int input) - { - var a = input & 0x3FF; - var b = (input >> 10) & 0xFFFFF; - - return new string(new [] - { - V2[a % 26], - V2[a / 26], - V2[b % 26], - V2[b / 26 % 26], - V2[b / (26 * 26) % 26], - V2[b / (26 * 26 * 26) % 26] - }); - } - - public static int GameNameToInt(string code) - { - var upper = code.ToUpperInvariant(); - if (upper.Any(x => !char.IsLetter(x))) - { - return -1; - } - - var len = code.Length; - if (len == 6) - { - return GameNameToIntV2(upper); - } - - if (len == 4) - { - return code[0] | ((code[1] | ((code[2] | (code[3] << 8)) << 8)) << 8); - } - - return -1; - } - - private static int GameNameToIntV2(string code) - { - var a = V2Map[code[0] - 65]; - var b = V2Map[code[1] - 65]; - var c = V2Map[code[2] - 65]; - var d = V2Map[code[3] - 65]; - var e = V2Map[code[4] - 65]; - var f = V2Map[code[5] - 65]; - - var one = (a + 26 * b) & 0x3FF; - var two = (c + 26 * (d + 26 * (e + 26 * f))); - - return (int) (one | ((two << 10) & 0x3FFFFC00) | 0x80000000); - } - - public static int GenerateCode(int len) - { - if (len != 4 && len != 6) - { - throw new ArgumentException("should be 4 or 6", nameof(len)); - } - - // Generate random bytes. -#if NET452 - var data = new byte[len]; -#else - Span data = stackalloc byte[len]; -#endif - Random.GetBytes(data); - - // Convert to their char representation. - Span dataChar = stackalloc char[len]; - for (var i = 0; i < len; i++) - { - dataChar[i] = V2[V2Map[data[i] % 26]]; - } - -#if NET452 - return GameNameToInt(new string(dataChar.ToArray())); -#else - return GameNameToInt(new string(dataChar)); -#endif - } - } -} \ No newline at end of file diff --git a/src/Impostor.Shared/Innersloth/GameCodeParser.cs b/src/Impostor.Shared/Innersloth/GameCodeParser.cs new file mode 100644 index 0000000..533ec8a --- /dev/null +++ b/src/Impostor.Shared/Innersloth/GameCodeParser.cs @@ -0,0 +1,142 @@ +using System; +using System.Buffers.Binary; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Impostor.Shared.Innersloth +{ + public static class GameCodeParser + { + private const string V2 = "QWXRTYLPESDFGHUJKZOCVBINMA"; + private static readonly int[] V2Map = { + 25, + 21, + 19, + 10, + 8, + 11, + 12, + 13, + 22, + 15, + 16, + 6, + 24, + 23, + 18, + 7, + 0, + 3, + 9, + 4, + 14, + 20, + 1, + 2, + 5, + 17 + }; + private static readonly RNGCryptoServiceProvider Random = new RNGCryptoServiceProvider(); + + public static string IntToGameName(int input) + { + // V2. + if (input < -1) + { + return IntToGameNameV2(input); + } + + // V1. + Span code = stackalloc byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(code, input); +#if NET452 + return Encoding.UTF8.GetString(code.Slice(0, 4).ToArray()); +#else + return Encoding.UTF8.GetString(code.Slice(0, 4)); +#endif + } + + private static string IntToGameNameV2(int input) + { + var a = input & 0x3FF; + var b = (input >> 10) & 0xFFFFF; + + return new string(new [] + { + V2[a % 26], + V2[a / 26], + V2[b % 26], + V2[b / 26 % 26], + V2[b / (26 * 26) % 26], + V2[b / (26 * 26 * 26) % 26] + }); + } + + public static int GameNameToInt(string code) + { + var upper = code.ToUpperInvariant(); + if (upper.Any(x => !char.IsLetter(x))) + { + return -1; + } + + var len = code.Length; + if (len == 6) + { + return GameNameToIntV2(upper); + } + + if (len == 4) + { + return code[0] | ((code[1] | ((code[2] | (code[3] << 8)) << 8)) << 8); + } + + return -1; + } + + private static int GameNameToIntV2(string code) + { + var a = V2Map[code[0] - 65]; + var b = V2Map[code[1] - 65]; + var c = V2Map[code[2] - 65]; + var d = V2Map[code[3] - 65]; + var e = V2Map[code[4] - 65]; + var f = V2Map[code[5] - 65]; + + var one = (a + 26 * b) & 0x3FF; + var two = (c + 26 * (d + 26 * (e + 26 * f))); + + return (int) (one | ((two << 10) & 0x3FFFFC00) | 0x80000000); + } + + public static int GenerateCode(int len) + { + if (len != 4 && len != 6) + { + throw new ArgumentException("should be 4 or 6", nameof(len)); + } + + // Generate random bytes. +#if NET452 + var data = new byte[len]; +#else + Span data = stackalloc byte[len]; +#endif + Random.GetBytes(data); + + // Convert to their char representation. + Span dataChar = stackalloc char[len]; + for (var i = 0; i < len; i++) + { + dataChar[i] = V2[V2Map[data[i] % 26]]; + } + +#if NET452 + return GameNameToInt(new string(dataChar.ToArray())); +#else + return GameNameToInt(new string(dataChar)); +#endif + } + } +} \ No newline at end of file diff --git a/src/Impostor.Shared/Innersloth/GameOptionsData.cs b/src/Impostor.Shared/Innersloth/GameOptionsData.cs index 4150a74..375b6ee 100644 --- a/src/Impostor.Shared/Innersloth/GameOptionsData.cs +++ b/src/Impostor.Shared/Innersloth/GameOptionsData.cs @@ -59,9 +59,11 @@ namespace Impostor.Shared.Innersloth } } - public static GameOptionsData Deserialize(byte[] bytes) + public static GameOptionsData Deserialize(ReadOnlyMemory bytes) { - using (var stream = new MemoryStream(bytes)) + // TODO: Remove memory allocation. + + using (var stream = new MemoryStream(bytes.ToArray())) using (var reader = new BinaryReader(stream)) { var result = new GameOptionsData(); diff --git a/src/Impostor.Tests/GameCodeTests.cs b/src/Impostor.Tests/GameCodeTests.cs index 24e6171..90ac515 100644 --- a/src/Impostor.Tests/GameCodeTests.cs +++ b/src/Impostor.Tests/GameCodeTests.cs @@ -11,8 +11,8 @@ namespace Impostor.Tests const string code = "ABCD"; const int codeInt = 0x44434241; - Assert.Equal(code, GameCode.IntToGameName(codeInt)); - Assert.Equal(codeInt, GameCode.GameNameToInt(code)); + Assert.Equal(code, GameCodeParser.IntToGameName(codeInt)); + Assert.Equal(codeInt, GameCodeParser.GameNameToInt(code)); } [Fact] @@ -21,8 +21,8 @@ namespace Impostor.Tests const string code = "ABCDEF"; const int codeInt = -1943683525; - Assert.Equal(code, GameCode.IntToGameName(codeInt)); - Assert.Equal(codeInt, GameCode.GameNameToInt(code)); + Assert.Equal(code, GameCodeParser.IntToGameName(codeInt)); + Assert.Equal(codeInt, GameCodeParser.GameNameToInt(code)); } } } \ No newline at end of file diff --git a/src/Impostor.sln b/src/Impostor.sln index 73e27cf..a81e19c 100644 --- a/src/Impostor.sln +++ b/src/Impostor.sln @@ -19,6 +19,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tools.Proxy", "Imp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client", "Impostor.Client\Impostor.Client.csproj", "{804CF172-0C87-4423-9688-BD97D549891E}" 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 GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -75,6 +81,30 @@ Global {804CF172-0C87-4423-9688-BD97D549891E}.Release|Any CPU.Build.0 = Release|Any CPU {804CF172-0C87-4423-9688-BD97D549891E}.Release|x86.ActiveCfg = Release|Any CPU {804CF172-0C87-4423-9688-BD97D549891E}.Release|x86.Build.0 = Release|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|x86.Build.0 = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.Build.0 = Release|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.ActiveCfg = Release|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.Build.0 = Release|Any CPU + {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 + {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Debug|x86.Build.0 = Debug|Any CPU + {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Release|Any CPU.Build.0 = Release|Any CPU + {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Release|x86.ActiveCfg = Release|Any CPU + {C9E8E1E2-BFE3-41AD-8CB9-82F0F52E2306}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE