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;
}
}
}