diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc29SetTasks.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc29SetTasks.cs index aba9a715..eb2bab49 100644 --- a/src/Impostor.Api/Net/Messages/Rpcs/Rpc29SetTasks.cs +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc29SetTasks.cs @@ -4,15 +4,13 @@ namespace Impostor.Api.Net.Messages.Rpcs { public static class Rpc29SetTasks { - public static void Serialize(IMessageWriter writer, byte playerId, ReadOnlyMemory taskTypeIds) + public static void Serialize(IMessageWriter writer, ReadOnlyMemory taskTypeIds) { - writer.Write(playerId); writer.Write(taskTypeIds); } - public static void Deserialize(IMessageReader reader, out byte playerId, out ReadOnlyMemory taskTypeIds) + public static void Deserialize(IMessageReader reader, out ReadOnlyMemory taskTypeIds) { - playerId = reader.ReadByte(); taskTypeIds = reader.ReadBytesAndSize(); } } diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc62CheckVanish.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc62CheckVanish.cs new file mode 100644 index 00000000..9011e43c --- /dev/null +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc62CheckVanish.cs @@ -0,0 +1,15 @@ +namespace Impostor.Api.Net.Messages.Rpcs +{ + public static class Rpc62CheckVanish + { + public static void Serialize(IMessageWriter writer, float maxDuration = 0f) + { + writer.Write(maxDuration); + } + + public static void Deserialize(IMessageReader reader, out float maxDuration) + { + maxDuration = reader.ReadSingle(); + } + } +} diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc63StartVanish.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc63StartVanish.cs new file mode 100644 index 00000000..9a0ecc18 --- /dev/null +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc63StartVanish.cs @@ -0,0 +1,13 @@ +namespace Impostor.Api.Net.Messages.Rpcs +{ + public static class Rpc63StartVanish + { + public static void Serialize(IMessageWriter writer) + { + } + + public static void Deserialize(IMessageReader reader) + { + } + } +} diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc64CheckAppear.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc64CheckAppear.cs new file mode 100644 index 00000000..cfa8a6b2 --- /dev/null +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc64CheckAppear.cs @@ -0,0 +1,15 @@ +namespace Impostor.Api.Net.Messages.Rpcs +{ + public static class Rpc64CheckAppear + { + public static void Serialize(IMessageWriter writer, bool shouldAnimate) + { + writer.Write(shouldAnimate); + } + + public static void Deserialize(IMessageReader reader, out bool shouldAnimate) + { + shouldAnimate = reader.ReadBoolean(); + } + } +} diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc65StartAppear.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc65StartAppear.cs new file mode 100644 index 00000000..7f354495 --- /dev/null +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc65StartAppear.cs @@ -0,0 +1,15 @@ +namespace Impostor.Api.Net.Messages.Rpcs +{ + public static class Rpc65StartAppear + { + public static void Serialize(IMessageWriter writer, bool shouldAnimate) + { + writer.Write(shouldAnimate); + } + + public static void Deserialize(IMessageReader reader, out bool shouldAnimate) + { + shouldAnimate = reader.ReadBoolean(); + } + } +} diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.Api.cs deleted file mode 100644 index c8aa17fa..00000000 --- a/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.Api.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Threading.Tasks; -using Impostor.Api; -using Impostor.Api.Net.Inner; -using Impostor.Api.Net.Messages.Rpcs; -using Impostor.Server.Events.Player; - -namespace Impostor.Server.Net.Inner.Objects -{ - internal partial class InnerGameData - { - public partial class TaskInfo - { - public async ValueTask CompleteAsync() - { - if (_playerInfo.Controller == null) - { - throw new ImpostorException("Can't complete a task that doesn't have a player assigned"); - } - - var player = _playerInfo.Controller; - - if (Complete) - { - throw new ImpostorException("Can't complete a task that is already completed"); - } - - Complete = true; - - // Send RPC. - using var writer = player.Game.StartRpc(player.NetId, RpcCalls.CompleteTask); - Rpc01CompleteTask.Serialize(writer, Id); - await player.Game.FinishRpcAsync(writer); - - // Notify plugins. - await _eventManager.CallAsync(new PlayerCompletedTaskEvent(player.Game, player.Game.GetClientPlayer(player.OwnerId)!, player, this)); - } - } - } -} diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs b/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs deleted file mode 100644 index b4a96209..00000000 --- a/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Impostor.Api.Events.Managers; -using Impostor.Api.Innersloth.Maps; -using Impostor.Api.Net.Inner.Objects; - -namespace Impostor.Server.Net.Inner.Objects -{ - internal partial class InnerGameData - { - public partial class TaskInfo : ITaskInfo - { - private readonly InnerPlayerInfo _playerInfo; - private readonly IEventManager _eventManager; - - public TaskInfo(InnerPlayerInfo playerInfo, IEventManager eventManager, uint id, TaskData? task) - { - _playerInfo = playerInfo; - _eventManager = eventManager; - Id = id; - Task = task; - } - - public uint Id { get; internal set; } - - public TaskData? Task { get; internal set; } - - public bool Complete { get; internal set; } - - public void Serialize(IMessageWriter writer) - { - writer.WritePacked(Id); - writer.Write(Complete); - } - - public void Deserialize(IMessageReader reader) - { - Id = reader.ReadPackedUInt32(); - Complete = reader.ReadBoolean(); - } - } - } -} diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs index bd711618..0742e6da 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs @@ -20,7 +20,7 @@ namespace Impostor.Server.Net.Inner.Objects public async ValueTask SetNameAsync(string name) { - PlayerInfo.PlayerName = name; + PlayerInfo.CurrentOutfit.PlayerName = name; using var writer = Game.StartRpc(NetId, RpcCalls.SetName); writer.Write(name); @@ -148,5 +148,18 @@ namespace Impostor.Server.Net.Inner.Objects // Notify plugins. await _eventManager.CallAsync(new PlayerExileEvent(Game, Game.GetClientPlayer(OwnerId)!, this)); } + + public async ValueTask StartVanishAsync() + { + using var writer = Game.StartRpc(NetId, RpcCalls.StartVanish); + await Game.FinishRpcAsync(writer); + } + + public async ValueTask StartAppearAsync(bool shouldAnimate) + { + using var writer = Game.StartRpc(NetId, RpcCalls.StartAppear); + writer.Write(shouldAnimate); + await Game.FinishRpcAsync(writer); + } } } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs index 16fa42ed..71a1efef 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs @@ -251,7 +251,7 @@ namespace Impostor.Server.Net.Inner.Objects } Rpc38SetLevel.Deserialize(reader, out var level); - return true; + return await HandleSetLevel(sender, level); } case RpcCalls.ReportDeadBody: @@ -511,7 +511,7 @@ namespace Impostor.Server.Net.Inner.Objects return false; } - Rpc55CheckShapeshift.Deserialize(reader, Game, out var playerControl, out var shouldAnimate); + Rpc55CheckShapeshift.Deserialize(reader, Game, out var playerControl, out _); break; } @@ -526,6 +526,56 @@ namespace Impostor.Server.Net.Inner.Objects break; } + case RpcCalls.CheckVanish: + { + if (!await ValidateOwnership(call, sender) || + !await ValidateRole(call, sender, PlayerInfo, RoleTypes.Phantom) || + !await ValidateCmd(call, sender, target)) + { + return false; + } + + Rpc62CheckVanish.Deserialize(reader, out var maxDuration); + return await HandleCheckVanish(sender, maxDuration); + } + + case RpcCalls.StartVanish: + { + if (!await ValidateHost(call, sender) || + !await ValidateBroadcast(call, sender, target)) + { + return false; + } + + Rpc63StartVanish.Deserialize(reader); + return await HandleStartVanish(sender); + } + + case RpcCalls.CheckAppear: + { + if (!await ValidateOwnership(call, sender) || + !await ValidateRole(call, sender, PlayerInfo, RoleTypes.Phantom) || + !await ValidateCmd(call, sender, target)) + { + return false; + } + + Rpc64CheckAppear.Deserialize(reader, out var shouldAnimate); + return await HandleCheckAppear(sender, shouldAnimate); + } + + case RpcCalls.StartAppear: + { + if (!await ValidateHost(call, sender) || + !await ValidateBroadcast(call, sender, target)) + { + return false; + } + + Rpc65StartAppear.Deserialize(reader, out var shouldAnimate); + return await HandleStartAppear(sender, shouldAnimate); + } + default: return await base.HandleRpcAsync(sender, target, call, reader); } @@ -686,7 +736,7 @@ namespace Impostor.Server.Net.Inner.Objects } } - PlayerInfo.PlayerName = name; + PlayerInfo.CurrentOutfit.PlayerName = name; return true; } @@ -834,6 +884,19 @@ namespace Impostor.Server.Net.Inner.Objects return true; } + private async ValueTask HandleSetLevel(ClientPlayer sender, uint level) + { + if (Game.GameState == GameStates.Started && + await sender.Client.ReportCheatAsync(RpcCalls.SetLevel, CheatCategory.GameFlow, "Client tried to set level while not in game")) + { + return false; + } + + PlayerInfo.PlayerLevel = level; + + return true; + } + private async ValueTask HandleCheckMurder(ClientPlayer sender, InnerPlayerControl? target) { if (!PlayerInfo.CanMurder(Game, _dateTimeProvider)) @@ -1008,5 +1071,61 @@ namespace Impostor.Server.Net.Inner.Objects return true; } + + private async ValueTask HandleCheckVanish(ClientPlayer sender, float maxDuration) + { + // TODO: Check if operation is taking place during lobby/meetings + // TODO: Check max duration + // If the game is host authoritive, the RPC is handled by the host, otherwise by the server + if (_game.IsHostAuthoritive) + { + return true; + } + else + { + await StartVanishAsync(); + return false; + } + } + + private async ValueTask HandleStartVanish(ClientPlayer sender) + { + if (!_game.IsHostAuthoritive) + { + if (await sender.Client.ReportCheatAsync(RpcCalls.StartVanish, CheatCategory.GameFlow, "Client tried to send StartVanish directly")) + { + return false; + } + } + + return true; + } + + private async ValueTask HandleCheckAppear(ClientPlayer sender, bool shouldAnimate) + { + // If the game is host authoritive, the RPC is handled by the host, otherwise by the server + if (_game.IsHostAuthoritive) + { + return true; + } + else + { + await StartAppearAsync(shouldAnimate); + return false; + } + } + + private async ValueTask HandleStartAppear(ClientPlayer sender, bool shouldAnimate) + { + if (!_game.IsHostAuthoritive) + { + if (await sender.Client.ReportCheatAsync(RpcCalls.StartAppear, CheatCategory.GameFlow, "Client tried to send StartAppear directly")) + { + return false; + } + } + + return true; + } } } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs index 498f8f6b..650289da 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Impostor.Api.Events.Managers; using Impostor.Api.Games; using Impostor.Api.Innersloth; using Impostor.Api.Innersloth.Customization; using Impostor.Api.Innersloth.GameOptions; using Impostor.Api.Net; using Impostor.Api.Net.Custom; +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Messages.Rpcs; using Impostor.Api.Utils; using Impostor.Server.Net.State; using Microsoft.Extensions.Logging; @@ -15,11 +18,13 @@ namespace Impostor.Server.Net.Inner.Objects { internal partial class InnerPlayerInfo { + private readonly IEventManager _eventManager; private readonly ILogger _logger; - public InnerPlayerInfo(ICustomMessageManager customMessageManager, Game game, ILogger logger) : base(customMessageManager, game) + public InnerPlayerInfo(ICustomMessageManager customMessageManager, IEventManager eventManager, Game game, ILogger logger) : base(customMessageManager, game) { Components.Add(this); + _eventManager = eventManager; _logger = logger; } @@ -29,7 +34,7 @@ namespace Impostor.Server.Net.Inner.Objects public int ClientId { get; internal set; } - public string PlayerName { get; internal set; } = string.Empty; + public string PlayerName => CurrentOutfit.PlayerName; public Dictionary Outfits { get; } = new() { @@ -46,7 +51,7 @@ namespace Impostor.Server.Net.Inner.Objects public bool Disconnected { get; internal set; } - public bool IsImpostor => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.ImpostorGhost; + public bool IsImpostor => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.ImpostorGhost or RoleTypes.Phantom; public bool CanVent => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Engineer; @@ -54,7 +59,7 @@ namespace Impostor.Server.Net.Inner.Objects public DeathReason LastDeathReason { get; internal set; } - public List Tasks { get; internal set; } = new List(0); + public List Tasks { get; internal set; } = new List(0); public DateTimeOffset LastMurder { get; set; } @@ -176,5 +181,39 @@ namespace Impostor.Server.Net.Inner.Objects return ValueTask.CompletedTask; } + + public override async ValueTask HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + switch (call) + { + case RpcCalls.SetTasks: + Rpc29SetTasks.Deserialize(reader, out var taskTypeIds); + SetTasks(taskTypeIds); + break; + + default: + return await base.HandleRpcAsync(sender, target, call, reader); + } + + return true; + } + + private void SetTasks(ReadOnlyMemory taskTypeIds) + { + if (Disconnected) + { + return; + } + + Tasks = new List(taskTypeIds.Length); + + var taskId = 0u; + foreach (var taskTypeId in taskTypeIds.Span) + { + var mapTasks = Game.GameNet!.ShipStatus?.Data.Tasks; + var taskType = (mapTasks != null && mapTasks.ContainsKey(taskTypeId)) ? mapTasks[taskTypeId] : null; + Tasks.Add(new TaskInfo(this, _eventManager, taskId++, taskType)); + } + } } } diff --git a/src/Impostor.Server/Net/Inner/Objects/TaskInfo.cs b/src/Impostor.Server/Net/Inner/Objects/TaskInfo.cs new file mode 100644 index 00000000..c04c27d9 --- /dev/null +++ b/src/Impostor.Server/Net/Inner/Objects/TaskInfo.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Innersloth.Maps; +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages.Rpcs; +using Impostor.Server.Events.Player; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal class TaskInfo : ITaskInfo + { + private readonly InnerPlayerInfo _playerInfo; + private readonly IEventManager _eventManager; + + public TaskInfo(InnerPlayerInfo playerInfo, IEventManager eventManager, uint id, TaskData? task) + { + _playerInfo = playerInfo; + _eventManager = eventManager; + Id = id; + Task = task; + } + + public uint Id { get; internal set; } + + public TaskData? Task { get; internal set; } + + public bool Complete { get; internal set; } + + public void Serialize(IMessageWriter writer) + { + writer.WritePacked(Id); + writer.Write(Complete); + } + + public void Deserialize(IMessageReader reader) + { + Id = reader.ReadPackedUInt32(); + Complete = reader.ReadBoolean(); + } + + public async ValueTask CompleteAsync() + { + if (_playerInfo.Controller == null) + { + throw new ImpostorException("Can't complete a task that doesn't have a player assigned"); + } + + var player = _playerInfo.Controller; + + if (Complete) + { + throw new ImpostorException("Can't complete a task that is already completed"); + } + + Complete = true; + + // Send RPC. + using var writer = player.Game.StartRpc(player.NetId, RpcCalls.CompleteTask); + Rpc01CompleteTask.Serialize(writer, Id); + await player.Game.FinishRpcAsync(writer); + + // Notify plugins. + await _eventManager.CallAsync(new PlayerCompletedTaskEvent(player.Game, player.Game.GetClientPlayer(player.OwnerId)!, player, this)); + } + } +} diff --git a/src/Impostor.Server/Net/State/Game.Data.cs b/src/Impostor.Server/Net/State/Game.Data.cs index a3ce69f2..4fdb3d2c 100644 --- a/src/Impostor.Server/Net/State/Game.Data.cs +++ b/src/Impostor.Server/Net/State/Game.Data.cs @@ -99,6 +99,8 @@ namespace Impostor.Server.Net.State { using var reader = parent.ReadMessage(); + _logger.LogTrace("Client {SenderId} sent GameData {Tag}", sender.Client.Id, reader.Tag); + switch (reader.Tag) { case GameDataTag.DataFlag: @@ -106,6 +108,7 @@ namespace Impostor.Server.Net.State var netId = reader.ReadPackedUInt32(); if (_allObjectsFast.TryGetValue(netId, out var obj)) { + _logger.LogTrace("Received Data for {NetId}, which is of type {Type}", netId, obj.GetType().Name); await obj.DeserializeAsync(sender, target, reader, false); } else @@ -121,7 +124,15 @@ namespace Impostor.Server.Net.State var netId = reader.ReadPackedUInt32(); if (_allObjectsFast.TryGetValue(netId, out var obj)) { - if (!await obj.HandleRpcAsync(sender, target, (RpcCalls)reader.ReadByte(), reader)) + var call = (RpcCalls)reader.ReadByte(); + _logger.LogTrace( + "Client {SenderId} called Rpc {Call} on NetId {CallerId} and sent it to {Target}", + sender.Client.Id, + call, + obj.NetId, + target?.Client.Id.ToString() ?? "everyone"); + + if (!await obj.HandleRpcAsync(sender, target, call, reader)) { parent.RemoveMessage(reader); continue; @@ -261,9 +272,10 @@ namespace Impostor.Server.Net.State if (scene != "OnlineGame") { _logger.LogWarning( - "Player {0} ({1}) tried to send SceneChangeFlag with disallowed scene.", + "Player {PlayerName} ({ClientId}) tried to send SceneChangeFlag with disallowed scene \"{Scene}\".", sender.Client.Name, - sender.Client.Id); + sender.Client.Id, + scene); return false; } @@ -510,6 +522,18 @@ namespace Impostor.Server.Net.State await SendObjectSpawnAsync(playerInfo); } + private async ValueTask DespawnPlayerInfoAsync(InnerPlayerInfo playerInfo) + { + if (playerInfo.OwnerId == ServerOwned) + { + _logger.LogDebug("Despawning PlayerInfo {nid}", playerInfo.NetId); + GameNet.GameData.RemovePlayer(playerInfo.PlayerId); + RemoveNetObject(playerInfo); + + await SendObjectDespawnAsync(playerInfo); + } + } + private bool AddNetObject(InnerNetObject obj) { if (_allObjectsFast.ContainsKey(obj.NetId)) diff --git a/src/Impostor.Server/Net/State/Game.Incoming.cs b/src/Impostor.Server/Net/State/Game.Incoming.cs index 3d637e4d..cce27c8d 100644 --- a/src/Impostor.Server/Net/State/Game.Incoming.cs +++ b/src/Impostor.Server/Net/State/Game.Incoming.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Impostor.Api.Games; @@ -43,6 +44,12 @@ namespace Impostor.Server.Net.State player.Value.Limbo = LimboStates.PreSpawn; } + // Delete all PlayerInfo objects + foreach (var playerInfo in GameNet.GameData.Players.Values.ToList()) + { + await DespawnPlayerInfoAsync(playerInfo); + } + await _eventManager.CallAsync(new GameEndedEvent(this, gameOverReason)); } diff --git a/src/Impostor.Server/Net/State/Game.Outgoing.cs b/src/Impostor.Server/Net/State/Game.Outgoing.cs index 1ab0086e..7e672547 100644 --- a/src/Impostor.Server/Net/State/Game.Outgoing.cs +++ b/src/Impostor.Server/Net/State/Game.Outgoing.cs @@ -134,5 +134,14 @@ namespace Impostor.Server.Net.State writer.EndMessage(); await FinishGameDataAsync(writer, targetClientId); } + + private ValueTask SendObjectDespawnAsync(InnerNetObject obj, int? targetClientId = null) + { + using var writer = StartGameData(targetClientId); + writer.StartMessage(GameDataTag.DespawnFlag); + writer.WritePacked(obj.NetId); + writer.EndMessage(); + return FinishGameDataAsync(writer, targetClientId); + } } } diff --git a/src/Impostor.Server/Net/State/Game.State.cs b/src/Impostor.Server/Net/State/Game.State.cs index e570c98f..fcd2c879 100644 --- a/src/Impostor.Server/Net/State/Game.State.cs +++ b/src/Impostor.Server/Net/State/Game.State.cs @@ -85,6 +85,13 @@ namespace Impostor.Server.Net.State } }); + // Clean up the PlayerInfo if we own it and we're still in the lobby + if (GameState == GameStates.NotStarted) + { + var playerInfo = GameNet.GameData.PlayersByClientId[playerId]; + await DespawnPlayerInfoAsync(playerInfo); + } + return true; }