summaryrefslogtreecommitdiff
path: root/Impostor-dev/src/Impostor.Server/Net
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
+init
Diffstat (limited to 'Impostor-dev/src/Impostor.Server/Net')
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Client.cs338
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/ClientBase.cs58
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs24
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs9
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs12
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs74
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs13
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs29
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs31
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs25
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs148
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs8
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs70
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs88
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs30
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs187
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs36
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs9
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs49
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs176
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs138
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs455
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs10
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs76
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs147
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs7
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs60
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs44
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs32
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs39
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs26
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs19
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs27
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs37
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs103
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs150
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs66
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs57
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs13
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs97
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs14
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs9
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs14
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs43
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs134
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs101
-rw-r--r--Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs43
-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
61 files changed, 4690 insertions, 0 deletions
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