Compare commits

..
12 Commits
Author SHA1 Message Date
miniduikboot 5ee0fdb61f Bump version 2026-08-30 18:48:37 +02:00
miniduikboot 44ebc0c6b0 Rerun Dumpostor on 18.0 2026-08-30 18:48:37 +02:00
miniduikboot 681126dfb5 Address my own comments 2026-08-30 18:48:37 +02:00
linmeideli 9f627b7b01 clean up warns 2026-08-30 18:48:37 +02:00
linmeideli 510ed07389 Basically support AMCI 2026-08-30 18:48:37 +02:00
linmeideli 8f41030faf fix 17.4 support i thk 2026-08-30 18:48:37 +02:00
linmeideli f23dd8aa25 Support Judge and new vote 2026-08-30 18:48:37 +02:00
linmeideli 857e04dbc3 Support New HideNSeekGameOptions 2026-08-30 18:48:37 +02:00
miniduikboot b78f86f0f3 Fix compile warnings 2026-08-30 18:48:37 +02:00
miniduikboot db9c6ab66c Add special game version for mobile
Thanks Niko
2026-08-30 18:48:37 +02:00
miniduikboot a161aa7045 Add basic implementation of judge overruling 2026-08-30 18:48:37 +02:00
miniduikboot caab877c80 Add support for RoleOptions v11
This is bare minimum bringup for AU v18
2026-08-30 18:48:37 +02:00
38 changed files with 61380 additions and 50 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.7</VersionPrefix>
<VersionPrefix>1.10.8</VersionPrefix>
<VersionSuffix>dev</VersionSuffix>
<EnforceCodeStyleInBuild Condition="$(MSBuildProjectName)!='Hazel'">true</EnforceCodeStyleInBuild>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
@@ -151,6 +151,7 @@
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
}
}
},
".NETStandard,Version=v2.0/win-x64": {}
}
}
@@ -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; }
}
}
+7 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using Impostor.Api.Innersloth;
@@ -47,6 +48,11 @@ namespace Impostor.Api.Games
/// </remarks>
bool IsHostAuthoritive { get; }
/// <summary>
/// Gets the mod GUID that the game was registered with through the AMCI.
/// </summary>
Guid? ModGuid { get; }
IClientPlayer? GetClientPlayer(int clientId);
T? FindObjectByNetId<T>(uint netId)
File diff suppressed because it is too large. Load diff
@@ -24,5 +24,8 @@
"TrackerCooldown": 1550,
"TrackerDuration": 1551,
"TrackerDelay": 1552,
"NoisemakerAlertDuration": 1600
"NoisemakerAlertDuration": 1600,
"ViperDissolveTime": 1601,
"DetectiveSuspectLimit": 1602,
"JudgeTaskRequirementPercentage": 1620
}
@@ -11,5 +11,6 @@
"Phantom": 9,
"Tracker": 10,
"Detective": 12,
"Viper": 18
"Viper": 18,
"Judge": 19
}
@@ -59,5 +59,6 @@
"CheckVanish": 62,
"StartVanish": 63,
"CheckAppear": 64,
"StartAppear": 65
"StartAppear": 65,
"QueueOverruleVotes": 66
}
@@ -626,6 +626,14 @@
"SanctionSelfHarmPromotion": 1720,
"SanctionRepeatOffender10": 1721,
"SanctionUnknown": 1722,
"ReplicatorAbility": 1800,
"ReplicatorRole": 1801,
"ReplicatorBlurb": 1802,
"ReplicatorBlurbMed": 1803,
"ReplicatorBlurbLong": 1804,
"ReplicatorCooldown": 1805,
"ReplicatorDuration": 1806,
"ReplicatorQuantity": 1807,
"ScreenShakeOption": 1900,
"FeaturedItems": 1901,
"FeaturedBundles": 1902,
@@ -1576,6 +1584,14 @@
"XboxCommsCreateGameError": 3702,
"XboxCommsSearchError": 3703,
"XboxCommsJoinError": 3704,
"WatchAdBody": 3800,
"WatchAdTitle": 3801,
"WatchAd": 3802,
"RewardAdTitle": 3803,
"RewardAdBody1": 3804,
"RewardAdBody2": 3805,
"WatchAdErrorBody": 3806,
"WatchAdErrorTitle": 3807,
"DetectiveRole": 4000,
"DetectiveAbility": 4001,
"DetectiveAbilityNotes": 4002,
@@ -1623,5 +1639,28 @@
"DetectiveNotesPrepositionMaybe": 4079,
"DetectiveNotesTitlePostIt": 4080,
"DetectiveNotesPostIt": 4081,
"DetectiveNotesSuspectNumber": 4082
}
"DetectiveNotesSuspectNumber": 4082,
"AprilFoolsMode": 4083,
"MatchInfoGuideTitle": 4084,
"MatchInfoGuidePlayersLabel": 4085,
"MatchInfoGuideNoRolesMessage": 4086,
"JudgeRole": 4090,
"JudgeAbility": 4091,
"JudgeBlurb": 4092,
"JudgeBlurbMed": 4093,
"JudgeBlurbLong": 4094,
"JudgeTaskRequirement": 4095,
"RolesHelp_Judge_01": 4096,
"RolesHelp_Judge_02": 4097,
"JudgeAbilityUnlocked": 4098,
"JudgeAbilityProgress": 4099,
"JudgeCourtAdjourned": 4100,
"JudgeHasSpoken": 4101,
"StatsJudgeSuccessfulOverrules": 4102,
"StatsJudgeFailedOverrules": 4103,
"QCJudges": 4104,
"QCJudgeOverrule": 4105,
"QCJudgePlease": 4106,
"QCJudgeWillOverrule": 4107,
"JudgeBeatYouToIt": 4108
}
+2 -2
View File
@@ -1,5 +1,5 @@
{
"dumpostorVersion": "1.0.1-dev",
"gameVersion": "2024.10.29",
"gameVersion": "2026.8.18",
"platformType": "StandaloneItch"
}
}
@@ -0,0 +1,37 @@
{
"hatIds": [
"hat_pk04_beanie",
"hat_pk05_Cheese",
"hat_pk05_Cherry",
"hat_Chocolate",
"hat_Zipper",
"hat_pk02_Crown",
"hat_babybean",
"hat_Sorry",
"hat_mareLwyd",
"hat_fishingHat",
"hat_schnapp",
"hat_pk03_Traffic",
"hat_Pot",
"hat_AbominalHat",
"hat_tombstone",
"hat_pk04_MiniCrewmate",
"hat_pk05_Wizardhat",
"hat_pk05_Ellryhat",
"hat_pk05_EllieToppat"
],
"visorIds": [
"visor_pk01_AngeryVisor",
"visor_pk01_DumStickerVisor",
"visor_vr_Vr-Black",
"visor_pk01_PaperMaskVisor",
"visor_wash"
],
"petIds": [
"pet_YuleGoatPet",
"pet_HamPet",
"pet_Lava",
"pet_Snow",
"pet_Bush"
]
}
File diff suppressed because it is too large. Load diff
@@ -73,6 +73,9 @@ namespace Impostor.Api.Innersloth.GameFilters
case "bool":
return JsonSerializer.Deserialize<BoolGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for BoolGameFilter");
case "mod":
return JsonSerializer.Deserialize<ModFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for ModFilter");
default:
throw new InvalidOperationException("No type matches subfilter");
}
@@ -0,0 +1,16 @@
using System;
using System.Text.Json.Serialization;
// https://github.com/Innersloth-LLC/AmongUsModdingInformation
namespace Impostor.Api.Innersloth.GameFilters
{
[Serializable]
public class ModFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "mod";
[JsonPropertyName("AcceptedValues")]
public required Guid AcceptedValues { get; set; }
}
}
@@ -2,7 +2,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
public class HideNSeekGameOptions : IGameOptions
{
public const int LatestVersion = 10;
public const int LatestVersion = 11;
public HideNSeekGameOptions(byte version = LatestVersion)
{
@@ -107,6 +107,11 @@ public class HideNSeekGameOptions : IGameOptions
public void Deserialize(IMessageReader reader)
{
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
if (Version >= 8)
{
SpecialMode = (SpecialGameModes)reader.ReadByte();
@@ -142,11 +147,6 @@ public class HideNSeekGameOptions : IGameOptions
{
Tag = (GameTags)reader.ReadByte();
}
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<HideNSeekGameOptions>(Version);
}
}
public void Serialize(IMessageWriter writer)
@@ -4,7 +4,7 @@ namespace Impostor.Api.Innersloth.GameOptions;
public class NormalGameOptions : IGameOptions
{
public const int LatestVersion = 10;
public const int LatestVersion = 11;
public NormalGameOptions(byte version = LatestVersion)
{
@@ -144,6 +144,11 @@ public class NormalGameOptions : IGameOptions
public void Deserialize(IMessageReader reader)
{
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
if (Version >= 8)
{
SpecialMode = (SpecialGameModes)reader.ReadByte();
@@ -184,11 +189,6 @@ public class NormalGameOptions : IGameOptions
}
RoleOptions.Deserialize(reader);
if (Version > LatestVersion)
{
IGameOptions.ThrowUnknownVersion<NormalGameOptions>(Version);
}
}
public void Serialize(IMessageWriter writer)
@@ -0,0 +1,29 @@
namespace Impostor.Api.Innersloth.GameOptions.RoleOptions;
public class JudgeRoleOptions : IRoleOptions
{
public JudgeRoleOptions(byte version)
{
Version = version;
}
public byte Version { get; }
public RoleTypes Type => RoleTypes.Judge;
public float JudgeTaskRequirementPercentage { get; set; } = 50f;
public static JudgeRoleOptions Deserialize(IMessageReader reader, byte version)
{
var options = new JudgeRoleOptions(version);
options.JudgeTaskRequirementPercentage = (int)reader.ReadByte();
return options;
}
public void Serialize(IMessageWriter writer)
{
writer.Write((byte)JudgeTaskRequirementPercentage);
}
}
@@ -34,6 +34,7 @@ public class RoleOptionsCollection
RoleTypes.Tracker => TrackerRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Detective => DetectiveRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Viper => ViperRoleOptions.Deserialize(roleOptionsReader, Version),
RoleTypes.Judge => JudgeRoleOptions.Deserialize(roleOptionsReader, Version),
_ => throw new ArgumentOutOfRangeException(nameof(roleType), roleType, null),
};
@@ -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; }
}
}
@@ -1,4 +1,4 @@
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
namespace Impostor.Api.Net.Messages.C2S
@@ -0,0 +1,25 @@
using System;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
namespace Impostor.Api.Net.Messages.C2S
{
/// <summary>
/// Special serialization rules for AU MCI HostGame messages.
/// <see href="https://github.com/Innersloth-LLC/AmongUsModdingInformation?tab=readme-ov-file#all-client-mod-client-identification"/>
/// for more information.
/// </summary>
public static class Message25HostModdedGameC2S
{
public static void Serialize(IMessageWriter writer, IGameOptions gameOptions, CrossplayFlags crossplayFlags, GameFilterOptions gameFilterOptions)
{
Message00HostGameC2S.Serialize(writer, gameOptions, crossplayFlags, gameFilterOptions);
}
public static void Deserialize(IMessageReader reader, out IGameOptions gameOptions, out CrossplayFlags crossplayFlags, out GameFilterOptions gameFilterOptions, out Guid modGuid)
{
Message00HostGameC2S.Deserialize(reader, out gameOptions, out crossplayFlags, out gameFilterOptions);
modGuid = new Guid(reader.ReadBytes(16).Span);
}
}
}
@@ -29,6 +29,7 @@ namespace Impostor.Api.Net.Messages
public const byte QueryPlatformIds = 22;
public const byte QueryLobbyInfo = 23;
public const byte EndGameHostMigration = 24;
public const byte HostModdedGame = 25;
public const byte PackedGameDataTo = 26;
private static readonly Dictionary<byte, string> FlagCache;
@@ -2,6 +2,8 @@ namespace Impostor.Api.Net.Messages.Rpcs
{
public static class Rpc23VotingComplete
{
// Pre 18.0 version
// TODO clean up when 17.4 and below are unsupported
public static void Serialize(IMessageWriter writer, byte[] states, byte playerId, bool tie)
{
writer.WriteBytesAndSize(states);
@@ -9,6 +11,15 @@ namespace Impostor.Api.Net.Messages.Rpcs
writer.Write(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)
{
var length = reader.ReadPackedInt32();
@@ -21,5 +32,21 @@ namespace Impostor.Api.Net.Messages.Rpcs
playerId = reader.ReadByte();
tie = reader.ReadBoolean();
}
public static void Deserialize(IMessageReader reader, out IMessageReader[] states, out byte playerId, out bool tie, out bool wasOverruled, out ushort overrideId)
{
var length = reader.ReadPackedInt32();
states = new IMessageReader[length];
for (var i = 0; i < length; i++)
{
states[i] = reader.ReadMessage();
}
playerId = reader.ReadByte();
tie = reader.ReadBoolean();
wasOverruled = reader.ReadBoolean();
overrideId = reader.ReadUInt16();
}
}
}
@@ -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();
}
}
}
+8 -1
View File
@@ -40,6 +40,12 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.30, )",
"resolved": "8.0.30",
"contentHash": "SfmwuZjUPBTEoBMAsXZ/hWkaEK9oL+Y/UtAx7N6RyWHfcMt9J0ExrNPuOgTyPbCXIyiTBFsxFNrPLEZNPmJcvA=="
},
"StyleCop.Analyzers": {
"type": "Direct",
"requested": "[1.2.0-beta.556, )",
@@ -117,6 +123,7 @@
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
}
}
},
"net8.0/win-x64": {}
}
}
@@ -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 -1
View File
@@ -11,7 +11,6 @@ using Impostor.Api.Games;
using Impostor.Api.Games.Managers;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameFilters;
using Impostor.Hazel.UPnP;
using Impostor.Server.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -37,6 +36,7 @@ public sealed class GamesController : ControllerBase
/// <param name="gameManager">GameManager containing a list of games.</param>
/// <param name="listingManager">ListingManager responsible for filtering.</param>
/// <param name="serverConfig">Impostor configuration section containing the public ip address of this server.</param>
/// <param name="logger">DI injected logger.</param>
public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions<ServerConfig> serverConfig, ILogger<GamesController> logger)
{
_gameManager = gameManager;
+17 -1
View File
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Impostor.Api.Config;
using Impostor.Api.Games;
using Impostor.Api.Games.Managers;
@@ -61,6 +60,11 @@ public sealed class ListingManager
continue;
}
if (game.ModGuid != null)
{
continue;
}
if (!_compatibilityConfig.AllowVersionMixing &&
game.Host != null &&
_compatibilityManager.CanJoinGame(game.Host.Client.GameVersion, gameVersion) != GameJoinError.None)
@@ -139,6 +143,11 @@ public sealed class ListingManager
continue;
}
if (game.ModGuid != null && !filterSet.Filters.Any(f => f.OptionType == "mod"))
{
continue;
}
var matchesAllFilters = true;
// Hard coded to only check map, languages, impostornum, chatmode, tags
@@ -192,6 +201,13 @@ public sealed class ListingManager
}
}
break;
case "mod":
if (filter.SubFilter is ModFilter modFilter && game.ModGuid != modFilter.AcceptedValues)
{
matchesAllFilters = false;
}
break;
}
+27 -3
View File
@@ -5,6 +5,7 @@ using Impostor.Api;
using Impostor.Api.Config;
using Impostor.Api.Games;
using Impostor.Api.Innersloth;
using Impostor.Api.Innersloth.GameOptions;
using Impostor.Api.Net;
using Impostor.Api.Net.Custom;
using Impostor.Api.Net.Messages;
@@ -42,6 +43,11 @@ namespace Impostor.Server.Net
return false;
}
if (Player != null && Player.Game.ModGuid != null)
{
return false;
}
if (Player != null && Player.IsHost)
{
var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch {
@@ -124,12 +130,25 @@ namespace Impostor.Server.Net
switch (flag)
{
case MessageFlags.HostGame:
case MessageFlags.HostModdedGame:
{
// Read game settings.
Message00HostGameC2S.Deserialize(reader, out var gameOptions, out _, out var gameFilterOptions);
IGameOptions gameOptions;
GameFilterOptions gameFilterOptions;
Guid? modGuid = null;
if (flag == MessageFlags.HostModdedGame)
{
Message25HostModdedGameC2S.Deserialize(reader, out gameOptions, out _, out gameFilterOptions, out var parsedModGuid);
modGuid = parsedModGuid;
}
else
{
// Read game settings.
Message00HostGameC2S.Deserialize(reader, out gameOptions, out _, out gameFilterOptions);
}
// Create game.
var game = await _gameManager.CreateAsync(this, gameOptions, gameFilterOptions);
var game = await _gameManager.CreateAsync(this, gameOptions, gameFilterOptions, modGuid);
if (game == null)
{
@@ -137,6 +156,11 @@ namespace Impostor.Server.Net
return;
}
if (modGuid != null)
{
_logger.LogInformation("Client {Name} ({Id}) hosted a modded game with mod GUID {ModGuid}.", Name, Id, modGuid);
}
// Code in the packet below will be used in JoinGame.
using (var writer = MessageWriter.Get(MessageType.Reliable))
{
@@ -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;
@@ -22,10 +23,12 @@ namespace Impostor.Server.Net.Inner.Objects
{
internal partial class InnerMeetingHud : InnerNetObject
{
private static readonly GameVersion JudgeMinVersion = new GameVersion(2026, 7, 15); // 18.0
private readonly ILogger<InnerMeetingHud> _logger;
private readonly IEventManager _eventManager;
private readonly CancellationTokenSource _timerToken;
private readonly List<JudgeOverrule> _judgeOverrules = new();
[AllowNull]
private PlayerVoteArea[] _playerStates;
@@ -65,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();
@@ -108,6 +113,19 @@ namespace Impostor.Server.Net.Inner.Objects
}
}
}
if (sender.Client.GameVersion >= JudgeMinVersion)
{
var overruleQueueListLength = reader.ReadPackedInt32();
if (overruleQueueListLength > 0)
{
_judgeOverrules.Clear();
for (var i = 0; i < overruleQueueListLength; i++)
{
_judgeOverrules.Add(JudgeOverrule.Deserialize(reader));
}
}
}
}
public override async ValueTask<bool> HandleRpcAsync(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader)
@@ -122,6 +140,7 @@ namespace Impostor.Server.Net.Inner.Objects
}
Rpc22Close.Deserialize(reader);
_judgeOverrules.Clear();
break;
}
@@ -132,10 +151,23 @@ namespace Impostor.Server.Net.Inner.Objects
return false;
}
Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie);
foreach (var messageReader in states)
if (sender.Client.GameVersion >= JudgeMinVersion)
{
messageReader.Dispose();
Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie, out var wasOverruled, out var overrideId);
foreach (var messageReader in states)
{
messageReader.Dispose();
}
}
else
{
Rpc23VotingComplete.Deserialize(reader, out var states, out var playerId, out var tie);
foreach (var messageReader in states)
{
messageReader.Dispose();
}
}
// This would be a nice place to implement an anti cheat.
@@ -161,6 +193,12 @@ namespace Impostor.Server.Net.Inner.Objects
break;
}
case RpcCalls.QueueOverruleVotes:
{
Rpc66QueueOverruleVotes.Deserialize(reader, out var judgePlayerId, out var targetPlayerId, out var overruleNonce);
return await HandleQueueOverruleVotesAsync(sender, target, judgePlayerId, targetPlayerId, overruleNonce);
}
default:
return await base.HandleRpcAsync(sender, target, call, reader);
}
@@ -221,6 +259,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))
@@ -287,13 +406,35 @@ namespace Impostor.Server.Net.Inner.Objects
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)
{
exiled.PlayerInfo.LastDeathReason = DeathReason.Exile;
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));
}
}
}
@@ -51,9 +51,9 @@ 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 or RoleTypes.Phantom;
public bool IsImpostor => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.ImpostorGhost or RoleTypes.Phantom or RoleTypes.Viper;
public bool CanVent => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom or RoleTypes.Engineer;
public bool CanVent => RoleType is RoleTypes.Impostor or RoleTypes.Shapeshifter or RoleTypes.Phantom or RoleTypes.Viper or RoleTypes.Engineer;
public bool IsDead { get; internal set; }
@@ -45,6 +45,11 @@ internal class CompatibilityManager : ICompatibilityManager
new GameVersion(2026, 3, 18), // 17.4 (2026-06-05, build 7044, pc only)
new GameVersion(2026, 4, 23), // 17.4 (2026-06-05, build 7045, mobile only)
},
new[]
{
new GameVersion(2026, 7, 15), // 18.0 (2026-08-18, build 7238, pc only)
new GameVersion(2026, 7, 16), // 18.0 (2026-08-18, build 7238, mobile only)
},
};
private readonly List<CompatibilityGroup> _compatibilityGroups = new();
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -85,7 +85,7 @@ namespace Impostor.Server.Net.Manager
await _eventManager.CallAsync(new GameDestroyedEvent(game));
}
public async ValueTask<IGame?> CreateAsync(IClient? owner, IGameOptions options, GameFilterOptions filterOptions)
public async ValueTask<IGame?> CreateAsync(IClient? owner, IGameOptions options, GameFilterOptions filterOptions, Guid? modGuid = null)
{
if (owner != null && !_gamesCreatedBy.TryAdd(owner, null))
{
@@ -101,11 +101,11 @@ namespace Impostor.Server.Net.Manager
return null;
}
var (success, game) = await TryCreateAsync(options, filterOptions, owner, @event.GameCode);
var (success, game) = await TryCreateAsync(options, filterOptions, owner, @event.GameCode, modGuid);
for (var i = 0; i < 10 && !success; i++)
{
(success, game) = await TryCreateAsync(options, filterOptions, owner);
(success, game) = await TryCreateAsync(options, filterOptions, owner, modGuid: modGuid);
}
if (owner != null)
@@ -126,10 +126,12 @@ namespace Impostor.Server.Net.Manager
return CreateAsync(null, options, filterOptions);
}
private async ValueTask<(bool Success, Game? Game)> TryCreateAsync(IGameOptions options, GameFilterOptions filterOptions, IClient? owner, GameCode? desiredGameCode = null)
private async ValueTask<(bool Success, Game? Game)> TryCreateAsync(IGameOptions options, GameFilterOptions filterOptions, IClient? owner, GameCode? desiredGameCode = null, Guid? modGuid = null)
{
var gameCode = desiredGameCode ?? _gameCodeFactory.Create();
var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options, filterOptions);
var game = modGuid.HasValue
? ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options, filterOptions, modGuid.Value)
: ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options, filterOptions);
if (!_games.TryAdd(gameCode, game))
{
+5 -1
View File
@@ -47,7 +47,8 @@ namespace Impostor.Server.Net.State
IEventManager eventManager,
ICompatibilityManager compatibilityManager,
IOptions<CompatibilityConfig> compatibilityConfig,
IOptions<TimeoutConfig> timeoutConfig)
IOptions<TimeoutConfig> timeoutConfig,
Guid? modGuid = null)
{
_logger = logger;
_serviceProvider = serviceProvider;
@@ -62,6 +63,7 @@ namespace Impostor.Server.Net.State
GameNet = new GameNet();
Options = options;
FilterOptions = filterOptions;
ModGuid = modGuid;
_clientManager = clientManager;
_eventManager = eventManager;
_compatibilityManager = compatibilityManager;
@@ -96,6 +98,8 @@ namespace Impostor.Server.Net.State
public bool IsHostAuthoritive => Host != null && Host.Client.GameVersion.HasDisableServerAuthorityFlag;
public Guid? ModGuid { get; }
internal GameNet GameNet { get; }
public bool TryGetPlayer(int id, [MaybeNullWhen(false)] out ClientPlayer player)
@@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Impostor.Api.Config;
using Impostor.Api.Innersloth;
using Impostor.Api.Net.Custom;
@@ -52,7 +52,7 @@ namespace Impostor.Server.Recorder
await base.HandleMessageAsync(reader, messageType);
// Player created a game.
if (reader.Tag == MessageFlags.HostGame)
if (reader.Tag is MessageFlags.HostGame or MessageFlags.HostModdedGame)
{
_createdGame = true;
}
+6
View File
@@ -56,6 +56,12 @@
"resolved": "8.0.1",
"contentHash": "X21b8D6z58EG34OMLH955750mFaEaGs5xLGe9Vyi3059Cgb3sk+p2k/JlQXaieEBRrk+SVlFzDei3Q6PdVzJOA=="
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.30, )",
"resolved": "8.0.30",
"contentHash": "SfmwuZjUPBTEoBMAsXZ/hWkaEK9oL+Y/UtAx7N6RyWHfcMt9J0ExrNPuOgTyPbCXIyiTBFsxFNrPLEZNPmJcvA=="
},
"Serilog.Extensions.Hosting": {
"type": "Direct",
"requested": "[8.0.0, )",