diff options
Diffstat (limited to 'Impostor-dev/src/Impostor.Server/Net/State')
10 files changed, 1263 insertions, 0 deletions
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 |