From 1cd298608a82d9d06423a7b4ff496e82cbb6f427 Mon Sep 17 00:00:00 2001 From: NikoCat233 Date: Fri, 14 Aug 2026 04:03:04 +0800 Subject: [PATCH] Support new http matchmaking (#685) * Add GameFilters interface * Add Newtonsoft.Json Used by InnerSloth * Port 2025.3.25 Data * Generate OptionNames * Fix missing CategorizedOptionNames * Remove Newtonsoft.Json * Add missing GameFilterList * Basic support for new filters * Add FiltersController Server need to tell clients its filters * Add Tag filter in filtergames * Add GameTags in legacy game options * Correctly deserialize Filters * Minor text fixes --------- Co-authored-by: miniduikboot --- .../SourceGenerator.cs | 8 ++ .../Data/enums/BoolOptionNames.json | 17 +++ .../Data/enums/ByteOptionNames.json | 4 + .../Data/enums/CategorizedOptionNames.json | 14 +++ .../Data/enums/FloatArrayOptionNames.json | 4 + .../Data/enums/FloatOptionNames.json | 28 +++++ .../Data/enums/Int32ArrayOptionNames.json | 5 + .../Data/enums/Int32OptionNames.json | 21 ++++ .../Innersloth/Data/enums/StringNames.json | 2 +- .../Data/enums/UInt32OptionNames.json | 4 + src/Impostor.Api/Innersloth/Data/info.json | 4 +- .../Innersloth/GameFilters/BoolGameFilter.cs | 19 +++ .../GameFilters/CategorizedGameFilter.cs | 19 +++ .../GameFilters/ChatModeGameFilter.cs | 15 +++ .../Innersloth/GameFilters/GameFilter.cs | 84 +++++++++++++ .../Innersloth/GameFilters/GameFilterSet.cs | 23 ++++ .../Innersloth/GameFilters/GameFiltersList.cs | 13 ++ .../Innersloth/GameFilters/ISubFilter.cs | 7 ++ .../Innersloth/GameFilters/IntGameFilter.cs | 19 +++ .../Innersloth/GameFilters/LanguageFilter.cs | 15 +++ .../Innersloth/GameFilters/MapGameFilter.cs | 15 +++ .../GameFilters/PlatformGameFilter.cs | 15 +++ .../Innersloth/GameOptions/IGameOptions.cs | 2 + .../GameOptions/LegacyGameOptionsData.cs | 2 + src/Impostor.Server/Http/FiltersController.cs | 56 +++++++++ src/Impostor.Server/Http/GamesController.cs | 66 ++++++++-- src/Impostor.Server/Http/ListingManager.cs | 118 ++++++++++++++++++ 27 files changed, 584 insertions(+), 15 deletions(-) create mode 100644 src/Impostor.Api/Innersloth/Data/enums/BoolOptionNames.json create mode 100644 src/Impostor.Api/Innersloth/Data/enums/ByteOptionNames.json create mode 100644 src/Impostor.Api/Innersloth/Data/enums/CategorizedOptionNames.json create mode 100644 src/Impostor.Api/Innersloth/Data/enums/FloatArrayOptionNames.json create mode 100644 src/Impostor.Api/Innersloth/Data/enums/FloatOptionNames.json create mode 100644 src/Impostor.Api/Innersloth/Data/enums/Int32ArrayOptionNames.json create mode 100644 src/Impostor.Api/Innersloth/Data/enums/Int32OptionNames.json create mode 100644 src/Impostor.Api/Innersloth/Data/enums/UInt32OptionNames.json create mode 100644 src/Impostor.Api/Innersloth/GameFilters/BoolGameFilter.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/CategorizedGameFilter.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/ChatModeGameFilter.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/GameFilter.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/GameFilterSet.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/GameFiltersList.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/ISubFilter.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/IntGameFilter.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/LanguageFilter.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/MapGameFilter.cs create mode 100644 src/Impostor.Api/Innersloth/GameFilters/PlatformGameFilter.cs create mode 100644 src/Impostor.Server/Http/FiltersController.cs diff --git a/src/Impostor.Api.Innersloth.Generator/SourceGenerator.cs b/src/Impostor.Api.Innersloth.Generator/SourceGenerator.cs index f385006..c9feb40 100644 --- a/src/Impostor.Api.Innersloth.Generator/SourceGenerator.cs +++ b/src/Impostor.Api.Innersloth.Generator/SourceGenerator.cs @@ -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", "Impostor.Api.Innersloth.GameFilters"); + enumGenerator.Generate("ByteOptionNames", "Impostor.Api.Innersloth.GameFilters"); + enumGenerator.Generate("CategorizedOptionNames", "Impostor.Api.Innersloth.GameFilters"); + enumGenerator.Generate("FloatArrayOptionNames", "Impostor.Api.Innersloth.GameFilters"); + enumGenerator.Generate("FloatOptionNames", "Impostor.Api.Innersloth.GameFilters"); + enumGenerator.Generate("Int32ArrayOptionNames", "Impostor.Api.Innersloth.GameFilters"); + enumGenerator.Generate("Int32OptionNames", "Impostor.Api.Innersloth.GameFilters"); + enumGenerator.Generate("UInt32OptionNames", "Impostor.Api.Innersloth.GameFilters"); enumGenerator.Generate("RpcCalls", "Impostor.Api.Net.Inner", underlyingType: CSharpEnumUnderlyingType.Byte); diff --git a/src/Impostor.Api/Innersloth/Data/enums/BoolOptionNames.json b/src/Impostor.Api/Innersloth/Data/enums/BoolOptionNames.json new file mode 100644 index 0000000..6550806 --- /dev/null +++ b/src/Impostor.Api/Innersloth/Data/enums/BoolOptionNames.json @@ -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 +} \ No newline at end of file diff --git a/src/Impostor.Api/Innersloth/Data/enums/ByteOptionNames.json b/src/Impostor.Api/Innersloth/Data/enums/ByteOptionNames.json new file mode 100644 index 0000000..162f1de --- /dev/null +++ b/src/Impostor.Api/Innersloth/Data/enums/ByteOptionNames.json @@ -0,0 +1,4 @@ +{ + "Invalid": 0, + "MapId": 1 +} \ No newline at end of file diff --git a/src/Impostor.Api/Innersloth/Data/enums/CategorizedOptionNames.json b/src/Impostor.Api/Innersloth/Data/enums/CategorizedOptionNames.json new file mode 100644 index 0000000..128022c --- /dev/null +++ b/src/Impostor.Api/Innersloth/Data/enums/CategorizedOptionNames.json @@ -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 +} \ No newline at end of file diff --git a/src/Impostor.Api/Innersloth/Data/enums/FloatArrayOptionNames.json b/src/Impostor.Api/Innersloth/Data/enums/FloatArrayOptionNames.json new file mode 100644 index 0000000..5831768 --- /dev/null +++ b/src/Impostor.Api/Innersloth/Data/enums/FloatArrayOptionNames.json @@ -0,0 +1,4 @@ +{ + "Invalid": 0, + "KillDistances": 1 +} \ No newline at end of file diff --git a/src/Impostor.Api/Innersloth/Data/enums/FloatOptionNames.json b/src/Impostor.Api/Innersloth/Data/enums/FloatOptionNames.json new file mode 100644 index 0000000..3548165 --- /dev/null +++ b/src/Impostor.Api/Innersloth/Data/enums/FloatOptionNames.json @@ -0,0 +1,28 @@ +{ + "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 +} \ No newline at end of file diff --git a/src/Impostor.Api/Innersloth/Data/enums/Int32ArrayOptionNames.json b/src/Impostor.Api/Innersloth/Data/enums/Int32ArrayOptionNames.json new file mode 100644 index 0000000..679cd57 --- /dev/null +++ b/src/Impostor.Api/Innersloth/Data/enums/Int32ArrayOptionNames.json @@ -0,0 +1,5 @@ +{ + "Invalid": 0, + "MaxImpostors": 1, + "MinPlayers": 2 +} \ No newline at end of file diff --git a/src/Impostor.Api/Innersloth/Data/enums/Int32OptionNames.json b/src/Impostor.Api/Innersloth/Data/enums/Int32OptionNames.json new file mode 100644 index 0000000..5c8006e --- /dev/null +++ b/src/Impostor.Api/Innersloth/Data/enums/Int32OptionNames.json @@ -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 +} \ No newline at end of file diff --git a/src/Impostor.Api/Innersloth/Data/enums/StringNames.json b/src/Impostor.Api/Innersloth/Data/enums/StringNames.json index 8972880..db95269 100644 --- a/src/Impostor.Api/Innersloth/Data/enums/StringNames.json +++ b/src/Impostor.Api/Innersloth/Data/enums/StringNames.json @@ -1624,4 +1624,4 @@ "DetectiveNotesTitlePostIt": 4080, "DetectiveNotesPostIt": 4081, "DetectiveNotesSuspectNumber": 4082 -} \ No newline at end of file +} diff --git a/src/Impostor.Api/Innersloth/Data/enums/UInt32OptionNames.json b/src/Impostor.Api/Innersloth/Data/enums/UInt32OptionNames.json new file mode 100644 index 0000000..ab6933b --- /dev/null +++ b/src/Impostor.Api/Innersloth/Data/enums/UInt32OptionNames.json @@ -0,0 +1,4 @@ +{ + "Invalid": 0, + "Keywords": 1 +} \ No newline at end of file diff --git a/src/Impostor.Api/Innersloth/Data/info.json b/src/Impostor.Api/Innersloth/Data/info.json index 2707152..d3614c5 100644 --- a/src/Impostor.Api/Innersloth/Data/info.json +++ b/src/Impostor.Api/Innersloth/Data/info.json @@ -1,5 +1,5 @@ { "dumpostorVersion": "1.0.1-dev", - "gameVersion": "2025.10.14", + "gameVersion": "2024.10.29", "platformType": "StandaloneItch" -} \ No newline at end of file +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/BoolGameFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/BoolGameFilter.cs new file mode 100644 index 0000000..412ee16 --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/BoolGameFilter.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class BoolGameFilter : ISubFilter + { + [JsonPropertyName("FilterType")] + public string FilterType { get; } = "bool"; + + [JsonPropertyName("AcceptedValues")] + public required List AcceptedValues { get; set; } + + [JsonPropertyName("OptionEnum")] + public required BoolOptionNames OptionEnum { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/CategorizedGameFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/CategorizedGameFilter.cs new file mode 100644 index 0000000..07d3bbf --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/CategorizedGameFilter.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class CategorizedGameFilter : ISubFilter + { + [JsonPropertyName("FilterType")] + public string FilterType { get; } = "cat"; + + [JsonPropertyName("AcceptedValues")] + public required List AcceptedValues { get; set; } + + [JsonPropertyName("OptionEnum")] + public required CategorizedOptionNames OptionEnum { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/ChatModeGameFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/ChatModeGameFilter.cs new file mode 100644 index 0000000..77399f5 --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/ChatModeGameFilter.cs @@ -0,0 +1,15 @@ +using System; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class ChatModeGameFilter : ISubFilter + { + [JsonPropertyName("FilterType")] + public string FilterType { get; } = "chat"; + + [JsonPropertyName("AcceptedValues")] + public required byte AcceptedValues { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/GameFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/GameFilter.cs new file mode 100644 index 0000000..43e7c91 --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/GameFilter.cs @@ -0,0 +1,84 @@ +using System; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Impostor.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(filterString) + ?? throw new InvalidOperationException("Deserialization returned null for IntGameFilter"); + case "platform": + return JsonSerializer.Deserialize(filterString) + ?? throw new InvalidOperationException("Deserialization returned null for PlatformGameFilter"); + case "cat": + return JsonSerializer.Deserialize(filterString) + ?? throw new InvalidOperationException("Deserialization returned null for CategorizedGameFilter"); + case "languages": + return JsonSerializer.Deserialize(filterString) + ?? throw new InvalidOperationException("Deserialization returned null for LanguageFilter"); + case "chat": + return JsonSerializer.Deserialize(filterString) + ?? throw new InvalidOperationException("Deserialization returned null for ChatModeGameFilter"); + case "map": + return JsonSerializer.Deserialize(filterString) + ?? throw new InvalidOperationException("Deserialization returned null for MapGameFilter"); + case "bool": + return JsonSerializer.Deserialize(filterString) + ?? throw new InvalidOperationException("Deserialization returned null for BoolGameFilter"); + default: + throw new InvalidOperationException("No type matches subfilter"); + } + } + + throw new InvalidOperationException("Must provide type for sub filter"); + } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/GameFilterSet.cs b/src/Impostor.Api/Innersloth/GameFilters/GameFilterSet.cs new file mode 100644 index 0000000..db77307 --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/GameFilterSet.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class GameFilterSet + { + [JsonConstructor] + public GameFilterSet(GameModes gameMode, List filters) + { + GameMode = gameMode; + Filters = filters; + } + + [JsonPropertyName("GameMode")] + public required GameModes GameMode { get; set; } + + [JsonPropertyName("Filters")] + public required List Filters { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/GameFiltersList.cs b/src/Impostor.Api/Innersloth/GameFilters/GameFiltersList.cs new file mode 100644 index 0000000..984e368 --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/GameFiltersList.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class GameFiltersList + { + [JsonPropertyName("FilterSets")] + public required List FilterSets { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/ISubFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/ISubFilter.cs new file mode 100644 index 0000000..e6a2a41 --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/ISubFilter.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Innersloth.GameFilters +{ + public interface ISubFilter + { + string FilterType { get; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/IntGameFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/IntGameFilter.cs new file mode 100644 index 0000000..faf3bec --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/IntGameFilter.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class IntGameFilter : ISubFilter + { + [JsonPropertyName("FilterType")] + public string FilterType { get; } = "int"; + + [JsonPropertyName("AcceptedValues")] + public required List AcceptedValues { get; set; } + + [JsonPropertyName("OptionEnum")] + public required Int32OptionNames OptionEnum { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/LanguageFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/LanguageFilter.cs new file mode 100644 index 0000000..024f7ed --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/LanguageFilter.cs @@ -0,0 +1,15 @@ +using System; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class LanguageFilter : ISubFilter + { + [JsonPropertyName("FilterType")] + public string FilterType { get; } = "languages"; + + [JsonPropertyName("AcceptedValues")] + public required uint AcceptedValues { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/MapGameFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/MapGameFilter.cs new file mode 100644 index 0000000..38e37e9 --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/MapGameFilter.cs @@ -0,0 +1,15 @@ +using System; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class MapGameFilter : ISubFilter + { + [JsonPropertyName("FilterType")] + public string FilterType { get; } = "map"; + + [JsonPropertyName("AcceptedValues")] + public required byte AcceptedValues { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameFilters/PlatformGameFilter.cs b/src/Impostor.Api/Innersloth/GameFilters/PlatformGameFilter.cs new file mode 100644 index 0000000..971a685 --- /dev/null +++ b/src/Impostor.Api/Innersloth/GameFilters/PlatformGameFilter.cs @@ -0,0 +1,15 @@ +using System; +using System.Text.Json.Serialization; + +namespace Impostor.Api.Innersloth.GameFilters +{ + [Serializable] + public class PlatformGameFilter : ISubFilter + { + [JsonPropertyName("FilterType")] + public string FilterType { get; } = "platform"; + + [JsonPropertyName("AcceptedValues")] + public required uint AcceptedValues { get; set; } + } +} diff --git a/src/Impostor.Api/Innersloth/GameOptions/IGameOptions.cs b/src/Impostor.Api/Innersloth/GameOptions/IGameOptions.cs index fd60495..56a54b6 100644 --- a/src/Impostor.Api/Innersloth/GameOptions/IGameOptions.cs +++ b/src/Impostor.Api/Innersloth/GameOptions/IGameOptions.cs @@ -49,6 +49,8 @@ public interface IGameOptions /// public bool IsDefaults { get; set; } + public GameTags Tag { get; set; } + public void Serialize(IMessageWriter writer); public static void EnsureVersionIsModular(byte version) diff --git a/src/Impostor.Api/Innersloth/GameOptions/LegacyGameOptionsData.cs b/src/Impostor.Api/Innersloth/GameOptions/LegacyGameOptionsData.cs index 25862c4..0738893 100644 --- a/src/Impostor.Api/Innersloth/GameOptions/LegacyGameOptionsData.cs +++ b/src/Impostor.Api/Innersloth/GameOptions/LegacyGameOptionsData.cs @@ -145,6 +145,8 @@ public class LegacyGameOptionsData : IGameOptions /// 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); diff --git a/src/Impostor.Server/Http/FiltersController.cs b/src/Impostor.Server/Http/FiltersController.cs new file mode 100644 index 0000000..fab195c --- /dev/null +++ b/src/Impostor.Server/Http/FiltersController.cs @@ -0,0 +1,56 @@ +using System; +using System.Net.Http.Headers; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; + +namespace Impostor.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.Token? token = null; + try + { + token = JsonSerializer.Deserialize(Convert.FromBase64String(authorization.Parameter)); + } + catch + { + return BadRequest(); + } + + return Ok(new + { + filters = Value, + }); + } +} diff --git a/src/Impostor.Server/Http/GamesController.cs b/src/Impostor.Server/Http/GamesController.cs index 2f0eecb..1fa72f9 100644 --- a/src/Impostor.Server/Http/GamesController.cs +++ b/src/Impostor.Server/Http/GamesController.cs @@ -5,12 +5,16 @@ using System.Net; using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Serialization; +using System.Web; using Impostor.Api.Config; 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; using Microsoft.Extensions.Options; namespace Impostor.Server.Http; @@ -25,6 +29,7 @@ public sealed class GamesController : ControllerBase private readonly IGameManager _gameManager; private readonly ListingManager _listingManager; private readonly HostServer _hostServer; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -32,12 +37,13 @@ public sealed class GamesController : ControllerBase /// GameManager containing a list of games. /// ListingManager responsible for filtering. /// Impostor configuration section containing the public ip address of this server. - public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions serverConfig) + public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions serverConfig, ILogger logger) { _gameManager = gameManager; _listingManager = listingManager; var config = serverConfig.Value; _hostServer = HostServer.From(IPAddress.Parse(config.ResolvePublicIp()), config.PublicPort); + _logger = logger; } /// @@ -116,20 +122,51 @@ public sealed class GamesController : ControllerBase } [HttpGet("filtered")] - public IActionResult ShowFilteredLobbies() + public IActionResult ShowFilteredLobbies([FromQuery] string filter) { - // TODO: implement this stub - var response = new + if (string.IsNullOrEmpty(filter)) { - games = Array.Empty(), - 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(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 = 0, - }, - }; + return BadRequest(new MatchmakerResponse(new MatchmakerError(DisconnectReason.ServerError, "Invalid filterSets"))); + } + + var filteredGames = _listingManager.FindListingsV2(HttpContext, filtersList); + var gameListings = filteredGames.Select(GameListing.From).ToList(); + + var response = new + { + games = gameListings, + metadata = new + { + allGamesCount = _gameManager.Games.Count(), + matchingGamesCount = gameListings.Count, + }, + }; - return Ok(response); + 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))); + } } private static uint ConvertAddressToNumber(IPAddress address) @@ -177,13 +214,18 @@ public sealed class GamesController : ControllerBase private class MatchmakerError { [SetsRequiredMembers] - public MatchmakerError(DisconnectReason 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 diff --git a/src/Impostor.Server/Http/ListingManager.cs b/src/Impostor.Server/Http/ListingManager.cs index 27867d7..c2d0cee 100644 --- a/src/Impostor.Server/Http/ListingManager.cs +++ b/src/Impostor.Server/Http/ListingManager.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json; using Impostor.Api.Config; using Impostor.Api.Games; using Impostor.Api.Games.Managers; using Impostor.Api.Http; using Impostor.Api.Innersloth; +using Impostor.Api.Innersloth.GameFilters; using Impostor.Api.Net.Manager; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -103,6 +105,122 @@ public sealed class ListingManager } } + public IEnumerable FindListingsV2(HttpContext ctx, GameFiltersList filtersList, int maxListings = 10) + { + if (filtersList == null || filtersList.FilterSets.Count == 0) + { + return Enumerable.Empty(); + } + + var result = new List(); + 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; + } + + 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; + } + + 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) -- 2.39.5