]> git.deb.at Git - rhonda/impostor.git/commitdiff
Support new http matchmaking (#685)
authorNikoCat233 <nikocat233@outlook.com>
Thu, 13 Aug 2026 20:03:04 +0000 (04:03 +0800)
committerGitHub <noreply@github.com>
Thu, 13 Aug 2026 20:03:04 +0000 (22:03 +0200)
* 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 <mini@duikbo.at>
27 files changed:
src/Impostor.Api.Innersloth.Generator/SourceGenerator.cs
src/Impostor.Api/Innersloth/Data/enums/BoolOptionNames.json [new file with mode: 0644]
src/Impostor.Api/Innersloth/Data/enums/ByteOptionNames.json [new file with mode: 0644]
src/Impostor.Api/Innersloth/Data/enums/CategorizedOptionNames.json [new file with mode: 0644]
src/Impostor.Api/Innersloth/Data/enums/FloatArrayOptionNames.json [new file with mode: 0644]
src/Impostor.Api/Innersloth/Data/enums/FloatOptionNames.json [new file with mode: 0644]
src/Impostor.Api/Innersloth/Data/enums/Int32ArrayOptionNames.json [new file with mode: 0644]
src/Impostor.Api/Innersloth/Data/enums/Int32OptionNames.json [new file with mode: 0644]
src/Impostor.Api/Innersloth/Data/enums/StringNames.json
src/Impostor.Api/Innersloth/Data/enums/UInt32OptionNames.json [new file with mode: 0644]
src/Impostor.Api/Innersloth/Data/info.json
src/Impostor.Api/Innersloth/GameFilters/BoolGameFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/CategorizedGameFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/ChatModeGameFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/GameFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/GameFilterSet.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/GameFiltersList.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/ISubFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/IntGameFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/LanguageFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/MapGameFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameFilters/PlatformGameFilter.cs [new file with mode: 0644]
src/Impostor.Api/Innersloth/GameOptions/IGameOptions.cs
src/Impostor.Api/Innersloth/GameOptions/LegacyGameOptionsData.cs
src/Impostor.Server/Http/FiltersController.cs [new file with mode: 0644]
src/Impostor.Server/Http/GamesController.cs
src/Impostor.Server/Http/ListingManager.cs

index f385006e67ba2f3c9d5d18e56cbf50ffe6060fb5..c9feb409f2e2d8a52e95373c587c8406092752a5 100644 (file)
@@ -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 (file)
index 0000000..6550806
--- /dev/null
@@ -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 (file)
index 0000000..162f1de
--- /dev/null
@@ -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 (file)
index 0000000..128022c
--- /dev/null
@@ -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 (file)
index 0000000..5831768
--- /dev/null
@@ -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 (file)
index 0000000..3548165
--- /dev/null
@@ -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 (file)
index 0000000..679cd57
--- /dev/null
@@ -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 (file)
index 0000000..5c8006e
--- /dev/null
@@ -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
index 89728807d783437d43f3cd9ea2924164af412a5d..db9526999d5e72f04bd6dc6567413ad51295624a 100644 (file)
   "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 (file)
index 0000000..ab6933b
--- /dev/null
@@ -0,0 +1,4 @@
+{
+  "Invalid": 0,
+  "Keywords": 1
+}
\ No newline at end of file
index 2707152f0aa4a3598885be0a8ea5e9a33c60f6a6..d3614c51bbd4f4c52aed8922667408a83819c8c6 100644 (file)
@@ -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 (file)
index 0000000..412ee16
--- /dev/null
@@ -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<bool> 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 (file)
index 0000000..07d3bbf
--- /dev/null
@@ -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<int> 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 (file)
index 0000000..77399f5
--- /dev/null
@@ -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 (file)
index 0000000..43e7c91
--- /dev/null
@@ -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<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");
+                    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 (file)
index 0000000..db77307
--- /dev/null
@@ -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<GameFilter> filters)
+        {
+            GameMode = gameMode;
+            Filters = filters;
+        }
+
+        [JsonPropertyName("GameMode")]
+        public required GameModes GameMode { get; set; }
+
+        [JsonPropertyName("Filters")]
+        public required List<GameFilter> 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 (file)
index 0000000..984e368
--- /dev/null
@@ -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<GameFilterSet> 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 (file)
index 0000000..e6a2a41
--- /dev/null
@@ -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 (file)
index 0000000..faf3bec
--- /dev/null
@@ -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<int> 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 (file)
index 0000000..024f7ed
--- /dev/null
@@ -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 (file)
index 0000000..38e37e9
--- /dev/null
@@ -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 (file)
index 0000000..971a685
--- /dev/null
@@ -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; }
+    }
+}
index fd6049580f9c355f506bc2660ba39813d95d9d71..56a54b633c12e00d949a4385c34fc265d2fa723a 100644 (file)
@@ -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)
index 25862c4633b1c766ca4c38e2af1221157483cd51..07388936d31f367897a21677f6c7f40d148cfe27 100644 (file)
@@ -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);
diff --git a/src/Impostor.Server/Http/FiltersController.cs b/src/Impostor.Server/Http/FiltersController.cs
new file mode 100644 (file)
index 0000000..fab195c
--- /dev/null
@@ -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<TokenController.Token>(Convert.FromBase64String(authorization.Parameter));
+        }
+        catch
+        {
+            return BadRequest();
+        }
+
+        return Ok(new
+        {
+            filters = Value,
+        });
+    }
+}
index 2f0eecbd198c1eae2087e43bacc7734bdf0a8f55..1fa72f9912161fc3097bc3c70495abad87d7067a 100644 (file)
@@ -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<GamesController> _logger;
 
     /// <summary>
     /// Initializes a new instance of the <see cref="GamesController"/> class.
@@ -32,12 +37,13 @@ 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>
-    public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions<ServerConfig> serverConfig)
+    public GamesController(IGameManager gameManager, ListingManager listingManager, IOptions<ServerConfig> serverConfig, ILogger<GamesController> logger)
     {
         _gameManager = gameManager;
         _listingManager = listingManager;
         var config = serverConfig.Value;
         _hostServer = HostServer.From(IPAddress.Parse(config.ResolvePublicIp()), config.PublicPort);
+        _logger = logger;
     }
 
     /// <summary>
@@ -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<GameListing>(),
-            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 = 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
index 27867d74ce2728e4970107a771e9857b298ce629..c2d0cee11758ee4a8bcfb1393707bebf5abe5507 100644 (file)
@@ -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<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;
+                }
+
+                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)