From 09bcb192f39de8b8eee0a716d7c82327d83c30d8 Mon Sep 17 00:00:00 2001 From: Carter Date: Sun, 24 May 2026 13:13:00 -0400 Subject: [PATCH 1/3] Create new branch offering custom game support. --- ServerPickerX/App.axaml.cs | 37 ++--- ServerPickerX/Constants/GameModes.cs | 15 -- ServerPickerX/ServerDefinitions.json | 49 ++++++ ServerPickerX/ServerPickerX.csproj | 7 +- .../CS2PerfectWorldServerDataService.cs | 151 ------------------ .../Servers/ConfiguredServerDataService.cs | 49 ++++++ .../Servers/DeadLockServerDataService.cs | 137 ---------------- ...2ServerDataService.cs => GenericServer.cs} | 50 +++--- .../Servers/MarathonServerDataService.cs | 138 ---------------- ServerPickerX/Services/Servers/ServerData.cs | 12 -- .../Services/Servers/ServerDefinition.cs | 24 +++ .../Servers/ServerDefinitionProvider.cs | 68 ++++++++ .../Services/Settings/JsonSetting.cs | 57 +++---- .../ViewModels/MainWindowViewModel.cs | 52 +++--- ServerPickerX/Views/MainWindow.axaml | 3 - ServerPickerX/Views/MainWindow.axaml.cs | 51 +++--- 16 files changed, 313 insertions(+), 587 deletions(-) delete mode 100644 ServerPickerX/Constants/GameModes.cs create mode 100644 ServerPickerX/ServerDefinitions.json delete mode 100644 ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs create mode 100644 ServerPickerX/Services/Servers/ConfiguredServerDataService.cs delete mode 100644 ServerPickerX/Services/Servers/DeadLockServerDataService.cs rename ServerPickerX/Services/Servers/{CS2ServerDataService.cs => GenericServer.cs} (80%) delete mode 100644 ServerPickerX/Services/Servers/MarathonServerDataService.cs delete mode 100644 ServerPickerX/Services/Servers/ServerData.cs create mode 100644 ServerPickerX/Services/Servers/ServerDefinition.cs create mode 100644 ServerPickerX/Services/Servers/ServerDefinitionProvider.cs diff --git a/ServerPickerX/App.axaml.cs b/ServerPickerX/App.axaml.cs index fef1f92..7abb7af 100644 --- a/ServerPickerX/App.axaml.cs +++ b/ServerPickerX/App.axaml.cs @@ -6,7 +6,6 @@ using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; -using ServerPickerX.Constants; using ServerPickerX.Services.Localizations; using ServerPickerX.Services.Loggers; using ServerPickerX.Services.MessageBoxes; @@ -44,34 +43,24 @@ public override void OnFrameworkInitializationCompleted() serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - // Register concrete services and contionally provide these services through parent interface service resolver - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); + // Register a provider that loads definitions once and expose it as a singleton + serviceCollection.AddSingleton(); + + // Register a factory for IServerDataService using JSON server definitions serviceCollection.AddTransient(serviceProvider => { JsonSetting jsonSetting = serviceProvider.GetRequiredService(); - ILoggerService loggerService = serviceProvider.GetRequiredService(); - try - { - // Factory method may be suitable if more entries are added in the future - return jsonSetting.game_mode switch + var provider = serviceProvider.GetRequiredService(); + var match = provider.GetDefinitionByGameMode(jsonSetting.game_mode); + + if (match != null) { - GameModes.CounterStrike2 => serviceProvider.GetRequiredService(), - GameModes.CounterStrike2PerfectWorld => serviceProvider.GetRequiredService(), - GameModes.Deadlock => serviceProvider.GetRequiredService(), - GameModes.Marathon => serviceProvider.GetRequiredService(), - _ => throw new NotSupportedException($"Unsupported game mode: {jsonSetting.game_mode}") - }; - } catch (NotSupportedException ex) - { - loggerService.LogErrorAsync(ex.Message); + var obj = ActivatorUtilities.CreateInstance(serviceProvider, typeof(ConfiguredServerDataService), match) as IServerDataService; + if (obj != null) return obj; + } - throw; - } - + throw new InvalidOperationException("Failed to create configured server"); }); serviceCollection.AddTransient(); serviceCollection.AddTransient(); @@ -130,4 +119,4 @@ private void DisableAvaloniaDataAnnotationValidation() } } } -} \ No newline at end of file +} diff --git a/ServerPickerX/Constants/GameModes.cs b/ServerPickerX/Constants/GameModes.cs deleted file mode 100644 index 0b92bdd..0000000 --- a/ServerPickerX/Constants/GameModes.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace ServerPickerX.Constants -{ - public static class GameModes - { - public const string CounterStrike2 = "Counter Strike 2"; - public const string CounterStrike2PerfectWorld = "Counter Strike 2 (Perfect World)"; - public const string Deadlock = "Deadlock"; - public const string Marathon = "Marathon"; - - // Read‑only list used as ItemsSource for the Game Mode ComboBox - public static readonly IReadOnlyList All = [ CounterStrike2, CounterStrike2PerfectWorld, Deadlock, Marathon ]; - } -} diff --git a/ServerPickerX/ServerDefinitions.json b/ServerPickerX/ServerDefinitions.json new file mode 100644 index 0000000..3162cba --- /dev/null +++ b/ServerPickerX/ServerDefinitions.json @@ -0,0 +1,49 @@ +{ + "servers": [ + { + "gameMode": "Counter Strike 2", + "id": "cs2", + "displayName": "CS2", + "appId": 730, + "keywordFilterMode": "exclude", + "keywords": ["China"], + "clusterKeywords": ["Hong Kong", "Sweden", "India", "Netherlands"] + }, + { + "gameMode": "Counter Strike 2 (Perfect World)", + "id": "cs2_perfect_world", + "displayName": "CS2 Perfect World", + "appId": 730, + "keywordFilterMode": "include", + "keywords": ["China"], + "clusterKeywords": ["Tencent", "Alibaba", "Perfect World"] + }, + { + "gameMode": "Deadlock", + "id": "deadlock", + "displayName": "Deadlock", + "appId": 1422450, + "keywordFilterMode": "none", + "keywords": [], + "clusterKeywords": ["China", "Hong Kong", "Sweden", "India", "Netherlands"] + }, + { + "gameMode": "Marathon", + "id": "marathon", + "displayName": "Marathon", + "appId": 3065800, + "keywordFilterMode": "none", + "keywords": [], + "clusterKeywords": ["Hong Kong", "Sweden", "India", "Netherlands"] + }, + { + "gameMode": "THE FINALS", + "id": "the_finals", + "displayName": "THE FINALS", + "appId": 2073850, + "keywordFilterMode": "none", + "keywords": [], + "clusterKeywords": ["Sweden", "India", "Washington"] + } + ] +} diff --git a/ServerPickerX/ServerPickerX.csproj b/ServerPickerX/ServerPickerX.csproj index e30a521..4e478c8 100644 --- a/ServerPickerX/ServerPickerX.csproj +++ b/ServerPickerX/ServerPickerX.csproj @@ -31,7 +31,12 @@ - + + + PreserveNewest + Always + + libs\Interop.NetFwTypeLib.dll diff --git a/ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs b/ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs deleted file mode 100644 index b4a56b3..0000000 --- a/ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs +++ /dev/null @@ -1,151 +0,0 @@ -using ServerPickerX.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json.Nodes; -using System.Threading.Tasks; -using ServerPickerX.Services.Loggers; -using ServerPickerX.Services.MessageBoxes; - -namespace ServerPickerX.Services.Servers -{ - public class CS2PerfectWorldServerDataService( - ILoggerService _logger, - IMessageBoxService _messageBoxService, - HttpClient _httpClient - ) : IServerDataService - { - private ServerData _serverData = new(); - - public async Task LoadServersAsync() - { - try - { - var response = await _httpClient.GetAsync("https://api.steampowered.com/ISteamApps/GetSDRConfig/v1/?appid=730"); - - if (!response.IsSuccessStatusCode) - { - throw new Exception( - "Failed to load servers!" + Environment.NewLine + Environment.NewLine + - "- Verify your internet connection or firewall are working and enabled" + Environment.NewLine + - "- Make sure to run the app as admin or with sudo level execution" - ); - } - - using var stream = await response.Content.ReadAsStreamAsync(); - var mainJson = await JsonNode.ParseAsync(stream) as JsonObject; - - if (mainJson?["revision"] == null || mainJson?["pops"] == null) - { - throw new Exception("Server relay data unavailable. Please try again later."); - } - - string revision = mainJson["revision"]!.ToString(); - - _serverData.Revision = revision; - - ProcessServers(mainJson, _serverData); - } - catch (Exception ex) - { - await _logger.LogErrorAsync("Failed to load cs2 perfect world servers", ex.Message); - - await _messageBoxService.ShowMessageBoxAsync("Error", ex.Message); - - return false; - } - - return true; - } - - private void ProcessServers(JsonObject mainJson, ServerData serverData) - { - var unclusteredServers = new List(); - var clusteredServers = new List(); - - foreach (KeyValuePair server in (JsonObject)mainJson["pops"]!) - { - if (server.Value?["relays"] == null) - { - continue; - } - - string serverDescription = server.Value["desc"]!.ToString(); - var whiteListedServerKeyword = GetWhiteListedServerKeywords() - .FirstOrDefault(keyword => serverDescription.Contains(keyword), ""); - - // Skip processing the server that doesn't contain the whitelisted keyword - if (string.IsNullOrEmpty(whiteListedServerKeyword)) - { - continue; - } - - var serverModel = new ServerModel - { - Flag = "/Assets/flags/" + serverDescription + $" ({server.Key}).png", - Name = server.Key, - Description = serverDescription, - }; - - foreach (JsonObject? relay in (JsonArray)server.Value["relays"]!) - { - serverModel.RelayModels.Add(new RelayModel - { - IPv4 = relay!["ipv4"]?.ToString() ?? "" - }); - } - - unclusteredServers.Add(serverModel); - - string clusterName = GetClusterKeywords().FirstOrDefault(keyword => serverDescription.Contains(keyword), ""); - if (!string.IsNullOrEmpty(clusterName)) - { - var clusteredServer = clusteredServers.FirstOrDefault(s => s.Description == clusterName, new ServerModel()); - - clusteredServer.RelayModels.AddRange(serverModel.RelayModels); - - // Initialize a server cluster where relay addresses will be appended - if (string.IsNullOrEmpty(clusteredServer.Description)) - { - clusteredServer.Flag = serverModel.Flag; - clusteredServer.Name = "cluster"; - clusteredServer.Description = clusterName; - - clusteredServers.Add(clusteredServer); - } - } - else - { - clusteredServers.Add(serverModel); - } - } - - serverData.UnclusteredServers = unclusteredServers; - serverData.ClusteredServers = clusteredServers; - } - - public string GetFetchedRevision() - { - return _serverData.Revision; - } - - public ServerData GetServerData() - { - return _serverData; - } - - public List GetClusterKeywords() - { - return - [ - "Tencent", "Alibaba", "Perfect World" - ]; - } - - private List GetWhiteListedServerKeywords() - { - return ["China"]; - } - } -} diff --git a/ServerPickerX/Services/Servers/ConfiguredServerDataService.cs b/ServerPickerX/Services/Servers/ConfiguredServerDataService.cs new file mode 100644 index 0000000..5d841cb --- /dev/null +++ b/ServerPickerX/Services/Servers/ConfiguredServerDataService.cs @@ -0,0 +1,49 @@ +using ServerPickerX.Models; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using ServerPickerX.Services.Loggers; +using ServerPickerX.Services.MessageBoxes; + +namespace ServerPickerX.Services.Servers +{ + public class ConfiguredServerDataService : GenericService + { + private readonly ServerDefinition _definition; + + public ConfiguredServerDataService( + ServerDefinition definition, + ILoggerService logger, + IMessageBoxService messageBoxService, + HttpClient httpClient + ) : base(logger, messageBoxService, httpClient) + { + _definition = definition; + } + + protected override string ResponseUrl => string.Format(_definition.ResponseUrlTemplate, _definition.AppId); + + protected override string ServiceDisplayName => _definition.DisplayName ?? _definition.Id; + + protected override bool IsServerAccepted(string serverDescription) + { + if (_definition.KeywordFilterMode?.ToLower() == "include") + { + return _definition.Keywords.Any(k => serverDescription.Contains(k)); + } + + if (_definition.KeywordFilterMode?.ToLower() == "exclude") + { + return !_definition.Keywords.Any(k => serverDescription.Contains(k)); + } + + return true; + } + + public override List GetClusterKeywords() + { + return _definition.ClusterKeywords ?? new List(); + } + } +} diff --git a/ServerPickerX/Services/Servers/DeadLockServerDataService.cs b/ServerPickerX/Services/Servers/DeadLockServerDataService.cs deleted file mode 100644 index d17beaf..0000000 --- a/ServerPickerX/Services/Servers/DeadLockServerDataService.cs +++ /dev/null @@ -1,137 +0,0 @@ -using ServerPickerX.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json.Nodes; -using System.Threading.Tasks; -using ServerPickerX.Services.Loggers; -using ServerPickerX.Services.MessageBoxes; - -namespace ServerPickerX.Services.Servers -{ - public class DeadLockServerDataService( - ILoggerService _logger, - IMessageBoxService _messageBoxService, - HttpClient _httpClient - ) : IServerDataService - { - private ServerData _serverData = new(); - - public async Task LoadServersAsync() - { - try - { - var response = await _httpClient.GetAsync("https://api.steampowered.com/ISteamApps/GetSDRConfig/v1/?appid=1422450"); - - if (!response.IsSuccessStatusCode) - { - throw new Exception( - "Failed to load servers!" + Environment.NewLine + Environment.NewLine + - "- Verify your internet connection or firewall is enabled and working properly" + Environment.NewLine + - "- Make sure to run the app as admin or with sudo access" - ); - } - - using var stream = await response.Content.ReadAsStreamAsync(); - var mainJson = await JsonNode.ParseAsync(stream) as JsonObject; - - if (mainJson?["revision"] == null || mainJson?["pops"] == null) - { - throw new Exception("Server relay data unavailable. Please try again later."); - } - - string revision = mainJson["revision"]!.ToString(); - - _serverData.Revision = revision; - - ProcessServers(mainJson, _serverData); - } - catch (Exception ex) - { - await _logger.LogErrorAsync("Failed to load deadlock servers", ex.Message); - - await _messageBoxService.ShowMessageBoxAsync("Error", ex.Message); - - return false; - } - - return true; - } - - private void ProcessServers(JsonObject mainJson, ServerData serverData) - { - var unclusteredServers = new List(); - var clusteredServers = new List(); - - foreach (KeyValuePair server in (JsonObject)mainJson["pops"]!) - { - if (server.Value?["relays"] == null) - { - continue; - } - - string serverDescription = server.Value["desc"]!.ToString(); - - var serverModel = new ServerModel - { - Flag = "/Assets/flags/" + serverDescription + $" ({server.Key}).png", - Name = server.Key, - Description = serverDescription, - }; - - foreach (JsonObject? relay in (JsonArray)server.Value["relays"]!) - { - serverModel.RelayModels.Add(new RelayModel - { - IPv4 = relay!["ipv4"]?.ToString() ?? "" - }); - } - - unclusteredServers.Add(serverModel); - - string clusterName = GetClusterKeywords().FirstOrDefault(keyword => serverDescription.Contains(keyword), ""); - if (!string.IsNullOrEmpty(clusterName)) - { - var clusteredServer = clusteredServers.FirstOrDefault(s => s.Description == clusterName, new ServerModel()); - - clusteredServer.RelayModels.AddRange(serverModel.RelayModels); - - if (string.IsNullOrEmpty(clusteredServer.Description)) - { - clusteredServer.Flag = serverModel.Flag; - clusteredServer.Name = "cluster"; - clusteredServer.Description = clusterName; - - clusteredServers.Add(clusteredServer); - } - } - else - { - clusteredServers.Add(serverModel); - } - } - - serverData.UnclusteredServers = unclusteredServers; - serverData.ClusteredServers = clusteredServers; - } - - public string GetFetchedRevision() - { - return _serverData.Revision; - } - - public ServerData GetServerData() - { - return _serverData; - } - - public List GetClusterKeywords() - { - return new List - { - "China", "Hong Kong", "Sweden", "India", "Netherlands" - }; - } - } -} diff --git a/ServerPickerX/Services/Servers/CS2ServerDataService.cs b/ServerPickerX/Services/Servers/GenericServer.cs similarity index 80% rename from ServerPickerX/Services/Servers/CS2ServerDataService.cs rename to ServerPickerX/Services/Servers/GenericServer.cs index e29bc45..054960c 100644 --- a/ServerPickerX/Services/Servers/CS2ServerDataService.cs +++ b/ServerPickerX/Services/Servers/GenericServer.cs @@ -10,19 +10,35 @@ namespace ServerPickerX.Services.Servers { - public class CS2ServerDataService( - ILoggerService _logger, - IMessageBoxService _messageBoxService, - HttpClient _httpClient - ) : IServerDataService + public abstract class GenericService : IServerDataService { + private readonly ILoggerService _logger; + private readonly IMessageBoxService _messageBoxService; + private readonly HttpClient _httpClient; private ServerData _serverData = new(); + protected GenericService( + ILoggerService logger, + IMessageBoxService messageBoxService, + HttpClient httpClient + ) + { + _logger = logger; + _messageBoxService = messageBoxService; + _httpClient = httpClient; + } + + protected abstract string ResponseUrl { get; } + + protected abstract string ServiceDisplayName { get; } + + protected abstract bool IsServerAccepted(string serverDescription); + public async Task LoadServersAsync() { try { - var response = await _httpClient.GetAsync("https://api.steampowered.com/ISteamApps/GetSDRConfig/v1/?appid=730"); + var response = await _httpClient.GetAsync(ResponseUrl); if (!response.IsSuccessStatusCode) { @@ -42,15 +58,13 @@ public async Task LoadServersAsync() } string revision = mainJson["revision"]!.ToString(); - _serverData.Revision = revision; ProcessServers(mainJson, _serverData); } catch (Exception ex) { - await _logger.LogErrorAsync("Failed to load cs2 servers", ex.Message); - + await _logger.LogErrorAsync($"Failed to load {ServiceDisplayName} servers", ex.Message); await _messageBoxService.ShowMessageBoxAsync("Error", ex.Message); return false; @@ -72,10 +86,8 @@ private void ProcessServers(JsonObject mainJson, ServerData serverData) } string serverDescription = server.Value["desc"]!.ToString(); - var excludedServerKeyword = GetExcludedServerKeywords().FirstOrDefault(keyword => serverDescription.Contains(keyword), ""); - // Skip processing the server that contains the excluded keyword - if (!string.IsNullOrEmpty(excludedServerKeyword)) + if (!IsServerAccepted(serverDescription)) { continue; } @@ -104,7 +116,6 @@ private void ProcessServers(JsonObject mainJson, ServerData serverData) clusteredServer.RelayModels.AddRange(serverModel.RelayModels); - // Initialize a server cluster where relay addresses will be appended if (string.IsNullOrEmpty(clusteredServer.Description)) { clusteredServer.Flag = serverModel.Flag; @@ -134,17 +145,6 @@ public ServerData GetServerData() return _serverData; } - public List GetClusterKeywords() - { - return - [ - "Hong Kong", "Sweden", "India", "Netherlands" - ]; - } - - private List GetExcludedServerKeywords() - { - return ["China"]; - } + public abstract List GetClusterKeywords(); } } diff --git a/ServerPickerX/Services/Servers/MarathonServerDataService.cs b/ServerPickerX/Services/Servers/MarathonServerDataService.cs deleted file mode 100644 index a8cb480..0000000 --- a/ServerPickerX/Services/Servers/MarathonServerDataService.cs +++ /dev/null @@ -1,138 +0,0 @@ -using ServerPickerX.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json.Nodes; -using System.Threading.Tasks; -using ServerPickerX.Services.Loggers; -using ServerPickerX.Services.MessageBoxes; - -namespace ServerPickerX.Services.Servers -{ - public class MarathonServerDataService( - ILoggerService _logger, - IMessageBoxService _messageBoxService, - HttpClient _httpClient - ) : IServerDataService - { - private ServerData _serverData = new(); - - public async Task LoadServersAsync() - { - try - { - var response = await _httpClient.GetAsync("https://api.steampowered.com/ISteamApps/GetSDRConfig/v1/?appid=3065800"); - - if (!response.IsSuccessStatusCode) - { - throw new Exception( - "Failed to load servers!" + Environment.NewLine + Environment.NewLine + - "- Verify your internet connection or firewall are working and enabled" + Environment.NewLine + - "- Make sure to run the app as admin or with sudo level execution" - ); - } - - using var stream = await response.Content.ReadAsStreamAsync(); - var mainJson = await JsonNode.ParseAsync(stream) as JsonObject; - - if (mainJson?["revision"] == null || mainJson?["pops"] == null) - { - throw new Exception("Server relay data unavailable. Please try again later."); - } - - string revision = mainJson["revision"]!.ToString(); - - _serverData.Revision = revision; - - ProcessServers(mainJson, _serverData); - } - catch (Exception ex) - { - await _logger.LogErrorAsync("Failed to load marathon servers", ex.Message); - - await _messageBoxService.ShowMessageBoxAsync("Error", ex.Message); - - return false; - } - - return true; - } - - private void ProcessServers(JsonObject mainJson, ServerData serverData) - { - var unclusteredServers = new List(); - var clusteredServers = new List(); - - foreach (KeyValuePair server in (JsonObject)mainJson["pops"]!) - { - if (server.Value?["relays"] == null) - { - continue; - } - - string serverDescription = server.Value["desc"]!.ToString(); - - var serverModel = new ServerModel - { - Flag = "/Assets/flags/" + serverDescription + $" ({server.Key}).png", - Name = server.Key, - Description = serverDescription, - }; - - foreach (JsonObject? relay in (JsonArray)server.Value["relays"]!) - { - serverModel.RelayModels.Add(new RelayModel - { - IPv4 = relay!["ipv4"]?.ToString() ?? "" - }); - } - - unclusteredServers.Add(serverModel); - - string clusterName = GetClusterKeywords().FirstOrDefault(keyword => serverDescription.Contains(keyword), ""); - if (!string.IsNullOrEmpty(clusterName)) - { - var clusteredServer = clusteredServers.FirstOrDefault(s => s.Description == clusterName, new ServerModel()); - - clusteredServer.RelayModels.AddRange(serverModel.RelayModels); - - // Initialize a server cluster where relay addresses will be appended - if (string.IsNullOrEmpty(clusteredServer.Description)) - { - clusteredServer.Flag = serverModel.Flag; - clusteredServer.Name = "cluster"; - clusteredServer.Description = clusterName; - - clusteredServers.Add(clusteredServer); - } - } - else - { - clusteredServers.Add(serverModel); - } - } - - serverData.UnclusteredServers = unclusteredServers; - serverData.ClusteredServers = clusteredServers; - } - - public string GetFetchedRevision() - { - return _serverData.Revision; - } - - public ServerData GetServerData() - { - return _serverData; - } - - public List GetClusterKeywords() - { - return - [ - "Hong Kong", "Sweden", "India", "Netherlands" - ]; - } - } -} diff --git a/ServerPickerX/Services/Servers/ServerData.cs b/ServerPickerX/Services/Servers/ServerData.cs deleted file mode 100644 index 7d4807d..0000000 --- a/ServerPickerX/Services/Servers/ServerData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ServerPickerX.Models; -using System.Collections.Generic; - -namespace ServerPickerX.Services.Servers -{ - public class ServerData - { - public string Revision { get; set; } = string.Empty; - public List UnclusteredServers { get; set; } = []; - public List ClusteredServers { get; set; } = []; - } -} \ No newline at end of file diff --git a/ServerPickerX/Services/Servers/ServerDefinition.cs b/ServerPickerX/Services/Servers/ServerDefinition.cs new file mode 100644 index 0000000..c5c8ec6 --- /dev/null +++ b/ServerPickerX/Services/Servers/ServerDefinition.cs @@ -0,0 +1,24 @@ +using ServerPickerX.Models; +using System.Collections.Generic; + +namespace ServerPickerX.Services.Servers +{ + public class ServerDefinition + { + public string GameMode { get; set; } = ""; + public string Id { get; set; } = ""; + public string DisplayName { get; set; } = ""; + public int AppId { get; set; } + public string KeywordFilterMode { get; set; } = "none"; + public List Keywords { get; set; } = new(); + public List ClusterKeywords { get; set; } = new(); + public string ResponseUrlTemplate { get; set; } = "https://api.steampowered.com/ISteamApps/GetSDRConfig/v1/?appid={0}"; + } + public class ServerData + { + public string Revision { get; set; } = string.Empty; + public List UnclusteredServers { get; set; } = []; + public List ClusteredServers { get; set; } = []; + } + +} \ No newline at end of file diff --git a/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs b/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs new file mode 100644 index 0000000..c03adfa --- /dev/null +++ b/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace ServerPickerX.Services.Servers +{ + public class ServerDefinitionProvider + { + private static readonly ServerDefinitionsFile _definitions = new(); + static ServerDefinitionProvider() + { + var path = Path.Combine(AppContext.BaseDirectory, "ServerDefinitions.json"); + var json = File.ReadAllText(path); + var doc = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + _definitions.Servers = doc?.Servers ?? new List(); + } + + public IReadOnlyList GetDefinitions() => _definitions.Servers.AsReadOnly(); + + public IReadOnlyList GetGameModes() + { + return _definitions.Servers + .Select(definition => definition.GameMode) + .Where(gameMode => !string.IsNullOrWhiteSpace(gameMode)) + .ToList() + .AsReadOnly(); + } + + public ServerDefinition? GetDefinitionByGameMode(string gameMode) + { + return _definitions.Servers.FirstOrDefault(definition => + definition.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)); + } + + public string GetRevisionKeyByGameMode(string gameMode) + { + ServerDefinition? definition = GetDefinitionByGameMode(gameMode); + + if (definition == null) + { + throw new InvalidOperationException($"Unsupported game mode: {gameMode}"); + } + + return definition.AppId.ToString(); + } + + public IReadOnlyList GetGameModesByRevisionKey(string revisionKey) + { + return _definitions.Servers + .Where(definition => definition.AppId.ToString() == revisionKey) + .Select(definition => definition.GameMode) + .Where(gameMode => !string.IsNullOrWhiteSpace(gameMode)) + .ToList() + .AsReadOnly(); + } + + private class ServerDefinitionsFile + { + public List Servers { get; set; } = new(); + } + } +} diff --git a/ServerPickerX/Services/Settings/JsonSetting.cs b/ServerPickerX/Services/Settings/JsonSetting.cs index 589dc86..5c07b99 100644 --- a/ServerPickerX/Services/Settings/JsonSetting.cs +++ b/ServerPickerX/Services/Settings/JsonSetting.cs @@ -1,16 +1,13 @@ -using ServerPickerX.Constants; -using ServerPickerX.Helpers; using ServerPickerX.Models; using ServerPickerX.Services.DependencyInjection; using ServerPickerX.Services.Loggers; using ServerPickerX.Services.MessageBoxes; +using ServerPickerX.Services.Servers; using ServerPickerX.Services.Settings; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -33,11 +30,7 @@ public class JsonSetting : ISetting public virtual string language { set; get; } = "English | en-us"; - public virtual string cs2_server_revision { get; set; } = "-1"; - - public virtual string deadlock_server_revision { get; set; } = "-1"; - - public virtual string marathon_server_revision { get; set; } = "-1"; + public virtual Dictionary server_revisions { get; set; } = new(StringComparer.OrdinalIgnoreCase); public virtual bool is_clustered { get; set; } = false; @@ -97,9 +90,9 @@ public async Task LoadSettingsAsync() game_mode = localSettings.game_mode; language = localSettings.language; - cs2_server_revision = localSettings.cs2_server_revision; - deadlock_server_revision = localSettings.deadlock_server_revision; - marathon_server_revision = localSettings.marathon_server_revision; + server_revisions = localSettings.server_revisions != null + ? new Dictionary(localSettings.server_revisions, StringComparer.OrdinalIgnoreCase) + : new Dictionary(StringComparer.OrdinalIgnoreCase); is_clustered = localSettings.is_clustered; version_check_on_startup = localSettings.version_check_on_startup; server_presets = localSettings.server_presets ?? []; @@ -146,14 +139,12 @@ public async Task GetRevisionByGameModeAsync() { try { - return this.game_mode switch - { - GameModes.CounterStrike2 or GameModes.CounterStrike2PerfectWorld => this.cs2_server_revision, - GameModes.Deadlock => this.deadlock_server_revision, - GameModes.Marathon => this.marathon_server_revision, - _ => throw new NotSupportedException($"Unsupported game mode: {this.game_mode}"), - }; - } catch (NotSupportedException ex) { + string revisionKey = GetCurrentRevisionKey(); + + return server_revisions.TryGetValue(revisionKey, out string? revision) + ? revision + : "-1"; + } catch (InvalidOperationException ex) { await _loggerService.LogErrorAsync("An error has occured while getting server revision by current game mode", ex.Message); throw; @@ -164,24 +155,12 @@ public async Task SetRevisionByGameModeAsync(string revision) { try { - switch (this.game_mode) - { - case GameModes.CounterStrike2 or GameModes.CounterStrike2PerfectWorld: - this.cs2_server_revision = revision; - break; - case GameModes.Deadlock: - this.deadlock_server_revision = revision; - break; - case GameModes.Marathon: - this.marathon_server_revision = revision; - break; - default: - throw new NotSupportedException($"Unsupported game mode: {this.game_mode}"); - }; + string revisionKey = GetCurrentRevisionKey(); + server_revisions[revisionKey] = revision; await this.SaveSettingsAsync(); } - catch (NotSupportedException ex) + catch (InvalidOperationException ex) { await _loggerService.LogErrorAsync("An error has occured while setting server revision by current game mode", ex.Message); @@ -189,6 +168,14 @@ public async Task SetRevisionByGameModeAsync(string revision) } } + private string GetCurrentRevisionKey() + { + ServerDefinitionProvider serverDefinitionProvider = + ServiceLocator.GetRequiredService(); + + return serverDefinitionProvider.GetRevisionKeyByGameMode(this.game_mode); + } + public async Task SetGameModeAsync(string gameMode) { this.game_mode = gameMode; diff --git a/ServerPickerX/ViewModels/MainWindowViewModel.cs b/ServerPickerX/ViewModels/MainWindowViewModel.cs index d5f63d6..4d394ad 100644 --- a/ServerPickerX/ViewModels/MainWindowViewModel.cs +++ b/ServerPickerX/ViewModels/MainWindowViewModel.cs @@ -1,7 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MsBox.Avalonia.Enums; -using ServerPickerX.Constants; using ServerPickerX.Extensions; using ServerPickerX.Models; using ServerPickerX.Services.DependencyInjection; @@ -16,6 +15,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; namespace ServerPickerX.ViewModels @@ -437,42 +437,48 @@ public async Task PruneCurrentGamePresetEntriesAsync() return await PrunePresetEntriesAsync(_jsonSetting.game_mode, _serverDataService.GetServerData()); } - public async Task PruneCounterStrikeFamilyPresetEntriesAsync() + public async Task PruneRelatedGamePresetEntriesAsync() { - // CS2 and Perfect World share one revision bucket, but their filtered server sets differ. - // When either mode syncs, prune the sibling mode too before marking the shared revision current. - if (_jsonSetting.game_mode == GameModes.CounterStrike2) + ServerDefinitionProvider serverDefinitionProvider = + ServiceLocator.GetRequiredService(); + string revisionKey = serverDefinitionProvider.GetRevisionKeyByGameMode(_jsonSetting.game_mode); + IReadOnlyList relatedGameModes = serverDefinitionProvider.GetGameModesByRevisionKey(revisionKey); + + foreach (string relatedGameMode in relatedGameModes.Where(gameMode => + !gameMode.Equals(_jsonSetting.game_mode, StringComparison.OrdinalIgnoreCase))) { - CS2PerfectWorldServerDataService perfectWorldServerDataService = - ServiceLocator.GetRequiredService(); + IServerDataService relatedServerDataService = CreateConfiguredServerDataService(relatedGameMode); - if (!await perfectWorldServerDataService.LoadServersAsync()) + if (!await relatedServerDataService.LoadServersAsync()) { return false; } - await PrunePresetEntriesAsync( - GameModes.CounterStrike2PerfectWorld, - perfectWorldServerDataService.GetServerData() - ); - - return true; + await PrunePresetEntriesAsync(relatedGameMode, relatedServerDataService.GetServerData()); } - CS2ServerDataService counterStrikeServerDataService = - ServiceLocator.GetRequiredService(); + return true; + } + + private IServerDataService CreateConfiguredServerDataService(string gameMode) + { + ServerDefinitionProvider serverDefinitionProvider = + ServiceLocator.GetRequiredService(); + ServerDefinition? serverDefinition = serverDefinitionProvider.GetDefinitionByGameMode(gameMode); - if (!await counterStrikeServerDataService.LoadServersAsync()) + if (serverDefinition == null) { - return false; + throw new InvalidOperationException($"Unsupported game mode: {gameMode}"); } - await PrunePresetEntriesAsync( - GameModes.CounterStrike2, - counterStrikeServerDataService.GetServerData() - ); + HttpClient httpClient = ServiceLocator.GetRequiredService(); - return true; + return new ConfiguredServerDataService( + serverDefinition, + _loggerService, + _messageBoxService, + httpClient + ); } public async Task PrunePresetEntriesAsync(string gameMode, ServerData serverData) diff --git a/ServerPickerX/Views/MainWindow.axaml b/ServerPickerX/Views/MainWindow.axaml index ccb08d9..3e747da 100644 --- a/ServerPickerX/Views/MainWindow.axaml +++ b/ServerPickerX/Views/MainWindow.axaml @@ -5,7 +5,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:converters="clr-namespace:ServerPickerX.Converters" xmlns:userControls="using:ServerPickerX" - xmlns:constants="clr-namespace:ServerPickerX.Constants" xmlns:i="https://github.com/projektanker/icons.avalonia" mc:Ignorable="d" x:Class="ServerPickerX.Views.MainWindow" @@ -97,11 +96,9 @@ x:Name="GameModeComboBox" Classes="game-combobox" SelectionChanged="GameModeComboBox_SelectionChanged" - ItemsSource="{Binding Source={x:Static constants:GameModes.All}}" IsEnabled="{Binding IsOperationAllowed}" ToolTip.Tip="{DynamicResource GameModeToolTip}" ToolTip.ShowDelay="20" - SelectedIndex="0" FontSize="12" VerticalAlignment="Center" Width="158" diff --git a/ServerPickerX/Views/MainWindow.axaml.cs b/ServerPickerX/Views/MainWindow.axaml.cs index e45d5ef..0f89041 100644 --- a/ServerPickerX/Views/MainWindow.axaml.cs +++ b/ServerPickerX/Views/MainWindow.axaml.cs @@ -2,17 +2,19 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; using ServerPickerX.Comparers; -using ServerPickerX.Constants; using ServerPickerX.Models; using ServerPickerX.Services.DependencyInjection; using ServerPickerX.Services.Localizations; using ServerPickerX.Services.Loggers; using ServerPickerX.Services.MessageBoxes; +using ServerPickerX.Services.Servers; using ServerPickerX.Services.Versions; using ServerPickerX.Settings; using ServerPickerX.ViewModels; using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Threading.Tasks; namespace ServerPickerX.Views @@ -43,6 +45,7 @@ public static bool IsDebugBuild private readonly IMessageBoxService _messageBoxService; private readonly IVersionService _versionService; private readonly ILocalizationService _localizationService; + private readonly ServerDefinitionProvider _serverDefinitionProvider; // Parameterless constructor, allows design previewer to create its own instance since it doesn't support DI public MainWindow() @@ -55,6 +58,7 @@ public MainWindow() _messageBoxService = ServiceLocator.GetRequiredService(); _versionService = ServiceLocator.GetRequiredService(); _localizationService = ServiceLocator.GetRequiredService(); + _serverDefinitionProvider = ServiceLocator.GetRequiredService(); } // DI constructor, allows inversion of control and unit tests mocking @@ -74,6 +78,7 @@ ILocalizationService localizationService _versionService = versionService; _jsonSetting = jsonSetting; _localizationService = localizationService; + _serverDefinitionProvider = ServiceLocator.GetRequiredService(); } private async void Window_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) @@ -210,22 +215,24 @@ private async Task ConfigureControls() { try { - switch (_jsonSetting.game_mode) + IReadOnlyList gameModes = _serverDefinitionProvider.GetGameModes(); + + if (gameModes.Count == 0) { - case GameModes.CounterStrike2 or GameModes.CounterStrike2PerfectWorld: - GameModeComboBox.SelectedIndex = !_jsonSetting.game_mode.Contains("Perfect") ? 0 : 1; - break; - case GameModes.Deadlock: - GameModeComboBox.SelectedIndex = 2; - break; - case GameModes.Marathon: - GameModeComboBox.SelectedIndex = 3; - break; - default: - throw new NotSupportedException($"Unsupported game mode: {_jsonSetting.game_mode}"); - }; + throw new InvalidOperationException("No server definitions were found."); + } + + if (!gameModes.Contains(_jsonSetting.game_mode, StringComparer.OrdinalIgnoreCase)) + { + await _jsonSetting.SetGameModeAsync(gameModes[0]); + } + + GameModeComboBox.SelectionChanged -= GameModeComboBox_SelectionChanged; + GameModeComboBox.ItemsSource = gameModes; + GameModeComboBox.SelectedItem = _jsonSetting.game_mode; + GameModeComboBox.SelectionChanged += GameModeComboBox_SelectionChanged; } - catch (NotSupportedException ex) + catch (InvalidOperationException ex) { await _loggerService.LogErrorAsync("An error has occured while setting game mode combo box", ex.Message); @@ -246,12 +253,10 @@ private async Task SyncServersAsync(MainWindowViewModel vm) var fetchedRevision = vm.GetServerDataService().GetFetchedRevision(); - bool isCounterStrikeFamilyGame = _jsonSetting.game_mode is - GameModes.CounterStrike2 or GameModes.CounterStrike2PerfectWorld; - bool hasAffectedPresets = isCounterStrikeFamilyGame - ? _jsonSetting.GetPresetsByGameMode(GameModes.CounterStrike2).Count > 0 || - _jsonSetting.GetPresetsByGameMode(GameModes.CounterStrike2PerfectWorld).Count > 0 - : _jsonSetting.GetPresetsByGameMode(_jsonSetting.game_mode).Count > 0; + string revisionKey = _serverDefinitionProvider.GetRevisionKeyByGameMode(_jsonSetting.game_mode); + IReadOnlyList affectedGameModes = _serverDefinitionProvider.GetGameModesByRevisionKey(revisionKey); + bool hasAffectedPresets = affectedGameModes.Any(gameMode => + _jsonSetting.GetPresetsByGameMode(gameMode).Count > 0); // Store the initial revision without a reset when this game has no saved presets yet. if (localRevision == "-1" && !hasAffectedPresets) @@ -282,9 +287,9 @@ await _messageBoxService.ShowMessageBoxAsync( await vm.PruneCurrentGamePresetEntriesAsync(); - if (isCounterStrikeFamilyGame) + if (affectedGameModes.Count > 1) { - if (!await vm.PruneCounterStrikeFamilyPresetEntriesAsync()) + if (!await vm.PruneRelatedGamePresetEntriesAsync()) { return; } From fee128d38fd8f2aad5e3c7f299a04aa0014b32a0 Mon Sep 17 00:00:00 2001 From: Carter Date: Wed, 27 May 2026 10:39:30 -0400 Subject: [PATCH 2/3] Remove dependency on existing JSON file for build. --- ServerPickerX/ServerPickerX.csproj | 6 -- .../Servers/ServerDefinitionProvider.cs | 87 +++++++++++++++++-- 2 files changed, 78 insertions(+), 15 deletions(-) diff --git a/ServerPickerX/ServerPickerX.csproj b/ServerPickerX/ServerPickerX.csproj index 4e478c8..cd1f1d1 100644 --- a/ServerPickerX/ServerPickerX.csproj +++ b/ServerPickerX/ServerPickerX.csproj @@ -30,12 +30,6 @@ - - - - PreserveNewest - Always - diff --git a/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs b/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs index c03adfa..d5c3754 100644 --- a/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs +++ b/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs @@ -8,17 +8,20 @@ namespace ServerPickerX.Services.Servers { public class ServerDefinitionProvider { - private static readonly ServerDefinitionsFile _definitions = new(); - static ServerDefinitionProvider() + private const string DefinitionsFileName = "ServerDefinitions.json"; + private readonly ServerDefinitionsFile _definitions; + public ServerDefinitionProvider() { - var path = Path.Combine(AppContext.BaseDirectory, "ServerDefinitions.json"); - var json = File.ReadAllText(path); - var doc = JsonSerializer.Deserialize(json, new JsonSerializerOptions + string path = Path.Combine(Environment.CurrentDirectory, DefinitionsFileName); + EnsureDefinitionsFileExists(path); + string json = File.ReadAllText(path); + + ServerDefinitionsFile? doc = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - _definitions.Servers = doc?.Servers ?? new List(); + _definitions = doc ?? new ServerDefinitionsFile(); } public IReadOnlyList GetDefinitions() => _definitions.Servers.AsReadOnly(); @@ -59,10 +62,76 @@ public IReadOnlyList GetGameModesByRevisionKey(string revisionKey) .ToList() .AsReadOnly(); } - - private class ServerDefinitionsFile + + private static void EnsureDefinitionsFileExists(string path) + { + if (File.Exists(path)) { - public List Servers { get; set; } = new(); + return; } + + ServerDefinitionsFile defaults = CreateDefaultDefinitions(); + string json = JsonSerializer.Serialize(defaults, new JsonSerializerOptions + { + WriteIndented = true + }); + + File.WriteAllText(path, json); + } + + private static ServerDefinitionsFile CreateDefaultDefinitions() + { + return new ServerDefinitionsFile + { + Servers = + [ + new ServerDefinition + { + GameMode = "Counter Strike 2", + Id = "cs2", + DisplayName = "CS2", + AppId = 730, + KeywordFilterMode = "exclude", + Keywords = ["China"], + ClusterKeywords = ["Hong Kong", "Sweden", "India", "Netherlands"] + }, + new ServerDefinition + { + GameMode = "Counter Strike 2 (Perfect World)", + Id = "cs2_perfect_world", + DisplayName = "CS2 Perfect World", + AppId = 730, + KeywordFilterMode = "include", + Keywords = ["China"], + ClusterKeywords = ["Tencent", "Alibaba", "Perfect World"] + }, + new ServerDefinition + { + GameMode = "Deadlock", + Id = "deadlock", + DisplayName = "Deadlock", + AppId = 1422450, + KeywordFilterMode = "none", + Keywords = [], + ClusterKeywords = ["China", "Hong Kong", "Sweden", "India", "Netherlands"] + }, + new ServerDefinition + { + GameMode = "Marathon", + Id = "marathon", + DisplayName = "Marathon", + AppId = 3065800, + KeywordFilterMode = "none", + Keywords = [], + ClusterKeywords = ["Hong Kong", "Sweden", "India", "Netherlands"] + } + ] + }; + } + + private class ServerDefinitionsFile + { + public List Servers { get; set; } = new(); + } } } From af5fa5302351f1643850305060f879862dfcaa3e Mon Sep 17 00:00:00 2001 From: FN-FAL113 Date: Wed, 27 May 2026 23:22:21 +0800 Subject: [PATCH 3/3] Refactor server definitions and data services Major refactor of server definitions, provider and data service handling: - Convert ServerDefinitions.json from an object wrapper to a flat JSON array and adjust default creation/serialization. - Replace the GenericService abstraction (deleted GenericServer.cs) with a concrete ConfiguredServerDataService that takes a ServerDefinition, ILoggerService, IMessageBoxService and HttpClient; it now fetches/parses SDR JSON (System.Text.Json), builds ServerData, exposes GetFetchedRevision/GetServerData and includes improved error logging and user messages. - Update DI registration in App.axaml.cs to instantiate ConfiguredServerDataService via ActivatorUtilities and log failures. - Rewrite ServerDefinitionProvider to load a List, add JSON (de)serialization options, create defaults when missing, and rename APIs to reflect appId usage: GetServerDefinitionByGameMode, GetAppIdByGameMode, GetGameModesByAppId. - Update consumers (JsonSetting, MainWindowViewModel, MainWindow.axaml.cs) to use appId-based APIs and revision storage keyed by appId. - Minor documentation edits in AGENTS.md (wording/formatting). This change centralizes server parsing logic, aligns definitions with a simpler JSON layout, and updates the codebase to use appId as the canonical key for revisions and group lookups. --- AGENTS.md | 13 +- ServerPickerX/App.axaml.cs | 24 ++- ServerPickerX/ServerDefinitions.json | 96 ++++++----- .../Servers/ConfiguredServerDataService.cs | 147 ++++++++++++++--- .../Services/Servers/GenericServer.cs | 150 ------------------ .../Services/Servers/ServerDefinition.cs | 4 +- .../Servers/ServerDefinitionProvider.cs | 101 ++++++------ .../Services/Settings/JsonSetting.cs | 17 +- .../ViewModels/MainWindowViewModel.cs | 8 +- ServerPickerX/Views/MainWindow.axaml.cs | 11 +- 10 files changed, 274 insertions(+), 297 deletions(-) delete mode 100644 ServerPickerX/Services/Servers/GenericServer.cs diff --git a/AGENTS.md b/AGENTS.md index 1a414c1..de69391 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md ## App Overview -The app is a Windows/Linux desktop application designed to manage access to global CS2 and Deadlock servers by blocking +A Windows/Linux desktop application designed to manage access to global CS2, Deadlock and other configured game servers by blocking or unblocking specific servers based on their geographic location. The primary function of this tool is server filtering for distributed gaming networks. ## Repository Overview @@ -30,7 +30,7 @@ The output binary is located under `ServerPickerX/bin/Release/net10.0/ **Note**: The repository currently contains no automated tests. When adding -> tests, follow these guidelines: +> **Note**: When adding tests, follow these guidelines: > > * Place test projects in a sibling folder named `Tests`. > * Target the same framework (`net10.0`). @@ -69,7 +68,7 @@ dotnet test --filter "FullyQualifiedName=ServerPickerX.Models.ServerModelTests.P ## Code Style Guidelines | Area | Guideline | |------|-----------| -| **Imports** | System namespaces first, then project namespaces. Keep `using` statements sorted alphabetically and grouped by scope. Remove unused usings with the built‑in IDE refactor. +| **Imports** | System namespaces first, then project namespaces. Keep `using` statements sorted alphabetically and grouped by scope. | **Formatting** | 4 spaces per indentation level; no tabs. End each file with a single newline. Do not leave trailing whitespace on any line. | **Naming** | |   *Public members (classes, methods, properties)* | PascalCase (e.g., `LoadServers`, `ClusterUnclusterServers`). @@ -81,7 +80,7 @@ dotnet test --filter "FullyQualifiedName=ServerPickerX.Models.ServerModelTests.P • Log errors with `FileLoggerService` and use `MessageBoxService` to display errors inside a catch block. | **MVVM Conventions** | |   *ViewModels* | Inherit from `ObservableObject` (CommunityToolkit.Mvvm). Use `[ObservableProperty]` for properties that should notify UI changes. Keep commands as `ICommand` or `RelayCommand`. -|   *Views* | Prefer code‑behind only for view logic that cannot be expressed in XAML, such as dynamic tooltips. Keep view models free of UI references. +|   *Views* | Prefer code‑behind only for view logic that cannot be expressed in XAML, such as dynamic tooltips. Keep view models free of UI references except for displaying errors using messageboxes. | **Resources** | |   *Images* | Store in `Assets/` and reference via pack URIs (`/Assets/...`). |   *Styles* | Define reusable styles in `Styles/*.axaml`. Register new style files by appending inside App.axaml inside ``. @@ -89,7 +88,7 @@ dotnet test --filter "FullyQualifiedName=ServerPickerX.Models.ServerModelTests.P ## Build & CI Checklist - [ ] All tests pass (`dotnet test ServerPickerX.Tests.slnx`). - [ ] Code passes linting (`dotnet format ServerPickerX.slnx --verify-no-changes`). -- [ ] Publish output contains a single executable without unnecessary dependencies except for files `libHarfBuzzSharp.so` and `libSkiaSharp.so`. +- [ ] Publish output contains an executable and other dependencies. ## Other Instructions - If you are unsure how to do something, use `gh_grep` tools to search code examples from GitHub or use `context7` tools to search for project/code documentations diff --git a/ServerPickerX/App.axaml.cs b/ServerPickerX/App.axaml.cs index 7abb7af..c427192 100644 --- a/ServerPickerX/App.axaml.cs +++ b/ServerPickerX/App.axaml.cs @@ -49,18 +49,32 @@ public override void OnFrameworkInitializationCompleted() // Register a factory for IServerDataService using JSON server definitions serviceCollection.AddTransient(serviceProvider => { + ILoggerService loggerService = serviceProvider.GetRequiredService(); JsonSetting jsonSetting = serviceProvider.GetRequiredService(); - var provider = serviceProvider.GetRequiredService(); - var match = provider.GetDefinitionByGameMode(jsonSetting.game_mode); + try + { + // Get server definition by current game mode that contains app related metadata + var serverDefinitionProvider = serviceProvider.GetRequiredService(); + var serverDefinition = serverDefinitionProvider.GetServerDefinitionByGameMode(jsonSetting.game_mode); - if (match != null) + if (serverDefinition != null) { - var obj = ActivatorUtilities.CreateInstance(serviceProvider, typeof(ConfiguredServerDataService), match) as IServerDataService; + // ActivatorUtilities will instantiate a given type and injects dependencies from existing DI container + // while missing dependencies are supplied as manual argument (serverDefinition) + IServerDataService? obj = ActivatorUtilities.CreateInstance(serviceProvider, serverDefinition); + if (obj != null) return obj; } - throw new InvalidOperationException("Failed to create configured server"); + throw new InvalidOperationException("Failed to register service [IServerDataService]"); + } + catch (InvalidOperationException ex) + { + loggerService.LogErrorAsync(ex.Message); + + throw; + } }); serviceCollection.AddTransient(); serviceCollection.AddTransient(); diff --git a/ServerPickerX/ServerDefinitions.json b/ServerPickerX/ServerDefinitions.json index 3162cba..6ee6bac 100644 --- a/ServerPickerX/ServerDefinitions.json +++ b/ServerPickerX/ServerDefinitions.json @@ -1,49 +1,47 @@ -{ - "servers": [ - { - "gameMode": "Counter Strike 2", - "id": "cs2", - "displayName": "CS2", - "appId": 730, - "keywordFilterMode": "exclude", - "keywords": ["China"], - "clusterKeywords": ["Hong Kong", "Sweden", "India", "Netherlands"] - }, - { - "gameMode": "Counter Strike 2 (Perfect World)", - "id": "cs2_perfect_world", - "displayName": "CS2 Perfect World", - "appId": 730, - "keywordFilterMode": "include", - "keywords": ["China"], - "clusterKeywords": ["Tencent", "Alibaba", "Perfect World"] - }, - { - "gameMode": "Deadlock", - "id": "deadlock", - "displayName": "Deadlock", - "appId": 1422450, - "keywordFilterMode": "none", - "keywords": [], - "clusterKeywords": ["China", "Hong Kong", "Sweden", "India", "Netherlands"] - }, - { - "gameMode": "Marathon", - "id": "marathon", - "displayName": "Marathon", - "appId": 3065800, - "keywordFilterMode": "none", - "keywords": [], - "clusterKeywords": ["Hong Kong", "Sweden", "India", "Netherlands"] - }, - { - "gameMode": "THE FINALS", - "id": "the_finals", - "displayName": "THE FINALS", - "appId": 2073850, - "keywordFilterMode": "none", - "keywords": [], - "clusterKeywords": ["Sweden", "India", "Washington"] - } - ] -} +[ + { + "gameMode": "Counter Strike 2", + "id": "cs2", + "displayName": "CS2", + "appId": 730, + "keywordFilterMode": "exclude", + "keywords": [ "China" ], + "clusterKeywords": [ "Hong Kong", "Sweden", "India", "Netherlands" ] + }, + { + "gameMode": "Counter Strike 2 (Perfect World)", + "id": "cs2_perfect_world", + "displayName": "CS2 Perfect World", + "appId": 730, + "keywordFilterMode": "include", + "keywords": [ "China" ], + "clusterKeywords": [ "Tencent", "Alibaba", "Perfect World" ] + }, + { + "gameMode": "Deadlock", + "id": "deadlock", + "displayName": "Deadlock", + "appId": 1422450, + "keywordFilterMode": "none", + "keywords": [], + "clusterKeywords": [ "China", "Hong Kong", "Sweden", "India", "Netherlands" ] + }, + { + "gameMode": "Marathon", + "id": "marathon", + "displayName": "Marathon", + "appId": 3065800, + "keywordFilterMode": "none", + "keywords": [], + "clusterKeywords": [ "Hong Kong", "Sweden", "India", "Netherlands" ] + }, + { + "gameMode": "THE FINALS", + "id": "the_finals", + "displayName": "THE FINALS", + "appId": 2073850, + "keywordFilterMode": "none", + "keywords": [], + "clusterKeywords": [ "Sweden", "India", "Washington" ] + } +] \ No newline at end of file diff --git a/ServerPickerX/Services/Servers/ConfiguredServerDataService.cs b/ServerPickerX/Services/Servers/ConfiguredServerDataService.cs index 5d841cb..47d571d 100644 --- a/ServerPickerX/Services/Servers/ConfiguredServerDataService.cs +++ b/ServerPickerX/Services/Servers/ConfiguredServerDataService.cs @@ -1,49 +1,154 @@ using ServerPickerX.Models; +using ServerPickerX.Services.Loggers; +using ServerPickerX.Services.MessageBoxes; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Text.Json.Nodes; using System.Threading.Tasks; -using ServerPickerX.Services.Loggers; -using ServerPickerX.Services.MessageBoxes; namespace ServerPickerX.Services.Servers { - public class ConfiguredServerDataService : GenericService + public class ConfiguredServerDataService( + ServerDefinition _serverDefinition, + ILoggerService _logger, + IMessageBoxService _messageBoxService, + HttpClient _httpClient + ) : IServerDataService { - private readonly ServerDefinition _definition; - - public ConfiguredServerDataService( - ServerDefinition definition, - ILoggerService logger, - IMessageBoxService messageBoxService, - HttpClient httpClient - ) : base(logger, messageBoxService, httpClient) + private ServerData _serverData = new(); + + public async Task LoadServersAsync() { - _definition = definition; + try + { + var response = await _httpClient.GetAsync(string.Format(_serverDefinition.ResponseUrlTemplate, _serverDefinition.AppId)); + + if (!response.IsSuccessStatusCode) + { + throw new Exception( + "Failed to load servers!" + Environment.NewLine + Environment.NewLine + + "- Verify your internet connection or firewall are working and enabled" + Environment.NewLine + + "- Make sure to run the app as admin or with sudo level execution" + ); + } + + using var stream = await response.Content.ReadAsStreamAsync(); + var mainJson = await JsonNode.ParseAsync(stream) as JsonObject; + + if (mainJson?["revision"] == null || mainJson?["pops"] == null) + { + throw new Exception("Server relay data unavailable. Please try again later."); + } + + string revision = mainJson["revision"]!.ToString(); + + _serverData.Revision = revision; + + ProcessServers(mainJson, _serverData); + } + catch (Exception ex) + { + await _logger.LogErrorAsync($"Failed to load {_serverDefinition.DisplayName ?? _serverDefinition.Id} servers", ex.Message); + await _messageBoxService.ShowMessageBoxAsync("Error", ex.Message); + + return false; + } + + return true; } - protected override string ResponseUrl => string.Format(_definition.ResponseUrlTemplate, _definition.AppId); + private void ProcessServers(JsonObject mainJson, ServerData serverData) + { + var unclusteredServers = new List(); + var clusteredServers = new List(); - protected override string ServiceDisplayName => _definition.DisplayName ?? _definition.Id; + foreach (KeyValuePair server in (JsonObject)mainJson["pops"]!) + { + if (server.Value?["relays"] == null) + { + continue; + } + + string serverDescription = server.Value["desc"]!.ToString(); + + if (!IsServerAccepted(serverDescription)) + { + continue; + } + + var serverModel = new ServerModel + { + Flag = "/Assets/flags/" + serverDescription + $" ({server.Key}).png", + Name = server.Key, + Description = serverDescription, + }; + + foreach (JsonObject? relay in (JsonArray)server.Value["relays"]!) + { + serverModel.RelayModels.Add(new RelayModel + { + IPv4 = relay!["ipv4"]?.ToString() ?? "" + }); + } + + unclusteredServers.Add(serverModel); + + string clusterName = GetClusterKeywords().FirstOrDefault(keyword => serverDescription.Contains(keyword), ""); + if (!string.IsNullOrEmpty(clusterName)) + { + var clusteredServer = clusteredServers.FirstOrDefault(s => s.Description == clusterName, new ServerModel()); + + clusteredServer.RelayModels.AddRange(serverModel.RelayModels); + + if (string.IsNullOrEmpty(clusteredServer.Description)) + { + clusteredServer.Flag = serverModel.Flag; + clusteredServer.Name = "cluster"; + clusteredServer.Description = clusterName; + + clusteredServers.Add(clusteredServer); + } + } + else + { + clusteredServers.Add(serverModel); + } + } + + serverData.UnclusteredServers = unclusteredServers; + serverData.ClusteredServers = clusteredServers; + } + + public string GetFetchedRevision() + { + return _serverData.Revision; + } + + public ServerData GetServerData() + { + return _serverData; + } - protected override bool IsServerAccepted(string serverDescription) + public bool IsServerAccepted(string serverDescription) { - if (_definition.KeywordFilterMode?.ToLower() == "include") + if (_serverDefinition.KeywordFilterMode?.ToLower() == "include") { - return _definition.Keywords.Any(k => serverDescription.Contains(k)); + return _serverDefinition.Keywords.Any(k => serverDescription.Contains(k)); } - if (_definition.KeywordFilterMode?.ToLower() == "exclude") + if (_serverDefinition.KeywordFilterMode?.ToLower() == "exclude") { - return !_definition.Keywords.Any(k => serverDescription.Contains(k)); + return !_serverDefinition.Keywords.Any(k => serverDescription.Contains(k)); } return true; } - public override List GetClusterKeywords() + public List GetClusterKeywords() { - return _definition.ClusterKeywords ?? new List(); + return _serverDefinition.ClusterKeywords ?? []; } } } diff --git a/ServerPickerX/Services/Servers/GenericServer.cs b/ServerPickerX/Services/Servers/GenericServer.cs deleted file mode 100644 index 054960c..0000000 --- a/ServerPickerX/Services/Servers/GenericServer.cs +++ /dev/null @@ -1,150 +0,0 @@ -using ServerPickerX.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text.Json.Nodes; -using System.Threading.Tasks; -using ServerPickerX.Services.Loggers; -using ServerPickerX.Services.MessageBoxes; - -namespace ServerPickerX.Services.Servers -{ - public abstract class GenericService : IServerDataService - { - private readonly ILoggerService _logger; - private readonly IMessageBoxService _messageBoxService; - private readonly HttpClient _httpClient; - private ServerData _serverData = new(); - - protected GenericService( - ILoggerService logger, - IMessageBoxService messageBoxService, - HttpClient httpClient - ) - { - _logger = logger; - _messageBoxService = messageBoxService; - _httpClient = httpClient; - } - - protected abstract string ResponseUrl { get; } - - protected abstract string ServiceDisplayName { get; } - - protected abstract bool IsServerAccepted(string serverDescription); - - public async Task LoadServersAsync() - { - try - { - var response = await _httpClient.GetAsync(ResponseUrl); - - if (!response.IsSuccessStatusCode) - { - throw new Exception( - "Failed to load servers!" + Environment.NewLine + Environment.NewLine + - "- Verify your internet connection or firewall are working and enabled" + Environment.NewLine + - "- Make sure to run the app as admin or with sudo level execution" - ); - } - - using var stream = await response.Content.ReadAsStreamAsync(); - var mainJson = await JsonNode.ParseAsync(stream) as JsonObject; - - if (mainJson?["revision"] == null || mainJson?["pops"] == null) - { - throw new Exception("Server relay data unavailable. Please try again later."); - } - - string revision = mainJson["revision"]!.ToString(); - _serverData.Revision = revision; - - ProcessServers(mainJson, _serverData); - } - catch (Exception ex) - { - await _logger.LogErrorAsync($"Failed to load {ServiceDisplayName} servers", ex.Message); - await _messageBoxService.ShowMessageBoxAsync("Error", ex.Message); - - return false; - } - - return true; - } - - private void ProcessServers(JsonObject mainJson, ServerData serverData) - { - var unclusteredServers = new List(); - var clusteredServers = new List(); - - foreach (KeyValuePair server in (JsonObject)mainJson["pops"]!) - { - if (server.Value?["relays"] == null) - { - continue; - } - - string serverDescription = server.Value["desc"]!.ToString(); - - if (!IsServerAccepted(serverDescription)) - { - continue; - } - - var serverModel = new ServerModel - { - Flag = "/Assets/flags/" + serverDescription + $" ({server.Key}).png", - Name = server.Key, - Description = serverDescription, - }; - - foreach (JsonObject? relay in (JsonArray)server.Value["relays"]!) - { - serverModel.RelayModels.Add(new RelayModel - { - IPv4 = relay!["ipv4"]?.ToString() ?? "" - }); - } - - unclusteredServers.Add(serverModel); - - string clusterName = GetClusterKeywords().FirstOrDefault(keyword => serverDescription.Contains(keyword), ""); - if (!string.IsNullOrEmpty(clusterName)) - { - var clusteredServer = clusteredServers.FirstOrDefault(s => s.Description == clusterName, new ServerModel()); - - clusteredServer.RelayModels.AddRange(serverModel.RelayModels); - - if (string.IsNullOrEmpty(clusteredServer.Description)) - { - clusteredServer.Flag = serverModel.Flag; - clusteredServer.Name = "cluster"; - clusteredServer.Description = clusterName; - - clusteredServers.Add(clusteredServer); - } - } - else - { - clusteredServers.Add(serverModel); - } - } - - serverData.UnclusteredServers = unclusteredServers; - serverData.ClusteredServers = clusteredServers; - } - - public string GetFetchedRevision() - { - return _serverData.Revision; - } - - public ServerData GetServerData() - { - return _serverData; - } - - public abstract List GetClusterKeywords(); - } -} diff --git a/ServerPickerX/Services/Servers/ServerDefinition.cs b/ServerPickerX/Services/Servers/ServerDefinition.cs index c5c8ec6..80a442a 100644 --- a/ServerPickerX/Services/Servers/ServerDefinition.cs +++ b/ServerPickerX/Services/Servers/ServerDefinition.cs @@ -10,8 +10,8 @@ public class ServerDefinition public string DisplayName { get; set; } = ""; public int AppId { get; set; } public string KeywordFilterMode { get; set; } = "none"; - public List Keywords { get; set; } = new(); - public List ClusterKeywords { get; set; } = new(); + public List Keywords { get; set; } = []; + public List ClusterKeywords { get; set; } = []; public string ResponseUrlTemplate { get; set; } = "https://api.steampowered.com/ISteamApps/GetSDRConfig/v1/?appid={0}"; } public class ServerData diff --git a/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs b/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs index d5c3754..6feef3f 100644 --- a/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs +++ b/ServerPickerX/Services/Servers/ServerDefinitionProvider.cs @@ -1,5 +1,8 @@ +using ServerPickerX.Services.Loggers; +using ServerPickerX.Services.MessageBoxes; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; @@ -8,42 +11,70 @@ namespace ServerPickerX.Services.Servers { public class ServerDefinitionProvider { - private const string DefinitionsFileName = "ServerDefinitions.json"; - private readonly ServerDefinitionsFile _definitions; - public ServerDefinitionProvider() + private readonly List _serverDefinitions = []; + private readonly JsonSerializerOptions _jsonDeserializerOptions = new() { - string path = Path.Combine(Environment.CurrentDirectory, DefinitionsFileName); - EnsureDefinitionsFileExists(path); - string json = File.ReadAllText(path); + PropertyNameCaseInsensitive = true, + AllowTrailingCommas = true, + }; + private readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + WriteIndented = true, + AllowTrailingCommas = true, + }; + + [RequiresUnreferencedCode(message: "Deserialization involves reflection which may be trimmed away.")] + public ServerDefinitionProvider(ILoggerService _loggerService) + { + var path = ""; + try + { + path = Path.Combine(AppContext.BaseDirectory, "ServerDefinitions.json"); + + if (!File.Exists(path)) + { + var defaults = CreateDefaultServerDefinitions(); + string serializedJson = JsonSerializer.Serialize(defaults, _jsonSerializerOptions); - ServerDefinitionsFile? doc = JsonSerializer.Deserialize(json, new JsonSerializerOptions + File.WriteAllText(path, serializedJson); + } + } + catch (Exception ex) { - PropertyNameCaseInsensitive = true - }); + _loggerService.LogErrorAsync(ex.Message); - _definitions = doc ?? new ServerDefinitionsFile(); + throw; + } + + var json = File.ReadAllText(path); + + var serverDefinitions = JsonSerializer.Deserialize>(json, _jsonDeserializerOptions); + + _serverDefinitions = serverDefinitions ?? []; } - public IReadOnlyList GetDefinitions() => _definitions.Servers.AsReadOnly(); + public IReadOnlyList GetDefinitions() => _serverDefinitions.AsReadOnly(); public IReadOnlyList GetGameModes() { - return _definitions.Servers + return _serverDefinitions .Select(definition => definition.GameMode) .Where(gameMode => !string.IsNullOrWhiteSpace(gameMode)) .ToList() .AsReadOnly(); } - public ServerDefinition? GetDefinitionByGameMode(string gameMode) + public ServerDefinition? GetServerDefinitionByGameMode(string gameMode) { - return _definitions.Servers.FirstOrDefault(definition => - definition.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)); + return _serverDefinitions + .FirstOrDefault( + definition => definition.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase) + ); } - public string GetRevisionKeyByGameMode(string gameMode) + public string GetAppIdByGameMode(string gameMode) { - ServerDefinition? definition = GetDefinitionByGameMode(gameMode); + ServerDefinition? definition = GetServerDefinitionByGameMode(gameMode); if (definition == null) { @@ -53,37 +84,19 @@ public string GetRevisionKeyByGameMode(string gameMode) return definition.AppId.ToString(); } - public IReadOnlyList GetGameModesByRevisionKey(string revisionKey) + public IReadOnlyList GetGameModesByAppId(string appId) { - return _definitions.Servers - .Where(definition => definition.AppId.ToString() == revisionKey) + return _serverDefinitions + .Where(definition => definition.AppId.ToString() == appId) .Select(definition => definition.GameMode) .Where(gameMode => !string.IsNullOrWhiteSpace(gameMode)) .ToList() .AsReadOnly(); } - - private static void EnsureDefinitionsFileExists(string path) - { - if (File.Exists(path)) - { - return; - } - - ServerDefinitionsFile defaults = CreateDefaultDefinitions(); - string json = JsonSerializer.Serialize(defaults, new JsonSerializerOptions - { - WriteIndented = true - }); - - File.WriteAllText(path, json); - } - private static ServerDefinitionsFile CreateDefaultDefinitions() + private List CreateDefaultServerDefinitions() { - return new ServerDefinitionsFile - { - Servers = + return [ new ServerDefinition { @@ -125,13 +138,7 @@ private static ServerDefinitionsFile CreateDefaultDefinitions() Keywords = [], ClusterKeywords = ["Hong Kong", "Sweden", "India", "Netherlands"] } - ] - }; - } - - private class ServerDefinitionsFile - { - public List Servers { get; set; } = new(); + ]; } } } diff --git a/ServerPickerX/Services/Settings/JsonSetting.cs b/ServerPickerX/Services/Settings/JsonSetting.cs index 5c07b99..c01d2a5 100644 --- a/ServerPickerX/Services/Settings/JsonSetting.cs +++ b/ServerPickerX/Services/Settings/JsonSetting.cs @@ -14,9 +14,8 @@ namespace ServerPickerX.Settings { - // Publishing an app with trimmed assemblies or using AOT compilation for reduced - // build size can limit the serialization functionality since it requires reflection - // to determine dynamic types on runtime which is not possible with trimmed or AOT apps. + // Publishing an app with trimmed assemblies or using AOT compilation for reduced build size + // can break serialization due to limitations when using Reflection which analyzes dynamic types on runtime. // JsonSerializerContext preserves the types and provides serialization metadata on compile-time. [JsonSerializable(typeof(JsonSetting))] internal partial class SourceGenerationContext : JsonSerializerContext { } @@ -139,9 +138,9 @@ public async Task GetRevisionByGameModeAsync() { try { - string revisionKey = GetCurrentRevisionKey(); + string appId = GetCurrentAppId(); - return server_revisions.TryGetValue(revisionKey, out string? revision) + return server_revisions.TryGetValue(appId, out string? revision) ? revision : "-1"; } catch (InvalidOperationException ex) { @@ -155,8 +154,8 @@ public async Task SetRevisionByGameModeAsync(string revision) { try { - string revisionKey = GetCurrentRevisionKey(); - server_revisions[revisionKey] = revision; + string appId = GetCurrentAppId(); + server_revisions[appId] = revision; await this.SaveSettingsAsync(); } @@ -168,12 +167,12 @@ public async Task SetRevisionByGameModeAsync(string revision) } } - private string GetCurrentRevisionKey() + private string GetCurrentAppId() { ServerDefinitionProvider serverDefinitionProvider = ServiceLocator.GetRequiredService(); - return serverDefinitionProvider.GetRevisionKeyByGameMode(this.game_mode); + return serverDefinitionProvider.GetAppIdByGameMode(this.game_mode); } public async Task SetGameModeAsync(string gameMode) diff --git a/ServerPickerX/ViewModels/MainWindowViewModel.cs b/ServerPickerX/ViewModels/MainWindowViewModel.cs index 4d394ad..26fe4fa 100644 --- a/ServerPickerX/ViewModels/MainWindowViewModel.cs +++ b/ServerPickerX/ViewModels/MainWindowViewModel.cs @@ -441,8 +441,10 @@ public async Task PruneRelatedGamePresetEntriesAsync() { ServerDefinitionProvider serverDefinitionProvider = ServiceLocator.GetRequiredService(); - string revisionKey = serverDefinitionProvider.GetRevisionKeyByGameMode(_jsonSetting.game_mode); - IReadOnlyList relatedGameModes = serverDefinitionProvider.GetGameModesByRevisionKey(revisionKey); + + string appId = serverDefinitionProvider.GetAppIdByGameMode(_jsonSetting.game_mode); + + IReadOnlyList relatedGameModes = serverDefinitionProvider.GetGameModesByAppId(appId); foreach (string relatedGameMode in relatedGameModes.Where(gameMode => !gameMode.Equals(_jsonSetting.game_mode, StringComparison.OrdinalIgnoreCase))) @@ -464,7 +466,7 @@ private IServerDataService CreateConfiguredServerDataService(string gameMode) { ServerDefinitionProvider serverDefinitionProvider = ServiceLocator.GetRequiredService(); - ServerDefinition? serverDefinition = serverDefinitionProvider.GetDefinitionByGameMode(gameMode); + ServerDefinition? serverDefinition = serverDefinitionProvider.GetServerDefinitionByGameMode(gameMode); if (serverDefinition == null) { diff --git a/ServerPickerX/Views/MainWindow.axaml.cs b/ServerPickerX/Views/MainWindow.axaml.cs index 0f89041..8a7a69a 100644 --- a/ServerPickerX/Views/MainWindow.axaml.cs +++ b/ServerPickerX/Views/MainWindow.axaml.cs @@ -253,10 +253,13 @@ private async Task SyncServersAsync(MainWindowViewModel vm) var fetchedRevision = vm.GetServerDataService().GetFetchedRevision(); - string revisionKey = _serverDefinitionProvider.GetRevisionKeyByGameMode(_jsonSetting.game_mode); - IReadOnlyList affectedGameModes = _serverDefinitionProvider.GetGameModesByRevisionKey(revisionKey); - bool hasAffectedPresets = affectedGameModes.Any(gameMode => - _jsonSetting.GetPresetsByGameMode(gameMode).Count > 0); + string appId = _serverDefinitionProvider.GetAppIdByGameMode(_jsonSetting.game_mode); + + IReadOnlyList affectedGameModes = _serverDefinitionProvider.GetGameModesByAppId(appId); + + bool hasAffectedPresets = affectedGameModes.Any( + gameMode => _jsonSetting.GetPresetsByGameMode(gameMode).Count > 0 + ); // Store the initial revision without a reset when this game has no saved presets yet. if (localRevision == "-1" && !hasAffectedPresets)