Support Judge and new vote

This commit is contained in:
linmeideli authored and miniduikboot committed 2026-08-30 18:48:37 +02:00
1 parent 857e04dbc3
commit f23dd8aa25
8 files changed
+228 -26

No files matched your search

@@ -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; }
}
}
@@ -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; }
}
}
@@ -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;
}
}
}
}
@@ -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();
}
}
}
@@ -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; }
}
}
@@ -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;
}
}
@@ -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());
}
}
}
}
@@ -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));
}
}
}