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; /// /// Used for global object, spawned by the host. /// private const int InvalidClient = -2; /// /// Used internally to set the OwnerId to the current ClientId. /// i.e: ownerId = ownerId == -3 ? this.ClientId : ownerId; /// 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 _allObjects = new List(); private readonly Dictionary _allObjectsFast = new Dictionary(); 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 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(); 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(uint netId) where T : InnerNetObject { if (_allObjectsFast.TryGetValue(netId, out var obj)) { return (T) obj; } return null; } } }