]> git.deb.at Git - rhonda/impostor.git/commitdiff
Support Judge and new vote
authorlinmeideli <2558527373@qq.com>
Wed, 19 Aug 2026 03:57:47 +0000 (11:57 +0800)
committerminiduikboot <mini@duikbo.at>
Sun, 30 Aug 2026 16:48:37 +0000 (18:48 +0200)
src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs
src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs
src/Impostor.Api/Net/Messages/Rpcs/Rpc23VotingComplete.cs
src/Impostor.Api/Net/Messages/Rpcs/Rpc66QueueOverruleVotes.cs [new file with mode: 0644]
src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.JudgeOverrule.cs [new file with mode: 0644]
src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs

index 7787be69ff248939ed6ea88ab9c7a556b5c2d51a..5231f9e8a1c86a4e4bf470fcd88446f0cbf0207b 100644 (file)
@@ -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.
         /// </summary>
         bool IsTie { get; }
+
+        /// <summary>
+        ///     Gets a value indicating whether a Judge overruled the vote result.
+        /// </summary>
+        bool WasOverruled { get; }
+
+        /// <summary>
+        ///     Gets the nonce of the winning Judge overrule, or 0 if the meeting wasn't overruled.
+        /// </summary>
+        ushort OverrideId { get; }
     }
 }
index b6c781fc3cd4d0249deeeaf3918b17e79f63635d..99f1ce1fbea871de9d21c1eb623e35704ccc9529 100644 (file)
@@ -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
+        {
+            /// <summary>
+            ///     Gets the player id of the Judge that queued this overrule.
+            /// </summary>
+            byte JudgePlayerId { get; }
+
+            /// <summary>
+            ///     Gets the player id of the player the Judge wants to overrule.
+            /// </summary>
+            byte OverruledPlayerId { get; }
+
+            /// <summary>
+            ///     Gets the overrule nonce used to match this overrule to the voting result.
+            /// </summary>
+            ushort OverruleNonce { get; }
+        }
+
         public interface IPlayerVoteArea
         {
             /// <summary>
@@ -55,5 +73,10 @@ namespace Impostor.Api.Net.Inner.Objects
         ///     Gets the player that started the meeting.
         /// </summary>
         IInnerPlayerInfo? Reporter { get; }
+
+        /// <summary>
+        ///     Gets the Judge overrules queued in this meeting.
+        /// </summary>
+        IReadOnlyCollection<IJudgeOverrule> JudgeOverrules { get; }
     }
 }
index 6a05e41d133631a31fc9e1a87a7989dc04b98ac8..a004bb5623642f8e00f661101f5f9883626bb282 100644 (file)
@@ -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 (file)
index 0000000..e3210d5
--- /dev/null
@@ -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();
+        }
+    }
+}
index b64699f61785a1cb3fa21efdfdfa8c57bd8b9458..b53612934c9df6322bf48a66fb949eb4b0673b73 100644 (file)
@@ -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; }
     }
 }
index 8b1528d41b07ae605521f80ef7412b912ae0fdcd..522945afd664d9b516c070f55f3aa4e8d03e0b00 100644 (file)
@@ -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.IPlayerVoteArea> IInnerMeetingHud.PlayerStates => Array.AsReadOnly(_playerStates);
 
         IInnerPlayerInfo? IInnerMeetingHud.Reporter => Reporter;
+
+        IReadOnlyCollection<IInnerMeetingHud.IJudgeOverrule> 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 (file)
index 0000000..3de73dc
--- /dev/null
@@ -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());
+            }
+        }
+    }
+}
index 515978d57f704951cec8a37fda780c3a159f1b15..d8330d041556e6f1b8345c1d3dd61cf75766d596 100644 (file)
@@ -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<InnerMeetingHud> _logger;
         private readonly IEventManager _eventManager;
-
         private readonly CancellationTokenSource _timerToken;
+        private readonly List<JudgeOverrule> _judgeOverrules = new();
 
         [AllowNull]
         private PlayerVoteArea[] _playerStates;
@@ -67,6 +68,8 @@ namespace Impostor.Server.Net.Inner.Objects
 
         public InnerPlayerInfo? Reporter { get; private set; }
 
+        public IReadOnlyCollection<IInnerMeetingHud.IJudgeOverrule> JudgeOverrules => _judgeOverrules;
+
         public override ValueTask<bool> 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<bool> 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));
         }
     }
 }