filter + upstream support

This commit is contained in:
linmeideli committed 2026-08-20 09:14:30 +08:00
1 parent 68ac3de0bd
commit 6498752430
54 files changed
+61942 -56

No files matched your search

@@ -85,6 +85,14 @@ public sealed class SourceGenerator : IIncrementalGenerator
enumGenerator.Generate("SystemTypes", underlyingType: CSharpEnumUnderlyingType.Byte);
enumGenerator.Generate("Language", sourceName: "SupportedLangs");
enumGenerator.Generate("TaskTypes");
enumGenerator.Generate("BoolOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("ByteOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("CategorizedOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("FloatArrayOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("FloatOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("Int32ArrayOptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("Int32OptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("UInt32OptionNames", "Empostor.Api.Innersloth.GameFilters");
enumGenerator.Generate("RpcCalls", "Empostor.Api.Net.Inner", underlyingType: CSharpEnumUnderlyingType.Byte);
@@ -1,4 +1,4 @@
using Empostor.Api.Net.Inner.Objects;
using Empostor.Api.Net.Inner.Objects;
namespace Empostor.Api.Events.Meeting
{
@@ -13,5 +13,15 @@ namespace Empostor.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; }
}
}
+6
View File
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
@@ -49,6 +50,11 @@ namespace Empostor.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
@@ -0,0 +1,17 @@
{
"Invalid": 0,
"VisualTasks": 1,
"GhostsDoTasks": 2,
"ConfirmImpostor": 3,
"AnonymousVotes": 4,
"IsDefaults": 5,
"UseFlashlight": 6,
"SeekerFinalVents": 7,
"SeekerFinalMap": 8,
"SeekerPings": 9,
"ShowCrewmateNames": 10,
"Roles": 11,
"ShapeshifterLeaveSkin": 1000,
"ImpostorsCanSeeProtect": 1100,
"NoisemakerImpostorAlert": 1300
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"MapId": 1
}
@@ -0,0 +1,14 @@
{
"KillCooldown": 0,
"PlayerSpeed": 1,
"EmergencyMeetingCount": 2,
"EmergencyMeetingCooldown": 3,
"DiscussionTime": 4,
"VotingTime": 5,
"CrewmateVision": 6,
"HidingTime": 7,
"MaxTimeInVent": 8,
"ImposterVision": 9,
"FinalHideTime": 10,
"FinalHideImposterSpeed": 11
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"KillDistances": 1
}
@@ -0,0 +1,31 @@
{
"Invalid": 0,
"KillCooldown": 1,
"PlayerSpeedMod": 2,
"ImpostorLightMod": 3,
"CrewLightMod": 4,
"CrewmateTimeInVent": 100,
"FinalEscapeTime": 101,
"EscapeTime": 102,
"SeekerFinalSpeed": 103,
"MaxPingTime": 104,
"CrewmateFlashlightSize": 105,
"ImpostorFlashlightSize": 106,
"ShapeshifterCooldown": 1000,
"ShapeshifterDuration": 1001,
"ProtectionDurationSeconds": 1100,
"GuardianAngelCooldown": 1101,
"ScientistCooldown": 1200,
"ScientistBatteryCharge": 1201,
"EngineerCooldown": 1300,
"EngineerInVentMaxTime": 1301,
"PhantomCooldown": 1500,
"PhantomDuration": 1501,
"TrackerCooldown": 1550,
"TrackerDuration": 1551,
"TrackerDelay": 1552,
"NoisemakerAlertDuration": 1600,
"ViperDissolveTime": 1601,
"DetectiveSuspectLimit": 1602,
"JudgeTaskRequirementPercentage": 1620
}
@@ -0,0 +1,5 @@
{
"Invalid": 0,
"MaxImpostors": 1,
"MinPlayers": 2
}
@@ -0,0 +1,21 @@
{
"Invalid": 0,
"NumImpostors": 1,
"KillDistance": 2,
"NumEmergencyMeetings": 3,
"EmergencyCooldown": 4,
"DiscussionTime": 5,
"VotingTime": 6,
"MaxImpostors": 7,
"MinPlayers": 8,
"MaxPlayers": 9,
"NumCommonTasks": 10,
"NumShortTasks": 11,
"NumLongTasks": 12,
"TaskBarMode": 13,
"Tag": 14,
"CrewmatesRemainingForVitals": 100,
"CrewmateVentUses": 101,
"ImpostorPlayerID": 102,
"RulePreset": 200
}
@@ -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
}
@@ -0,0 +1,4 @@
{
"Invalid": 0,
"Keywords": 1
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"dumpostorVersion": "1.0.1-dev",
"gameVersion": "2025.10.14",
"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
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class BoolGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "bool";
[JsonPropertyName("AcceptedValues")]
public required List<bool> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required BoolOptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class CategorizedGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "cat";
[JsonPropertyName("AcceptedValues")]
public required List<int> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required CategorizedOptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class ChatModeGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "chat";
[JsonPropertyName("AcceptedValues")]
public required byte AcceptedValues { get; set; }
}
}
@@ -0,0 +1,87 @@
using System;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFilter
{
[JsonConstructor]
public GameFilter(string optionType, string key, string subFilterString)
{
this.OptionType = optionType;
this.Key = key;
this.SubFilterString = subFilterString;
this.SubFilter = ResolveSubFilter(OptionType, SubFilterString);
}
[JsonIgnore]
public ISubFilter SubFilter { get; set; }
[JsonPropertyName("OptionType")]
public required string OptionType { get; set; }
[JsonPropertyName("Key")]
public required string Key { get; set; }
[JsonPropertyName("SubFilterString")]
public required string SubFilterString { get; set; }
public void ModifySubFilter(ISubFilter subFilter)
{
this.SubFilter = subFilter;
}
[OnSerializing]
internal void OnSerializing(StreamingContext context)
{
this.SubFilterString = JsonSerializer.Serialize(this.SubFilter);
}
[OnDeserialized]
internal void OnDeserialized(StreamingContext context)
{
this.SubFilter = this.ResolveSubFilter(this.OptionType, this.SubFilterString);
}
private ISubFilter ResolveSubFilter(string type, string filterString)
{
if (type != null)
{
switch (type.ToLower())
{
case "int":
return JsonSerializer.Deserialize<IntGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for IntGameFilter");
case "platform":
return JsonSerializer.Deserialize<PlatformGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for PlatformGameFilter");
case "cat":
return JsonSerializer.Deserialize<CategorizedGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for CategorizedGameFilter");
case "languages":
return JsonSerializer.Deserialize<LanguageFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for LanguageFilter");
case "chat":
return JsonSerializer.Deserialize<ChatModeGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for ChatModeGameFilter");
case "map":
return JsonSerializer.Deserialize<MapGameFilter>(filterString)
?? throw new InvalidOperationException("Deserialization returned null for MapGameFilter");
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");
}
}
throw new InvalidOperationException("Must provide type for sub filter");
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFilterSet
{
[JsonConstructor]
public GameFilterSet(GameModes gameMode, List<GameFilter> filters)
{
GameMode = gameMode;
Filters = filters;
}
[JsonPropertyName("GameMode")]
public required GameModes GameMode { get; set; }
[JsonPropertyName("Filters")]
public required List<GameFilter> Filters { get; set; }
}
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class GameFiltersList
{
[JsonPropertyName("FilterSets")]
public required List<GameFilterSet> FilterSets { get; set; }
}
}
@@ -0,0 +1,7 @@
namespace Empostor.Api.Innersloth.GameFilters
{
public interface ISubFilter
{
string FilterType { get; }
}
}
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class IntGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "int";
[JsonPropertyName("AcceptedValues")]
public required List<int> AcceptedValues { get; set; }
[JsonPropertyName("OptionEnum")]
public required Int32OptionNames OptionEnum { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class LanguageFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "languages";
[JsonPropertyName("AcceptedValues")]
public required uint AcceptedValues { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class MapGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "map";
[JsonPropertyName("AcceptedValues")]
public required byte AcceptedValues { get; set; }
}
}
@@ -0,0 +1,16 @@
using System;
using System.Text.Json.Serialization;
// https://github.com/Innersloth-LLC/AmongUsModdingInformation
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class ModFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "mod";
[JsonPropertyName("AcceptedValues")]
public required Guid AcceptedValues { get; set; }
}
}
@@ -0,0 +1,15 @@
using System;
using System.Text.Json.Serialization;
namespace Empostor.Api.Innersloth.GameFilters
{
[Serializable]
public class PlatformGameFilter : ISubFilter
{
[JsonPropertyName("FilterType")]
public string FilterType { get; } = "platform";
[JsonPropertyName("AcceptedValues")]
public required uint AcceptedValues { get; set; }
}
}
@@ -2,7 +2,7 @@ namespace Empostor.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)
@@ -49,6 +49,8 @@ public interface IGameOptions
/// </summary>
public bool IsDefaults { get; set; }
public GameTags Tag { get; set; }
public void Serialize(IMessageWriter writer);
public static void EnsureVersionIsModular<TCaller>(byte version)
@@ -145,6 +145,8 @@ public class LegacyGameOptionsData : IGameOptions
/// </summary>
public bool IsDefaults { get; set; } = true;
public GameTags Tag { get; set; } = 0;
public static LegacyGameOptionsData Deserialize(IMessageReader reader, byte version)
{
var options = new LegacyGameOptionsData(version);
@@ -4,7 +4,7 @@ namespace Empostor.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 Empostor.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 Empostor.Api.Events.Player;
@@ -6,6 +6,24 @@ namespace Empostor.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 Empostor.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; }
}
}
@@ -0,0 +1,25 @@
using System;
using Empostor.Api.Innersloth;
using Empostor.Api.Innersloth.GameOptions;
namespace Empostor.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);
}
}
}
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -29,6 +29,7 @@ namespace Empostor.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 Empostor.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 Empostor.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 Empostor.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 Empostor.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 Empostor.Api.Events.Meeting;
using Empostor.Api.Events.Meeting;
using Empostor.Api.Games;
using Empostor.Api.Net.Inner.Objects;
@@ -6,12 +6,14 @@ namespace Empostor.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 Empostor.Server.Events.Meeting
public IInnerPlayerControl? Exiled { get; }
public bool IsTie { get; }
public bool WasOverruled { get; }
public ushort OverrideId { get; }
}
}
@@ -0,0 +1,56 @@
using System;
using System.Net.Http.Headers;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
namespace Empostor.Server.Http;
[Route("/api/filters")]
public sealed class FiltersController : ControllerBase
{
private static readonly string[] Value = new[]
{
"Tags",
// "PlayerSpeed",
// "Roles",
// "KillCooldown",
// "VotingTime",
"NumImposters",
// "VisualTasks",
// "AnonymousVotes",
// "ConfirmEjects",
// "DiscussionTime",
// "EmergencyCooldown",
// "NumEmergencyMeetings",
// "NumCommonTasks",
// "NumShortTasks",
// "NumLongTasks",
// "KillDistance",
};
[HttpGet]
public IActionResult GetFilters([FromHeader] AuthenticationHeaderValue authorization)
{
if (authorization.Scheme != "Bearer" || authorization.Parameter == null)
{
return BadRequest();
}
TokenController.TokenResponse? token = null;
try
{
token = JsonSerializer.Deserialize<TokenController.TokenResponse>(Convert.FromBase64String(authorization.Parameter));
}
catch
{
return BadRequest();
}
return Ok(new
{
filters = Value,
});
}
}
+60 -17
View File
@@ -5,11 +5,15 @@ using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Web;
using Empostor.Api.Config;
using Empostor.Api.Games;
using Empostor.Api.Games.Managers;
using Empostor.Api.Innersloth;
using Empostor.Api.Innersloth.GameFilters;
using Empostor.Server.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Empostor.Server.Http;
@@ -21,15 +25,18 @@ public sealed class GamesController : ControllerBase
private readonly IGameManager _gameManager;
private readonly ListingManager _listingManager;
private readonly ServerConfig _serverConfig;
private readonly ILogger<GamesController> _logger;
public GamesController(
IGameManager gameManager,
ListingManager listingManager,
IOptions<ServerConfig> serverConfig)
IOptions<ServerConfig> serverConfig,
ILogger<GamesController> logger)
{
_gameManager = gameManager;
_listingManager = listingManager;
_serverConfig = serverConfig.Value;
_logger = logger;
}
/// <summary>
@@ -108,26 +115,54 @@ public sealed class GamesController : ControllerBase
/// <summary>
/// Filtered lobby list (Among Us 16.0.0+).
/// No support filter conditions on the latest version.
/// </summary>
[HttpGet("filtered")]
public IActionResult ShowFilteredLobbies()
public IActionResult ShowFilteredLobbies([FromQuery] string filter)
{
var port = GetDeltaPort();
var listings = _gameManager.Games
.Where(g => g.IsPublic)
.Select(g => GameListing.From(g, port))
.ToList();
return Ok(new
if (string.IsNullOrEmpty(filter))
{
games = listings,
metadata = new
return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "filter query parameter not provided")));
}
try
{
var decodedFilter = HttpUtility.UrlDecode(filter);
var filtersList = JsonSerializer.Deserialize<GameFiltersList>(decodedFilter);
// filterSets wont be null. It must at least have ChatFilter and LangFilter
// Vanilla game only builds one filterSet and InnerSloth officials only handles first one (though you can send multiple filter sets. sloths only handle the first)
if (filtersList == null || filtersList.FilterSets.Count != 1
|| filtersList.FilterSets[0].Filters.Count < 2
|| !filtersList.FilterSets[0].Filters.Any(x => x.OptionType == "languages")
|| !filtersList.FilterSets[0].Filters.Any(x => x.OptionType == "chat"))
{
allGamesCount = _gameManager.Games.Count(),
matchingGamesCount = listings.Count,
},
});
return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "Invalid filterSets")));
}
var filteredGames = _listingManager.FindListingsV2(HttpContext, filtersList);
var port = GetDeltaPort();
var gameListings = filteredGames.Select(g => GameListing.From(g, port)).ToList();
var response = new
{
games = gameListings,
metadata = new
{
allGamesCount = _gameManager.Games.Count(),
matchingGamesCount = gameListings.Count,
},
};
return Ok(response);
}
catch (JsonException ex)
{
return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "Unable to deserialize filter json" + ex)));
}
catch (Exception ex)
{
return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "Unknown exception caught in filter" + ex)));
}
}
/// <summary>
@@ -236,10 +271,18 @@ public sealed class GamesController : ControllerBase
private class MatchmakerError
{
[SetsRequiredMembers]
public MatchmakerError(DisconnectReason reason) { Reason = reason; }
public MatchmakerError(DisconnectReason reason, string message = "")
{
Reason = reason;
Message = message;
}
[JsonPropertyName("Reason")]
public required DisconnectReason Reason { get; init; }
[JsonPropertyName("Message")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public required string Message { get; init; } = string.Empty;
}
private class FindGameByCodeResponse
+134
View File
@@ -4,6 +4,7 @@ using Empostor.Api.Config;
using Empostor.Api.Games;
using Empostor.Api.Games.Managers;
using Empostor.Api.Http;
using Empostor.Api.Innersloth.GameFilters;
using Empostor.Api.Net.Manager;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
@@ -45,6 +46,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)
@@ -89,6 +95,134 @@ public sealed class ListingManager
}
}
public IEnumerable<IGame> FindListingsV2(HttpContext ctx, GameFiltersList filtersList, int maxListings = 10)
{
if (filtersList == null || filtersList.FilterSets.Count == 0)
{
return Enumerable.Empty<IGame>();
}
var result = new List<IGame>();
var resultCount = 0;
// Apply custom filters from API
var filters = _listingFilters.Select(f => f.GetFilter(ctx)).ToArray();
foreach (var game in _gameManager.Games)
{
if (!game.IsPublic || game.GameState != GameStates.NotStarted || game.PlayerCount >= game.Options.MaxPlayers)
{
continue;
}
if (!filters.All(filter => filter(game)))
{
continue;
}
var matchesAnyFilterSet = false;
foreach (var filterSet in filtersList.FilterSets)
{
// Not sure how to handle normal fools and seek fools
if (game.Options.GameMode != filterSet.GameMode)
{
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
foreach (var filter in filterSet.Filters)
{
switch (filter.OptionType)
{
case "map":
if (filter.SubFilter is MapGameFilter mapFilter && ((1 << (int)game.Options.Map) & mapFilter.AcceptedValues) == 0)
{
matchesAllFilters = false;
}
break;
case "languages":
if (filter.SubFilter is LanguageFilter langFilter && game.Options.Keywords != (GameKeywords)langFilter.AcceptedValues)
{
matchesAllFilters = false;
}
break;
case "int":
if (filter.SubFilter is IntGameFilter intFilter)
{
switch (intFilter.OptionEnum)
{
case Int32OptionNames.NumImpostors:
if (!intFilter.AcceptedValues.Contains(game.Options.NumImpostors))
{
matchesAllFilters = false;
}
break;
case Int32OptionNames.Tag:
if (!intFilter.AcceptedValues.Contains((int)game.Options.Tag))
{
matchesAllFilters = false;
}
break;
}
}
break;
case "chat":
if (filter.SubFilter is ChatModeGameFilter chatFilter && game.Host != null)
{
if (((byte)game.Host.Client.ChatMode & chatFilter.AcceptedValues) == 0)
{
matchesAllFilters = false;
}
}
break;
case "mod":
if (filter.SubFilter is ModFilter modFilter && game.ModGuid != modFilter.AcceptedValues)
{
matchesAllFilters = false;
}
break;
}
if (!matchesAllFilters)
{
break;
}
}
if (matchesAllFilters)
{
matchesAnyFilterSet = true;
break;
}
}
if (matchesAnyFilterSet)
{
result.Add(game);
if (++resultCount >= maxListings)
{
break;
}
}
}
return result;
}
private static bool IsGameDesired(IGame game, int map, int impostorCount, GameKeywords language)
{
if ((map & (1 << (int)game.Options.Map)) == 0)
+27 -3
View File
@@ -5,6 +5,7 @@ using Empostor.Api;
using Empostor.Api.Config;
using Empostor.Api.Events.Managers;
using Empostor.Api.Games;
using Empostor.Api.Innersloth.GameOptions;
using Empostor.Api.Net;
using Empostor.Api.Net.Custom;
using Empostor.Api.Net.Messages;
@@ -48,6 +49,11 @@ namespace Empostor.Server.Net
return false;
}
if (Player != null && Player.Game.ModGuid != null)
{
return false;
}
if (Player != null && Player.IsHost)
{
var isHostCheatingAllowed = _antiCheatConfig.AllowCheatingHosts switch
@@ -125,12 +131,25 @@ namespace Empostor.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)
{
@@ -138,6 +157,11 @@ namespace Empostor.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 Empostor.Api.Net.Inner.Objects;
@@ -9,5 +9,7 @@ namespace Empostor.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 Empostor.Api.Net.Inner.Objects;
namespace Empostor.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());
}
}
}
}
@@ -11,6 +11,7 @@ using Empostor.Api.Innersloth.GameOptions;
using Empostor.Api.Net;
using Empostor.Api.Net.Custom;
using Empostor.Api.Net.Inner;
using Empostor.Api.Net.Inner.Objects;
using Empostor.Api.Net.Messages.Rpcs;
using Empostor.Server.Events.Meeting;
using Empostor.Server.Events.Player;
@@ -21,10 +22,12 @@ namespace Empostor.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;
@@ -64,6 +67,8 @@ namespace Empostor.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();
@@ -107,6 +112,19 @@ namespace Empostor.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)
@@ -121,6 +139,7 @@ namespace Empostor.Server.Net.Inner.Objects
}
Rpc22Close.Deserialize(reader);
_judgeOverrules.Clear();
break;
}
@@ -131,10 +150,23 @@ namespace Empostor.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.
@@ -160,6 +192,12 @@ namespace Empostor.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);
}
@@ -220,6 +258,87 @@ namespace Empostor.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))
@@ -286,13 +405,35 @@ namespace Empostor.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 Empostor.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; }
@@ -44,6 +44,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();
@@ -81,7 +81,7 @@ namespace Empostor.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))
{
@@ -96,10 +96,10 @@ namespace Empostor.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)
@@ -120,10 +120,12 @@ namespace Empostor.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))
{
return (false, null);
+5 -1
View File
@@ -46,7 +46,8 @@ namespace Empostor.Server.Net.State
IEventManager eventManager,
ICompatibilityManager compatibilityManager,
IOptions<CompatibilityConfig> compatibilityConfig,
IOptions<TimeoutConfig> timeoutConfig)
IOptions<TimeoutConfig> timeoutConfig,
Guid? modGuid = null)
{
_logger = logger;
_serviceProvider = serviceProvider;
@@ -60,6 +61,7 @@ namespace Empostor.Server.Net.State
GameNet = new GameNet();
Options = options;
FilterOptions = filterOptions;
ModGuid = modGuid;
_clientManager = clientManager;
_eventManager = eventManager;
_compatibilityManager = compatibilityManager;
@@ -96,6 +98,8 @@ namespace Empostor.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)
@@ -53,7 +53,7 @@ namespace Empostor.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;
}