diff options
author | chai <chaifix@163.com> | 2020-12-30 20:59:04 +0800 |
---|---|---|
committer | chai <chaifix@163.com> | 2020-12-30 20:59:04 +0800 |
commit | e9ea621b93fbb58d9edfca8375918791637bbd52 (patch) | |
tree | 19ce3b1c1f2d51eda6878c9d0f2c9edc27f13650 /Impostor-dev/src/Impostor.Server/Net |
+init
Diffstat (limited to 'Impostor-dev/src/Impostor.Server/Net')
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 |