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