Compare commits

..
Author SHA1 Message Date
miniduikboot bb980096ff Bump version to 1.10.2 2024-12-22 17:02:56 +01:00
miniduikbootandjs6pak 58a8bda8e7 Check for disconnection between handling messages
Without this, it is possible for clients to get the remainder of their
messages handled in a packet.

Co-Authored-By: js6pak <me@6pak.dev>
2024-12-22 17:02:56 +01:00
miniduikbootandjs6pak a7c37b6bcf Clean up abandoned games on disconnect
When a client disconnects without ever having joined a game, it leaves
behind an empty game on the server. Remove these when a player
disconnects.

Co-Authored-By: js6pak <me@6pak.dev>
2024-12-22 17:02:56 +01:00
miniduikbootandjs6pak 792c9626e4 Prevent clients from creating a second game
Legitimate clients will never create a second game using the same
connection.

Co-Authored-By: js6pak <me@6pak.dev>
2024-12-22 17:02:56 +01:00
miniduikboot e4f81b4a30 Bump to Among Us 2024.10.29 (#656)
Inspected mono, no networking changes found.

Ran dumpostor as well

Bumped Impostor version
2024-10-24 19:47:01 +02:00
danbudworthmead 2603f34b89 Bugfix/fix outfit RPCs (#653)
* implemented the sequence ID

* implemented the sequence ID for the player outfits

the sequence ID in the set call must be greater than that stored on the client otherwise the client disregards the set call.

* make sequence IDs internal

* added space after closing brace

* make the sequence ID addition +5
2024-10-05 12:44:08 +02:00
Niko233 9b27edbb11 Remove unused HandleSetInfected 2024-10-05 12:28:50 +02:00
danbudworthmead 2f9406cb6a updated the api call to use the setnamerpc serialise
the client now expects a byte which is the netid otherwise the setname call wont work
2024-10-05 12:28:16 +02:00
Niko233 d5f48aa798 Fix vanilla venting as Phantom cause a ban 2024-09-07 08:53:45 +04:00
js6pak ff5501aab7 Add support for 2024.9.4 2024-09-05 05:09:34 +02:00
js6pak acc4da1210 Fix RemovePlayer reason on internal disconnects 2024-08-24 16:52:50 +02:00
js6pak a0f0a65cbf Send DisconnectReason.Hacking to other players when disconnecting for cheating
Base game doesn't have a specific message for it so it results in a generic "left the game due to error" but it's still better than the default "left the game".
This also allows client side mods to fix the message.
2024-08-24 16:51:51 +02:00
js6pak 6c91280125 Fix cheating disconnect message
DisconnectReason.Hacking has been broken in base game for a long time so replace it with a custom message.
Also add a random support code for easier debugging in case of false positives.
2024-08-24 16:51:51 +02:00
miniduikboot 2ddbb8891c Refactor SetColor anticheat
One issue with the ColorLimits checks is that it didn't account for mods
that add extra colors, this is fixed by checking if the route followed
is plausible instead of mimicking client logic and only accepting a
single answer that it expected. A false negative is possible where the
host decides to wrap back to the front but there are still custom colors
available at the end of the list. This is acceptable because Impostor
doesn't currently know the amount of colors available and this issue
isn't major enough to ask all mod authors to declare their custom color
amount to the server.

As extra colors should now be handled by this code, it makes sense to
change the category of the unknown colors check to ProtocolExtensions.
For these mods an exemption of the ColorLimits category is no longer
necessary.

Note that mods that allow duplicate colors that do not use a custom rpc
to set colors can only be supported by disabling the ColorLimits
category, this was already the case and I don't think it can be unified
with the existing check.
2024-08-23 18:04:52 +02:00
Niko233 8c52b13af4 更新 Game.State.cs 2024-08-22 08:12:47 +02:00
miniduikboot f6318c7040 Merge pull request #609 from Impostor/feature/2024.6.18-prepare
Prepare for 2024.6.18 update
2024-08-20 21:50:45 +02:00
Niko233 4d6c060c7c Add handler for phantom rpcs 2024-08-20 21:41:38 +02:00
Niko233 83bf1c4368 Add Phantom Rpcs support 2024-08-20 21:41:38 +02:00
miniduikboot ed9f6e3134 Add Phantom to the list of Impostor roles 2024-08-20 21:41:38 +02:00
miniduikboot 219b72a3d1 Clean up owned playerinfo's when disconnecting
This commit does what official servers do:
- despawn the PlayerInfo if someone disconnects while in the lobby
- despawn all PlayerInfos at the end of a game when the EndGame screen
is shown. At this point a lot of other netobjects are destroyed as well,
with the PlayerInfos generally done first.
2024-08-20 21:41:38 +02:00
miniduikboot 01e9cccdf5 Set PlayerLevel in PlayerInfo on RPC SetLevel
Previously this used to be done by the host, but this is now the
server's responsibility.
2024-08-20 21:41:38 +02:00
miniduikbootandjs6pak fc79bd1536 Add Message24EndGameHostMigration serialization
Looks like Innersloth added this but hasn't actually used this. Weird..

When sent it makes the new host perform a full rejoin, which gums up
Impostor's spawning logic. Fixing that causes the client to gum itself
up. Sigh.

After some testing it seems like host migration still works fine, so
this investigation wasn't needed. Committing for posterity.

Co-Authored-By: js6pak <me@6pak.dev>
2024-08-20 21:41:38 +02:00
miniduikboot 9e1abe07ef Switch PlayerName writes to Outfit
In 2024.6.18 Innersloth changed the storage location of the PlayerName
from PlayerInfo to PlayerOutfit. If we don't set the PlayerName, we'll
send an incomplete PlayerInfo to new players, so new players are unable
to stay connected as they will not receive complete PlayerInfo objects
before a timeout in the client activates.

Because we publish PlayerName in the API, we can't completely remove
the PlayerName field from PlayerInfo. We also use it internally in a few
places, so I don't this is worth removing.
2024-08-20 21:41:38 +02:00
miniduikboot ccd36b6571 Sync over tasks
As tasks used to be part of the GameData, 2024.6.18 has moved them to
the PlayerInfo. Because they are now called on the PlayerInfo of the
correct player, the playerId field in the RPC is no longer necessary.
2024-08-20 21:41:38 +02:00
miniduikboot 44e9dfe28d Bump to 2024.8.13
Network side seems unchanged and officials allow crossplay
2024-08-20 21:41:38 +02:00
miniduikboot 09cadcb63b Spawn PlayerInfo's if host is not authoritive
This PlayerInfo also needs to be synced over if it is server owned
2024-08-20 21:41:38 +02:00
miniduikboot b118af12be Add NetId to SetName and SetColor
2024.6.18 adds a NetId field *in front* of the existing fields to help link back to the PlayerInfo. We don't actually need this NetId and assume it's called on the correct PlayerControl, so we ignore it.
2024-08-20 21:41:38 +02:00
miniduikboot 372ed8511f Set RoleWhenAlive when set to a Ghost role 2024-08-20 21:41:38 +02:00
miniduikboot 85296d2d46 Serialize Color -1 as 0 in PlayerOutfit
Official servers send 0 instead of -1 when the outfit is still
incomplete to prevent some client issues.
2024-08-20 21:41:38 +02:00
miniduikboot c43fdd2a69 Demote InnerGameData, promote InnerPlayerInfo
In 2024.6.18 Innersloth promoted PlayerInfo to its own network object, and gave it to the server. InnerGameData is now a container that holds these players. This commit implements the changes necessary to handle these separate PlayerInfo network objects.
2024-08-20 21:41:38 +02:00
miniduikboot 615469c6c8 Add dirty tracking to PlayerOutfit
To prevent excessive modifications in the codebase, which is prone to
issues if a callsite is missed, automatically do this using properties.

Keep the access modifiers intact for now.
2024-08-20 21:41:38 +02:00
miniduikboot 482b8c2b72 Add sequence id's to playeroutfit 2024-08-20 21:41:38 +02:00
miniduikboot 2eca9b4e9f Add canOverrideRole param to SetRole
This is currently unused by Innersloth
2024-08-20 21:41:38 +02:00
miniduikboot 079f58a7b7 Implement GameOptions V8
This adds a field for RulesPresets and SpecialGameModes, as well as
RoleOptions for the new roles.
2024-08-20 21:41:38 +02:00
miniduikboot c315ef9c28 VoteBanSystem now contains itself as a component
This makes it consistent with the other spawnables.
2024-08-20 21:41:38 +02:00
miniduikboot 0e7372d9d9 Update Dumpostor to v1.0.1 and run for 6.18
Add the RulesPresets and SpecialGamemodes enums
2024-08-20 21:41:38 +02:00
miniduikboot bf5a2c2be5 Prepare for 2024.6.18 update
Major changes are incoming, this doesn't yet work.
2024-08-20 21:41:38 +02:00
miniduikboot 066eca09ee Add scene param to forbidden scene log statement
I'm seeing multiple warnings in my server log about forbidden scene
changes. Unfortunately this warning does not include the actual scene
that was triggering the issue, which makes diagnosis/remediation difficult.

Additionally change this logging call to use names suitable for
structured logging.
2024-08-20 17:18:00 +02:00
34 changed files with 623 additions and 232 deletions

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>11</LangVersion>
<VersionPrefix>1.10.0</VersionPrefix>
<VersionPrefix>1.10.2</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<EnforceCodeStyleInBuild Condition="$(MSBuildProjectName)!='Hazel'">true</EnforceCodeStyleInBuild>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
@@ -59,15 +59,15 @@ namespace Impostor.Api.Innersloth.Customization
internal set => SetField(ref _namePlateId, value);
}
private byte HatSequenceId { get; set; } = 0;
internal byte HatSequenceId { get; set; } = 0;
private byte PetSequenceId { get; set; } = 0;
internal byte PetSequenceId { get; set; } = 0;
private byte SkinSequenceId { get; set; } = 0;
internal byte SkinSequenceId { get; set; } = 0;
private byte VisorSequenceId { get; set; } = 0;
internal byte VisorSequenceId { get; set; } = 0;
private byte NamePlateSequenceId { get; set; } = 0;
internal byte NamePlateSequenceId { get; set; } = 0;
/// <summary>
/// Gets a value indicating whether a player outfit is complete or whether it needs to receive additional cosmetics.
@@ -1439,6 +1439,9 @@
"CookFish": 3431,
"PrepVegetables": 3432,
"MushroomMixupName": 3433,
"EosAccountSilentMergeFail": 3434,
"AskForEosAccount": 3435,
"KwsCreateFail": 3436,
"GameType": 3500,
"GameTypeError": 3501,
"GameTypeClassic": 3502,
@@ -1479,5 +1482,15 @@
"QCLobbyRolePhantoms": 3537,
"QCSelfWasTracking": 3538,
"QCSelfGoingToNoisemaker": 3539,
"QCSelfSawRoleInLocation": 3540
"QCSelfSawRoleInLocation": 3540,
"QCCoreSettings": 3541,
"QCRolesGalore": 3542,
"QCPitchDark": 3543,
"QCFlashlight": 3544,
"QCPleaseWait": 3545,
"QCWaitMinute": 3546,
"QCLobbyClosing": 3547,
"QCChangeCosmetics": 3548,
"QCWaitingPlayers": 3549,
"QCLetsWaitPlayers": 3550
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"dumpostorVersion": "1.0.1",
"gameVersion": "2024.6.18",
"gameVersion": "2024.10.29",
"platformType": "StandaloneItch"
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ namespace Impostor.Api.Net
/// <summary>
/// Represents a connected game client.
/// </summary>
public interface IClient
public interface IClient : IEquatable<IClient>
{
/// <summary>
/// Gets or sets the unique ID of the client.
@@ -28,6 +28,7 @@ namespace Impostor.Api.Net.Messages
public const byte SetActivePodType = 21;
public const byte QueryPlatformIds = 22;
public const byte QueryLobbyInfo = 23;
public const byte EndGameHostMigration = 24;
private static readonly Dictionary<byte, string> FlagCache;
@@ -4,15 +4,13 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc29SetTasks
{
public static void Serialize(IMessageWriter writer, byte playerId, ReadOnlyMemory<byte> taskTypeIds)
public static void Serialize(IMessageWriter writer, ReadOnlyMemory<byte> taskTypeIds)
{
writer.Write(playerId);
writer.Write(taskTypeIds);
}
public static void Deserialize(IMessageReader reader, out byte playerId, out ReadOnlyMemory<byte> taskTypeIds)
public static void Deserialize(IMessageReader reader, out ReadOnlyMemory<byte> taskTypeIds)
{
playerId = reader.ReadByte();
taskTypeIds = reader.ReadBytesAndSize();
}
}
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc39SetHatStr
{
public static void Serialize(IMessageWriter writer, string hat)
public static void Serialize(IMessageWriter writer, string hat, byte nextRpcSequenceId)
{
writer.Write(hat);
writer.Write(nextRpcSequenceId);
}
public static void Deserialize(IMessageReader reader, out string hat)
public static void Deserialize(IMessageReader reader, out string hat, out byte nextRpcSequenceId)
{
hat = reader.ReadString();
nextRpcSequenceId = reader.ReadByte();
}
}
}
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc40SetSkinStr
{
public static void Serialize(IMessageWriter writer, string skin)
public static void Serialize(IMessageWriter writer, string skin, byte nextRpcSequenceId)
{
writer.Write(skin);
writer.Write(nextRpcSequenceId);
}
public static void Deserialize(IMessageReader reader, out string skin)
public static void Deserialize(IMessageReader reader, out string skin, out byte nextRpcSequenceId)
{
skin = reader.ReadString();
nextRpcSequenceId = reader.ReadByte();
}
}
}
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc41SetPetStr
{
public static void Serialize(IMessageWriter writer, string pet)
public static void Serialize(IMessageWriter writer, string pet, byte nextRpcSequenceId)
{
writer.Write(pet);
writer.Write(nextRpcSequenceId);
}
public static void Deserialize(IMessageReader reader, out string pet)
public static void Deserialize(IMessageReader reader, out string pet, out byte nextRpcSequenceId)
{
pet = reader.ReadString();
nextRpcSequenceId = reader.ReadByte();
}
}
}
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc42SetVisorStr
{
public static void Serialize(IMessageWriter writer, string visor)
public static void Serialize(IMessageWriter writer, string visor, byte nextRpcSequenceId)
{
writer.Write(visor);
writer.Write(nextRpcSequenceId);
}
public static void Deserialize(IMessageReader reader, out string visor)
public static void Deserialize(IMessageReader reader, out string visor, out byte nextRpcSequenceId)
{
visor = reader.ReadString();
nextRpcSequenceId = reader.ReadByte();
}
}
}
@@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc43SetNamePlateStr
{
public static void Serialize(IMessageWriter writer, string namePlate)
public static void Serialize(IMessageWriter writer, string namePlate, byte nextRpcSequenceId)
{
writer.Write(namePlate);
writer.Write(nextRpcSequenceId);
}
public static void Deserialize(IMessageReader reader, out string namePlate)
public static void Deserialize(IMessageReader reader, out string namePlate, out byte nextRpcSequenceId)
{
namePlate = reader.ReadString();
nextRpcSequenceId = reader.ReadByte();
}
}
}
@@ -0,0 +1,15 @@
namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc62CheckVanish
{
public static void Serialize(IMessageWriter writer, float maxDuration)
{
writer.Write(maxDuration);
}
public static void Deserialize(IMessageReader reader, out float maxDuration)
{
maxDuration = reader.ReadSingle();
}
}
}
@@ -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)
{
}
}
}
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -0,0 +1,21 @@
using Impostor.Api.Games;
namespace Impostor.Api.Net.Messages.S2C
{
public class Message24EndGameHostMigrationS2C
{
public static void Serialize(IMessageWriter writer, GameCode gameCode, int hostClientId)
{
writer.StartMessage(MessageFlags.EndGameHostMigration);
gameCode.Serialize(writer);
writer.Write(hostClientId);
writer.EndMessage();
}
public static void Deserialize(IMessageReader reader, out GameCode gameCode, out int hostClientId)
{
gameCode = reader.ReadInt32();
hostClientId = reader.ReadInt32();
}
}
}
+21 -5
View File
@@ -82,14 +82,27 @@ namespace Impostor.Server.Net
return false;
}
_logger.LogWarning("Client {Name} ({Id}) was caught cheating: [{Context}-{Category}] {Message}", Name, Id, context.Name, category, message);
var supportCode = Random.Shared.Next(0, 999_999).ToString("000-000");
if (_antiCheatConfig.BanIpFromGame)
_logger.LogWarning("Client {Name} ({Id}) was caught cheating: [{SupportCode}] [{Context}-{Category}] {Message}", Name, Id, supportCode, context.Name, category, message);
if (Player is { } player)
{
Player?.Game.BanIp(Connection.EndPoint.Address);
if (_antiCheatConfig.BanIpFromGame)
{
player.Game.BanIp(Connection.EndPoint.Address);
}
await player.Game.HandleRemovePlayer(Id, DisconnectReason.Hacking);
}
await DisconnectAsync(DisconnectReason.Hacking, context.Name + ": " + message);
var disconnectMessage =
$"""
You have been caught cheating and were {(_antiCheatConfig.BanIpFromGame ? "banned" : "kicked")} from the lobby.
For questions, contact your server admin and share the following code: {supportCode}.
""";
await DisconnectAsync(DisconnectReason.Custom, disconnectMessage);
return true;
}
@@ -348,7 +361,9 @@ namespace Impostor.Server.Net
{
if (Player != null)
{
await Player.Game.HandleRemovePlayer(Id, DisconnectReason.ExitGame);
// The client never sends over the real disconnect reason so we always assume ExitGame
var isRemote = reason == "The remote sent a disconnect request";
await Player.Game.HandleRemovePlayer(Id, isRemote ? DisconnectReason.ExitGame : DisconnectReason.Error);
}
}
catch (Exception ex)
@@ -358,6 +373,7 @@ namespace Impostor.Server.Net
_logger.LogInformation("Client {0} disconnecting, reason: {1}", Id, reason);
_clientManager.Remove(this);
await _gameManager.OnClientDisconnectAsync(this);
}
private bool IsPacketAllowed(IMessageReader message, bool hostOnly)
+15
View File
@@ -59,5 +59,20 @@ namespace Impostor.Server.Net
{
await Connection.CustomDisconnectAsync(reason, message);
}
public bool Equals(IClient? other)
{
return other != null && Id == other.Id;
}
public override bool Equals(object? obj)
{
return Equals(obj as ClientBase);
}
public override int GetHashCode()
{
return Id;
}
}
}
@@ -69,6 +69,11 @@ namespace Impostor.Server.Net.Hazel
{
await Client.HandleMessageAsync(message, e.Type);
}
if (!IsConnected)
{
break;
}
}
}
}
@@ -29,8 +29,6 @@ namespace Impostor.Server.Net.Inner
public SpawnFlags SpawnFlags { get; internal set; }
public virtual bool IsDirty { get; internal set; }
public bool IsOwnedBy(IClientPlayer player)
{
return OwnerId == player.Client.Id ||
@@ -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));
}
}
}
}
@@ -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();
}
}
}
}
@@ -22,9 +22,8 @@ namespace Impostor.Server.Net.Inner.Objects
{
PlayerInfo.CurrentOutfit.PlayerName = name;
// TODO check playerinfo ownership
using var writer = Game.StartRpc(NetId, RpcCalls.SetName);
writer.Write(name);
Rpc06SetName.Serialize(writer, PlayerInfo.NetId, name);
await Game.FinishRpcAsync(writer);
}
@@ -41,8 +40,8 @@ namespace Impostor.Server.Net.Inner.Objects
{
PlayerInfo.CurrentOutfit.HatId = hatId;
using var writer = Game.StartRpc(NetId, RpcCalls.SetHat);
Rpc39SetHatStr.Serialize(writer, hatId);
using var writer = Game.StartRpc(NetId, RpcCalls.SetHatStr);
Rpc39SetHatStr.Serialize(writer, hatId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetHatStr));
await Game.FinishRpcAsync(writer);
}
@@ -50,8 +49,8 @@ namespace Impostor.Server.Net.Inner.Objects
{
PlayerInfo.CurrentOutfit.PetId = petId;
using var writer = Game.StartRpc(NetId, RpcCalls.SetPet);
Rpc41SetPetStr.Serialize(writer, petId);
using var writer = Game.StartRpc(NetId, RpcCalls.SetPetStr);
Rpc41SetPetStr.Serialize(writer, petId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetPetStr));
await Game.FinishRpcAsync(writer);
}
@@ -59,8 +58,26 @@ namespace Impostor.Server.Net.Inner.Objects
{
PlayerInfo.CurrentOutfit.SkinId = skinId;
using var writer = Game.StartRpc(NetId, RpcCalls.SetSkin);
Rpc40SetSkinStr.Serialize(writer, skinId);
using var writer = Game.StartRpc(NetId, RpcCalls.SetSkinStr);
Rpc40SetSkinStr.Serialize(writer, skinId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetSkinStr));
await Game.FinishRpcAsync(writer);
}
public async ValueTask SetVisorAsync(string visorId)
{
PlayerInfo.CurrentOutfit.VisorId = visorId;
using var writer = Game.StartRpc(NetId, RpcCalls.SetVisorStr);
Rpc42SetVisorStr.Serialize(writer, visorId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetVisorStr));
await Game.FinishRpcAsync(writer);
}
public async ValueTask SetNamePlateAsync(string nameplateId)
{
PlayerInfo.CurrentOutfit.NamePlateId = nameplateId;
using var writer = Game.StartRpc(NetId, RpcCalls.SetNamePlateStr);
Rpc43SetNamePlateStr.Serialize(writer, nameplateId, PlayerInfo.GetNextRpcSequenceId(RpcCalls.SetNamePlateStr));
await Game.FinishRpcAsync(writer);
}
@@ -149,5 +166,19 @@ 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);
Rpc63StartVanish.Serialize(writer);
await Game.FinishRpcAsync(writer);
}
public async ValueTask StartAppearAsync(bool shouldAnimate)
{
using var writer = Game.StartRpc(NetId, RpcCalls.StartAppear);
Rpc65StartAppear.Serialize(writer, shouldAnimate);
await Game.FinishRpcAsync(writer);
}
}
}
@@ -203,8 +203,8 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc39SetHatStr.Deserialize(reader, out var hat);
return await HandleSetHat(sender, hat);
Rpc39SetHatStr.Deserialize(reader, out var hat, out var nextRpcSequenceId);
return await HandleSetHat(sender, hat, nextRpcSequenceId);
}
case RpcCalls.SetSkinStr:
@@ -215,8 +215,8 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc40SetSkinStr.Deserialize(reader, out var skin);
return await HandleSetSkin(sender, skin);
Rpc40SetSkinStr.Deserialize(reader, out var skin, out var nextRpcSequenceId);
return await HandleSetSkin(sender, skin, nextRpcSequenceId);
}
case RpcCalls.SetVisorStr:
@@ -227,8 +227,8 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc42SetVisorStr.Deserialize(reader, out var visor);
return await HandleSetVisor(sender, visor);
Rpc42SetVisorStr.Deserialize(reader, out var visor, out var nextRpcSequenceId);
return await HandleSetVisor(sender, visor, nextRpcSequenceId);
}
case RpcCalls.SetNamePlateStr:
@@ -238,8 +238,8 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc43SetNamePlateStr.Deserialize(reader, out var namePlate);
return await HandleSetNamePlate(sender, namePlate);
Rpc43SetNamePlateStr.Deserialize(reader, out var namePlate, out var nextRpcSequenceId);
return await HandleSetNamePlate(sender, namePlate, nextRpcSequenceId);
}
case RpcCalls.SetLevel:
@@ -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:
@@ -335,8 +335,8 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc41SetPetStr.Deserialize(reader, out var pet);
return await HandleSetPet(sender, pet);
Rpc41SetPetStr.Deserialize(reader, out var pet, out var nextRpcSequenceId);
return await HandleSetPet(sender, pet, nextRpcSequenceId);
}
case RpcCalls.SetStartCounter:
@@ -392,7 +392,6 @@ namespace Impostor.Server.Net.Inner.Objects
}
PlayerInfo.RoleType = role;
PlayerInfo.IsDirty = true;
if (Game.GameState == GameStates.Starting && Game.Players.All(clientPlayer => clientPlayer.Character?.PlayerInfo.RoleType != null))
{
@@ -512,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;
}
@@ -527,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);
}
@@ -538,7 +587,6 @@ namespace Impostor.Server.Net.Inner.Objects
{
PlayerInfo.IsDead = true;
PlayerInfo.LastDeathReason = reason;
PlayerInfo.IsDirty = true;
}
internal void Protect(InnerPlayerControl guardianAngel)
@@ -563,23 +611,6 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
private async ValueTask HandleSetInfected(ReadOnlyMemory<byte> infectedIds)
{
for (var i = 0; i < infectedIds.Length; i++)
{
var player = Game.GameNet.GameData!.GetPlayerById(infectedIds.Span[i]);
if (player != null)
{
// player.IsImpostor = true;
}
}
if (Game.GameState == GameStates.Starting)
{
await Game.StartedAsync();
}
}
private async ValueTask<bool> HandleCheckName(ClientPlayer sender, string name)
{
if (Game.GameState == GameStates.Started)
@@ -652,6 +683,7 @@ namespace Impostor.Server.Net.Inner.Objects
if (RequestedPlayerName.Any())
{
var expected = RequestedPlayerName.Dequeue();
var requested = expected;
if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.PlayerName == expected))
{
@@ -672,7 +704,10 @@ namespace Impostor.Server.Net.Inner.Objects
if (name != expected)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetName, CheatCategory.NameLimits, "Client sent SetName with incorrect name"))
if (await sender.Client.ReportCheatAsync(
RpcCalls.SetName,
CheatCategory.NameLimits,
$"Client sent SetName with incorrect name, got '{name}', requested '{requested}', expected '{expected}'"))
{
await SetNameAsync(expected);
return false;
@@ -703,9 +738,9 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
if ((byte)color > ColorsCount)
if ((byte)color >= ColorsCount)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckColor, CheatCategory.ColorLimits, "Client sent invalid color"))
if (await sender.Client.ReportCheatAsync(RpcCalls.CheckColor, CheatCategory.ProtocolExtension, "Client sent unknown color"))
{
return false;
}
@@ -726,9 +761,17 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
if ((byte)color >= ColorsCount)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ProtocolExtension, "Client sent unknown color"))
{
return false;
}
}
if (sender.IsOwner(this))
{
if (Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.CurrentOutfit.Color == color))
if (Game.IsColorUsed(color, this))
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ColorLimits, "Client sent a color that is already used"))
{
@@ -740,33 +783,54 @@ namespace Impostor.Server.Net.Inner.Objects
{
if (RequestedColorId.Any())
{
var expected = RequestedColorId.Dequeue();
var requested = RequestedColorId.Dequeue();
for (var colorOffset = 0; colorOffset <= ColorsCount; colorOffset++)
if (Game.IsColorUsed(color, this))
{
var possibleColor = (ColorType)((byte)(expected + colorOffset) % ColorsCount);
if (!Game.Players.Any(x => x.Character != null && x.Character != this && x.Character.PlayerInfo.CurrentOutfit.Color == possibleColor))
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ColorLimits, "Client selected a color that is already used"))
{
expected = possibleColor;
break;
}
if (colorOffset == ColorsCount)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ColorLimits, "Client sent SetColor but all colors are already in use"))
{
await SetColorAsync(expected);
return false;
}
return false;
}
}
if (color != expected)
var startFrom = requested;
// Among Us wraps to the front if all colors between the requested and the final color are in use, check for this
if (requested > color)
{
if (await sender.Client.ReportCheatAsync(RpcCalls.SetColor, CheatCategory.ColorLimits, "Client sent SetColor with incorrect color"))
// Among Us wrapped, so check all colors between requested and the final color
for (var c = requested; (byte)c < ColorsCount; c++)
{
await SetColorAsync(expected);
return false;
if (!Game.IsColorUsed(c, this))
{
if (await sender.Client.ReportCheatAsync(
RpcCalls.SetColor,
CheatCategory.ColorLimits,
$"Client skipped color {c} that could be used, but wrapped instead. Player requested {requested} but was given {color}"))
{
await SetColorAsync(c);
return false;
}
}
}
// Start checking from the first color, fallthrough to normal case
startFrom = ColorType.Red;
}
// Check all colors between the requested color and the assigned color
for (var c = startFrom; c < color; c++)
{
if (!Game.IsColorUsed(c, this))
{
if (await sender.Client.ReportCheatAsync(
RpcCalls.SetColor,
CheatCategory.ColorLimits,
$"Client skipped color {c} that could be used. Player requested {requested} but was given {color}"))
{
await SetColorAsync(c);
return false;
}
}
}
}
@@ -784,54 +848,71 @@ namespace Impostor.Server.Net.Inner.Objects
return true;
}
private async ValueTask<bool> HandleSetHat(ClientPlayer sender, string hat)
private async ValueTask<bool> HandleSetHat(ClientPlayer sender, string hat, byte nextRpcSequenceId)
{
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetHat, CheatCategory.GameFlow, "Client tried to change hat while not in lobby"))
await sender.Client.ReportCheatAsync(RpcCalls.SetHatStr, CheatCategory.GameFlow, "Client tried to change hat while not in lobby"))
{
return false;
}
PlayerInfo.CurrentOutfit.HatId = hat;
PlayerInfo.CurrentOutfit.HatSequenceId = nextRpcSequenceId;
return true;
}
private async ValueTask<bool> HandleSetSkin(ClientPlayer sender, string skin)
private async ValueTask<bool> HandleSetSkin(ClientPlayer sender, string skin, byte nextRpcSequenceId)
{
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetSkin, CheatCategory.GameFlow, "Client tried to change skin while not in lobby"))
await sender.Client.ReportCheatAsync(RpcCalls.SetSkinStr, CheatCategory.GameFlow, "Client tried to change skin while not in lobby"))
{
return false;
}
PlayerInfo.CurrentOutfit.SkinId = skin;
PlayerInfo.CurrentOutfit.SkinSequenceId = nextRpcSequenceId;
return true;
}
private async ValueTask<bool> HandleSetVisor(ClientPlayer sender, string visor)
private async ValueTask<bool> HandleSetVisor(ClientPlayer sender, string visor, byte nextRpcSequenceId)
{
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetVisor, CheatCategory.GameFlow, "Client tried to change visor while not in lobby"))
await sender.Client.ReportCheatAsync(RpcCalls.SetVisorStr, CheatCategory.GameFlow, "Client tried to change visor while not in lobby"))
{
return false;
}
PlayerInfo.CurrentOutfit.VisorId = visor;
PlayerInfo.CurrentOutfit.VisorSequenceId = nextRpcSequenceId;
return true;
}
private async ValueTask<bool> HandleSetNamePlate(ClientPlayer sender, string skin)
private async ValueTask<bool> HandleSetNamePlate(ClientPlayer sender, string namePlate, byte nextRpcSequenceId)
{
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetNamePlate, CheatCategory.GameFlow, "Client tried to change skin while not in lobby"))
await sender.Client.ReportCheatAsync(RpcCalls.SetNamePlateStr, CheatCategory.GameFlow, "Client tried to change skin while not in lobby"))
{
return false;
}
PlayerInfo.CurrentOutfit.NamePlateId = skin;
PlayerInfo.CurrentOutfit.NamePlateId = namePlate;
PlayerInfo.CurrentOutfit.NamePlateSequenceId = nextRpcSequenceId;
return true;
}
private async ValueTask<bool> 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;
}
@@ -958,7 +1039,7 @@ namespace Impostor.Server.Net.Inner.Objects
if (!await ValidateRole(RpcCalls.ProtectPlayer, sender, PlayerInfo, RoleTypes.GuardianAngel))
{
return false;
return false;
}
((InnerPlayerControl)target).Protect(this);
@@ -980,15 +1061,16 @@ namespace Impostor.Server.Net.Inner.Objects
await _eventManager.CallAsync(new PlayerStartMeetingEvent(Game, Game.GetClientPlayer(this.OwnerId)!, this, deadPlayer));
}
private async ValueTask<bool> HandleSetPet(ClientPlayer sender, string pet)
private async ValueTask<bool> HandleSetPet(ClientPlayer sender, string pet, byte nextRpcSequenceId)
{
if (Game.GameState == GameStates.Started &&
await sender.Client.ReportCheatAsync(RpcCalls.SetPet, CheatCategory.GameFlow, "Client tried to change pet while not in lobby"))
await sender.Client.ReportCheatAsync(RpcCalls.SetPetStr, CheatCategory.GameFlow, "Client tried to change pet while not in lobby"))
{
return false;
}
PlayerInfo.CurrentOutfit.PetId = pet;
PlayerInfo.CurrentOutfit.PetSequenceId = nextRpcSequenceId;
return true;
}
@@ -1010,5 +1092,61 @@ namespace Impostor.Server.Net.Inner.Objects
return true;
}
private async ValueTask<bool> HandleCheckVanish(ClientPlayer sender, float maxDuration)
{
// TODO: Check if operation is taking place during lobby/meetings
// TODO: Check that the player unvanished before the maximum duration is reached
// 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<bool> 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<bool> 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<bool> 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;
}
}
}
@@ -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<InnerPlayerInfo> _logger;
public InnerPlayerInfo(ICustomMessageManager<ICustomRpc> customMessageManager, Game game, ILogger<InnerPlayerInfo> logger) : base(customMessageManager, game)
public InnerPlayerInfo(ICustomMessageManager<ICustomRpc> customMessageManager, IEventManager eventManager, Game game, ILogger<InnerPlayerInfo> logger) : base(customMessageManager, game)
{
Components.Add(this);
_eventManager = eventManager;
_logger = logger;
}
@@ -46,26 +51,20 @@ 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;
public bool CanVent => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom or RoleTypes.Engineer;
public bool IsDead { get; internal set; }
public DeathReason LastDeathReason { get; internal set; }
public List<InnerGameData.TaskInfo> Tasks { get; internal set; } = new List<InnerGameData.TaskInfo>(0);
public List<TaskInfo> Tasks { get; internal set; } = new List<TaskInfo>(0);
public DateTimeOffset LastMurder { get; set; }
public uint PlayerLevel { get; internal set; }
public override bool IsDirty
{
get => CurrentOutfit.IsDirty;
internal set => CurrentOutfit.IsDirty = value;
}
public bool CanMurder(IGame game, IDateTimeProvider dateTimeProvider)
{
if (!IsImpostor)
@@ -182,5 +181,80 @@ namespace Impostor.Server.Net.Inner.Objects
return ValueTask.CompletedTask;
}
public override async ValueTask<bool> 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<byte> taskTypeIds)
{
if (Disconnected)
{
return;
}
Tasks = new List<TaskInfo>(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));
}
}
public byte GetNextRpcSequenceId(RpcCalls rpc)
{
var defaultOutfit = this.Outfits[PlayerOutfitType.Default];
byte nextRpcSequenceId = 0;
const byte SequenceIdAddition = 5;
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (rpc)
{
case RpcCalls.SetHatStr:
defaultOutfit.HatSequenceId += SequenceIdAddition;
nextRpcSequenceId = defaultOutfit.HatSequenceId;
break;
case RpcCalls.SetSkinStr:
defaultOutfit.SkinSequenceId += SequenceIdAddition;
nextRpcSequenceId = defaultOutfit.SkinSequenceId;
break;
case RpcCalls.SetPetStr:
defaultOutfit.PetSequenceId += SequenceIdAddition;
nextRpcSequenceId = defaultOutfit.PetSequenceId;
break;
case RpcCalls.SetVisorStr:
defaultOutfit.VisorSequenceId += SequenceIdAddition;
nextRpcSequenceId = defaultOutfit.VisorSequenceId;
break;
case RpcCalls.SetNamePlateStr:
defaultOutfit.NamePlateSequenceId += SequenceIdAddition;
nextRpcSequenceId = defaultOutfit.NamePlateSequenceId;
break;
default:
_logger.LogError(
"{NextRpcSequenceIdName} was called for an RPC it doesn\'t know how to handle: {Rpc}",
nameof(GetNextRpcSequenceId),
rpc);
break;
}
return nextRpcSequenceId;
}
}
}
@@ -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));
}
}
}
@@ -19,6 +19,8 @@ internal class CompatibilityManager : ICompatibilityManager
{
new GameVersion(2024, 3, 1), // 2024.6.18
new GameVersion(2024, 4, 1), // 2024.8.13
new GameVersion(2024, 4, 2), // 2024.9.4
new GameVersion(2024, 8, 10), // 2024.10.29
},
};
@@ -31,6 +31,7 @@ namespace Impostor.Server.Net.Manager
private readonly IEventManager _eventManager;
private readonly IGameCodeFactory _gameCodeFactory;
private readonly ICompatibilityManager _compatibilityManager;
private readonly ConcurrentDictionary<IClient, Game?> _gamesCreatedBy;
public GameManager(
ILogger<GameManager> logger,
@@ -49,6 +50,7 @@ namespace Impostor.Server.Net.Manager
_games = new ConcurrentDictionary<int, Game>();
_compatibilityConfig = compatibilityConfig.Value;
_compatibilityManager = compatibilityManager;
_gamesCreatedBy = new ConcurrentDictionary<IClient, Game?>();
}
IEnumerable<IGame> IGameManager.Games => _games.Select(kv => kv.Value);
@@ -85,6 +87,12 @@ namespace Impostor.Server.Net.Manager
public async ValueTask<IGame?> CreateAsync(IClient? owner, IGameOptions options, GameFilterOptions filterOptions)
{
if (owner != null && !_gamesCreatedBy.TryAdd(owner, null))
{
_logger.LogWarning("Connection {Name}({ClientId}) has tried to create a second game, blocked", owner.Name, owner.Id);
return null;
}
var @event = new GameCreationEvent(this, owner);
await _eventManager.CallAsync(@event);
@@ -100,6 +108,11 @@ namespace Impostor.Server.Net.Manager
(success, game) = await TryCreateAsync(options, filterOptions, owner);
}
if (owner != null)
{
_gamesCreatedBy[owner] = game;
}
if (!success || game == null)
{
throw new ImpostorException("Could not create new game"); // TODO: Fix generic exception.
@@ -129,5 +142,14 @@ namespace Impostor.Server.Net.Manager
return (true, game);
}
internal async ValueTask OnClientDisconnectAsync(IClient client)
{
if (_gamesCreatedBy.TryRemove(client, out var game) && game is { PlayerCount: 0, GameState: not GameStates.Destroyed })
{
_logger.LogWarning("Client {Name}({ClientId}) left empty game open when disconnecting", client.Name, client.Id);
await RemoveAsync(game.Code);
}
}
}
}
+15 -13
View File
@@ -261,9 +261,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;
}
@@ -326,17 +327,6 @@ namespace Impostor.Server.Net.State
}
}
// Check for dirty netobjects
foreach (var netObject in _allObjectsFast.Values)
{
if (netObject.IsDirty && netObject.OwnerId == ServerOwned)
{
_logger.LogTrace("Sending over {Type} {NetId}", netObject.GetType().Name, netObject.NetId);
await SendObjectData(netObject);
netObject.IsDirty = false;
}
}
return true;
}
@@ -521,6 +511,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))
@@ -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.ToArray())
{
await DespawnPlayerInfoAsync(playerInfo);
}
await _eventManager.CallAsync(new GameEndedEvent(this, gameOverReason));
}
+1 -11
View File
@@ -135,22 +135,12 @@ namespace Impostor.Server.Net.State
await FinishGameDataAsync(writer, targetClientId);
}
private ValueTask SendObjectDespawn(InnerNetObject obj, int? targetClientId = null)
private async 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);
}
private async ValueTask SendObjectData(InnerNetObject obj, int? targetClientId = null)
{
using var writer = StartGameData(targetClientId);
writer.StartMessage(GameDataTag.DataFlag);
writer.WritePacked(obj.NetId);
await obj.SerializeAsync(writer, false);
writer.EndMessage();
await FinishGameDataAsync(writer, targetClientId);
}
}
+4 -13
View File
@@ -85,21 +85,12 @@ namespace Impostor.Server.Net.State
}
});
// Clean up the PlayerInfo if we own it
foreach (var playerInfo in GameNet.GameData.Players.Values)
// Clean up the PlayerInfo if we own it and we're still in the lobby
if (GameState == GameStates.NotStarted)
{
if (playerInfo.ClientId == playerId)
if (GameNet.GameData.PlayersByClientId.TryGetValue(playerId, out var playerInfo))
{
if (playerInfo.OwnerId == ServerOwned)
{
_logger.LogInformation("Destroying PlayerInfo {nid}", playerInfo.NetId);
GameNet.GameData.RemovePlayer(playerInfo.PlayerId);
RemoveNetObject(playerInfo);
await SendObjectDespawn(playerInfo);
}
break;
await DespawnPlayerInfoAsync(playerInfo);
}
}
+11
View File
@@ -9,8 +9,10 @@ using Impostor.Api.Config;
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.Inner.Objects;
using Impostor.Api.Net.Manager;
using Impostor.Api.Net.Messages.S2C;
using Impostor.Server.Events;
@@ -131,6 +133,15 @@ namespace Impostor.Server.Net.State
}
}
/// <summary>Check if there are players using a color.</summary>
/// <param name="color">The color to check for.</param>
/// <param name="exceptBy">Exempt a player from being checked.</param>
/// <returns>True if there is player other than exceptBy that uses that color.</returns>
internal bool IsColorUsed(ColorType color, IInnerPlayerControl? exceptBy = null)
{
return Players.Any(p => p.Character != null && p.Character != exceptBy && p.Character.PlayerInfo.CurrentOutfit.Color == color);
}
private ValueTask BroadcastJoinMessage(IMessageWriter message, bool clear, ClientPlayer player)
{
Message01JoinGameS2C.SerializeJoin(message, clear, Code, player, HostId);