summaryrefslogtreecommitdiff
path: root/Impostor-dev/src/Impostor.Server/Net/State
diff options
context:
space:
mode:
authorchai <chaifix@163.com>2020-12-30 20:59:04 +0800
committerchai <chaifix@163.com>2020-12-30 20:59:04 +0800
commite9ea621b93fbb58d9edfca8375918791637bbd52 (patch)
tree19ce3b1c1f2d51eda6878c9d0f2c9edc27f13650 /Impostor-dev/src/Impostor.Server/Net/State
+init
Diffstat (limited to 'Impostor-dev/src/Impostor.Server/Net/State')
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs18
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs88
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs70
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs449
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs240
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs103
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs136
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/Game.cs126
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs17
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs16
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