From f23dd8aa2568e169586a4e9faa623b46c66ee2ee Mon Sep 17 00:00:00 2001 From: linmeideli <2558527373@qq.com> Date: Wed, 19 Aug 2026 11:57:47 +0800 Subject: [PATCH] Support Judge and new vote --- .../Events/Game/Meeting/IMeetingEndedEvent.cs | 12 +- .../Net/Inner/Objects/IInnerMeetingHud.cs | 25 +++- .../Net/Messages/Rpcs/Rpc23VotingComplete.cs | 17 ++- .../Messages/Rpcs/Rpc66QueueOverruleVotes.cs | 19 +++ .../Events/Game/Meeting/MeetingEndedEvent.cs | 10 +- .../Net/Inner/Objects/InnerMeetingHud.Api.cs | 4 +- .../Objects/InnerMeetingHud.JudgeOverrule.cs | 29 ++++ .../Net/Inner/Objects/InnerMeetingHud.cs | 138 +++++++++++++++--- 8 files changed, 228 insertions(+), 26 deletions(-) create mode 100644 src/Impostor.Api/Net/Messages/Rpcs/Rpc66QueueOverruleVotes.cs create mode 100644 src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.JudgeOverrule.cs diff --git a/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs b/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs index 7787be6..5231f9e 100644 --- a/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs +++ b/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs @@ -1,4 +1,4 @@ -using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Inner.Objects; namespace Impostor.Api.Events.Meeting { @@ -13,5 +13,15 @@ namespace Impostor.Api.Events.Meeting /// Gets a value indicating whether meeting result is a tie. /// bool IsTie { get; } + + /// + /// Gets a value indicating whether a Judge overruled the vote result. + /// + bool WasOverruled { get; } + + /// + /// Gets the nonce of the winning Judge overrule, or 0 if the meeting wasn't overruled. + /// + ushort OverrideId { get; } } } diff --git a/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs b/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs index b6c781f..99f1ce1 100644 --- a/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs +++ b/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Impostor.Api.Events.Player; @@ -6,6 +6,24 @@ namespace Impostor.Api.Net.Inner.Objects { public interface IInnerMeetingHud : IInnerNetObject { + public interface IJudgeOverrule + { + /// + /// Gets the player id of the Judge that queued this overrule. + /// + byte JudgePlayerId { get; } + + /// + /// Gets the player id of the player the Judge wants to overrule. + /// + byte OverruledPlayerId { get; } + + /// + /// Gets the overrule nonce used to match this overrule to the voting result. + /// + ushort OverruleNonce { get; } + } + public interface IPlayerVoteArea { /// @@ -55,5 +73,10 @@ namespace Impostor.Api.Net.Inner.Objects /// Gets the player that started the meeting. /// IInnerPlayerInfo? Reporter { get; } + + /// + /// Gets the Judge overrules queued in this meeting. + /// + IReadOnlyCollection JudgeOverrules { get; } } } diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc23VotingComplete.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc23VotingComplete.cs index 6a05e41..a004bb5 100644 --- a/src/Impostor.Api/Net/Messages/Rpcs/Rpc23VotingComplete.cs +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc23VotingComplete.cs @@ -2,14 +2,16 @@ namespace Impostor.Api.Net.Messages.Rpcs { public static class Rpc23VotingComplete { - public static void Serialize(IMessageWriter writer, byte[] states, byte playerId, bool tie) + public static void Serialize(IMessageWriter writer, byte[] states, byte playerId, bool tie, bool wasOverruled, ushort overrideId) { writer.WriteBytesAndSize(states); writer.Write(playerId); writer.Write(tie); + writer.Write(wasOverruled); + writer.Write(overrideId); } - public static void Deserialize(IMessageReader reader, out IMessageReader[] states, out byte playerId, out bool tie) + public static void Deserialize(IMessageReader reader, bool hasOverruleFields, out IMessageReader[] states, out byte playerId, out bool tie, out bool wasOverruled, out ushort overrideId) { var length = reader.ReadPackedInt32(); states = new IMessageReader[length]; @@ -20,6 +22,17 @@ namespace Impostor.Api.Net.Messages.Rpcs playerId = reader.ReadByte(); tie = reader.ReadBoolean(); + + if (hasOverruleFields) + { + wasOverruled = reader.ReadBoolean(); + overrideId = reader.ReadUInt16(); + } + else + { + wasOverruled = false; + overrideId = 0; + } } } } diff --git a/src/Impostor.Api/Net/Messages/Rpcs/Rpc66QueueOverruleVotes.cs b/src/Impostor.Api/Net/Messages/Rpcs/Rpc66QueueOverruleVotes.cs new file mode 100644 index 0000000..e3210d5 --- /dev/null +++ b/src/Impostor.Api/Net/Messages/Rpcs/Rpc66QueueOverruleVotes.cs @@ -0,0 +1,19 @@ +namespace Impostor.Api.Net.Messages.Rpcs +{ + public static class Rpc66QueueOverruleVotes + { + public static void Serialize(IMessageWriter writer, byte judgePlayerId, byte targetPlayerId, ushort overruleNonce) + { + writer.Write(judgePlayerId); + writer.Write(targetPlayerId); + writer.Write(overruleNonce); + } + + public static void Deserialize(IMessageReader reader, out byte judgePlayerId, out byte targetPlayerId, out ushort overruleNonce) + { + judgePlayerId = reader.ReadByte(); + targetPlayerId = reader.ReadByte(); + overruleNonce = reader.ReadUInt16(); + } + } +} diff --git a/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs b/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs index b64699f..b536129 100644 --- a/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs +++ b/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs @@ -1,4 +1,4 @@ -using Impostor.Api.Events.Meeting; +using Impostor.Api.Events.Meeting; using Impostor.Api.Games; using Impostor.Api.Net.Inner.Objects; @@ -6,12 +6,14 @@ namespace Impostor.Server.Events.Meeting { public class MeetingEndedEvent : IMeetingEndedEvent { - public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud, IInnerPlayerControl? exiled, bool isTie) + public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud, IInnerPlayerControl? exiled, bool isTie, bool wasOverruled = false, ushort overrideId = 0) { Game = game; MeetingHud = meetingHud; Exiled = exiled; IsTie = isTie; + WasOverruled = wasOverruled; + OverrideId = overrideId; } public IGame Game { get; } @@ -21,5 +23,9 @@ namespace Impostor.Server.Events.Meeting public IInnerPlayerControl? Exiled { get; } public bool IsTie { get; } + + public bool WasOverruled { get; } + + public ushort OverrideId { get; } } } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs index 8b1528d..522945a 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Impostor.Api.Net.Inner.Objects; @@ -9,5 +9,7 @@ namespace Impostor.Server.Net.Inner.Objects IReadOnlyCollection IInnerMeetingHud.PlayerStates => Array.AsReadOnly(_playerStates); IInnerPlayerInfo? IInnerMeetingHud.Reporter => Reporter; + + IReadOnlyCollection IInnerMeetingHud.JudgeOverrules => JudgeOverrules; } } diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.JudgeOverrule.cs b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.JudgeOverrule.cs new file mode 100644 index 0000000..3de73dc --- /dev/null +++ b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.JudgeOverrule.cs @@ -0,0 +1,29 @@ +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerMeetingHud + { + public sealed class JudgeOverrule : IInnerMeetingHud.IJudgeOverrule + { + public JudgeOverrule(byte judgePlayerId, byte overruledPlayerId, ushort overruleNonce) + { + JudgePlayerId = judgePlayerId; + OverruledPlayerId = overruledPlayerId; + OverruleNonce = overruleNonce; + } + + public byte JudgePlayerId { get; set; } + + public byte OverruledPlayerId { get; set; } + + public ushort OverruleNonce { get; set; } + + public static JudgeOverrule Deserialize(IMessageReader reader) + { + var inner = reader.ReadMessage(); + return new JudgeOverrule(inner.Tag, inner.ReadByte(), inner.ReadUInt16()); + } + } + } +} diff --git a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs index 515978d..d8330d0 100644 --- a/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs +++ b/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs @@ -12,6 +12,7 @@ using Impostor.Api.Innersloth.GameOptions; using Impostor.Api.Net; using Impostor.Api.Net.Custom; using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Inner.Objects; using Impostor.Api.Net.Messages.Rpcs; using Impostor.Server.Events.Meeting; using Impostor.Server.Events.Player; @@ -26,8 +27,8 @@ namespace Impostor.Server.Net.Inner.Objects private readonly ILogger _logger; private readonly IEventManager _eventManager; - private readonly CancellationTokenSource _timerToken; + private readonly List _judgeOverrules = new(); [AllowNull] private PlayerVoteArea[] _playerStates; @@ -67,6 +68,8 @@ namespace Impostor.Server.Net.Inner.Objects public InnerPlayerInfo? Reporter { get; private set; } + public IReadOnlyCollection JudgeOverrules => _judgeOverrules; + public override ValueTask SerializeAsync(IMessageWriter writer, bool initialState) { throw new NotImplementedException(); @@ -114,11 +117,13 @@ namespace Impostor.Server.Net.Inner.Objects if (sender.Client.GameVersion >= JudgeMinVersion) { var overruleQueueListLength = reader.ReadPackedInt32(); - for (var i = 0; i < overruleQueueListLength; i++) + if (overruleQueueListLength > 0) { - var msg = reader.ReadMessage(); - - // TODO parse and handle JudgeOverrule + _judgeOverrules.Clear(); + for (var i = 0; i < overruleQueueListLength; i++) + { + _judgeOverrules.Add(JudgeOverrule.Deserialize(reader)); + } } } } @@ -135,6 +140,7 @@ namespace Impostor.Server.Net.Inner.Objects } Rpc22Close.Deserialize(reader); + _judgeOverrules.Clear(); break; } @@ -145,7 +151,8 @@ namespace Impostor.Server.Net.Inner.Objects return false; } - Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie); + var hasOverruleFields = sender.Client.GameVersion >= JudgeMinVersion; + Rpc23VotingComplete.Deserialize(reader, hasOverruleFields, out var states, out var playerId, out var tie, out var wasOverruled, out var overrideId); foreach (var messageReader in states) { messageReader.Dispose(); @@ -176,18 +183,8 @@ namespace Impostor.Server.Net.Inner.Objects case RpcCalls.QueueOverruleVotes: { - if (!await ValidateRole(call, sender, sender.Character!.PlayerInfo, RoleTypes.Judge) || - !await ValidateTarget(call, sender, target) || - !await ValidateHost(call, target!)) - { - return false; - } - - var judgePlayerId = reader.ReadByte(); - var targetPlayerId = reader.ReadByte(); - var overruleNonce = reader.ReadUInt16(); - - break; + Rpc66QueueOverruleVotes.Deserialize(reader, out var judgePlayerId, out var targetPlayerId, out var overruleNonce); + return await HandleQueueOverruleVotesAsync(sender, target, judgePlayerId, targetPlayerId, overruleNonce); } default: @@ -250,6 +247,87 @@ namespace Impostor.Server.Net.Inner.Objects return true; } + private async ValueTask HandleQueueOverruleVotesAsync(ClientPlayer sender, ClientPlayer? target, byte judgePlayerId, byte targetPlayerId, ushort overruleNonce) + { + if (!await ValidateRole(RpcCalls.QueueOverruleVotes, sender, sender.Character?.PlayerInfo, RoleTypes.Judge) || + !await ValidateTarget(RpcCalls.QueueOverruleVotes, sender, target) || + !await ValidateHost(RpcCalls.QueueOverruleVotes, target!)) + { + return false; + } + + // A Judge can only queue an overrule for themself. + if (judgePlayerId != sender.Character!.PlayerId) + { + if (await sender.Client.ReportCheatAsync(RpcCalls.QueueOverruleVotes, CheatCategory.Ownership, "Client sent a Judge overrule for another player")) + { + return false; + } + } + + // The client only generates nonces in the 1..65535 range. + if (overruleNonce == 0) + { + if (await sender.Client.ReportCheatAsync(RpcCalls.QueueOverruleVotes, CheatCategory.GameFlow, "Client sent a Judge overrule with an uninitialized nonce")) + { + return false; + } + } + + // A Judge can only overrule once per meeting. + if (_judgeOverrules.Any(overrule => overrule.JudgePlayerId == judgePlayerId)) + { + if (await sender.Client.ReportCheatAsync(RpcCalls.QueueOverruleVotes, CheatCategory.GameFlow, "Client sent more than one Judge overrule in the same meeting")) + { + return false; + } + } + + // The overruled player must exist. + if (Game.GameNet.GameData!.GetPlayerById(targetPlayerId) == null) + { + if (await sender.Client.ReportCheatAsync(RpcCalls.QueueOverruleVotes, CheatCategory.InvalidObject, "Client sent a Judge overrule for an unknown player")) + { + return false; + } + } + + AddOrUpdateJudgeOverrule(judgePlayerId, targetPlayerId, overruleNonce); + + return true; + } + + private void AddOrUpdateJudgeOverrule(byte judgePlayerId, byte targetPlayerId, ushort overruleNonce) + { + var existing = _judgeOverrules.Find(overrule => overrule.JudgePlayerId == judgePlayerId); + if (existing != null) + { + existing.OverruledPlayerId = targetPlayerId; + existing.OverruleNonce = overruleNonce; + } + else + { + _judgeOverrules.Add(new JudgeOverrule(judgePlayerId, targetPlayerId, overruleNonce)); + } + } + + private JudgeOverrule? GetWinningOverrule() + { + var gameData = Game.GameNet.GameData!; + foreach (var overrule in _judgeOverrules) + { + var judge = gameData.GetPlayerById(overrule.JudgePlayerId); + var target = gameData.GetPlayerById(overrule.OverruledPlayerId); + + if (judge != null && target != null && !judge.Disconnected && !target.Disconnected) + { + return overrule; + } + } + + return null; + } + private async ValueTask CheckForEndVotingAsync() { if (_playerStates.All(ps => ps.IsDead || ps.DidVote)) @@ -315,6 +393,28 @@ namespace Impostor.Server.Net.Inner.Objects var self = this.CalculateVotes(); var max = MaxPair(self, out var tie); var exiled = tie ? null : Game.GameNet.GameData!.GetPlayerById(max.Key)?.Controller; + + // exile or others? + var wasOverruled = false; + ushort overrideId = 0; + + var winningOverrule = GetWinningOverrule(); + if (winningOverrule != null) + { + wasOverruled = true; + overrideId = winningOverrule.OverruleNonce; + + var overruled = Game.GameNet.GameData!.GetPlayerById(winningOverrule.OverruledPlayerId); + if (overruled != null && overruled.IsImpostor) + { + exiled = overruled.Controller; + } + else + { + var judge = Game.GameNet.GameData!.GetPlayerById(winningOverrule.JudgePlayerId); + exiled = judge?.Controller; + } + } if (exiled != null && exiled.PlayerInfo != null) { @@ -322,7 +422,7 @@ namespace Impostor.Server.Net.Inner.Objects await _eventManager.CallAsync(new PlayerExileEvent(Game, Game.GetClientPlayer(exiled!.OwnerId)!, exiled)); } - await _eventManager.CallAsync(new MeetingEndedEvent(Game, this, exiled, tie)); + await _eventManager.CallAsync(new MeetingEndedEvent(Game, this, exiled, tie, wasOverruled, overrideId)); } } } -- 2.39.5