diff options
Diffstat (limited to 'Impostor-dev/src/Impostor.Server')
123 files changed, 6991 insertions, 0 deletions
diff --git a/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs b/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs new file mode 100644 index 0000000..f4807e7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs @@ -0,0 +1,9 @@ +namespace Impostor.Server.Config +{ + public class AntiCheatConfig + { + public const string Section = "AntiCheat"; + + public bool BanIpFromGame { get; set; } = true; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs b/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs new file mode 100644 index 0000000..630d1b4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs @@ -0,0 +1,11 @@ +namespace Impostor.Server.Config +{ + public class DebugConfig + { + public const string Section = "Debug"; + + public bool GameRecorderEnabled { get; set; } + + public string GameRecorderPath { get; set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs b/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs new file mode 100644 index 0000000..a86735f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs @@ -0,0 +1,19 @@ +namespace Impostor.Server.Config +{ + public static class DisconnectMessages + { + public const string Error = "There was an internal server error. " + + "Check the server console for more information. " + + "Please report the issue on the AmongUsServer GitHub if it keeps happening."; + + public const string Destroyed = "The game you tried to join is being destroyed. " + + "Please create a new game."; + + public const string NotImplemented = "Game listing has not been implemented in Impostor yet for servers " + + "running in server redirection mode."; + + public const string UsernameLength = "Your username is too long, please make it shorter."; + + public const string UsernameIllegalCharacters = "Your username contains illegal characters, please remove them."; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs b/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs new file mode 100644 index 0000000..1c58433 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs @@ -0,0 +1,30 @@ +using Impostor.Server.Utils; + +namespace Impostor.Server.Config +{ + internal class ServerConfig + { + private string? _resolvedPublicIp; + private string? _resolvedListenIp; + + public const string Section = "Server"; + + public string PublicIp { get; set; } = "127.0.0.1"; + + public ushort PublicPort { get; set; } = 22023; + + public string ListenIp { get; set; } = "127.0.0.1"; + + public ushort ListenPort { get; set; } = 22023; + + public string ResolvePublicIp() + { + return _resolvedPublicIp ??= IpUtils.ResolveIp(PublicIp); + } + + public string ResolveListenIp() + { + return _resolvedListenIp ??= IpUtils.ResolveIp(ListenIp); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs new file mode 100644 index 0000000..0ccfa0d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Impostor.Server.Config +{ + public class ServerRedirectorConfig + { + public const string Section = "ServerRedirector"; + + public bool Enabled { get; set; } + + public bool Master { get; set; } + + public NodeLocator Locator { get; set; } + + public List<ServerRedirectorNode> Nodes { get; set; } + + public class NodeLocator + { + public string Redis { get; set; } + + public string UdpMasterEndpoint { get; set; } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs new file mode 100644 index 0000000..d11b60f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs @@ -0,0 +1,9 @@ +namespace Impostor.Server.Config +{ + public class ServerRedirectorNode + { + public string Ip { get; set; } + + public ushort Port { get; set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Constants.cs b/Impostor-dev/src/Impostor.Server/Constants.cs new file mode 100644 index 0000000..62d90b2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Constants.cs @@ -0,0 +1,8 @@ +namespace Impostor.Server +{ + internal static class Constants + { + public const int SpawnTimeout = 2500; + public const int ConnectionTimeout = 2500; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs b/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs new file mode 100644 index 0000000..190f7f3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs @@ -0,0 +1,24 @@ +using Impostor.Api.Events; +using Impostor.Server.Events.Register; + +namespace Impostor.Server.Events +{ + internal readonly struct EventHandler + { + public EventHandler(IEventListener o, IRegisteredEventListener listener) + { + Object = o; + Listener = listener; + } + + public IEventListener Object { get; } + + public IRegisteredEventListener Listener { get; } + + public void Deconstruct(out IEventListener o, out IRegisteredEventListener listener) + { + o = Object; + listener = Listener; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/EventManager.cs b/Impostor-dev/src/Impostor.Server/Events/EventManager.cs new file mode 100644 index 0000000..5625c4d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/EventManager.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events; +using Impostor.Api.Events.Managers; +using Impostor.Server.Events.Register; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Events +{ + internal class EventManager : IEventManager + { + private readonly ConcurrentDictionary<Type, TemporaryEventRegister> _temporaryEventListeners; + private readonly ConcurrentDictionary<Type, List<EventHandler>> _cachedEventHandlers; + private readonly ILogger<EventManager> _logger; + private readonly IServiceProvider _serviceProvider; + + public EventManager(ILogger<EventManager> logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + _temporaryEventListeners = new ConcurrentDictionary<Type, TemporaryEventRegister>(); + _cachedEventHandlers = new ConcurrentDictionary<Type, List<EventHandler>>(); + } + + /// <inheritdoc /> + public IDisposable RegisterListener<TListener>(TListener listener, Func<Func<Task>, Task> invoker = null) + where TListener : IEventListener + { + if (listener == null) + { + throw new ArgumentNullException(nameof(listener)); + } + + var eventListeners = RegisteredEventListener.FromType(listener.GetType()); + var disposes = new IDisposable[eventListeners.Count]; + + foreach (var eventListener in eventListeners) + { + IRegisteredEventListener wrappedEventListener = new WrappedRegisteredEventListener(eventListener, listener); + + if (invoker != null) + { + wrappedEventListener = new InvokedRegisteredEventListener(wrappedEventListener, invoker); + } + + var register = _temporaryEventListeners.GetOrAdd( + wrappedEventListener.EventType, + _ => new TemporaryEventRegister()); + + register.Add(wrappedEventListener); + } + + if (eventListeners.Count > 0) + { + _cachedEventHandlers.TryRemove(typeof(TListener), out _); + } + + return new MultiDisposable(disposes); + } + + /// <inheritdoc /> + public bool IsRegistered<TEvent>() + where TEvent : IEvent + { + if (_cachedEventHandlers.TryGetValue(typeof(TEvent), out var handlers)) + { + return handlers.Count > 0; + } + + return GetHandlers<TEvent>().Any(); + } + + /// <inheritdoc /> + public async ValueTask CallAsync<T>(T @event) + where T : IEvent + { + try + { + if (!_cachedEventHandlers.TryGetValue(typeof(T), out var handlers)) + { + handlers = CacheEventHandlers<T>(); + } + + foreach (var (handler, eventListener) in handlers) + { + await eventListener.InvokeAsync(handler, @event, _serviceProvider); + } + } + catch (ImpostorCheatException) + { + throw; + } + catch (Exception e) + { + _logger.LogError(e, "Invocation of event {0} threw an exception.", @event.GetType().Name); + } + } + + private List<EventHandler> CacheEventHandlers<TEvent>() + where TEvent : IEvent + { + var handlers = GetHandlers<TEvent>() + .OrderByDescending(e => e.Listener.Priority) + .ToList(); + + _cachedEventHandlers[typeof(TEvent)] = handlers; + + return handlers; + } + + /// <summary> + /// Get all the event listeners for the given event type. + /// </summary> + /// <returns>The event listeners.</returns> + private IEnumerable<EventHandler> GetHandlers<TEvent>() + where TEvent : IEvent + { + var eventType = typeof(TEvent); + var interfaces = eventType.GetInterfaces(); + + foreach (var @interface in interfaces) + { + if (_temporaryEventListeners.TryGetValue(@interface, out var cb)) + { + foreach (var eventListener in cb.GetEventListeners()) + { + yield return new EventHandler(null, eventListener); + } + } + } + + foreach (var handler in _serviceProvider.GetServices<IEventListener>()) + { + if (handler is IManualEventListener manualEventListener && manualEventListener.CanExecute<TEvent>()) + { + yield return new EventHandler(handler, new ManualRegisteredEventListener(manualEventListener)); + continue; + } + + var events = RegisteredEventListener.FromType(handler.GetType()); + + foreach (var eventHandler in events) + { + if (eventHandler.EventType != typeof(TEvent) && !interfaces.Contains(eventHandler.EventType)) + { + continue; + } + + yield return new EventHandler(handler, eventHandler); + } + } + + if (_temporaryEventListeners.TryGetValue(eventType, out var cb2)) + { + foreach (var eventListener in cb2.GetEventListeners()) + { + yield return new EventHandler(null, eventListener); + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs new file mode 100644 index 0000000..3fb2368 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs @@ -0,0 +1,18 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameAlterEvent : IGameAlterEvent + { + public GameAlterEvent(IGame game, bool isPublic) + { + Game = game; + IsPublic = isPublic; + } + + public IGame Game { get; } + + public bool IsPublic { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs new file mode 100644 index 0000000..57e7a20 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameCreatedEvent : IGameCreatedEvent + { + public GameCreatedEvent(IGame game) + { + Game = game; + } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs new file mode 100644 index 0000000..5ee1b11 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameDestroyedEvent : IGameDestroyedEvent + { + public GameDestroyedEvent(IGame game) + { + Game = game; + } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs new file mode 100644 index 0000000..4ec4dcf --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; + +namespace Impostor.Server.Events +{ + public class GameEndedEvent : IGameEndedEvent + { + public GameEndedEvent(IGame game, GameOverReason gameOverReason) + { + Game = game; + GameOverReason = gameOverReason; + } + + public IGame Game { get; } + + public GameOverReason GameOverReason { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs new file mode 100644 index 0000000..d728c59 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; +using Impostor.Api.Net; + +namespace Impostor.Server.Events +{ + public class GamePlayerJoinedEvent : IGamePlayerJoinedEvent + { + public GamePlayerJoinedEvent(IGame game, IClientPlayer player) + { + Game = game; + Player = player; + } + + public IGame Game { get; } + + public IClientPlayer Player { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs new file mode 100644 index 0000000..d295103 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs @@ -0,0 +1,22 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; +using Impostor.Api.Net; + +namespace Impostor.Server.Events +{ + public class GamePlayerLeftEvent : IGamePlayerLeftEvent + { + public GamePlayerLeftEvent(IGame game, IClientPlayer player, bool isBan) + { + Game = game; + Player = player; + IsBan = isBan; + } + + public IGame Game { get; } + + public IClientPlayer Player { get; } + + public bool IsBan { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs new file mode 100644 index 0000000..d21f9ec --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameStartedEvent : IGameStartedEvent + { + public GameStartedEvent(IGame game) + { + Game = game; + } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs new file mode 100644 index 0000000..a0763e8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameStartingEvent : IGameStartingEvent + { + public GameStartingEvent(IGame game) + { + Game = game; + } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs new file mode 100644 index 0000000..cf55f7d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Events.Meeting; +using Impostor.Api.Games; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Meeting +{ + public class MeetingEndedEvent : IMeetingEndedEvent + { + public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud) + { + Game = game; + MeetingHud = meetingHud; + } + + public IGame Game { get; } + + public IInnerMeetingHud MeetingHud { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs new file mode 100644 index 0000000..aa689a3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Events.Meeting; +using Impostor.Api.Games; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Meeting +{ + public class MeetingStartedEvent : IMeetingStartedEvent + { + public MeetingStartedEvent(IGame game, IInnerMeetingHud meetingHud) + { + Game = game; + MeetingHud = meetingHud; + } + + public IGame Game { get; } + + public IInnerMeetingHud MeetingHud { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs new file mode 100644 index 0000000..7b7eb22 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerChatEvent : IPlayerChatEvent + { + public PlayerChatEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, string message) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + Message = message; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public string Message { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs new file mode 100644 index 0000000..330135d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs @@ -0,0 +1,27 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerCompletedTaskEvent : IPlayerCompletedTaskEvent + { + public PlayerCompletedTaskEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, ITaskInfo task) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + Task = task; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public ITaskInfo Task { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs new file mode 100644 index 0000000..69a20c9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs @@ -0,0 +1,23 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerDestroyedEvent : IPlayerDestroyedEvent + { + public PlayerDestroyedEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs new file mode 100644 index 0000000..a8660da --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs @@ -0,0 +1,23 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerExileEvent : IPlayerExileEvent + { + public PlayerExileEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs new file mode 100644 index 0000000..31ac388 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs @@ -0,0 +1,24 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + // TODO: Finish and use event, needs to be pooled + public class PlayerMovementEvent : IPlayerEvent + { + public PlayerMovementEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs new file mode 100644 index 0000000..ca64c35 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerMurderEvent : IPlayerMurderEvent + { + public PlayerMurderEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl victim) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + Victim = victim; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public IInnerPlayerControl Victim { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs new file mode 100644 index 0000000..71c25d7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerSetStartCounterEvent : IPlayerSetStartCounterEvent + { + public PlayerSetStartCounterEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, byte secondsLeft) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + SecondsLeft = secondsLeft; + } + + public byte SecondsLeft { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs new file mode 100644 index 0000000..4b6fda1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs @@ -0,0 +1,23 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerSpawnedEvent : IPlayerSpawnedEvent + { + public PlayerSpawnedEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs new file mode 100644 index 0000000..70cb0d8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerStartMeetingEvent : IPlayerStartMeetingEvent + { + public PlayerStartMeetingEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl? body) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + Body = body; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public IInnerPlayerControl? Body { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs new file mode 100644 index 0000000..0798d40 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs @@ -0,0 +1,30 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerVentEvent : IPlayerVentEvent + { + public PlayerVentEvent(IGame game, IClientPlayer sender, IInnerPlayerControl innerPlayerPhysics, VentLocation ventId, bool ventEnter) + { + Game = game; + ClientPlayer = sender; + PlayerControl = innerPlayerPhysics; + VentId = ventId; + VentEnter = ventEnter; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public VentLocation VentId { get; } + + public bool VentEnter { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs b/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs new file mode 100644 index 0000000..b68f064 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Impostor.Server.Events +{ + /// <summary> + /// Disposes multiple <see cref="IDisposable"/>. + /// </summary> + internal class MultiDisposable : IDisposable + { + private readonly IEnumerable<IDisposable> _disposables; + + public MultiDisposable(IEnumerable<IDisposable> disposables) + { + _disposables = disposables; + } + + public void Dispose() + { + foreach (var disposable in _disposables) + { + disposable?.Dispose(); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs new file mode 100644 index 0000000..479a3f6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Server.Events.Register +{ + internal interface IRegisteredEventListener + { + Type EventType { get; } + + EventPriority Priority { get; } + + ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs new file mode 100644 index 0000000..a21c3b1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Server.Events.Register +{ + internal class InvokedRegisteredEventListener : IRegisteredEventListener + { + private readonly IRegisteredEventListener _innerObject; + private readonly Func<Func<Task>, Task> _invoker; + + public InvokedRegisteredEventListener(IRegisteredEventListener innerObject, Func<Func<Task>, Task> invoker) + { + _innerObject = innerObject; + _invoker = invoker; + } + + public Type EventType => _innerObject.EventType; + + public EventPriority Priority => _innerObject.Priority; + + public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider) + { + return new ValueTask(_invoker(() => _innerObject.InvokeAsync(eventHandler, @event, provider).AsTask())); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs new file mode 100644 index 0000000..e81e8f8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Server.Events.Register +{ + internal class ManualRegisteredEventListener : IRegisteredEventListener + { + public Type EventType { get; } = typeof(object); + + private readonly IManualEventListener _manualEventListener; + + public ManualRegisteredEventListener(IManualEventListener manualEventListener) + { + _manualEventListener = manualEventListener; + } + + public EventPriority Priority => _manualEventListener.Priority; + + public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider) + { + if (@event is IEvent typedEvent) + { + return _manualEventListener.Execute(typedEvent); + } + + return ValueTask.CompletedTask; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs new file mode 100644 index 0000000..120a45e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Impostor.Api.Events; +using Microsoft.Extensions.DependencyInjection; + +namespace Impostor.Server.Events.Register +{ + internal class RegisteredEventListener : IRegisteredEventListener + { + private static readonly PropertyInfo IsCancelledProperty = typeof(IEventCancelable).GetProperty(nameof(IEventCancelable.IsCancelled))!; + + private static readonly ConcurrentDictionary<Type, RegisteredEventListener[]> Instances = new ConcurrentDictionary<Type, RegisteredEventListener[]>(); + private readonly Func<object, object, IServiceProvider, ValueTask> _invoker; + private readonly Type _eventListenerType; + + public RegisteredEventListener(Type eventType, MethodInfo method, EventListenerAttribute attribute, Type eventListenerType) + { + EventType = eventType; + _eventListenerType = eventListenerType; + Priority = attribute.Priority; + IgnoreCancelled = attribute.IgnoreCancelled; + Method = method.GetFriendlyName(showParameters: false); + _invoker = CreateInvoker(method, attribute.IgnoreCancelled); + } + + public Type EventType { get; } + + public EventPriority Priority { get; } + + public int PriorityOrder { get; set; } + + public bool IgnoreCancelled { get; } + + public string Method { get; } + + public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider) + { + return _invoker(eventHandler, @event, provider); + } + + private Func<object, object, IServiceProvider, ValueTask> CreateInvoker(MethodInfo method, bool ignoreCancelled) + { + var instance = Expression.Parameter(typeof(object), "instance"); + var eventParameter = Expression.Parameter(typeof(object), "event"); + var provider = Expression.Parameter(typeof(IServiceProvider), "provider"); + var @event = Expression.Convert(eventParameter, EventType); + + var getRequiredService = typeof(ServiceProviderServiceExtensions) + .GetMethod("GetRequiredService", new[] { typeof(IServiceProvider) }); + + if (getRequiredService == null) + { + throw new InvalidOperationException("The method GetRequiredService could not be found."); + } + + var methodArguments = method.GetParameters(); + var arguments = new Expression[methodArguments.Length]; + + for (var i = 0; i < methodArguments.Length; i++) + { + var methodArgument = methodArguments[i]; + + if (typeof(IEvent).IsAssignableFrom(methodArgument.ParameterType) + && methodArgument.ParameterType.IsAssignableFrom(EventType)) + { + arguments[i] = @event; + } + else + { + arguments[i] = Expression.Call( + getRequiredService.MakeGenericMethod(methodArgument.ParameterType), + provider); + } + } + + var returnTarget = Expression.Label(typeof(ValueTask)); + + Expression invoke = Expression.Call(Expression.Convert(instance, _eventListenerType), method, arguments); + + if (method.ReturnType == typeof(void)) + { + if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType)) + { + invoke = Expression.Block( + Expression.IfThenElse( + Expression.Property(@event, IsCancelledProperty), + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))), + Expression.Block( + invoke, + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))))), + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); + } + else + { + invoke = Expression.Block( + invoke, + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); + } + } + else if (method.ReturnType == typeof(ValueTask)) + { + if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType)) + { + invoke = Expression.Block( + Expression.IfThenElse( + Expression.Property(@event, IsCancelledProperty), + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))), + Expression.Return(returnTarget, invoke)), + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); + } + } + else + { + throw new InvalidOperationException($"The method {method.GetFriendlyName()} must return void or ValueTask."); + } + + return Expression.Lambda<Func<object, object, IServiceProvider, ValueTask>>(invoke, instance, eventParameter, provider) + .Compile(); + } + + public static IReadOnlyList<RegisteredEventListener> FromType(Type type) + { + return Instances.GetOrAdd(type, t => + { + return t.GetMethods() + .Where(m => !m.IsStatic && m.GetCustomAttributes(typeof(EventListenerAttribute), false).Any()) + .SelectMany(m => FromMethod(t, m)) + .ToArray(); + }); + } + + public static IEnumerable<RegisteredEventListener> FromMethod(Type listenerType, MethodInfo methodType) + { + // Get the return type. + var returnType = methodType.ReturnType; + + if (returnType != typeof(void) && returnType != typeof(ValueTask)) + { + throw new InvalidOperationException($"The method {methodType.GetFriendlyName()} does not return void or ValueTask."); + } + + // Register the event. + foreach (var attribute in methodType.GetCustomAttributes<EventListenerAttribute>(false)) + { + var eventType = attribute.Event; + + if (eventType == null) + { + if (methodType.GetParameters().Length == 0 || !typeof(IEvent).IsAssignableFrom(methodType.GetParameters()[0].ParameterType)) + { + throw new InvalidOperationException($"The first parameter of the method {methodType.GetFriendlyName()} should be the type {nameof(IEvent)}."); + } + + eventType = methodType.GetParameters()[0].ParameterType; + } + + yield return new RegisteredEventListener(eventType, methodType, attribute, listenerType); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs b/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs new file mode 100644 index 0000000..1446ad1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Impostor.Server.Events.Register +{ + internal class TemporaryEventRegister + { + private readonly ConcurrentDictionary<int, IRegisteredEventListener> _callbacks; + private int _idLast; + + public TemporaryEventRegister() + { + _callbacks = new ConcurrentDictionary<int, IRegisteredEventListener>(); + } + + public IEnumerable<IRegisteredEventListener> GetEventListeners() + { + return _callbacks.Select(i => i.Value); + } + + public IDisposable Add(IRegisteredEventListener callback) + { + var id = Interlocked.Increment(ref _idLast); + + if (!_callbacks.TryAdd(id, callback)) + { + Debug.Fail("Failed to register the event listener"); + } + + return new UnregisterEvent(this, id); + } + + private void Remove(int id) + { + _callbacks.TryRemove(id, out _); + } + + private class UnregisterEvent : IDisposable + { + private readonly TemporaryEventRegister _register; + private readonly int _id; + + public UnregisterEvent(TemporaryEventRegister register, int id) + { + _register = register; + _id = id; + } + + public void Dispose() + { + _register.Remove(_id); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs new file mode 100644 index 0000000..dd668c5 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Server.Events.Register +{ + internal class WrappedRegisteredEventListener : IRegisteredEventListener + { + private readonly IRegisteredEventListener _innerObject; + private readonly object _object; + + public WrappedRegisteredEventListener(IRegisteredEventListener innerObject, object o) + { + _innerObject = innerObject; + _object = o; + } + + public Type EventType => _innerObject.EventType; + + public EventPriority Priority => _innerObject.Priority; + + public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider) + { + return _innerObject.InvokeAsync(_object, @event, provider); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs new file mode 100644 index 0000000..5f25e89 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.Inner; +using Impostor.Server.Net.State; + +namespace Impostor.Server +{ + internal static class MessageReaderExtensions + { + public static T ReadNetObject<T>(this IMessageReader reader, Game game) + where T : InnerNetObject + { + return game.FindObjectByNetId<T>(reader.ReadPackedUInt32()); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs new file mode 100644 index 0000000..370bca6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Impostor.Server.Net.Redirector; + +namespace Impostor.Server +{ + public static class NodeLocatorExtensions + { + public static async ValueTask<bool> ExistsAsync(this INodeLocator nodeLocator, string gameCode) + { + return await nodeLocator.FindAsync(gameCode) != null; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..55d42fb --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + +namespace Impostor.Server +{ + internal static class TypeExtensions + { + /// <summary> + /// Get the friendly name for the type. + /// </summary> + /// <param name="type">The type.</param> + /// <returns>The friendly name.</returns> + [SuppressMessage("ReSharper", "SA1503", Justification = "Readability")] + public static string GetFriendlyName(this Type type) + { + if (type == null) + return "null"; + if (type == typeof(int)) + return "int"; + if (type == typeof(short)) + return "short"; + if (type == typeof(byte)) + return "byte"; + if (type == typeof(bool)) + return "bool"; + if (type == typeof(long)) + return "long"; + if (type == typeof(float)) + return "float"; + if (type == typeof(double)) + return "double"; + if (type == typeof(decimal)) + return "decimal"; + if (type == typeof(string)) + return "string"; + if (type.IsGenericType) + return type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(GetFriendlyName).ToArray()) + ">"; + return type.Name; + } + + /// <summary> + /// Get the friendly name for the method. + /// </summary> + /// <param name="method">The method.</param> + /// <param name="showParameters">True if the parameters should be included in the name.</param> + /// <returns>Friendly name of the method</returns> + public static string GetFriendlyName(this MethodBase method, bool showParameters = true) + { + var str = method.Name; + + if (method.DeclaringType != null) + { + str = method.DeclaringType.GetFriendlyName() + '.' + str; + } + + if (showParameters) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.GetFriendlyName())); + str += $"({parameters})"; + } + + return str; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj new file mode 100644 index 0000000..ad5a6db --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj @@ -0,0 +1,58 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + <RuntimeIdentifiers>win-x64;linux-x64;linux-arm;linux-arm64;osx-x64</RuntimeIdentifiers> + <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> + <ApplicationIcon>icon.ico</ApplicationIcon> + <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet> + <Nullable>enable</Nullable> + <SelfContained>false</SelfContained> + </PropertyGroup> + + <PropertyGroup> + <AssemblyName>Impostor.Server</AssemblyName> + <AssemblyTitle>Impostor.Server</AssemblyTitle> + <Product>Impostor.Server</Product> + <Copyright>Copyright © AeonLucid 2020</Copyright> + <Version>1.0.0</Version> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" /> + <ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.0" /> + <PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" /> + <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <Content Include="config.json"> + <CopyToPublishDirectory>Always</CopyToPublishDirectory> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <ExcludeFromSingleFile>true</ExcludeFromSingleFile> + </Content> + <Content Include="config.*.json"> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <ExcludeFromSingleFile>true</ExcludeFromSingleFile> + </Content> + <Content Include="config-full.json"> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + <CopyToOutputDirectory>Never</CopyToOutputDirectory> + <ExcludeFromSingleFile>true</ExcludeFromSingleFile> + </Content> + </ItemGroup> + +</Project>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings new file mode 100644 index 0000000..5c07b47 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings @@ -0,0 +1,3 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cgame/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Client.cs b/Impostor-dev/src/Impostor.Server/Net/Client.cs new file mode 100644 index 0000000..87a1bb4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Client.cs @@ -0,0 +1,338 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.C2S; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Config; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net +{ + internal class Client : ClientBase + { + private readonly ILogger<Client> _logger; + private readonly AntiCheatConfig _antiCheatConfig; + private readonly ClientManager _clientManager; + private readonly GameManager _gameManager; + + public Client(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, IHazelConnection connection) + : base(name, connection) + { + _logger = logger; + _antiCheatConfig = antiCheatOptions.Value; + _clientManager = clientManager; + _gameManager = gameManager; + } + + public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType) + { + var flag = reader.Tag; + + _logger.LogTrace("[{0}] Server got {1}.", Id, flag); + + switch (flag) + { + case MessageFlags.HostGame: + { + // Read game settings. + var gameInfo = Message00HostGameC2S.Deserialize(reader); + + // Create game. + var game = await _gameManager.CreateAsync(gameInfo); + + // Code in the packet below will be used in JoinGame. + using (var writer = MessageWriter.Get(MessageType.Reliable)) + { + Message00HostGameS2C.Serialize(writer, game.Code); + await Connection.SendAsync(writer); + } + + break; + } + + case MessageFlags.JoinGame: + { + Message01JoinGameC2S.Deserialize( + reader, + out var gameCode, + out _); + + var game = _gameManager.Find(gameCode); + if (game == null) + { + await DisconnectAsync(DisconnectReason.GameMissing); + return; + } + + var result = await game.AddClientAsync(this); + + switch (result.Error) + { + case GameJoinError.None: + break; + case GameJoinError.InvalidClient: + await DisconnectAsync(DisconnectReason.Custom, "Client is in an invalid state."); + break; + case GameJoinError.Banned: + await DisconnectAsync(DisconnectReason.Banned); + break; + case GameJoinError.GameFull: + await DisconnectAsync(DisconnectReason.GameFull); + break; + case GameJoinError.InvalidLimbo: + await DisconnectAsync(DisconnectReason.Custom, "Invalid limbo state while joining."); + break; + case GameJoinError.GameStarted: + await DisconnectAsync(DisconnectReason.GameStarted); + break; + case GameJoinError.GameDestroyed: + await DisconnectAsync(DisconnectReason.Custom, DisconnectMessages.Destroyed); + break; + case GameJoinError.Custom: + await DisconnectAsync(DisconnectReason.Custom, result.Message); + break; + default: + await DisconnectAsync(DisconnectReason.Custom, "Unknown error."); + break; + } + + break; + } + + case MessageFlags.StartGame: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + await Player.Game.HandleStartGame(reader); + break; + } + + // No idea how this flag is triggered. + case MessageFlags.RemoveGame: + break; + + case MessageFlags.RemovePlayer: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + Message04RemovePlayerC2S.Deserialize( + reader, + out var playerId, + out var reason); + + await Player.Game.HandleRemovePlayer(playerId, (DisconnectReason)reason); + break; + } + + case MessageFlags.GameData: + case MessageFlags.GameDataTo: + { + if (!IsPacketAllowed(reader, false)) + { + return; + } + + var toPlayer = flag == MessageFlags.GameDataTo; + + // Handle packet. + using var readerCopy = reader.Copy(); + + // TODO: Return value, either a bool (to cancel) or a writer (to cancel (null) or modify/overwrite). + try + { + var verified = await Player.Game.HandleGameDataAsync(readerCopy, Player, toPlayer); + if (verified) + { + // Broadcast packet to all other players. + using (var writer = MessageWriter.Get(messageType)) + { + if (toPlayer) + { + var target = reader.ReadPackedInt32(); + reader.CopyTo(writer); + await Player.Game.SendToAsync(writer, target); + } + else + { + reader.CopyTo(writer); + await Player.Game.SendToAllExceptAsync(writer, Id); + } + } + } + } + catch (ImpostorCheatException e) + { + if (_antiCheatConfig.BanIpFromGame) + { + Player.Game.BanIp(Connection.EndPoint.Address); + } + + await DisconnectAsync(DisconnectReason.Hacking, e.Message); + } + + break; + } + + case MessageFlags.EndGame: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + Message08EndGameC2S.Deserialize( + reader, + out var gameOverReason); + + await Player.Game.HandleEndGame(reader, gameOverReason); + break; + } + + case MessageFlags.AlterGame: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + Message10AlterGameC2S.Deserialize( + reader, + out var gameTag, + out var value); + + if (gameTag != AlterGameTags.ChangePrivacy) + { + return; + } + + await Player.Game.HandleAlterGame(reader, Player, value); + break; + } + + case MessageFlags.KickPlayer: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + Message11KickPlayerC2S.Deserialize( + reader, + out var playerId, + out var isBan); + + await Player.Game.HandleKickPlayer(playerId, isBan); + break; + } + + case MessageFlags.GetGameListV2: + { + Message16GetGameListC2S.Deserialize(reader, out var options); + await OnRequestGameListAsync(options); + break; + } + + default: + _logger.LogWarning("Server received unknown flag {0}.", flag); + break; + } + +#if DEBUG + if (flag != MessageFlags.GameData && + flag != MessageFlags.GameDataTo && + flag != MessageFlags.EndGame && + reader.Position < reader.Length) + { + _logger.LogWarning( + "Server did not consume all bytes from {0} ({1} < {2}).", + flag, + reader.Position, + reader.Length); + } +#endif + } + + public override async ValueTask HandleDisconnectAsync(string reason) + { + try + { + if (Player != null) + { + await Player.Game.HandleRemovePlayer(Id, DisconnectReason.ExitGame); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception caught in client disconnection."); + } + + _logger.LogInformation("Client {0} disconnecting, reason: {1}", Id, reason); + _clientManager.Remove(this); + } + + private bool IsPacketAllowed(IMessageReader message, bool hostOnly) + { + if (Player == null) + { + return false; + } + + var game = Player.Game; + + // GameCode must match code of the current game assigned to the player. + if (message.ReadInt32() != game.Code) + { + return false; + } + + // Some packets should only be sent by the host of the game. + if (hostOnly) + { + if (game.HostId == Id) + { + return true; + } + + _logger.LogWarning("[{0}] Client sent packet only allowed by the host ({1}).", Id, game.HostId); + return false; + } + + return true; + } + + /// <summary> + /// Triggered when the connected client requests the game listing. + /// </summary> + /// <param name="options"> + /// All options given. + /// At this moment, the client can only specify the map, impostor count and chat language. + /// </param> + private ValueTask OnRequestGameListAsync(GameOptionsData options) + { + using var message = MessageWriter.Get(MessageType.Reliable); + + var games = _gameManager.FindListings((MapFlags)options.MapId, options.NumImpostors, options.Keywords); + + var skeldGameCount = _gameManager.GetGameCount(MapFlags.Skeld); + var miraHqGameCount = _gameManager.GetGameCount(MapFlags.MiraHQ); + var polusGameCount = _gameManager.GetGameCount(MapFlags.Polus); + + Message16GetGameListS2C.Serialize(message, skeldGameCount, miraHqGameCount, polusGameCount, games); + + return Connection.SendAsync(message); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs b/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs new file mode 100644 index 0000000..5279192 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Net.State; + +namespace Impostor.Server.Net +{ + internal abstract class ClientBase : IClient + { + protected ClientBase(string name, IHazelConnection connection) + { + Name = name; + Connection = connection; + Items = new ConcurrentDictionary<object, object>(); + } + + public int Id { get; set; } + + public string Name { get; } + + public IHazelConnection Connection { get; } + + public IDictionary<object, object> Items { get; } + + public ClientPlayer? Player { get; set; } + + IClientPlayer? IClient.Player => Player; + + public abstract ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType); + + public abstract ValueTask HandleDisconnectAsync(string reason); + + + public async ValueTask DisconnectAsync(DisconnectReason reason, string? message = null) + { + if (!Connection.IsConnected) + { + return; + } + + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, reason, message); + + await Connection.SendAsync(packet); + + // Need this to show the correct message, otherwise it shows a generic disconnect message. + await Task.Delay(TimeSpan.FromMilliseconds(250)); + + await Connection.DisconnectAsync(message ?? reason.ToString()); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs b/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs new file mode 100644 index 0000000..ad1fdc7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs @@ -0,0 +1,24 @@ +using System; +using Impostor.Api.Net; +using Microsoft.Extensions.DependencyInjection; + +namespace Impostor.Server.Net.Factories +{ + internal class ClientFactory<TClient> : IClientFactory + where TClient : ClientBase + { + private readonly IServiceProvider _serviceProvider; + + public ClientFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public ClientBase Create(IHazelConnection connection, string name, int clientVersion) + { + var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, name, connection); + connection.Client = client; + return client; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs b/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs new file mode 100644 index 0000000..6859ae3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs @@ -0,0 +1,9 @@ +using Impostor.Api.Net; + +namespace Impostor.Server.Net.Factories +{ + internal interface IClientFactory + { + ClientBase Create(IHazelConnection connection, string name, int clientVersion); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs b/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs new file mode 100644 index 0000000..2a0553f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs @@ -0,0 +1,12 @@ +using Impostor.Api.Games; + +namespace Impostor.Server.Net +{ + public class GameCodeFactory : IGameCodeFactory + { + public GameCode Create() + { + return GameCode.Create(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs b/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs new file mode 100644 index 0000000..45d2be8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs @@ -0,0 +1,74 @@ +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Hazel; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Hazel +{ + internal class HazelConnection : IHazelConnection + { + private readonly ILogger<HazelConnection> _logger; + + public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger) + { + _logger = logger; + InnerConnection = innerConnection; + innerConnection.DataReceived = ConnectionOnDataReceived; + innerConnection.Disconnected = ConnectionOnDisconnected; + } + + public Connection InnerConnection { get; } + + public IPEndPoint EndPoint => InnerConnection.EndPoint; + + public bool IsConnected => InnerConnection.State == ConnectionState.Connected; + + public IClient Client { get; set; } + + public ValueTask SendAsync(IMessageWriter writer) + { + return InnerConnection.SendAsync(writer); + } + + public ValueTask DisconnectAsync(string reason) + { + return InnerConnection.Disconnect(reason); + } + + public void DisposeInnerConnection() + { + InnerConnection.Dispose(); + } + + private async ValueTask ConnectionOnDisconnected(DisconnectedEventArgs e) + { + if (Client != null) + { + await Client.HandleDisconnectAsync(e.Reason); + } + } + + private async ValueTask ConnectionOnDataReceived(DataReceivedEventArgs e) + { + if (Client == null) + { + return; + } + + while (true) + { + if (e.Message.Position >= e.Message.Length) + { + break; + } + + using (var message = e.Message.ReadMessage()) + { + await Client.HandleMessageAsync(message, e.Type); + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs new file mode 100644 index 0000000..fa9ddb8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs @@ -0,0 +1,13 @@ +namespace Impostor.Server.Net.Inner +{ + public static class GameDataTag + { + public const byte DataFlag = 1; + public const byte RpcFlag = 2; + public const byte SpawnFlag = 4; + public const byte DespawnFlag = 5; + public const byte SceneChangeFlag = 6; + public const byte ReadyFlag = 7; + public const byte ChangeSettingsFlag = 8; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs new file mode 100644 index 0000000..9b53d70 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Impostor.Server.Net.Inner +{ + internal class GameObject + { + public GameObject() + { + Components = new List<object>(); + } + + protected List<object> Components { get; } + + public List<T> GetComponentsInChildren<T>() + { + var result = new List<T>(); + + foreach (var component in Components) + { + if (component is T c) + { + result.Add(c); + } + } + + return result; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs new file mode 100644 index 0000000..78f1a55 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.State; + +namespace Impostor.Server.Net.Inner +{ + internal abstract class InnerNetObject : GameObject, IInnerNetObject + { + private const int HostInheritId = -2; + + public uint NetId { get; internal set; } + + public int OwnerId { get; internal set; } + + public SpawnFlags SpawnFlags { get; internal set; } + + public abstract ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader); + + public abstract bool Serialize(IMessageWriter writer, bool initialState); + + public abstract void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState); + + public bool IsOwnedBy(IClientPlayer player) + { + return OwnerId == player.Client.Id || + (OwnerId == HostInheritId && player.IsHost); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs new file mode 100644 index 0000000..6cdd6b9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs @@ -0,0 +1,25 @@ +using System.Numerics; +using System.Threading.Tasks; +using Impostor.Api.Net.Inner.Objects.Components; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal partial class InnerCustomNetworkTransform : IInnerCustomNetworkTransform + { + public async ValueTask SnapToAsync(Vector2 position) + { + var minSid = (ushort)(_lastSequenceId + 5U); + + // Snap in the server. + SnapTo(position, minSid); + + // Broadcast to all clients. + using (var writer = _game.StartRpc(NetId, RpcCalls.SnapTo)) + { + WriteVector2(writer, position); + writer.Write(_lastSequenceId); + await _game.FinishRpcAsync(writer); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs new file mode 100644 index 0000000..d261c11 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs @@ -0,0 +1,148 @@ +using System.Numerics; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal partial class InnerCustomNetworkTransform : InnerNetObject + { + private static readonly FloatRange XRange = new FloatRange(-40f, 40f); + private static readonly FloatRange YRange = new FloatRange(-40f, 40f); + + private readonly ILogger<InnerCustomNetworkTransform> _logger; + private readonly InnerPlayerControl _playerControl; + private readonly Game _game; + + private ushort _lastSequenceId; + private Vector2 _targetSyncPosition; + private Vector2 _targetSyncVelocity; + + public InnerCustomNetworkTransform(ILogger<InnerCustomNetworkTransform> logger, InnerPlayerControl playerControl, Game game) + { + _logger = logger; + _playerControl = playerControl; + _game = game; + } + + private static bool SidGreaterThan(ushort newSid, ushort prevSid) + { + var num = (ushort)(prevSid + (uint) short.MaxValue); + + return (int) prevSid < (int) num + ? newSid > prevSid && newSid <= num + : newSid > prevSid || newSid <= num; + } + + private static void WriteVector2(IMessageWriter writer, Vector2 vec) + { + writer.Write((ushort)(XRange.ReverseLerp(vec.X) * (double) ushort.MaxValue)); + writer.Write((ushort)(YRange.ReverseLerp(vec.Y) * (double) ushort.MaxValue)); + } + + private static Vector2 ReadVector2(IMessageReader reader) + { + var v1 = reader.ReadUInt16() / (float) ushort.MaxValue; + var v2 = reader.ReadUInt16() / (float) ushort.MaxValue; + + return new Vector2(XRange.Lerp(v1), YRange.Lerp(v2)); + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + if (call == RpcCalls.SnapTo) + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} to a specific player instead of broadcast"); + } + + if (!sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} as crewmate"); + } + + SnapTo(ReadVector2(reader), reader.ReadUInt16()); + } + else + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerCustomNetworkTransform), call); + } + + return default; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + if (initialState) + { + writer.Write(_lastSequenceId); + WriteVector2(writer, _targetSyncPosition); + WriteVector2(writer, _targetSyncVelocity); + return true; + } + + // TODO: DirtyBits == 0 return false. + _lastSequenceId++; + + writer.Write(_lastSequenceId); + WriteVector2(writer, _targetSyncPosition); + WriteVector2(writer, _targetSyncVelocity); + return true; + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + var sequenceId = reader.ReadUInt16(); + + if (initialState) + { + _lastSequenceId = sequenceId; + _targetSyncPosition = ReadVector2(reader); + _targetSyncVelocity = ReadVector2(reader); + } + else + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client attempted to send unowned {nameof(InnerCustomNetworkTransform)} data"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client attempted to send {nameof(InnerCustomNetworkTransform)} data to a specific player, must be broadcast"); + } + + if (!SidGreaterThan(sequenceId, _lastSequenceId)) + { + return; + } + + _lastSequenceId = sequenceId; + _targetSyncPosition = ReadVector2(reader); + _targetSyncVelocity = ReadVector2(reader); + } + } + + private void SnapTo(Vector2 position, ushort minSid) + { + if (!SidGreaterThan(minSid, _lastSequenceId)) + { + return; + } + + _lastSequenceId = minSid; + _targetSyncPosition = position; + _targetSyncVelocity = Vector2.Zero; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs new file mode 100644 index 0000000..6af54a0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs @@ -0,0 +1,8 @@ +using Impostor.Api.Net.Inner.Objects.Components; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal partial class InnerPlayerPhysics : IInnerPlayerPhysics + { + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs new file mode 100644 index 0000000..29bc996 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Server.Events.Player; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal partial class InnerPlayerPhysics : InnerNetObject + { + private readonly ILogger<InnerPlayerPhysics> _logger; + private readonly InnerPlayerControl _playerControl; + private readonly IEventManager _eventManager; + private readonly Game _game; + + public InnerPlayerPhysics(ILogger<InnerPlayerPhysics> logger, InnerPlayerControl playerControl, IEventManager eventManager, Game game) + { + _logger = logger; + _playerControl = playerControl; + _eventManager = eventManager; + _game = game; + } + + public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + if (call != RpcCalls.EnterVent && call != RpcCalls.ExitVent) + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerPlayerPhysics), call); + return; + } + + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {call} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {call} to a specific player instead of broadcast"); + } + + if (!sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {call} as crewmate"); + } + + var ventId = reader.ReadPackedUInt32(); + var ventEnter = call == RpcCalls.EnterVent; + + await _eventManager.CallAsync(new PlayerVentEvent(_game, sender, _playerControl, (VentLocation)ventId, ventEnter)); + + return; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + throw new NotImplementedException(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs new file mode 100644 index 0000000..58b9b54 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal class InnerVoteBanSystem : InnerNetObject, IInnerVoteBanSystem + { + private readonly ILogger<InnerVoteBanSystem> _logger; + private readonly Dictionary<int, int[]> _votes; + + public InnerVoteBanSystem(ILogger<InnerVoteBanSystem> logger) + { + _logger = logger; + _votes = new Dictionary<int, int[]>(); + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + if (call != RpcCalls.AddVote) + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerVoteBanSystem), call); + return default; + } + + var clientId = reader.ReadInt32(); + if (clientId != sender.Client.Id) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.AddVote)} as other client"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to wrong destinition, must be broadcast"); + } + + var targetClientId = reader.ReadInt32(); + + // TODO: Use. + + return default; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerShipStatus)} as non-host"); + } + + var votes = _votes; + var unknown = reader.ReadByte(); + if (unknown != 0) + { + for (var i = 0; i < unknown; i++) + { + var v4 = reader.ReadInt32(); + if (v4 == 0) + { + break; + } + + if (!votes.TryGetValue(v4, out var v12)) + { + v12 = new int[3]; + votes[v4] = v12; + } + + for (var j = 0; j < 3; j++) + { + v12[j] = reader.ReadPackedInt32(); + } + } + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs new file mode 100644 index 0000000..29ca50a --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs @@ -0,0 +1,30 @@ +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerGameData + { + public class TaskInfo : ITaskInfo + { + public uint Id { get; internal set; } + + public bool Complete { get; internal set; } + + public TaskTypes Type { get; internal set; } + + public void Serialize(IMessageWriter writer) + { + writer.WritePacked((uint)Id); + writer.Write(Complete); + } + + public void Deserialize(IMessageReader reader) + { + Id = reader.ReadPackedUInt32(); + Complete = reader.ReadBoolean(); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs new file mode 100644 index 0000000..ed68038 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.Inner.Objects.Components; +using Impostor.Server.Net.State; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerGameData : InnerNetObject, IInnerGameData + { + private readonly ILogger<InnerGameData> _logger; + private readonly Game _game; + private readonly ConcurrentDictionary<byte, InnerPlayerInfo> _allPlayers; + + public InnerGameData(ILogger<InnerGameData> logger, Game game, IServiceProvider serviceProvider) + { + _logger = logger; + _game = game; + _allPlayers = new ConcurrentDictionary<byte, InnerPlayerInfo>(); + + Components.Add(this); + Components.Add(ActivatorUtilities.CreateInstance<InnerVoteBanSystem>(serviceProvider)); + } + + public int PlayerCount => _allPlayers.Count; + + public IReadOnlyDictionary<byte, InnerPlayerInfo> Players => _allPlayers; + + public InnerPlayerInfo? GetPlayerById(byte id) + { + if (id == byte.MaxValue) + { + return null; + } + + return _allPlayers.TryGetValue(id, out var player) ? player : null; + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + switch (call) + { + case RpcCalls.SetTasks: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} to a specific player instead of broadcast"); + } + + var playerId = reader.ReadByte(); + var taskTypeIds = reader.ReadBytesAndSize(); + + SetTasks(playerId, taskTypeIds); + break; + } + + case RpcCalls.UpdateGameData: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} to a specific player instead of broadcast"); + } + + while (reader.Position < reader.Length) + { + using var message = reader.ReadMessage(); + var player = GetPlayerById(message.Tag); + if (player != null) + { + player.Deserialize(message); + } + else + { + var playerInfo = new InnerPlayerInfo(message.Tag); + + playerInfo.Deserialize(reader); + + if (!_allPlayers.TryAdd(playerInfo.PlayerId, playerInfo)) + { + throw new ImpostorException("Failed to add player to InnerGameData."); + } + } + } + + break; + } + + default: + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerGameData), call); + break; + } + } + + return default; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerGameData)} as non-host"); + } + + if (initialState) + { + var num = reader.ReadPackedInt32(); + + for (var i = 0; i < num; i++) + { + var playerId = reader.ReadByte(); + var playerInfo = new InnerPlayerInfo(playerId); + + playerInfo.Deserialize(reader); + + if (!_allPlayers.TryAdd(playerInfo.PlayerId, playerInfo)) + { + throw new ImpostorException("Failed to add player to InnerGameData."); + } + } + } + else + { + throw new NotImplementedException("This shouldn't happen, according to Among Us disassembly."); + } + } + + internal void AddPlayer(InnerPlayerControl control) + { + var playerId = control.PlayerId; + var playerInfo = new InnerPlayerInfo(control.PlayerId); + + if (_allPlayers.TryAdd(playerId, playerInfo)) + { + control.PlayerInfo = playerInfo; + } + } + + private void SetTasks(byte playerId, ReadOnlyMemory<byte> taskTypeIds) + { + var player = GetPlayerById(playerId); + if (player == null) + { + _logger.LogTrace("Could not set tasks for playerId {0}.", playerId); + return; + } + + if (player.Disconnected) + { + return; + } + + player.Tasks = new List<TaskInfo>(taskTypeIds.Length); + + foreach (var taskId in taskTypeIds.ToArray()) + { + player.Tasks.Add(new TaskInfo + { + Id = taskId, + }); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs new file mode 100644 index 0000000..63448ed --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.State; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal class InnerLobbyBehaviour : InnerNetObject, IInnerLobbyBehaviour + { + private readonly IGame _game; + + public InnerLobbyBehaviour(IGame game) + { + _game = game; + + Components.Add(this); + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + throw new System.NotImplementedException(); + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs new file mode 100644 index 0000000..5d120c5 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs @@ -0,0 +1,9 @@ +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerMeetingHud : IInnerMeetingHud + { + + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs new file mode 100644 index 0000000..fbf2510 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs @@ -0,0 +1,49 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerMeetingHud + { + public class PlayerVoteArea + { + private const byte VoteMask = 15; + private const byte ReportedBit = 32; + private const byte VotedBit = 64; + private const byte DeadBit = 128; + + public PlayerVoteArea(InnerMeetingHud parent, byte targetPlayerId) + { + Parent = parent; + TargetPlayerId = targetPlayerId; + } + + public InnerMeetingHud Parent { get; } + + public byte TargetPlayerId { get; } + + public bool IsDead { get; private set; } + + public bool DidVote { get; private set; } + + public bool DidReport { get; private set; } + + public sbyte VotedFor { get; private set; } + + internal void SetDead(bool didReport, bool isDead) + { + DidReport = didReport; + IsDead = isDead; + } + + public void Deserialize(IMessageReader reader) + { + var num = reader.ReadByte(); + + VotedFor = (sbyte)((num & VoteMask) - 1); + IsDead = (num & DeadBit) > 0; + DidVote = (num & VotedBit) > 0; + DidReport = (num & ReportedBit) > 0; + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs new file mode 100644 index 0000000..c2bcb9d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs @@ -0,0 +1,176 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Server.Events.Meeting; +using Impostor.Server.Events.Player; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerMeetingHud : InnerNetObject + { + private readonly ILogger<InnerMeetingHud> _logger; + private readonly IEventManager _eventManager; + private readonly Game _game; + private readonly GameNet _gameNet; + private PlayerVoteArea[] _playerStates; + + public InnerMeetingHud(ILogger<InnerMeetingHud> logger, IEventManager eventManager, Game game) + { + _logger = logger; + _eventManager = eventManager; + _game = game; + _gameNet = game.GameNet; + _playerStates = null; + + Components.Add(this); + } + + public byte ReporterId { get; private set; } + + private void PopulateButtons(byte reporter) + { + _playerStates = _gameNet.GameData.Players + .Select(x => + { + var area = new PlayerVoteArea(this, x.Key); + area.SetDead(x.Value.PlayerId == reporter, x.Value.Disconnected || x.Value.IsDead); + return area; + }) + .ToArray(); + } + + public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + switch (call) + { + case RpcCalls.Close: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Close)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Close)} to a specific player instead of broadcast"); + } + + break; + } + + case RpcCalls.VotingComplete: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.VotingComplete)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.VotingComplete)} to a specific player instead of broadcast"); + } + + var states = reader.ReadBytesAndSize(); + var playerId = reader.ReadByte(); + var tie = reader.ReadBoolean(); + + if (playerId != byte.MaxValue) + { + var player = _game.GameNet.GameData.GetPlayerById(playerId); + if (player != null) + { + player.Controller.Die(DeathReason.Exile); + await _eventManager.CallAsync(new PlayerExileEvent(_game, sender, player.Controller)); + } + } + + await _eventManager.CallAsync(new MeetingEndedEvent(_game, this)); + + break; + } + + case RpcCalls.CastVote: + { + var srcPlayerId = reader.ReadByte(); + if (srcPlayerId != sender.Character.PlayerId) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to an unowned {nameof(InnerPlayerControl)}"); + } + + // Host broadcasts vote to others. + if (sender.IsHost && target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to a specific player instead of broadcast"); + } + + // Player sends vote to host. + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to wrong destinition, must be host"); + } + + var targetPlayerId = reader.ReadByte(); + break; + } + + default: + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerMeetingHud), call); + break; + } + } + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerMeetingHud)} as non-host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client attempted to send {nameof(InnerMeetingHud)} data to a specific player, must be broadcast"); + } + + if (initialState) + { + PopulateButtons(0); + + foreach (var playerState in _playerStates) + { + playerState.Deserialize(reader); + + if (playerState.DidReport) + { + ReporterId = playerState.TargetPlayerId; + } + } + } + else + { + var num = reader.ReadPackedUInt32(); + + for (var i = 0; i < _playerStates.Length; i++) + { + if ((num & 1 << i) != 0) + { + _playerStates[i].Deserialize(reader); + } + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs new file mode 100644 index 0000000..0a7997d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs @@ -0,0 +1,138 @@ +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Innersloth.Customization; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Inner.Objects.Components; +using Impostor.Server.Events.Player; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerPlayerControl : IInnerPlayerControl + { + IInnerPlayerPhysics IInnerPlayerControl.Physics => Physics; + + IInnerCustomNetworkTransform IInnerPlayerControl.NetworkTransform => NetworkTransform; + + IInnerPlayerInfo IInnerPlayerControl.PlayerInfo => PlayerInfo; + + public async ValueTask SetNameAsync(string name) + { + PlayerInfo.PlayerName = name; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetName); + writer.Write(name); + await _game.FinishRpcAsync(writer); + } + + public async ValueTask SetColorAsync(byte colorId) + { + PlayerInfo.ColorId = colorId; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetColor); + writer.Write(colorId); + await _game.FinishRpcAsync(writer); + } + + public ValueTask SetColorAsync(ColorType colorType) + { + return SetColorAsync((byte)colorType); + } + + public async ValueTask SetHatAsync(uint hatId) + { + PlayerInfo.HatId = hatId; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetHat); + writer.WritePacked(hatId); + await _game.FinishRpcAsync(writer); + } + + public ValueTask SetHatAsync(HatType hatType) + { + return SetHatAsync((uint)hatType); + } + + public async ValueTask SetPetAsync(uint petId) + { + PlayerInfo.PetId = petId; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetPet); + writer.WritePacked(petId); + await _game.FinishRpcAsync(writer); + } + + public ValueTask SetPetAsync(PetType petType) + { + return SetPetAsync((uint)petType); + } + + public async ValueTask SetSkinAsync(uint skinId) + { + PlayerInfo.SkinId = skinId; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetSkin); + writer.WritePacked(skinId); + await _game.FinishRpcAsync(writer); + } + + public ValueTask SetSkinAsync(SkinType skinType) + { + return SetSkinAsync((uint)skinType); + } + + public async ValueTask SendChatAsync(string text) + { + using var writer = _game.StartRpc(NetId, RpcCalls.SendChat); + writer.Write(text); + await _game.FinishRpcAsync(writer); + } + + public async ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null) + { + if (player == null) + { + player = this; + } + + using var writer = _game.StartRpc(NetId, RpcCalls.SendChat); + writer.Write(text); + await _game.FinishRpcAsync(writer, player.OwnerId); + } + + public async ValueTask SetMurderedByAsync(IClientPlayer impostor) + { + if (impostor.Character == null) + { + throw new ImpostorException("Character is null."); + } + + if (!impostor.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorProtocolException("Plugin tried to murder a player while the impostor specified was not an impostor."); + } + + if (impostor.Character.PlayerInfo.IsDead) + { + throw new ImpostorProtocolException("Plugin tried to murder a player while the impostor specified was dead."); + } + + if (PlayerInfo.IsDead) + { + return; + } + + // Update player. + Die(DeathReason.Kill); + + // Send RPC. + using var writer = _game.StartRpc(impostor.Character.NetId, RpcCalls.MurderPlayer); + writer.WritePacked(NetId); + await _game.FinishRpcAsync(writer); + + // Notify plugins. + await _eventManager.CallAsync(new PlayerMurderEvent(_game, impostor, impostor.Character, this)); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs new file mode 100644 index 0000000..c71fcf7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs @@ -0,0 +1,455 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Server.Events.Player; +using Impostor.Server.Net.Inner.Objects.Components; +using Impostor.Server.Net.State; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerPlayerControl : InnerNetObject + { + private readonly ILogger<InnerPlayerControl> _logger; + private readonly IEventManager _eventManager; + private readonly Game _game; + + public InnerPlayerControl(ILogger<InnerPlayerControl> logger, IServiceProvider serviceProvider, IEventManager eventManager, Game game) + { + _logger = logger; + _eventManager = eventManager; + _game = game; + + Physics = ActivatorUtilities.CreateInstance<InnerPlayerPhysics>(serviceProvider, this, _eventManager, _game); + NetworkTransform = ActivatorUtilities.CreateInstance<InnerCustomNetworkTransform>(serviceProvider, this, _game); + + Components.Add(this); + Components.Add(Physics); + Components.Add(NetworkTransform); + + PlayerId = byte.MaxValue; + } + + public bool IsNew { get; private set; } + + public byte PlayerId { get; private set; } + + public InnerPlayerPhysics Physics { get; } + + public InnerCustomNetworkTransform NetworkTransform { get; } + + public InnerPlayerInfo PlayerInfo { get; internal set; } + + public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + switch (call) + { + // Play an animation. + case RpcCalls.PlayAnimation: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.PlayAnimation)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.PlayAnimation)} to a specific player instead of broadcast"); + } + + var animation = reader.ReadByte(); + break; + } + + // Complete a task. + case RpcCalls.CompleteTask: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CompleteTask)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CompleteTask)} to a specific player instead of broadcast"); + } + + var taskId = reader.ReadPackedUInt32(); + var task = PlayerInfo.Tasks[(int)taskId]; + if (task == null) + { + _logger.LogWarning($"Client sent {nameof(RpcCalls.CompleteTask)} with a taskIndex that is not in their {nameof(InnerPlayerInfo)}"); + } + else + { + task.Complete = true; + await _eventManager.CallAsync(new PlayerCompletedTaskEvent(_game, sender, this, task)); + } + + break; + } + + // Update GameOptions. + case RpcCalls.SyncSettings: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SyncSettings)} but was not a host"); + } + + _game.Options.Deserialize(reader.ReadBytesAndSize()); + break; + } + + // Set Impostors. + case RpcCalls.SetInfected: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetInfected)} but was not a host"); + } + + var length = reader.ReadPackedInt32(); + + for (var i = 0; i < length; i++) + { + var playerId = reader.ReadByte(); + var player = _game.GameNet.GameData.GetPlayerById(playerId); + if (player != null) + { + player.IsImpostor = true; + } + } + + if (_game.GameState == GameStates.Starting) + { + await _game.StartedAsync(); + } + + break; + } + + // Player was voted out. + case RpcCalls.Exiled: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Exiled)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Exiled)} to a specific player instead of broadcast"); + } + + // TODO: Not hit? + Die(DeathReason.Exile); + + await _eventManager.CallAsync(new PlayerExileEvent(_game, sender, this)); + break; + } + + // Validates the player name at the host. + case RpcCalls.CheckName: + { + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CheckName)} to the wrong player"); + } + + var name = reader.ReadString(); + break; + } + + // Update the name of a player. + case RpcCalls.SetName: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetName)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetName)} to a specific player instead of broadcast"); + } + + PlayerInfo.PlayerName = reader.ReadString(); + break; + } + + // Validates the color at the host. + case RpcCalls.CheckColor: + { + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CheckColor)} to the wrong player"); + } + + var color = reader.ReadByte(); + break; + } + + // Update the color of a player. + case RpcCalls.SetColor: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetColor)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetColor)} to a specific player instead of broadcast"); + } + + PlayerInfo.ColorId = reader.ReadByte(); + break; + } + + // Update the hat of a player. + case RpcCalls.SetHat: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to a specific player instead of broadcast"); + } + + PlayerInfo.HatId = reader.ReadPackedUInt32(); + break; + } + + case RpcCalls.SetSkin: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetSkin)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to a specific player instead of broadcast"); + } + + PlayerInfo.SkinId = reader.ReadPackedUInt32(); + break; + } + + // TODO: (ANTICHEAT) Location check? + // only called by a non-host player on to start meeting + case RpcCalls.ReportDeadBody: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.ReportDeadBody)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.ReportDeadBody)} to a specific player instead of broadcast"); + } + + + var deadBodyPlayerId = reader.ReadByte(); + // deadBodyPlayerId == byte.MaxValue -- means emergency call by button + + break; + } + + // TODO: (ANTICHEAT) Cooldown check? + case RpcCalls.MurderPlayer: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} to a specific player instead of broadcast"); + } + + if (!sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} as crewmate"); + } + + if (!sender.Character.PlayerInfo.CanMurder(_game)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} too fast"); + } + + sender.Character.PlayerInfo.LastMurder = DateTimeOffset.UtcNow; + + var player = reader.ReadNetObject<InnerPlayerControl>(_game); + if (!player.PlayerInfo.IsDead) + { + player.Die(DeathReason.Kill); + await _eventManager.CallAsync(new PlayerMurderEvent(_game, sender, this, player)); + } + + break; + } + + case RpcCalls.SendChat: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChat)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChat)} to a specific player instead of broadcast"); + } + + var chat = reader.ReadString(); + + await _eventManager.CallAsync(new PlayerChatEvent(_game, sender, this, chat)); + break; + } + + case RpcCalls.StartMeeting: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.StartMeeting)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.StartMeeting)} to a specific player instead of broadcast"); + } + + // deadBodyPlayerId == byte.MaxValue -- means emergency call by button + var deadBodyPlayerId = reader.ReadByte(); + var deadPlayer = deadBodyPlayerId != byte.MaxValue + ? _game.GameNet.GameData.GetPlayerById(deadBodyPlayerId)?.Controller + : null; + + await _eventManager.CallAsync(new PlayerStartMeetingEvent(_game, _game.GetClientPlayer(this.OwnerId), this, deadPlayer)); + break; + } + + case RpcCalls.SetScanner: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetScanner)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetScanner)} to a specific player instead of broadcast"); + } + + var on = reader.ReadBoolean(); + var count = reader.ReadByte(); + break; + } + + case RpcCalls.SendChatNote: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChatNote)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChatNote)} to a specific player instead of broadcast"); + } + + var playerId = reader.ReadByte(); + var chatNote = (ChatNoteType)reader.ReadByte(); + break; + } + + case RpcCalls.SetPet: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetPet)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetPet)} to a specific player instead of broadcast"); + } + + PlayerInfo.PetId = reader.ReadPackedUInt32(); + break; + } + + // TODO: Understand this RPC + case RpcCalls.SetStartCounter: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetStartCounter)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetStartCounter)} to a specific player instead of broadcast"); + } + + // Used to compare with LastStartCounter. + var startCounter = reader.ReadPackedUInt32(); + + // Is either start countdown or byte.MaxValue + var secondsLeft = reader.ReadByte(); + if (secondsLeft < byte.MaxValue) + { + await _eventManager.CallAsync(new PlayerSetStartCounterEvent(_game, sender, this, secondsLeft)); + } + + break; + } + + default: + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerPlayerControl), call); + break; + } + } + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerPlayerControl)} as non-host"); + } + + if (initialState) + { + IsNew = reader.ReadBoolean(); + } + + PlayerId = reader.ReadByte(); + } + + internal void Die(DeathReason reason) + { + PlayerInfo.IsDead = true; + PlayerInfo.LastDeathReason = reason; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs new file mode 100644 index 0000000..512a4f1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerPlayerInfo : IInnerPlayerInfo + { + IEnumerable<ITaskInfo> IInnerPlayerInfo.Tasks => Tasks; + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs new file mode 100644 index 0000000..f248994 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerPlayerInfo + { + public InnerPlayerInfo(byte playerId) + { + PlayerId = playerId; + } + + public InnerPlayerControl Controller { get; internal set; } + + public byte PlayerId { get; } + + public string PlayerName { get; internal set; } + + public byte ColorId { get; internal set; } + + public uint HatId { get; internal set; } + + public uint PetId { get; internal set; } + + public uint SkinId { get; internal set; } + + public bool Disconnected { get; internal set; } + + public bool IsImpostor { get; internal set; } + + public bool IsDead { get; internal set; } + + public DeathReason LastDeathReason { get; internal set; } + + public List<InnerGameData.TaskInfo> Tasks { get; internal set; } + + public DateTimeOffset LastMurder { get; set; } + + public bool CanMurder(IGame game) + { + if (!IsImpostor) + { + return false; + } + + return DateTimeOffset.UtcNow.Subtract(LastMurder).TotalSeconds >= game.Options.KillCooldown; + } + + public void Serialize(IMessageWriter writer) + { + throw new NotImplementedException(); + } + + public void Deserialize(IMessageReader reader) + { + PlayerName = reader.ReadString(); + ColorId = reader.ReadByte(); + HatId = reader.ReadPackedUInt32(); + PetId = reader.ReadPackedUInt32(); + SkinId = reader.ReadPackedUInt32(); + var flag = reader.ReadByte(); + Disconnected = (flag & 1) > 0; + IsImpostor = (flag & 2) > 0; + IsDead = (flag & 4) > 0; + var taskCount = reader.ReadByte(); + for (var i = 0; i < taskCount; i++) + { + Tasks[i] ??= new InnerGameData.TaskInfo(); + Tasks[i].Deserialize(reader); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs new file mode 100644 index 0000000..b1a3f18 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.Inner.Objects.Systems; +using Impostor.Server.Net.Inner.Objects.Systems.ShipStatus; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal class InnerShipStatus : InnerNetObject, IInnerShipStatus + { + private readonly ILogger<InnerShipStatus> _logger; + private readonly Game _game; + private readonly Dictionary<SystemTypes, ISystemType> _systems; + + public InnerShipStatus(ILogger<InnerShipStatus> logger, Game game) + { + _logger = logger; + _game = game; + + _systems = new Dictionary<SystemTypes, ISystemType> + { + [SystemTypes.Electrical] = new SwitchSystem(), + [SystemTypes.MedBay] = new MedScanSystem(), + [SystemTypes.Reactor] = new ReactorSystemType(), + [SystemTypes.LifeSupp] = new LifeSuppSystemType(), + [SystemTypes.Security] = new SecurityCameraSystemType(), + [SystemTypes.Comms] = new HudOverrideSystemType(), + [SystemTypes.Doors] = new DoorsSystemType(_game), + }; + + _systems.Add(SystemTypes.Sabotage, new SabotageSystemType(new[] + { + (IActivatable)_systems[SystemTypes.Comms], + (IActivatable)_systems[SystemTypes.Reactor], + (IActivatable)_systems[SystemTypes.LifeSupp], + (IActivatable)_systems[SystemTypes.Electrical], + })); + + Components.Add(this); + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, + IMessageReader reader) + { + switch (call) + { + case RpcCalls.CloseDoorsOfType: + { + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CloseDoorsOfType)} to wrong destinition, must be host"); + } + + if (!sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CloseDoorsOfType)} as crewmate"); + } + + var systemType = (SystemTypes)reader.ReadByte(); + + break; + } + + case RpcCalls.RepairSystem: + { + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.RepairSystem)} to wrong destinition, must be host"); + } + + var systemType = (SystemTypes)reader.ReadByte(); + if (systemType == SystemTypes.Sabotage && !sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.RepairSystem)} for {systemType} as crewmate"); + } + + var player = reader.ReadNetObject<InnerPlayerControl>(_game); + var amount = reader.ReadByte(); + + // TODO: Modify data (?) + break; + } + + default: + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerShipStatus), call); + break; + } + } + + return default; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerShipStatus)} as non-host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client attempted to send {nameof(InnerShipStatus)} data to a specific player, must be broadcast"); + } + + if (initialState) + { + // TODO: (_systems[SystemTypes.Doors] as DoorsSystemType).SetDoors(); + foreach (var systemType in SystemTypeHelpers.AllTypes) + { + if (_systems.TryGetValue(systemType, out var system)) + { + system.Deserialize(reader, true); + } + } + } + else + { + var count = reader.ReadPackedUInt32(); + + foreach (var systemType in SystemTypeHelpers.AllTypes) + { + // TODO: Not sure what is going on here, check. + if ((count & 1 << (int)(systemType & (SystemTypes.ShipTasks | SystemTypes.Doors))) != 0L) + { + if (_systems.TryGetValue(systemType, out var system)) + { + system.Deserialize(reader, false); + } + } + } + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs new file mode 100644 index 0000000..e595b25 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs @@ -0,0 +1,7 @@ +namespace Impostor.Server.Net.Inner.Objects.Systems +{ + public interface IActivatable + { + bool IsActive { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs new file mode 100644 index 0000000..a6ef88e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs @@ -0,0 +1,11 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems +{ + public interface ISystemType + { + void Serialize(IMessageWriter writer, bool initialState); + + void Deserialize(IMessageReader reader, bool initialState); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs new file mode 100644 index 0000000..64b1f5f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class DoorsSystemType : ISystemType + { + // TODO: AutoDoors + private readonly Dictionary<int, bool> _doors; + + public DoorsSystemType(IGame game) + { + var doorCount = game.Options.Map switch + { + MapTypes.Skeld => 13, + MapTypes.MiraHQ => 2, + MapTypes.Polus => 12, + _ => throw new ArgumentOutOfRangeException() + }; + + _doors = new Dictionary<int, bool>(doorCount); + + for (var i = 0; i < doorCount; i++) + { + _doors.Add(i, false); + } + } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + if (initialState) + { + for (var i = 0; i < _doors.Count; i++) + { + _doors[i] = reader.ReadBoolean(); + } + } + else + { + var num = reader.ReadPackedUInt32(); + + for (var i = 0; i < _doors.Count; i++) + { + if ((num & 1 << i) != 0) + { + _doors[i] = reader.ReadBoolean(); + } + } + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs new file mode 100644 index 0000000..42aa8d3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class HudOverrideSystemType : ISystemType, IActivatable + { + public bool IsActive { get; private set; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + IsActive = reader.ReadBoolean(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs new file mode 100644 index 0000000..f644024 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class LifeSuppSystemType : ISystemType, IActivatable + { + public LifeSuppSystemType() + { + Countdown = 10000f; + CompletedConsoles = new HashSet<int>(); + } + + public float Countdown { get; private set; } + + public HashSet<int> CompletedConsoles { get; } + + public bool IsActive => Countdown < 10000.0; + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + Countdown = reader.ReadSingle(); + + if (reader.Position >= reader.Length) + { + return; + } + + CompletedConsoles.Clear(); // TODO: Thread safety + + var num = reader.ReadPackedInt32(); + + for (var i = 0; i < num; i++) + { + CompletedConsoles.Add(reader.ReadPackedInt32()); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs new file mode 100644 index 0000000..007b9d0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class MedScanSystem : ISystemType + { + public MedScanSystem() + { + UsersList = new List<byte>(); + } + + public List<byte> UsersList { get; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + UsersList.Clear(); + + var num = reader.ReadPackedInt32(); + + for (var i = 0; i < num; i++) + { + UsersList.Add(reader.ReadByte()); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs new file mode 100644 index 0000000..4380918 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class ReactorSystemType : ISystemType, IActivatable + { + public ReactorSystemType() + { + Countdown = 10000f; + UserConsolePairs = new HashSet<Tuple<byte, byte>>(); + } + + public float Countdown { get; private set; } + + public HashSet<Tuple<byte, byte>> UserConsolePairs { get; } + + public bool IsActive => Countdown < 10000.0; + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + Countdown = reader.ReadSingle(); + UserConsolePairs.Clear(); // TODO: Thread safety + + var count = reader.ReadPackedInt32(); + + for (var i = 0; i < count; i++) + { + UserConsolePairs.Add(new Tuple<byte, byte>(reader.ReadByte(), reader.ReadByte())); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs new file mode 100644 index 0000000..193cfe8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class SabotageSystemType : ISystemType + { + private readonly IActivatable[] _specials; + + public SabotageSystemType(IActivatable[] specials) + { + _specials = specials; + } + + public float Timer { get; set; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + Timer = reader.ReadSingle(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs new file mode 100644 index 0000000..df41b68 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class SecurityCameraSystemType : ISystemType + { + public byte InUse { get; internal set; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + InUse = reader.ReadByte(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs new file mode 100644 index 0000000..925774a --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs @@ -0,0 +1,27 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class SwitchSystem : ISystemType, IActivatable + { + public byte ExpectedSwitches { get; set; } + + public byte ActualSwitches { get; set; } + + public byte Value { get; set; } = byte.MaxValue; + + public bool IsActive { get; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + ExpectedSwitches = reader.ReadByte(); + ActualSwitches = reader.ReadByte(); + Value = reader.ReadByte(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs new file mode 100644 index 0000000..ce48965 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs @@ -0,0 +1,37 @@ +namespace Impostor.Server.Net.Inner +{ + public enum RpcCalls : byte + { + PlayAnimation = 0, + CompleteTask = 1, + SyncSettings = 2, + SetInfected = 3, + Exiled = 4, + CheckName = 5, + SetName = 6, + CheckColor = 7, + SetColor = 8, + SetHat = 9, + SetSkin = 10, + ReportDeadBody = 11, + MurderPlayer = 12, + SendChat = 13, + StartMeeting = 14, + SetScanner = 15, + SendChatNote = 16, + SetPet = 17, + SetStartCounter = 18, + EnterVent = 19, + ExitVent = 20, + SnapTo = 21, + Close = 22, + VotingComplete = 23, + CastVote = 24, + ClearVote = 25, + AddVote = 26, + CloseDoorsOfType = 27, + RepairSystem = 28, + SetTasks = 29, + UpdateGameData = 30, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs new file mode 100644 index 0000000..1860098 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs @@ -0,0 +1,11 @@ +using System; + +namespace Impostor.Server.Net.Inner +{ + [Flags] + public enum SpawnFlags : byte + { + None = 0, + IsClientCharacter = 1, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs new file mode 100644 index 0000000..6cbe5bf --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Impostor.Api.Net; +using Impostor.Api.Net.Manager; + +namespace Impostor.Server.Net.Manager +{ + internal partial class ClientManager : IClientManager + { + IEnumerable<IClient> IClientManager.Clients => _clients.Values; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs new file mode 100644 index 0000000..51e22d8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs @@ -0,0 +1,103 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Config; +using Impostor.Server.Net.Factories; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Manager +{ + internal partial class ClientManager + { + private static HashSet<int> SupportedVersions { get; } = new HashSet<int> + { + GameVersion.GetVersion(2020, 09, 07), // 2020.09.07 - 2020.09.22 + GameVersion.GetVersion(2020, 10, 08), // 2020.10.08 + GameVersion.GetVersion(2020, 11, 17), // 2020.11.17 + }; + + private readonly ILogger<ClientManager> _logger; + private readonly ConcurrentDictionary<int, ClientBase> _clients; + private readonly IClientFactory _clientFactory; + private int _idLast; + + public ClientManager(ILogger<ClientManager> logger, IClientFactory clientFactory) + { + _logger = logger; + _clientFactory = clientFactory; + _clients = new ConcurrentDictionary<int, ClientBase>(); + } + + public IEnumerable<ClientBase> Clients => _clients.Values; + + 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 RegisterConnectionAsync(IHazelConnection connection, string name, int clientVersion) + { + if (!SupportedVersions.Contains(clientVersion)) + { + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.IncorrectVersion); + await connection.SendAsync(packet); + return; + } + + if (name.Length > 10) + { + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.UsernameLength); + await connection.SendAsync(packet); + return; + } + + if (string.IsNullOrWhiteSpace(name) || !name.All(TextBox.IsCharAllowed)) + { + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.UsernameIllegalCharacters); + await connection.SendAsync(packet); + return; + } + + var client = _clientFactory.Create(connection, name, clientVersion); + var id = NextId(); + + client.Id = id; + _logger.LogTrace("Client connected."); + _clients.TryAdd(id, client); + } + + public void Remove(IClient client) + { + _logger.LogTrace("Client disconnected."); + _clients.TryRemove(client.Id, out _); + } + + public bool Validate(IClient client) + { + return client.Id != 0 + && _clients.TryGetValue(client.Id, out var registeredClient) + && ReferenceEquals(client, registeredClient); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs new file mode 100644 index 0000000..a37829e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Games; +using Impostor.Api.Games.Managers; +using Impostor.Api.Innersloth; +using Impostor.Server.Config; +using Impostor.Server.Events; +using Impostor.Server.Net.Redirector; +using Impostor.Server.Net.State; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Manager +{ + internal class GameManager : IGameManager + { + private readonly ILogger<GameManager> _logger; + private readonly INodeLocator _nodeLocator; + private readonly IPEndPoint _publicIp; + private readonly ConcurrentDictionary<int, Game> _games; + private readonly IServiceProvider _serviceProvider; + private readonly IEventManager _eventManager; + private readonly IGameCodeFactory _gameCodeFactory; + + public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeLocator nodeLocator, IServiceProvider serviceProvider, IEventManager eventManager, IGameCodeFactory gameCodeFactory) + { + _logger = logger; + _nodeLocator = nodeLocator; + _serviceProvider = serviceProvider; + _eventManager = eventManager; + _gameCodeFactory = gameCodeFactory; + _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.ResolvePublicIp()), config.Value.PublicPort); + _games = new ConcurrentDictionary<int, Game>(); + } + + IEnumerable<IGame> IGameManager.Games => _games.Select(kv => kv.Value); + + IGame IGameManager.Find(GameCode code) => Find(code); + + public async ValueTask<IGame> CreateAsync(GameOptionsData options) + { + // TODO: Prevent duplicates when using server redirector using INodeProvider. + var (success, game) = await TryCreateAsync(options); + + for (int i = 0; i < 10 && !success; i++) + { + (success, game) = await TryCreateAsync(options); + } + + if (!success) + { + throw new ImpostorException("Could not create new game"); // TODO: Fix generic exception. + } + + return game; + } + + private async ValueTask<(bool success, Game game)> TryCreateAsync(GameOptionsData options) + { + var gameCode = _gameCodeFactory.Create(); + var gameCodeStr = gameCode.Code; + var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options); + + if (await _nodeLocator.ExistsAsync(gameCodeStr) || !_games.TryAdd(gameCode, game)) + { + return (false, null); + } + + await _nodeLocator.SaveAsync(gameCodeStr, _publicIp); + _logger.LogDebug("Created game with code {0}.", game.Code); + + await _eventManager.CallAsync(new GameCreatedEvent(game)); + + return (true, game); + } + + public Game Find(GameCode code) + { + _games.TryGetValue(code, out var game); + return game; + } + + public IEnumerable<Game> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10) + { + var results = 0; + + // Find games that have not started yet. + foreach (var (_, game) in _games.Where(x => + x.Value.IsPublic && + x.Value.GameState == GameStates.NotStarted && + x.Value.PlayerCount < x.Value.Options.MaxPlayers)) + { + // Check for options. + if (!map.HasFlag((MapFlags)(1 << game.Options.MapId))) + { + continue; + } + + if (!language.HasFlag(game.Options.Keywords)) + { + continue; + } + + if (impostorCount != 0 && game.Options.NumImpostors != impostorCount) + { + continue; + } + + // Add to result. + yield return game; + + // Break out if we have enough. + if (++results == count) + { + yield break; + } + } + } + + public async ValueTask RemoveAsync(GameCode gameCode) + { + if (_games.TryGetValue(gameCode, out var game) && game.PlayerCount > 0) + { + foreach (var player in game.Players) + { + await player.KickAsync(); + } + + return; + } + + if (!_games.TryRemove(gameCode, out game)) + { + return; + } + + _logger.LogDebug("Remove game with code {0} ({1}).", GameCodeParser.IntToGameName(gameCode), gameCode); + await _nodeLocator.RemoveAsync(GameCodeParser.IntToGameName(gameCode)); + + await _eventManager.CallAsync(new GameDestroyedEvent(game)); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs b/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs new file mode 100644 index 0000000..64ece55 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs @@ -0,0 +1,66 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Impostor.Hazel; +using Impostor.Hazel.Udp; +using Impostor.Server.Net.Hazel; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Server.Net +{ + internal class Matchmaker + { + private readonly ClientManager _clientManager; + private readonly ObjectPool<MessageReader> _readerPool; + private readonly ILogger<Matchmaker> _logger; + private readonly ILogger<HazelConnection> _connectionLogger; + private UdpConnectionListener _connection; + + public Matchmaker( + ILogger<Matchmaker> logger, + ClientManager clientManager, + ObjectPool<MessageReader> readerPool, + ILogger<HazelConnection> connectionLogger) + { + _logger = logger; + _clientManager = clientManager; + _readerPool = readerPool; + _connectionLogger = connectionLogger; + } + + public async ValueTask StartAsync(IPEndPoint ipEndPoint) + { + var mode = ipEndPoint.AddressFamily switch + { + AddressFamily.InterNetwork => IPMode.IPv4, + AddressFamily.InterNetworkV6 => IPMode.IPv6, + _ => throw new InvalidOperationException() + }; + + _connection = new UdpConnectionListener(ipEndPoint, _readerPool, mode); + _connection.NewConnection = OnNewConnection; + + await _connection.StartAsync(); + } + + public async ValueTask StopAsync() + { + await _connection.DisposeAsync(); + } + + private async ValueTask OnNewConnection(NewConnectionEventArgs e) + { + // Handshake. + var clientVersion = e.HandshakeData.ReadInt32(); + var name = e.HandshakeData.ReadString(); + + var connection = new HazelConnection(e.Connection, _connectionLogger); + + // Register client + await _clientManager.RegisterConnectionAsync(connection, name, clientVersion); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs b/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs new file mode 100644 index 0000000..bd9a855 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Server.Config; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net +{ + internal class MatchmakerService : IHostedService + { + private readonly ILogger<MatchmakerService> _logger; + private readonly ServerConfig _serverConfig; + private readonly ServerRedirectorConfig _redirectorConfig; + private readonly Matchmaker _matchmaker; + + public MatchmakerService( + ILogger<MatchmakerService> logger, + IOptions<ServerConfig> serverConfig, + IOptions<ServerRedirectorConfig> redirectorConfig, + Matchmaker matchmaker) + { + _logger = logger; + _serverConfig = serverConfig.Value; + _redirectorConfig = redirectorConfig.Value; + _matchmaker = matchmaker; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var endpoint = new IPEndPoint(IPAddress.Parse(_serverConfig.ResolveListenIp()), _serverConfig.ListenPort); + + await _matchmaker.StartAsync(endpoint); + + _logger.LogInformation( + "Matchmaker is listening on {0}:{1}, the public server ip is {2}:{3}.", + endpoint.Address, + endpoint.Port, + _serverConfig.ResolvePublicIp(), + _serverConfig.PublicPort); + + if (_redirectorConfig.Enabled) + { + _logger.LogWarning(_redirectorConfig.Master + ? "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."); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogWarning("Matchmaker is shutting down!"); + await _matchmaker.StopAsync(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs b/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs new file mode 100644 index 0000000..707860e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs @@ -0,0 +1,13 @@ +using Impostor.Api.Net.Messages; +using Impostor.Hazel; + +namespace Impostor.Server.Net.Messages +{ + public class MessageWriterProvider : IMessageWriterProvider + { + public IMessageWriter Get(MessageType sendOption = MessageType.Unreliable) + { + return MessageWriter.Get(sendOption); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs new file mode 100644 index 0000000..3dad4b3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs @@ -0,0 +1,97 @@ +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.C2S; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Config; +using Impostor.Server.Net.Hazel; +using Impostor.Server.Net.Manager; +using Serilog; +using ILogger = Serilog.ILogger; + +namespace Impostor.Server.Net.Redirector +{ + internal class ClientRedirector : ClientBase + { + private static readonly ILogger Logger = Log.ForContext<ClientRedirector>(); + + private readonly ClientManager _clientManager; + private readonly INodeProvider _nodeProvider; + private readonly INodeLocator _nodeLocator; + + public ClientRedirector( + string name, + HazelConnection connection, + ClientManager clientManager, + INodeProvider nodeProvider, + INodeLocator nodeLocator) + : base(name, connection) + { + _clientManager = clientManager; + _nodeProvider = nodeProvider; + _nodeLocator = nodeLocator; + } + + public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType) + { + var flag = reader.Tag; + + Logger.Verbose("Server got {0}.", flag); + + switch (flag) + { + case MessageFlags.HostGame: + { + using var packet = MessageWriter.Get(MessageType.Reliable); + Message13RedirectS2C.Serialize(packet, false, _nodeProvider.Get()); + await Connection.SendAsync(packet); + break; + } + + case MessageFlags.JoinGame: + { + Message01JoinGameC2S.Deserialize( + reader, + out var gameCode, + out _); + + using var packet = MessageWriter.Get(MessageType.Reliable); + var endpoint = await _nodeLocator.FindAsync(GameCodeParser.IntToGameName(gameCode)); + if (endpoint == null) + { + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.GameMissing); + } + else + { + Message13RedirectS2C.Serialize(packet, false, endpoint); + } + + await Connection.SendAsync(packet); + break; + } + + case MessageFlags.GetGameListV2: + { + // TODO: Implement. + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.NotImplemented); + await Connection.SendAsync(packet); + break; + } + + default: + { + Logger.Warning("Received unsupported message flag on the redirector ({0}).", flag); + break; + } + } + } + + public override ValueTask HandleDisconnectAsync(string reason) + { + _clientManager.Remove(this); + return default; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs new file mode 100644 index 0000000..12563b9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs @@ -0,0 +1,14 @@ +using System.Net; +using System.Threading.Tasks; + +namespace Impostor.Server.Net.Redirector +{ + public interface INodeLocator + { + ValueTask<IPEndPoint> FindAsync(string gameCode); + + ValueTask SaveAsync(string gameCode, IPEndPoint endPoint); + + ValueTask RemoveAsync(string gameCode); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs new file mode 100644 index 0000000..318fcb2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace Impostor.Server.Net.Redirector +{ + internal interface INodeProvider + { + IPEndPoint Get(); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs new file mode 100644 index 0000000..fd4cd56 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs @@ -0,0 +1,14 @@ +using System.Net; +using System.Threading.Tasks; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorNoOp : INodeLocator + { + public ValueTask<IPEndPoint> FindAsync(string gameCode) => ValueTask.FromResult(default(IPEndPoint)); + + public ValueTask SaveAsync(string gameCode, IPEndPoint endPoint) => ValueTask.CompletedTask; + + public ValueTask RemoveAsync(string gameCode) => ValueTask.CompletedTask; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs new file mode 100644 index 0000000..0b6fdff --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs @@ -0,0 +1,43 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorRedis : INodeLocator + { + private readonly IDistributedCache _cache; + + public NodeLocatorRedis(ILogger<NodeLocatorRedis> logger, IDistributedCache cache) + { + logger.LogWarning("Using the redis NodeLocator."); + _cache = cache; + } + + public async ValueTask<IPEndPoint> FindAsync(string gameCode) + { + var entry = await _cache.GetStringAsync(gameCode); + if (entry == null) + { + return null; + } + + return IPEndPoint.Parse(entry); + } + + public async ValueTask SaveAsync(string gameCode, IPEndPoint endPoint) + { + await _cache.SetStringAsync(gameCode, endPoint.ToString(), new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromHours(1), + }); + } + + public async ValueTask RemoveAsync(string gameCode) + { + await _cache.RemoveAsync(gameCode); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs new file mode 100644 index 0000000..2539a8f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Impostor.Server.Config; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorUdp : INodeLocator, IDisposable + { + private readonly ILogger<NodeLocatorUdp> _logger; + private readonly bool _isMaster; + private readonly IPEndPoint _server; + private readonly UdpClient _client; + private readonly ConcurrentDictionary<string, AvailableNode> _availableNodes; + + public NodeLocatorUdp(ILogger<NodeLocatorUdp> logger, IOptions<ServerRedirectorConfig> config) + { + _logger = logger; + + if (config.Value.Master) + { + _isMaster = true; + _availableNodes = new ConcurrentDictionary<string, AvailableNode>(); + } + else + { + _isMaster = false; + + if (!IPEndPoint.TryParse(config.Value.Locator.UdpMasterEndpoint, out var endpoint)) + { + throw new ArgumentException("UdpMasterEndpoint should be in the ip:port format."); + } + + _logger.LogWarning("Node server will send updates to {0}.", endpoint); + _server = endpoint; + _client = new UdpClient(); + + try + { + _client.DontFragment = true; + } + catch (SocketException) + { + } + } + } + + public void Update(IPEndPoint ip, string gameCode) + { + _logger.LogDebug("Received update {0} -> {1}", gameCode, ip); + + _availableNodes.AddOrUpdate( + gameCode, + s => new AvailableNode + { + Endpoint = ip, + LastUpdated = DateTimeOffset.UtcNow, + }, + (s, node) => + { + node.Endpoint = ip; + node.LastUpdated = DateTimeOffset.UtcNow; + + return node; + }); + + foreach (var (key, value) in _availableNodes) + { + if (value.Expired) + { + _availableNodes.TryRemove(key, out _); + } + } + } + + public ValueTask<IPEndPoint> FindAsync(string gameCode) + { + if (!_isMaster) + { + return ValueTask.FromResult(default(IPEndPoint)); + } + + if (_availableNodes.TryGetValue(gameCode, out var node)) + { + if (node.Expired) + { + _availableNodes.TryRemove(gameCode, out _); + return ValueTask.FromResult(default(IPEndPoint)); + } + + return ValueTask.FromResult(node.Endpoint); + } + + return ValueTask.FromResult(default(IPEndPoint)); + } + + public ValueTask RemoveAsync(string gameCode) + { + if (!_isMaster) + { + return ValueTask.CompletedTask; + } + + _availableNodes.TryRemove(gameCode, out _); + return ValueTask.CompletedTask; + } + + public ValueTask SaveAsync(string gameCode, IPEndPoint endPoint) + { + var data = Encoding.UTF8.GetBytes($"{gameCode},{endPoint}"); + _client.Send(data, data.Length, _server); + return ValueTask.CompletedTask; + } + + public void Dispose() + { + _client?.Dispose(); + } + + private class AvailableNode + { + public IPEndPoint Endpoint { get; set; } + + public DateTimeOffset LastUpdated { get; set; } + + public bool Expired => LastUpdated < DateTimeOffset.UtcNow.AddHours(-1); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs new file mode 100644 index 0000000..3706bb4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs @@ -0,0 +1,101 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Server.Config; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorUdpService : BackgroundService + { + private readonly NodeLocatorUdp _nodeLocator; + private readonly ILogger<NodeLocatorUdpService> _logger; + private readonly UdpClient _client; + + public NodeLocatorUdpService( + INodeLocator nodeLocator, + ILogger<NodeLocatorUdpService> logger, + IOptions<ServerRedirectorConfig> options) + { + _nodeLocator = (NodeLocatorUdp)nodeLocator; + _logger = logger; + + if (!IPEndPoint.TryParse(options.Value.Locator.UdpMasterEndpoint, out var endpoint)) + { + throw new ArgumentException("UdpMasterEndpoint should be in the ip:port format."); + } + + _client = new UdpClient(endpoint); + + try + { + _client.DontFragment = true; + } + catch (SocketException) + { + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogWarning("Master server is listening for node updates on {0}.", _client.Client.LocalEndPoint); + + stoppingToken.Register(() => + { + _client.Close(); + _client.Dispose(); + }); + + try + { + while (!stoppingToken.IsCancellationRequested) + { + // Receive data from a node. + UdpReceiveResult data; + + try + { + data = await _client.ReceiveAsync(); + } + catch (ObjectDisposedException) + { + break; + } + + // Check if data is valid. + if (data.Buffer.Length == 0) + { + break; + } + + // Parse the data. + var message = Encoding.UTF8.GetString(data.Buffer); + var parts = message.Split(',', 2); + if (parts.Length != 2) + { + continue; + } + + if (!IPEndPoint.TryParse(parts[1], out var ipEndPoint)) + { + continue; + } + + // Update the NodeLocator. + _nodeLocator.Update(ipEndPoint, parts[0]); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error in NodeLocatorUDPService."); + } + + _logger.LogWarning("Master server node update listener is stopping."); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs new file mode 100644 index 0000000..4e19c43 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Net; +using Impostor.Server.Config; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Redirector +{ + internal class NodeProviderConfig : INodeProvider + { + private readonly List<IPEndPoint> _nodes; + private readonly object _lock; + private int _currentIndex; + + public NodeProviderConfig(IOptions<ServerRedirectorConfig> redirectorConfig) + { + _nodes = new List<IPEndPoint>(); + _lock = new object(); + + if (redirectorConfig.Value.Nodes != null) + { + foreach (var node in redirectorConfig.Value.Nodes) + { + _nodes.Add(new IPEndPoint(IPAddress.Parse(node.Ip), node.Port)); + } + } + } + + public IPEndPoint Get() + { + lock (_lock) + { + var node = _nodes[_currentIndex++]; + + if (_currentIndex == _nodes.Count) + { + _currentIndex = 0; + } + + return node; + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs new file mode 100644 index 0000000..2e2467e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs @@ -0,0 +1,18 @@ +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.State +{ + internal partial class ClientPlayer + { + /// <inheritdoc /> + IClient IClientPlayer.Client => Client; + + /// <inheritdoc /> + IGame IClientPlayer.Game => Game; + + /// <inheritdoc /> + IInnerPlayerControl? IClientPlayer.Character => Character; + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs new file mode 100644 index 0000000..107edbf --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs @@ -0,0 +1,88 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner; +using Impostor.Server.Net.Inner.Objects; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class ClientPlayer : IClientPlayer + { + private readonly ILogger<ClientPlayer> _logger; + private readonly Timer _spawnTimeout; + + public ClientPlayer(ILogger<ClientPlayer> logger, ClientBase client, Game game) + { + _logger = logger; + _spawnTimeout = new Timer(RunSpawnTimeout, null, -1, -1); + + Game = game; + Client = client; + Limbo = LimboStates.PreSpawn; + } + + public ClientBase Client { get; } + + public Game Game { get; } + + /// <inheritdoc /> + public LimboStates Limbo { get; set; } + + public InnerPlayerControl? Character { get; internal set; } + + public bool IsHost => Game?.Host == this; + + public string Scene { get; internal set; } + + public void InitializeSpawnTimeout() + { + _spawnTimeout.Change(Constants.SpawnTimeout, -1); + } + + public void DisableSpawnTimeout() + { + _spawnTimeout.Change(-1, -1); + } + + /// <inheritdoc /> + public bool IsOwner(IInnerNetObject netObject) + { + return Client.Id == netObject.OwnerId; + } + + /// <inheritdoc /> + public ValueTask KickAsync() + { + return Game.HandleKickPlayer(Client.Id, false); + } + + /// <inheritdoc /> + public ValueTask BanAsync() + { + return Game.HandleKickPlayer(Client.Id, true); + } + + private async void RunSpawnTimeout(object state) + { + try + { + if (Character == null) + { + _logger.LogInformation("{0} - Player {1} spawn timed out, kicking.", Game.Code, Client.Id); + + await KickAsync(); + } + } + catch (Exception e) + { + _logger.LogError(e, "Exception caught while kicking player for spawn timeout."); + } + finally + { + await _spawnTimeout.DisposeAsync(); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs new file mode 100644 index 0000000..f395be2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Server.Net.Inner; + +namespace Impostor.Server.Net.State +{ + internal partial class Game : IGame + { + IClientPlayer IGame.Host => Host; + + IGameNet IGame.GameNet => GameNet; + + public void BanIp(IPAddress ipAddress) + { + _bannedIps.Add(ipAddress); + } + + public async ValueTask SyncSettingsAsync() + { + if (Host.Character == null) + { + throw new ImpostorException("Attempted to set infected when the host was not spawned."); + } + + using (var writer = StartRpc(Host.Character.NetId, RpcCalls.SyncSettings)) + { + // Someone will probably forget to do this, so we include it here. + // If this is not done, the host will overwrite changes later with the defaults. + Options.IsDefaults = false; + + await using (var memory = new MemoryStream()) + await using (var writerBin = new BinaryWriter(memory)) + { + Options.Serialize(writerBin, GameOptionsData.LatestVersion); + writer.WriteBytesAndSize(memory.ToArray()); + } + + await FinishRpcAsync(writer); + } + } + + public async ValueTask SetInfectedAsync(IEnumerable<IInnerPlayerControl> players) + { + if (Host.Character == null) + { + throw new ImpostorException("Attempted to set infected when the host was not spawned."); + } + + using (var writer = StartRpc(Host.Character.NetId, RpcCalls.SetInfected)) + { + writer.Write((byte)Host.Character.NetId); + + foreach (var player in players) + { + writer.Write((byte)player.PlayerId); + } + + await FinishRpcAsync(writer); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs new file mode 100644 index 0000000..a84d9b5 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Events.Meeting; +using Impostor.Server.Events.Player; +using Impostor.Server.Net.Inner; +using Impostor.Server.Net.Inner.Objects; +using Impostor.Server.Net.Inner.Objects.Components; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + private const int FakeClientId = int.MaxValue - 1; + + /// <summary> + /// Used for global object, spawned by the host. + /// </summary> + private const int InvalidClient = -2; + + /// <summary> + /// Used internally to set the OwnerId to the current ClientId. + /// i.e: <code>ownerId = ownerId == -3 ? this.ClientId : ownerId;</code> + /// </summary> + private const int CurrentClient = -3; + + private static readonly Type[] SpawnableObjects = + { + typeof(InnerShipStatus), // ShipStatus + typeof(InnerMeetingHud), + typeof(InnerLobbyBehaviour), + typeof(InnerGameData), + typeof(InnerPlayerControl), + typeof(InnerShipStatus), // HeadQuarters + typeof(InnerShipStatus), // PlanetMap + typeof(InnerShipStatus), // AprilShipStatus + }; + + private readonly List<InnerNetObject> _allObjects = new List<InnerNetObject>(); + private readonly Dictionary<uint, InnerNetObject> _allObjectsFast = new Dictionary<uint, InnerNetObject>(); + + private int _gamedataInitialized; + private bool _gamedataFakeReceived; + + private async ValueTask OnSpawnAsync(InnerNetObject netObj) + { + switch (netObj) + { + case InnerLobbyBehaviour lobby: + { + GameNet.LobbyBehaviour = lobby; + break; + } + + case InnerGameData data: + { + GameNet.GameData = data; + break; + } + + case InnerVoteBanSystem voteBan: + { + GameNet.VoteBan = voteBan; + break; + } + + case InnerShipStatus shipStatus: + { + GameNet.ShipStatus = shipStatus; + break; + } + + case InnerPlayerControl control: + { + // Hook up InnerPlayerControl <-> IClientPlayer. + if (!TryGetPlayer(control.OwnerId, out var player)) + { + throw new ImpostorException("Failed to find player that spawned the InnerPlayerControl"); + } + + player.Character = control; + player.DisableSpawnTimeout(); + + // Hook up InnerPlayerControl <-> InnerPlayerControl.PlayerInfo. + control.PlayerInfo = GameNet.GameData.GetPlayerById(control.PlayerId)!; + + if (control.PlayerInfo == null) + { + GameNet.GameData.AddPlayer(control); + } + + if (control.PlayerInfo != null) + { + control.PlayerInfo!.Controller = control; + } + + await _eventManager.CallAsync(new PlayerSpawnedEvent(this, player, control)); + + break; + } + + case InnerMeetingHud meetingHud: + { + await _eventManager.CallAsync(new MeetingStartedEvent(this, meetingHud)); + break; + } + } + } + + private async ValueTask OnDestroyAsync(InnerNetObject netObj) + { + switch (netObj) + { + case InnerLobbyBehaviour: + { + GameNet.LobbyBehaviour = null; + break; + } + + case InnerGameData: + { + GameNet.GameData = null; + break; + } + + case InnerVoteBanSystem: + { + GameNet.VoteBan = null; + break; + } + + case InnerShipStatus: + { + GameNet.ShipStatus = null; + break; + } + + case InnerPlayerControl control: + { + // Remove InnerPlayerControl <-> IClientPlayer. + if (TryGetPlayer(control.OwnerId, out var player)) + { + player.Character = null; + } + + await _eventManager.CallAsync(new PlayerDestroyedEvent(this, player, control)); + + break; + } + } + } + + private async ValueTask InitGameDataAsync(ClientPlayer player) + { + if (Interlocked.Exchange(ref _gamedataInitialized, 1) != 0) + { + return; + } + + /* + * The Among Us client on 20.9.22i spawns some components on the host side and + * only spawns these on other clients when someone else connects. This means that we can't + * parse data until someone connects because we don't know which component belongs to the NetId. + * + * We solve this by spawning a fake player and removing the player when the spawn GameData + * is received in HandleGameDataAsync. + */ + using (var message = MessageWriter.Get(MessageType.Reliable)) + { + // Spawn a fake player. + Message01JoinGameS2C.SerializeJoin(message, false, Code, FakeClientId, HostId); + + message.StartMessage(MessageFlags.GameData); + message.Write(Code); + message.StartMessage(GameDataTag.SceneChangeFlag); + message.WritePacked(FakeClientId); + message.Write("OnlineGame"); + message.EndMessage(); + message.EndMessage(); + + await player.Client.Connection.SendAsync(message); + } + } + + public async ValueTask<bool> HandleGameDataAsync(IMessageReader parent, ClientPlayer sender, bool toPlayer) + { + // Find target player. + ClientPlayer target = null; + + if (toPlayer) + { + var targetId = parent.ReadPackedInt32(); + if (targetId == FakeClientId && !_gamedataFakeReceived && sender.IsHost) + { + _gamedataFakeReceived = true; + + // Remove the fake client, we received the data. + using (var message = MessageWriter.Get(MessageType.Reliable)) + { + WriteRemovePlayerMessage(message, false, FakeClientId, (byte)DisconnectReason.ExitGame); + + await sender.Client.Connection.SendAsync(message); + } + } + else if (!TryGetPlayer(targetId, out target)) + { + _logger.LogWarning("Player {0} tried to send GameData to unknown player {1}.", sender.Client.Id, targetId); + return false; + } + + _logger.LogTrace("Received GameData for target {0}.", targetId); + } + + // Parse GameData messages. + while (parent.Position < parent.Length) + { + using var reader = parent.ReadMessage(); + + switch (reader.Tag) + { + case GameDataTag.DataFlag: + { + var netId = reader.ReadPackedUInt32(); + if (_allObjectsFast.TryGetValue(netId, out var obj)) + { + obj.Deserialize(sender, target, reader, false); + } + else + { + _logger.LogWarning("Received DataFlag for unregistered NetId {0}.", netId); + } + + break; + } + + case GameDataTag.RpcFlag: + { + var netId = reader.ReadPackedUInt32(); + if (_allObjectsFast.TryGetValue(netId, out var obj)) + { + await obj.HandleRpc(sender, target, (RpcCalls) reader.ReadByte(), reader); + } + else + { + _logger.LogWarning("Received RpcFlag for unregistered NetId {0}.", netId); + } + + break; + } + + case GameDataTag.SpawnFlag: + { + // Only the host is allowed to despawn objects. + if (!sender.IsHost) + { + throw new ImpostorCheatException("Tried to send SpawnFlag as non-host."); + } + + var objectId = reader.ReadPackedUInt32(); + if (objectId < SpawnableObjects.Length) + { + var innerNetObject = (InnerNetObject) ActivatorUtilities.CreateInstance(_serviceProvider, SpawnableObjects[objectId], this); + var ownerClientId = reader.ReadPackedInt32(); + + // Prevent fake client from being broadcasted. + // TODO: Remove message from stream properly. + if (ownerClientId == FakeClientId) + { + return false; + } + + innerNetObject.SpawnFlags = (SpawnFlags) reader.ReadByte(); + + var components = innerNetObject.GetComponentsInChildren<InnerNetObject>(); + var componentsCount = reader.ReadPackedInt32(); + + if (componentsCount != components.Count) + { + _logger.LogError( + "Children didn't match for spawnable {0}, name {1} ({2} != {3})", + objectId, + innerNetObject.GetType().Name, + componentsCount, + components.Count); + continue; + } + + _logger.LogDebug( + "Spawning {0} components, SpawnFlags {1}", + innerNetObject.GetType().Name, + innerNetObject.SpawnFlags); + + for (var i = 0; i < componentsCount; i++) + { + var obj = components[i]; + + obj.NetId = reader.ReadPackedUInt32(); + obj.OwnerId = ownerClientId; + + _logger.LogDebug( + "- {0}, NetId {1}, OwnerId {2}", + obj.GetType().Name, + obj.NetId, + obj.OwnerId); + + if (!AddNetObject(obj)) + { + _logger.LogTrace("Failed to AddNetObject, it already exists."); + + obj.NetId = uint.MaxValue; + break; + } + + using var readerSub = reader.ReadMessage(); + if (readerSub.Length > 0) + { + obj.Deserialize(sender, target, readerSub, true); + } + + await OnSpawnAsync(obj); + } + + continue; + } + + _logger.LogError("Couldn't find spawnable object {0}.", objectId); + break; + } + + // Only the host is allowed to despawn objects. + case GameDataTag.DespawnFlag: + { + var netId = reader.ReadPackedUInt32(); + if (_allObjectsFast.TryGetValue(netId, out var obj)) + { + if (sender.Client.Id != obj.OwnerId && !sender.IsHost) + { + _logger.LogWarning( + "Player {0} ({1}) tried to send DespawnFlag for {2} but was denied.", + sender.Client.Name, + sender.Client.Id, + netId); + return false; + } + + RemoveNetObject(obj); + await OnDestroyAsync(obj); + _logger.LogDebug("Destroyed InnerNetObject {0} ({1}), OwnerId {2}", obj.GetType().Name, netId, obj.OwnerId); + } + else + { + _logger.LogDebug( + "Player {0} ({1}) sent DespawnFlag for unregistered NetId {2}.", + sender.Client.Name, + sender.Client.Id, + netId); + } + + break; + } + + case GameDataTag.SceneChangeFlag: + { + // Sender is only allowed to change his own scene. + var clientId = reader.ReadPackedInt32(); + if (clientId != sender.Client.Id) + { + _logger.LogWarning( + "Player {0} ({1}) tried to send SceneChangeFlag for another player.", + sender.Client.Name, + sender.Client.Id); + return false; + } + + sender.Scene = reader.ReadString(); + + _logger.LogTrace("> Scene {0} to {1}", clientId, sender.Scene); + break; + } + + case GameDataTag.ReadyFlag: + { + var clientId = reader.ReadPackedInt32(); + _logger.LogTrace("> IsReady {0}", clientId); + break; + } + + default: + { + _logger.LogTrace("Bad GameData tag {0}", reader.Tag); + break; + } + } + + if (sender.Client.Player == null) + { + // Disconnect handler was probably invoked, cancel the rest. + return false; + } + } + + return true; + } + + private bool AddNetObject(InnerNetObject obj) + { + if (_allObjectsFast.ContainsKey(obj.NetId)) + { + return false; + } + + _allObjects.Add(obj); + _allObjectsFast.Add(obj.NetId, obj); + return true; + } + + private void RemoveNetObject(InnerNetObject obj) + { + var index = _allObjects.IndexOf(obj); + if (index > -1) + { + _allObjects.RemoveAt(index); + } + + _allObjectsFast.Remove(obj.NetId); + + obj.NetId = uint.MaxValue; + } + + public T FindObjectByNetId<T>(uint netId) + where T : InnerNetObject + { + if (_allObjectsFast.TryGetValue(netId, out var obj)) + { + return (T) obj; + } + + return null; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs new file mode 100644 index 0000000..4bf1c43 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs @@ -0,0 +1,240 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Hazel; +using Impostor.Server.Events; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + private readonly SemaphoreSlim _clientAddLock = new SemaphoreSlim(1, 1); + + public async ValueTask HandleStartGame(IMessageReader message) + { + GameState = GameStates.Starting; + + using var packet = MessageWriter.Get(MessageType.Reliable); + message.CopyTo(packet); + await SendToAllAsync(packet); + + await _eventManager.CallAsync(new GameStartingEvent(this)); + } + + public async ValueTask<GameJoinResult> AddClientAsync(ClientBase client) + { + var hasLock = false; + + try + { + hasLock = await _clientAddLock.WaitAsync(TimeSpan.FromMinutes(1)); + + if (hasLock) + { + return await AddClientSafeAsync(client); + } + } + finally + { + if (hasLock) + { + _clientAddLock.Release(); + } + } + + return GameJoinResult.FromError(GameJoinError.InvalidClient); + } + + private async ValueTask<GameJoinResult> AddClientSafeAsync(ClientBase client) + { + // Check if the IP of the player is banned. + if (client.Connection != null && _bannedIps.Contains(client.Connection.EndPoint.Address)) + { + return GameJoinResult.FromError(GameJoinError.Banned); + } + + var player = client.Player; + + // Check if; + // - The player is already in this game. + // - The game is full. + if (player?.Game != this && _players.Count >= Options.MaxPlayers) + { + return GameJoinResult.FromError(GameJoinError.GameFull); + } + + if (GameState == GameStates.Starting || GameState == GameStates.Started) + { + return GameJoinResult.FromError(GameJoinError.GameStarted); + } + + if (GameState == GameStates.Destroyed) + { + return GameJoinResult.FromError(GameJoinError.GameDestroyed); + } + + var isNew = false; + + if (player == null || player.Game != this) + { + var clientPlayer = new ClientPlayer(_serviceProvider.GetRequiredService<ILogger<ClientPlayer>>(), client, this); + + if (!_clientManager.Validate(client)) + { + return GameJoinResult.FromError(GameJoinError.InvalidClient); + } + + isNew = true; + player = clientPlayer; + client.Player = clientPlayer; + } + + // Check current player state. + if (player.Limbo == LimboStates.NotLimbo) + { + return GameJoinResult.FromError(GameJoinError.InvalidLimbo); + } + + if (GameState == GameStates.Ended) + { + await HandleJoinGameNext(player, isNew); + return GameJoinResult.CreateSuccess(player); + } + + await HandleJoinGameNew(player, isNew); + return GameJoinResult.CreateSuccess(player); + } + + public async ValueTask HandleEndGame(IMessageReader message, GameOverReason gameOverReason) + { + GameState = GameStates.Ended; + + // Broadcast end of the game. + using (var packet = MessageWriter.Get(MessageType.Reliable)) + { + message.CopyTo(packet); + await SendToAllAsync(packet); + } + + // Put all players in the correct limbo state. + foreach (var player in _players) + { + player.Value.Limbo = LimboStates.PreSpawn; + } + + await _eventManager.CallAsync(new GameEndedEvent(this, gameOverReason)); + } + + public async ValueTask HandleAlterGame(IMessageReader message, IClientPlayer sender, bool isPublic) + { + IsPublic = isPublic; + + using var packet = MessageWriter.Get(MessageType.Reliable); + message.CopyTo(packet); + await SendToAllExceptAsync(packet, sender.Client.Id); + + await _eventManager.CallAsync(new GameAlterEvent(this, isPublic)); + } + + public async ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason) + { + 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(MessageType.Reliable); + WriteRemovePlayerMessage(packet, false, playerId, reason); + await SendToAllExceptAsync(packet, playerId); + } + + public async ValueTask HandleKickPlayer(int playerId, bool isBan) + { + _logger.LogInformation("{0} - Player {1} has left.", Code, playerId); + + using var message = MessageWriter.Get(MessageType.Reliable); + + // Send message to everyone that this player was kicked. + WriteKickPlayerMessage(message, false, playerId, isBan); + + await SendToAllAsync(message); + await PlayerRemove(playerId, isBan); + + // Remove the player from everyone's game. + WriteRemovePlayerMessage( + message, + true, + playerId, + isBan ? DisconnectReason.Banned : DisconnectReason.Kicked); + + await SendToAllExceptAsync(message, playerId); + } + + private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew) + { + _logger.LogInformation("{0} - Player {1} ({2}) is joining.", Code, sender.Client.Name, sender.Client.Id); + + // Add player to the game. + if (isNew) + { + await PlayerAdd(sender); + } + + sender.InitializeSpawnTimeout(); + + using (var message = MessageWriter.Get(MessageType.Reliable)) + { + WriteJoinedGameMessage(message, false, sender); + WriteAlterGameMessage(message, false, IsPublic); + + sender.Limbo = LimboStates.NotLimbo; + + await SendToAsync(message, sender.Client.Id); + await BroadcastJoinMessage(message, true, sender); + } + } + + private async ValueTask HandleJoinGameNext(ClientPlayer sender, bool isNew) + { + _logger.LogInformation("{0} - Player {1} ({2}) is rejoining.", Code, sender.Client.Name, sender.Client.Id); + + // Add player to the game. + if (isNew) + { + await PlayerAdd(sender); + } + + // Check if the host joined and let everyone join. + if (sender.Client.Id == HostId) + { + GameState = GameStates.NotStarted; + + // Spawn the host. + await HandleJoinGameNew(sender, false); + + // Pull players out of limbo. + await CheckLimboPlayers(); + return; + } + + sender.Limbo = LimboStates.WaitingForHost; + + using (var packet = MessageWriter.Get(MessageType.Reliable)) + { + WriteWaitForHostMessage(packet, false, sender); + + await SendToAsync(packet, sender.Client.Id); + await BroadcastJoinMessage(packet, true, sender); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs new file mode 100644 index 0000000..21b6067 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs @@ -0,0 +1,103 @@ +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Net.Inner; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + public async ValueTask SendToAllAsync(IMessageWriter writer, LimboStates states = LimboStates.NotLimbo) + { + foreach (var connection in GetConnections(x => x.Limbo.HasFlag(states))) + { + await connection.SendAsync(writer); + } + } + + public async ValueTask SendToAllExceptAsync(IMessageWriter writer, int senderId, LimboStates states = LimboStates.NotLimbo) + { + foreach (var connection in GetConnections(x => + x.Limbo.HasFlag(states) && + x.Client.Id != senderId)) + { + await connection.SendAsync(writer); + } + } + + public async ValueTask SendToAsync(IMessageWriter writer, int id) + { + if (TryGetPlayer(id, out var player)) + { + await player.Client.Connection.SendAsync(writer); + } + } + + internal IMessageWriter StartRpc(uint targetNetId, RpcCalls callId, int targetClientId = -1, MessageType type = MessageType.Reliable) + { + var writer = MessageWriter.Get(type); + + if (targetClientId < 0) + { + writer.StartMessage(MessageFlags.GameData); + writer.Write(Code); + } + else + { + writer.StartMessage(MessageFlags.GameDataTo); + writer.Write(Code); + writer.WritePacked(targetClientId); + } + + writer.StartMessage(GameDataTag.RpcFlag); + writer.WritePacked(targetNetId); + writer.Write((byte) callId); + + return writer; + } + + internal ValueTask FinishRpcAsync(IMessageWriter writer, int? targetClientId = null) + { + writer.EndMessage(); + writer.EndMessage(); + + return targetClientId.HasValue + ? SendToAsync(writer, targetClientId.Value) + : SendToAllAsync(writer); + } + + private void WriteRemovePlayerMessage(IMessageWriter message, bool clear, int playerId, DisconnectReason reason) + { + Message04RemovePlayerS2C.Serialize(message, clear, Code, playerId, HostId, reason); + } + + private void WriteJoinedGameMessage(IMessageWriter message, bool clear, IClientPlayer player) + { + var playerIds = _players + .Where(x => x.Value != player) + .Select(x => x.Key) + .ToArray(); + + Message07JoinedGameS2C.Serialize(message, clear, Code, player.Client.Id, HostId, playerIds); + } + + private void WriteAlterGameMessage(IMessageWriter message, bool clear, bool isPublic) + { + Message10AlterGameS2C.Serialize(message, clear, Code, isPublic); + } + + private void WriteKickPlayerMessage(IMessageWriter message, bool clear, int playerId, bool isBan) + { + Message11KickPlayerS2C.Serialize(message, clear, Code, playerId, isBan); + } + + private void WriteWaitForHostMessage(IMessageWriter message, bool clear, IClientPlayer player) + { + Message12WaitForHostS2C.Serialize(message, clear, Code, player.Client.Id); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs new file mode 100644 index 0000000..e311776 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs @@ -0,0 +1,136 @@ +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Hazel; +using Impostor.Server.Events; +using Impostor.Server.Net.Hazel; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + private async ValueTask PlayerAdd(ClientPlayer player) + { + // Store player. + if (!_players.TryAdd(player.Client.Id, player)) + { + throw new ImpostorException("Failed to add player to game."); + } + + // Assign hostId if none is set. + if (HostId == -1) + { + HostId = player.Client.Id; + await InitGameDataAsync(player); + } + + await _eventManager.CallAsync(new GamePlayerJoinedEvent(this, player)); + } + + private async ValueTask<bool> PlayerRemove(int playerId, bool isBan = false) + { + if (!_players.TryRemove(playerId, out var player)) + { + return false; + } + + _logger.LogInformation("{0} - Player {1} ({2}) has left.", Code, player.Client.Name, playerId); + + if (GameState == GameStates.Starting || GameState == GameStates.Started) + { + if (player.Character?.PlayerInfo != null) + { + player.Character.PlayerInfo.Disconnected = true; + player.Character.PlayerInfo.LastDeathReason = DeathReason.Disconnect; + } + } + + player.Client.Player = null; + + // Game is empty, remove it. + if (_players.IsEmpty) + { + GameState = GameStates.Destroyed; + + // Remove instance reference. + await _gameManager.RemoveAsync(Code); + return true; + } + + // Host migration. + if (HostId == playerId) + { + await MigrateHost(); + } + + if (isBan && player.Client.Connection != null) + { + BanIp(player.Client.Connection.EndPoint.Address); + } + + await _eventManager.CallAsync(new GamePlayerLeftEvent(this, player, isBan)); + + // Player can refuse to be kicked and keep the connection open, check for this. + _ = Task.Run(async () => + { + await Task.Delay(Constants.ConnectionTimeout); + + if (player.Client.Connection.IsConnected && player.Client.Connection is HazelConnection hazel) + { + _logger.LogInformation("{0} - Player {1} ({2}) kept connection open after leaving, disposing.", Code, player.Client.Name, playerId); + hazel.DisposeInnerConnection(); + } + }); + + return true; + } + + private async ValueTask MigrateHost() + { + // Pick the first player as new host. + var host = _players + .Select(p => p.Value) + .FirstOrDefault(); + + if (host == null) + { + await EndAsync(); + return; + } + + HostId = host.Client.Id; + _logger.LogInformation("{0} - Assigned {1} ({2}) as new host.", Code, host.Client.Name, host.Client.Id); + + // Check our current game state. + if (GameState == GameStates.Ended && host.Limbo == LimboStates.WaitingForHost) + { + GameState = GameStates.NotStarted; + + // Spawn the host. + await HandleJoinGameNew(host, false); + + // Pull players out of limbo. + await CheckLimboPlayers(); + } + } + + private async ValueTask CheckLimboPlayers() + { + using var message = MessageWriter.Get(MessageType.Reliable); + + foreach (var (_, player) in _players.Where(x => x.Value.Limbo == LimboStates.WaitingForHost)) + { + WriteJoinedGameMessage(message, true, player); + WriteAlterGameMessage(message, false, IsPublic); + + player.Limbo = LimboStates.NotLimbo; + + await SendToAsync(message, player.Client.Id); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.cs new file mode 100644 index 0000000..1919867 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Events.Managers; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Server.Events; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + private readonly ILogger<Game> _logger; + private readonly IServiceProvider _serviceProvider; + private readonly GameManager _gameManager; + private readonly ClientManager _clientManager; + private readonly ConcurrentDictionary<int, ClientPlayer> _players; + private readonly HashSet<IPAddress> _bannedIps; + private readonly IEventManager _eventManager; + + public Game( + ILogger<Game> logger, + IServiceProvider serviceProvider, + GameManager gameManager, + IPEndPoint publicIp, + GameCode code, + GameOptionsData options, + ClientManager clientManager, + IEventManager eventManager) + { + _logger = logger; + _serviceProvider = serviceProvider; + _gameManager = gameManager; + _players = new ConcurrentDictionary<int, ClientPlayer>(); + _bannedIps = new HashSet<IPAddress>(); + + PublicIp = publicIp; + Code = code; + HostId = -1; + GameState = GameStates.NotStarted; + GameNet = new GameNet(); + Options = options; + _clientManager = clientManager; + _eventManager = eventManager; + Items = new ConcurrentDictionary<object, object>(); + } + + public IPEndPoint PublicIp { get; } + + public GameCode Code { get; } + + public bool IsPublic { get; private set; } + + public int HostId { get; private set; } + + public GameStates GameState { get; private set; } + + internal GameNet GameNet { get; } + + public GameOptionsData Options { get; } + + public IDictionary<object, object> Items { get; } + + public int PlayerCount => _players.Count; + + public ClientPlayer Host => _players[HostId]; + + public IEnumerable<IClientPlayer> Players => _players.Select(p => p.Value); + + public bool TryGetPlayer(int id, out ClientPlayer player) + { + if (_players.TryGetValue(id, out var result)) + { + player = result; + return true; + } + + player = default; + return false; + } + + public IClientPlayer GetClientPlayer(int clientId) + { + return _players.TryGetValue(clientId, out var clientPlayer) ? clientPlayer : null; + } + + internal ValueTask StartedAsync() + { + if (GameState == GameStates.Starting) + { + GameState = GameStates.Started; + + return _eventManager.CallAsync(new GameStartedEvent(this)); + } + + return default; + } + + public ValueTask EndAsync() + { + return _gameManager.RemoveAsync(Code); + } + + private ValueTask BroadcastJoinMessage(IMessageWriter message, bool clear, ClientPlayer player) + { + Message01JoinGameS2C.SerializeJoin(message, clear, Code, player.Client.Id, HostId); + + return SendToAllExceptAsync(message, player.Client.Id); + } + + private IEnumerable<IHazelConnection> GetConnections(Func<IClientPlayer, bool> filter) + { + return Players + .Where(filter) + .Select(p => p.Client.Connection); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs new file mode 100644 index 0000000..34ea0fe --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs @@ -0,0 +1,17 @@ +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.State +{ + /// <inheritdoc /> + internal partial class GameNet : IGameNet + { + IInnerLobbyBehaviour IGameNet.LobbyBehaviour => LobbyBehaviour; + + IInnerGameData IGameNet.GameData => GameData; + + IInnerVoteBanSystem IGameNet.VoteBan => VoteBan; + + IInnerShipStatus IGameNet.ShipStatus => ShipStatus; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs new file mode 100644 index 0000000..c5542f9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs @@ -0,0 +1,16 @@ +using Impostor.Server.Net.Inner.Objects; +using Impostor.Server.Net.Inner.Objects.Components; + +namespace Impostor.Server.Net.State +{ + internal partial class GameNet + { + public InnerLobbyBehaviour LobbyBehaviour { get; internal set; } + + public InnerGameData GameData { get; internal set; } + + public InnerVoteBanSystem VoteBan { get; internal set; } + + public InnerShipStatus ShipStatus { get; internal set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs new file mode 100644 index 0000000..5f6aee1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public class AssemblyInformation : IAssemblyInformation + { + private Assembly _assembly; + + public AssemblyInformation(AssemblyName assemblyName, string path, bool isPlugin) + { + AssemblyName = assemblyName; + Path = path; + IsPlugin = isPlugin; + } + + public string Path { get; } + + public bool IsPlugin { get; } + + public AssemblyName AssemblyName { get; } + + public Assembly Load(AssemblyLoadContext context) + { + if (_assembly != null) + { + return _assembly; + } + + using var stream = File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read); + + _assembly = context.LoadFromStream(stream); + + return _assembly; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs new file mode 100644 index 0000000..fb36e92 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public interface IAssemblyInformation + { + AssemblyName AssemblyName { get; } + + bool IsPlugin { get; } + + Assembly Load(AssemblyLoadContext context); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs new file mode 100644 index 0000000..720367c --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public class LoadedAssemblyInformation : IAssemblyInformation + { + private readonly Assembly _assembly; + + public LoadedAssemblyInformation(Assembly assembly) + { + AssemblyName = assembly.GetName(); + _assembly = assembly; + } + + public AssemblyName AssemblyName { get; } + + public bool IsPlugin => false; + + public Assembly Load(AssemblyLoadContext context) + { + return _assembly; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs new file mode 100644 index 0000000..22dc9e9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Impostor.Server.Plugins +{ + public class PluginConfig + { + public List<string> Paths { get; set; } = new List<string>(); + + public List<string> LibraryPaths { get; set; } = new List<string>(); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs new file mode 100644 index 0000000..e6a5b6c --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using Impostor.Api.Plugins; + +namespace Impostor.Server.Plugins +{ + public class PluginInformation + { + private readonly ImpostorPluginAttribute _attribute; + + public PluginInformation(IPluginStartup startup, Type pluginType) + { + _attribute = pluginType.GetCustomAttribute<ImpostorPluginAttribute>(); + + Startup = startup; + PluginType = pluginType; + } + + public string Package => _attribute.Package; + + public string Name => _attribute.Name; + + public string Author => _attribute.Author; + + public string Version => _attribute.Version; + + public IPluginStartup Startup { get; } + + public Type PluginType { get; } + + public IPlugin Instance { get; set; } + + public override string ToString() + { + return $"{Package} {Name} ({Version}) by {Author}"; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs new file mode 100644 index 0000000..4e72886 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Impostor.Api.Plugins; +using Impostor.Server.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace Impostor.Server.Plugins +{ + public static class PluginLoader + { + private static readonly ILogger Logger = Log.ForContext(typeof(PluginLoader)); + + public static IHostBuilder UsePluginLoader(this IHostBuilder builder, PluginConfig config) + { + var assemblyInfos = new List<IAssemblyInformation>(); + var context = AssemblyLoadContext.Default; + + // Add the plugins and libraries. + var pluginPaths = new List<string>(config.Paths); + var libraryPaths = new List<string>(config.LibraryPaths); + + var rootFolder = Directory.GetCurrentDirectory(); + + pluginPaths.Add(Path.Combine(rootFolder, "plugins")); + libraryPaths.Add(Path.Combine(rootFolder, "libraries")); + + var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); + matcher.AddInclude("*.dll"); + matcher.AddExclude("Impostor.Api.dll"); + + RegisterAssemblies(pluginPaths, matcher, assemblyInfos, true); + RegisterAssemblies(libraryPaths, matcher, assemblyInfos, false); + + // Register the resolver to the current context. + // TODO: Move this to a new context so we can unload/reload plugins. + context.Resolving += (loadContext, name) => + { + Logger.Verbose("Loading assembly {0} v{1}", name.Name, name.Version); + + // Some plugins may be referencing another Impostor.Api version and try to load it. + // We want to only use the one shipped with the server. + if (name.Name.Equals("Impostor.Api")) + { + return typeof(IPlugin).Assembly; + } + + var info = assemblyInfos.FirstOrDefault(a => a.AssemblyName.Name == name.Name); + + return info?.Load(loadContext); + }; + + // TODO: Catch uncaught exceptions. + var assemblies = assemblyInfos + .Where(a => a.IsPlugin) + .Select(a => context.LoadFromAssemblyName(a.AssemblyName)) + .ToList(); + + // Find all plugins. + var plugins = new List<PluginInformation>(); + + foreach (var assembly in assemblies) + { + // Find plugin startup. + var pluginStartup = assembly + .GetTypes() + .Where(t => typeof(IPluginStartup).IsAssignableFrom(t) && t.IsClass) + .ToList(); + + if (pluginStartup.Count > 1) + { + Logger.Warning("A plugin may only define zero or one IPluginStartup implementation ({0}).", assembly); + continue; + } + + // Find plugin. + var plugin = assembly + .GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t) + && t.IsClass + && !t.IsAbstract + && t.GetCustomAttribute<ImpostorPluginAttribute>() != null) + .ToList(); + + if (plugin.Count != 1) + { + Logger.Warning("A plugin must define exactly one IPlugin or PluginBase implementation ({0}).", assembly); + continue; + } + + // Save plugin. + plugins.Add(new PluginInformation( + pluginStartup + .Select(Activator.CreateInstance) + .Cast<IPluginStartup>() + .FirstOrDefault(), + plugin.First())); + } + + foreach (var plugin in plugins.Where(plugin => plugin.Startup != null)) + { + plugin.Startup.ConfigureHost(builder); + } + + builder.ConfigureServices(services => + { + services.AddHostedService(provider => ActivatorUtilities.CreateInstance<PluginLoaderService>(provider, plugins)); + + foreach (var plugin in plugins.Where(plugin => plugin.Startup != null)) + { + plugin.Startup.ConfigureServices(services); + } + }); + + return builder; + } + + private static void RegisterAssemblies( + IEnumerable<string> paths, + Matcher matcher, + ICollection<IAssemblyInformation> assemblyInfos, + bool isPlugin) + { + foreach (var path in paths.SelectMany(matcher.GetResultsInFullPath)) + { + AssemblyName assemblyName; + + try + { + assemblyName = AssemblyName.GetAssemblyName(path); + } + catch (BadImageFormatException) + { + continue; + } + + assemblyInfos.Add(new AssemblyInformation(assemblyName, path, isPlugin)); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs new file mode 100644 index 0000000..64424a1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; +using Impostor.Api; + +namespace Impostor.Server.Plugins +{ + public class PluginLoaderException : ImpostorException + { + public PluginLoaderException() + { + } + + protected PluginLoaderException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public PluginLoaderException(string? message) : base(message) + { + } + + public PluginLoaderException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs new file mode 100644 index 0000000..0afbc22 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Plugins; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Plugins +{ + public class PluginLoaderService : IHostedService + { + private readonly ILogger<PluginLoaderService> _logger; + private readonly IServiceProvider _serviceProvider; + private readonly List<PluginInformation> _plugins; + + public PluginLoaderService(ILogger<PluginLoaderService> logger, IServiceProvider serviceProvider, List<PluginInformation> plugins) + { + _logger = logger; + _serviceProvider = serviceProvider; + _plugins = plugins; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Loading plugins."); + + foreach (var plugin in _plugins) + { + _logger.LogInformation("Enabling plugin {0}.", plugin); + + // Create instance and inject services. + plugin.Instance = (IPlugin) ActivatorUtilities.CreateInstance(_serviceProvider, plugin.PluginType); + + // Enable plugin. + await plugin.Instance.EnableAsync(); + } + + _logger.LogInformation( + _plugins.Count == 1 + ? "Loaded {0} plugin." + : "Loaded {0} plugins.", _plugins.Count); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + // Disable all plugins with a valid instance set. + // In the case of a failed startup, some can be null. + foreach (var plugin in _plugins.Where(plugin => plugin.Instance != null)) + { + _logger.LogInformation("Disabling plugin {0}.", plugin); + + // Disable plugin. + await plugin.Instance.DisableAsync(); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Program.cs b/Impostor-dev/src/Impostor.Server/Program.cs new file mode 100644 index 0000000..32a28fb --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Program.cs @@ -0,0 +1,207 @@ +using System; +using System.IO; +using System.Linq; +using Impostor.Api.Events.Managers; +using Impostor.Api.Games; +using Impostor.Api.Games.Managers; +using Impostor.Api.Net.Manager; +using Impostor.Api.Net.Messages; +using Impostor.Hazel.Extensions; +using Impostor.Server.Config; +using Impostor.Server.Events; +using Impostor.Server.Net; +using Impostor.Server.Net.Factories; +using Impostor.Server.Net.Manager; +using Impostor.Server.Net.Messages; +using Impostor.Server.Net.Redirector; +using Impostor.Server.Plugins; +using Impostor.Server.Recorder; +using Impostor.Server.Utils; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ObjectPool; +using Serilog; +using Serilog.Events; + +namespace Impostor.Server +{ + internal static class Program + { + private static int Main(string[] args) + { +#if DEBUG + var logLevel = LogEventLevel.Debug; +#else + var logLevel = LogEventLevel.Information; +#endif + + if (args.Contains("--verbose")) + { + logLevel = LogEventLevel.Verbose; + } + else if (args.Contains("--errors-only")) + { + logLevel = LogEventLevel.Error; + } + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Is(logLevel) +#if DEBUG + .MinimumLevel.Override("Microsoft", LogEventLevel.Debug) +#else + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) +#endif + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + try + { + Log.Information("Starting Impostor v{0}", DotnetUtils.GetVersion()); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Impostor terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + private static IConfiguration CreateConfiguration(string[] args) + { + var configurationBuilder = new ConfigurationBuilder(); + + configurationBuilder.AddJsonFile("config.json", true); + configurationBuilder.AddJsonFile("config.Development.json", true); + configurationBuilder.AddEnvironmentVariables(prefix: "IMPOSTOR_"); + configurationBuilder.AddCommandLine(args); + + return configurationBuilder.Build(); + } + + //c 入口 + private static IHostBuilder CreateHostBuilder(string[] args) + { + var configuration = CreateConfiguration(args); + var pluginConfig = configuration.GetSection("PluginLoader") + .Get<PluginConfig>() ?? new PluginConfig(); + + return Host.CreateDefaultBuilder(args) + .UseContentRoot(Directory.GetCurrentDirectory()) +#if DEBUG + .UseEnvironment(Environment.GetEnvironmentVariable("IMPOSTOR_ENV") ?? "Development") +#else + .UseEnvironment("Production") +#endif + .ConfigureAppConfiguration(builder => + { + builder.AddConfiguration(configuration); + }) + .ConfigureServices((host, services) => + { + var debug = host.Configuration + .GetSection(DebugConfig.Section) + .Get<DebugConfig>() ?? new DebugConfig(); + + var redirector = host.Configuration + .GetSection(ServerRedirectorConfig.Section) + .Get<ServerRedirectorConfig>() ?? new ServerRedirectorConfig(); + + services.Configure<DebugConfig>(host.Configuration.GetSection(DebugConfig.Section)); + services.Configure<AntiCheatConfig>(host.Configuration.GetSection(AntiCheatConfig.Section)); + services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section)); + services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section)); + + if (redirector.Enabled) + { + if (!string.IsNullOrEmpty(redirector.Locator.Redis)) + { + // When joining a game, it retrieves the game server ip from redis. + // When a game has been created on this node, it stores the game code with its ip in redis. + services.AddSingleton<INodeLocator, NodeLocatorRedis>(); + + // Dependency for the NodeLocatorRedis. + services.AddStackExchangeRedisCache(options => + { + options.Configuration = redirector.Locator.Redis; + options.InstanceName = "ImpostorRedis"; + }); + } + else if (!string.IsNullOrEmpty(redirector.Locator.UdpMasterEndpoint)) + { + services.AddSingleton<INodeLocator, NodeLocatorUdp>(); + + if (redirector.Master) + { + services.AddHostedService<NodeLocatorUdpService>(); + } + } + else + { + throw new Exception("Missing a valid NodeLocator config."); + } + + // Use the configuration as source for the list of nodes to provide + // when creating a game. + services.AddSingleton<INodeProvider, NodeProviderConfig>(); + } + else + { + // Redirector is not enabled but the dependency is still required. + // So we provide one that ignores all calls. + services.AddSingleton<INodeLocator, NodeLocatorNoOp>(); + } + + services.AddSingleton<ClientManager>(); + services.AddSingleton<IClientManager>(p => p.GetRequiredService<ClientManager>()); + + if (redirector.Enabled && redirector.Master) + { + services.AddSingleton<IClientFactory, ClientFactory<ClientRedirector>>(); + + // For a master server, we don't need a GameManager. + } + else + { + if (debug.GameRecorderEnabled) + { + services.AddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider()); + services.AddSingleton<ObjectPool<PacketSerializationContext>>(serviceProvider => + { + var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>(); + var policy = new PacketSerializationContextPooledObjectPolicy(); + return provider.Create(policy); + }); + + services.AddSingleton<PacketRecorder>(); + services.AddHostedService(sp => sp.GetRequiredService<PacketRecorder>()); + services.AddSingleton<IClientFactory, ClientFactory<ClientRecorder>>(); + } + else + { + services.AddSingleton<IClientFactory, ClientFactory<Client>>(); + } + + services.AddSingleton<GameManager>(); + services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>()); + } + + services.AddHazel(); + services.AddSingleton<IMessageWriterProvider, MessageWriterProvider>(); + services.AddSingleton<IGameCodeFactory, GameCodeFactory>(); + services.AddSingleton<IEventManager, EventManager>(); + services.AddSingleton<Matchmaker>(); + services.AddHostedService<MatchmakerService>(); + }) + .UseSerilog() + .UseConsoleLifetime() + .UsePluginLoader(pluginConfig); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset b/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset new file mode 100644 index 0000000..3654bc3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset @@ -0,0 +1,21 @@ +<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0"> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules"> + <Rule Id="SA1200" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules"> + <Rule Id="SA1600" Action="None" /> + <Rule Id="SA1601" Action="None" /> + <Rule Id="SA1615" Action="None" /> + <Rule Id="SA1633" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules"> + <Rule Id="SA1101" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules"> + <Rule Id="SA1309" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.SpacingRules"> + <Rule Id="SA1003" Action="None" /> + <Rule Id="SA1009" Action="None" /> + </Rules> +</RuleSet>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs b/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..84c5158 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Impostor.Benchmarks")] +[assembly:InternalsVisibleTo("Impostor.Tests")] +[assembly:InternalsVisibleTo("Impostor.Tools.ServerReplay")] diff --git a/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs b/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs new file mode 100644 index 0000000..5763c70 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs @@ -0,0 +1,86 @@ +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Impostor.Server.Config; +using Impostor.Server.Net; +using Impostor.Server.Net.Hazel; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Recorder +{ + internal class ClientRecorder : Client + { + private readonly PacketRecorder _recorder; + private bool _isFirst; + private bool _createdGame; + private bool _recordAfter; + + public ClientRecorder(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, PacketRecorder recorder) + : base(logger, antiCheatOptions, clientManager, gameManager, name, connection) + { + _recorder = recorder; + _isFirst = true; + _createdGame = false; + _recordAfter = true; + } + + public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType) + { + using var messageCopy = reader.Copy(); + + // Trigger connect event. + if (_isFirst) + { + _isFirst = false; + + await _recorder.WriteConnectAsync(this); + } + + // Check if we were in-game before handling the message. + var inGame = Player?.Game != null; + + if (!_recordAfter) + { + // Trigger message event. + await _recorder.WriteMessageAsync(this, messageCopy, messageType); + } + + // Handle the message. + await base.HandleMessageAsync(reader, messageType); + + // Player created a game. + if (reader.Tag == MessageFlags.HostGame) + { + _createdGame = true; + } + else if (reader.Tag == MessageFlags.JoinGame && _createdGame) + { + _createdGame = false; + + // We created a game and are now in-game, store that event. + if (!inGame && Player?.Game != null) + { + await _recorder.WriteGameCreatedAsync(this, Player.Game.Code); + } + + _recordAfter = false; + + // Trigger message event. + await _recorder.WriteMessageAsync(this, messageCopy, messageType); + } + + if (_recordAfter) + { + // Trigger message event. + await _recorder.WriteMessageAsync(this, messageCopy, messageType); + } + } + + public override async ValueTask HandleDisconnectAsync(string reason) + { + await _recorder.WriteDisconnectAsync(this, reason); + await base.HandleDisconnectAsync(reason); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs new file mode 100644 index 0000000..af2ea4f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs @@ -0,0 +1,201 @@ +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Impostor.Api.Games; +using Impostor.Api.Net.Messages; +using Impostor.Server.Config; +using Impostor.Server.Net; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Recorder +{ + /// <summary> + /// Records all packets received in <see cref="ClientRecorder.HandleMessageAsync"/>. + /// </summary> + internal class PacketRecorder : BackgroundService + { + private readonly string _path; + private readonly ILogger<PacketRecorder> _logger; + private readonly ObjectPool<PacketSerializationContext> _pool; + private readonly Channel<byte[]> _channel; + + public PacketRecorder(ILogger<PacketRecorder> logger, IOptions<DebugConfig> options, ObjectPool<PacketSerializationContext> pool) + { + var name = $"session_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.dat"; + + _path = Path.Combine(options.Value.GameRecorderPath, name); + _logger = logger; + _pool = pool; + + _channel = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = false, + }); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("PacketRecorder is enabled, writing packets to {0}.", _path); + + var writer = File.Open(_path, FileMode.CreateNew, FileAccess.Write, FileShare.Read); + + // Handle messages. + try + { + while (!stoppingToken.IsCancellationRequested) + { + var result = await _channel.Reader.ReadAsync(stoppingToken); + + await writer.WriteAsync(result, stoppingToken); + await writer.FlushAsync(stoppingToken); + } + } + catch (TaskCanceledException) + { + } + + // Clean up. + await writer.DisposeAsync(); + } + + public async Task WriteConnectAsync(ClientRecorder client) + { + _logger.LogTrace("Writing Connect."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Connect); + WriteClient(context, client, true); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteDisconnectAsync(ClientRecorder client, string reason) + { + _logger.LogTrace("Writing Disconnect."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Disconnect); + WriteClient(context, client, false); + context.Writer.Write(reason); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteMessageAsync(ClientRecorder client, IMessageReader reader, MessageType messageType) + { + _logger.LogTrace("Writing Message."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Message); + WriteClient(context, client, false); + WritePacket(context, reader, messageType); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteGameCreatedAsync(ClientRecorder client, GameCode gameCode) + { + _logger.LogTrace("Writing GameCreated {0}.", gameCode); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.GameCreated); + WriteClient(context, client, false); + WriteGameCode(context, gameCode); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + private static void WriteHeader(PacketSerializationContext context, RecordedPacketType type) + { + // Length placeholder. + context.Writer.Write((int) 0); + context.Writer.Write((byte) type); + } + + private static void WriteClient(PacketSerializationContext context, ClientBase client, bool full) + { + var address = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12345); + var addressBytes = address.Address.GetAddressBytes(); + + context.Writer.Write(client.Id); + + if (full) + { + context.Writer.Write((byte) addressBytes.Length); + context.Writer.Write(addressBytes); + context.Writer.Write((ushort) address.Port); + context.Writer.Write(client.Name); + } + } + + private static void WritePacket(PacketSerializationContext context, IMessageReader reader, MessageType messageType) + { + context.Writer.Write((byte) messageType); + context.Writer.Write((byte) reader.Tag); + context.Writer.Write((int) reader.Length); + context.Writer.Write(reader.Buffer, reader.Offset, reader.Length); + } + + private static void WriteGameCode(PacketSerializationContext context, in GameCode gameCode) + { + context.Writer.Write(gameCode.Code); + } + + private static void WriteLength(PacketSerializationContext context) + { + var length = context.Stream.Position; + + context.Stream.Position = 0; + context.Writer.Write((int) length); + context.Stream.Position = length; + } + + private async Task WriteAsync(MemoryStream data) + { + await _channel.Writer.WriteAsync(data.ToArray()); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs new file mode 100644 index 0000000..07755f6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.Text; + +namespace Impostor.Server.Recorder +{ + public class PacketSerializationContext + { + private const int InitialStreamSize = 0x100; + private const int MaximumStreamSize = 0x100000; + + private MemoryStream _memory; + private BinaryWriter _writer; + + public MemoryStream Stream + { + get + { + if (_memory == null) + { + _memory = new MemoryStream(InitialStreamSize); + } + + return _memory; + } + private set => _memory = value; + } + + public BinaryWriter Writer + { + get + { + if (_writer == null) + { + _writer = new BinaryWriter(Stream, Encoding.UTF8, true); + } + + return _writer; + } + private set => _writer = value; + } + + public void Reset() + { + if (Stream.Capacity > MaximumStreamSize) + { + Stream = null; + Writer = null; + } + else + { + Stream.Position = 0L; + Stream.SetLength(0L); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs new file mode 100644 index 0000000..17b355f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Server.Recorder +{ + public class PacketSerializationContextPooledObjectPolicy : IPooledObjectPolicy<PacketSerializationContext> + { + public PacketSerializationContext Create() + { + return new PacketSerializationContext(); + } + + public bool Return(PacketSerializationContext obj) + { + obj.Reset(); + return true; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs b/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs new file mode 100644 index 0000000..a8a20bc --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs @@ -0,0 +1,10 @@ +namespace Impostor.Server.Recorder +{ + internal enum RecordedPacketType : byte + { + Connect = 1, + Disconnect = 2, + Message = 3, + GameCreated = 4 + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs b/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs new file mode 100644 index 0000000..48a0ac0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace Impostor.Server.Utils +{ + internal static class DotnetUtils + { + private const string DefaultUnknownBuild = "UNKNOWN"; + + public static string GetVersion() + { + var attribute = typeof(DotnetUtils).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>(); + if (attribute != null) + { + return attribute.InformationalVersion; + } + + return DefaultUnknownBuild; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs b/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs new file mode 100644 index 0000000..649d45a --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Net; +using System.Net.Sockets; +using Impostor.Api; + +namespace Impostor.Server.Utils +{ + internal static class IpUtils + { + public static string ResolveIp(string ip) + { + // Check if valid ip was entered. + if (!IPAddress.TryParse(ip, out var ipAddress)) + { + // Attempt to resolve DNS. + try + { + var hostAddresses = Dns.GetHostAddresses(ip); + if (hostAddresses.Length == 0) + { + throw new ImpostorConfigException($"Invalid IP Address entered '{ip}'."); + } + + // Use first IPv4 result. + ipAddress = hostAddresses.First(x => x.AddressFamily == AddressFamily.InterNetwork); + } + catch (SocketException) + { + throw new ImpostorConfigException($"Failed to resolve hostname '{ip}'."); + } + } + + // Only IPv4. + if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + throw new ImpostorConfigException($"Invalid IP Address entered '{ipAddress}', only IPv4 is supported by Among Us."); + } + + return ipAddress.ToString(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/config-full.json b/Impostor-dev/src/Impostor.Server/config-full.json new file mode 100644 index 0000000..dfee1bc --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/config-full.json @@ -0,0 +1,29 @@ +{ + "Server": { + "PublicIp": "127.0.0.1", + "PublicPort": 22023, + "ListenIp": "0.0.0.0", + "ListenPort": 22023 + }, + "AntiCheat": { + "BanIpFromGame": true + }, + "ServerRedirector": { + "Enabled": false, + "Master": true, + "Locator": { + "Redis": "127.0.0.1.6379", + "UdpMasterEndpoint": "127.0.0.1:32320" + }, + "Nodes": [ + { + "Ip": "127.0.0.1", + "Port": 22024 + } + ] + }, + "Debug": { + "GameRecorderEnabled": true, + "GameRecorderPath": "" + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/config.json b/Impostor-dev/src/Impostor.Server/config.json new file mode 100644 index 0000000..d477c74 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/config.json @@ -0,0 +1,11 @@ +{ + "Server": { + "PublicIp": "127.0.0.1", + "PublicPort": 22023, + "ListenIp": "0.0.0.0", + "ListenPort": 22023 + }, + "AntiCheat": { + "BanIpFromGame": true + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/icon.ico b/Impostor-dev/src/Impostor.Server/icon.ico Binary files differnew file mode 100644 index 0000000..8cc46f3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/icon.ico |