From a61abf7c3e5ff5a465d81a35999a7488b55d0dc8 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 16 Jan 2026 13:47:23 -0500 Subject: [PATCH 1/7] First Working Connection --- .vscode/launch.json | 7 + SS14.Launcher/Assets/Locale/en-US/text.ftl | 2 + .../ServerStatus/ClassicServerListCache.cs | 179 ++++++++++++++++++ .../ServerStatus/ClassicServerStatusData.cs | 19 ++ SS14.Launcher/Program.cs | 1 + .../ClassicServerEntryViewModel.cs | 90 +++++++++ .../ClassicServerListTabViewModel.cs | 52 +++++ .../ViewModels/MainWindowViewModel.cs | 3 + .../ClassicServerEntryView.xaml | 35 ++++ .../ClassicServerEntryView.xaml.cs | 17 ++ .../ClassicServerListTabView.xaml | 26 +++ .../ClassicServerListTabView.xaml.cs | 17 ++ 12 files changed, 448 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs create mode 100644 SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs create mode 100644 SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs create mode 100644 SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerListTabViewModel.cs create mode 100644 SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml create mode 100644 SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml.cs create mode 100644 SS14.Launcher/Views/MainWindowTabs/ClassicServerListTabView.xaml create mode 100644 SS14.Launcher/Views/MainWindowTabs/ClassicServerListTabView.xaml.cs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5c7247b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/SS14.Launcher/Assets/Locale/en-US/text.ftl b/SS14.Launcher/Assets/Locale/en-US/text.ftl index e8fce43..5f46a0d 100644 --- a/SS14.Launcher/Assets/Locale/en-US/text.ftl +++ b/SS14.Launcher/Assets/Locale/en-US/text.ftl @@ -234,6 +234,8 @@ region-short-south-america-west = SA West ## Strings for the "servers" tab tab-servers-title = Servers +tab-servers-classic-title = Classic Servers +tab-servers-classic-desc = All servers listed here connect via the BYOND client. tab-servers-refresh = Refresh filters = Filters ({ $filteredServers } / { $totalServers }) tab-servers-search-watermark = Search For Servers… diff --git a/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs b/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs new file mode 100644 index 0000000..60cd120 --- /dev/null +++ b/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Serilog; +using Splat; +using SS14.Launcher.Utility; + +namespace SS14.Launcher.Models.ServerStatus; + +public sealed class ClassicServerListCache +{ + private readonly HttpClient _http; + private readonly ObservableCollection _allServers = new(); + + public ReadOnlyObservableCollection AllServers { get; } + + public ClassicServerListCache() + { + _http = Locator.Current.GetRequiredService(); + AllServers = new ReadOnlyObservableCollection(_allServers); + } + + public async Task Refresh() + { + try + { + var response = await _http.GetStringAsync("http://www.byond.com/games/exadv1/spacestation13?format=text"); + // Log.Information("BYOND Response: {Response}", response); + await File.WriteAllTextAsync("byond_dump.txt", response); + var servers = ParseByondResponse(response); + + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + _allServers.Clear(); + foreach (var server in servers) + { + _allServers.Add(server); + } + }); + } + catch (Exception e) + { + Log.Error(e, "Failed to fetch Classic SS13 server list."); + } + } + + private List ParseByondResponse(string response) + { + var list = new List(); + using var reader = new StringReader(response); + + string? line; + string? currentName = null; + string? currentUrl = null; + string? currentStatus = null; + int currentPlayers = 0; + + // Simple state machine to parse the text format + // The format uses 'world/ID' blocks for servers. + + bool inServerBlock = false; + + while ((line = reader.ReadLine()) != null) + { + var trimmed = line.Trim(); + if (string.IsNullOrWhiteSpace(trimmed)) continue; + + if (trimmed.StartsWith("world/")) + { + // If we were parsing a server, save it + if (inServerBlock && currentUrl != null) + { + // Name might be missing, try to extract from status or use URL + var name = currentName ?? ExtractNameFromStatus(currentStatus) ?? "Unknown Server"; + list.Add(new ClassicServerStatusData(name, currentUrl, currentPlayers, CleanStatus(currentStatus) ?? "")); + } + + // Reset for new server + inServerBlock = true; + currentName = null; + currentUrl = null; + currentStatus = null; + currentPlayers = 0; + } + else if (inServerBlock) + { + if (trimmed.StartsWith("name =")) + { + currentName = ParseStringValue(trimmed); + } + else if (trimmed.StartsWith("url =")) + { + currentUrl = ParseStringValue(trimmed); + } + else if (trimmed.StartsWith("status =")) + { + currentStatus = ParseStringValue(trimmed); + } + else if (trimmed.StartsWith("players = list(")) + { + // "players = list("Bob","Alice")" + // Just count the commas + 1, correcting for empty list "list()" + var content = trimmed.Substring("players = list(".Length); + if (content.EndsWith(")")) + { + content = content.Substring(0, content.Length - 1); + if (string.IsNullOrWhiteSpace(content)) + { + currentPlayers = 0; + } + else + { + // A simple Count(',') + 1 is risky if names contain commas, but usually they are quoted. + // However, parsing full CSV is safer but 'Splitting by ",' might be enough? + // Let's iterate and count quoted segments. + // Or simpler: Splitting by ',' is mostly fine for SS13 ckeys. + currentPlayers = content.Split(',').Length; + } + } + } + else if (trimmed.StartsWith("players =")) + { + // Fallback for simple number if ever used + var parts = trimmed.Split('='); + if (parts.Length > 1 && int.TryParse(parts[1].Trim(), out var p)) + { + currentPlayers = p; + } + } + } + } + + // Add the last one if exists + if (inServerBlock && currentUrl != null) + { + var name = currentName ?? ExtractNameFromStatus(currentStatus) ?? "Unknown Server"; + list.Add(new ClassicServerStatusData(name, currentUrl, currentPlayers, CleanStatus(currentStatus) ?? "")); + } + + return list; + } + + private string? ExtractNameFromStatus(string? status) + { + if (string.IsNullOrEmpty(status)) return null; + // Usually starts with Name + var match = System.Text.RegularExpressions.Regex.Match(status, @"(.*?)"); + if (match.Success) + { + var raw = match.Groups[1].Value; + // Remove nested tags if any + return System.Text.RegularExpressions.Regex.Replace(raw, "<.*?>", String.Empty); + } + return null; + } + + private string? CleanStatus(string? status) + { + if (string.IsNullOrEmpty(status)) return null; + // Replace
with newlines, remove other tags + var s = status.Replace("
", "\n").Replace("
", "\n"); + return System.Text.RegularExpressions.Regex.Replace(s, "<.*?>", String.Empty).Trim(); + } + + private string ParseStringValue(string line) + { + // format: key = "value" + var idx = line.IndexOf('"'); + if (idx == -1) return string.Empty; + var lastIdx = line.LastIndexOf('"'); + if (lastIdx <= idx) return string.Empty; + + return line.Substring(idx + 1, lastIdx - idx - 1); + } +} diff --git a/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs b/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs new file mode 100644 index 0000000..a0f4525 --- /dev/null +++ b/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace SS14.Launcher.Models.ServerStatus; + +public class ClassicServerStatusData +{ + public string Name { get; set; } = string.Empty; + public string Address { get; set; } = string.Empty; + public int PlayerCount { get; set; } + public string Status { get; set; } = string.Empty; + + public ClassicServerStatusData(string name, string address, int playerCount, string status) + { + Name = name; + Address = address; + PlayerCount = playerCount; + Status = status; + } +} diff --git a/SS14.Launcher/Program.cs b/SS14.Launcher/Program.cs index 12d6c08..81787f0 100644 --- a/SS14.Launcher/Program.cs +++ b/SS14.Launcher/Program.cs @@ -234,6 +234,7 @@ private static AppBuilder BuildAvaloniaApp(DataManager cfg) locator.RegisterConstant(authApi); locator.RegisterConstant(hubApi); locator.RegisterConstant(new ServerListCache()); + locator.RegisterConstant(new ClassicServerListCache()); locator.RegisterConstant(loginManager); locator.RegisterConstant(overrideAssets); locator.RegisterConstant(launcherInfo); diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs new file mode 100644 index 0000000..b71079d --- /dev/null +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using Microsoft.Win32; +using ReactiveUI; +using SS14.Launcher; +using SS14.Launcher.Localization; +using SS14.Launcher.Models.ServerStatus; +using SS14.Launcher.ViewModels; + +namespace SS14.Launcher.ViewModels.MainWindowTabs; + +public class ClassicServerEntryViewModel : ViewModelBase +{ + private readonly ClassicServerStatusData _server; + + public string Name => _server.Name; + public string Address => _server.Address; + public string PlayerCount => _server.PlayerCount.ToString(); + public string Status => _server.Status; + + public ReactiveCommand ConnectCommand { get; } + + public ClassicServerEntryViewModel(ClassicServerStatusData server) + { + _server = server; + + ConnectCommand = ReactiveCommand.Create(Connect); + } + + private void Connect() + { + if (IsByondInstalled()) + { + Helpers.OpenUri(new Uri(_server.Address)); + } + else + { + // Prompt to download + // We can use the native MessageBox helper from Helpers if available or just open the link. + // Following the prompt instructions mostly literally: "prompted to install it first by going to this link" + + // On Windows we can use the MessageBox to be nicer. + // NOTE: Helper MessageBox returns int, 1 is usually OK. + if (OperatingSystem.IsWindows()) + { + var res = Helpers.MessageBoxHelper( + "BYOND not detected. You need the BYOND client to play Space Station 13. Go to download page?", + "BYOND Missing", + 0x00000004 | 0x00000030); // MB_YESNO | MB_ICONWARNING + + if (res == 6) // IDYES + { + Helpers.OpenUri(new Uri("https://www.byond.com/download/")); + } + } + else + { + // Non-windows, just open the link? Or maybe they have it via Wine? + // For now, let's open the link if we can't be sure, or maybe just try launching it? + // The prompt was "Check if they have BYOND... If not... prompt". + // Since I can't check on Linux easily, I'll assume they might not have it if I can't check. + // But actually, opening the URI is the best 'try'. + // Let's just try to open it on non-windows. + Helpers.OpenUri(new Uri(_server.Address)); + } + } + } + + private bool IsByondInstalled() + { + if (!OperatingSystem.IsWindows()) + { + // On Linux/Mac, we can't easily check for BYOND (usually running under Wine). + // We'll return true to let the OS/Wine try to handle the protocol. + return true; + } + + try + { + // Check for BYOND in Registry + // HKCU\Software\Dantom\BYOND is the standard key. + using var key = Registry.CurrentUser.OpenSubKey(@"Software\Dantom\BYOND"); + return key != null; + } + catch + { + return false; + } + } +} diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerListTabViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerListTabViewModel.cs new file mode 100644 index 0000000..51a4ebf --- /dev/null +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerListTabViewModel.cs @@ -0,0 +1,52 @@ +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using Splat; +using SS14.Launcher.Models.ServerStatus; +using SS14.Launcher.Localization; +using SS14.Launcher.Utility; + +namespace SS14.Launcher.ViewModels.MainWindowTabs; + +public class ClassicServerListTabViewModel : MainWindowTabViewModel +{ + private readonly ClassicServerListCache _cache; + + private readonly LocalizationManager _loc = LocalizationManager.Instance; + + public override string Name => _loc.GetString("tab-servers-classic-title"); + + public ObservableCollection AllServers { get; } = new(); + + public ClassicServerListTabViewModel() + { + _cache = Locator.Current.GetRequiredService(); + + // Initial populate if any + UpdateList(); + + ((INotifyCollectionChanged)_cache.AllServers).CollectionChanged += OnServersChanged; + } + + private void OnServersChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateList(); + } + + private void UpdateList() + { + AllServers.Clear(); + // Sort by Players descending + var sorted = _cache.AllServers.OrderByDescending(s => s.PlayerCount).ToList(); + + foreach (var s in sorted) + { + AllServers.Add(new ClassicServerEntryViewModel(s)); + } + } + + public override async void Selected() + { + await _cache.Refresh(); + } +} diff --git a/SS14.Launcher/ViewModels/MainWindowViewModel.cs b/SS14.Launcher/ViewModels/MainWindowViewModel.cs index 9380753..987c71d 100644 --- a/SS14.Launcher/ViewModels/MainWindowViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowViewModel.cs @@ -39,6 +39,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IErrorOverlayOwner public HomePageViewModel HomeTab { get; } public ServerListTabViewModel ServersTab { get; } + public ClassicServerListTabViewModel ClassicServersTab { get; } public NewsTabViewModel NewsTab { get; } public OptionsTabViewModel OptionsTab { get; } @@ -51,6 +52,7 @@ public MainWindowViewModel() _loc = LocalizationManager.Instance; ServersTab = new ServerListTabViewModel(this); + ClassicServersTab = new ClassicServerListTabViewModel(); NewsTab = new NewsTabViewModel(); HomeTab = new HomePageViewModel(this); OptionsTab = new OptionsTabViewModel(); @@ -58,6 +60,7 @@ public MainWindowViewModel() var tabs = new List(); tabs.Add(HomeTab); tabs.Add(ServersTab); + tabs.Add(ClassicServersTab); // tabs.Add(NewsTab); //TODO: Make our own news site tabs.Add(OptionsTab); #if DEVELOPMENT diff --git a/SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml b/SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml new file mode 100644 index 0000000..ab06e4f --- /dev/null +++ b/SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + - + Margin="4 0 0 0" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a5e638df57b7b67492282c443f3cdafe563ccf63 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 16 Jan 2026 15:07:52 -0500 Subject: [PATCH 5/7] Last cleanup for UI --- .../ServerStatus/ClassicServerListCache.cs | 19 +++++++++++++++++-- .../ServerStatus/ClassicServerStatusData.cs | 4 +++- .../ClassicServerEntryViewModel.cs | 1 + .../ClassicServerEntryView.xaml | 4 ++-- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs b/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs index e89c845..1bb005c 100644 --- a/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs +++ b/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs @@ -76,7 +76,8 @@ private List ParseByondResponse(string response) { // Name might be missing, try to extract from status or use URL var name = currentName ?? ExtractNameFromStatus(currentStatus) ?? "Unknown Server"; - list.Add(new ClassicServerStatusData(name, currentUrl, currentPlayers, CleanStatus(currentStatus, name) ?? "")); + var roundTime = ExtractRoundTimeFromStatus(currentStatus); + list.Add(new ClassicServerStatusData(name, currentUrl, currentPlayers, CleanStatus(currentStatus, name) ?? "", roundTime ?? "In-Lobby")); } // Reset for new server @@ -138,12 +139,26 @@ private List ParseByondResponse(string response) if (inServerBlock && currentUrl != null) { var name = currentName ?? ExtractNameFromStatus(currentStatus) ?? "Unknown Server"; - list.Add(new ClassicServerStatusData(name, currentUrl, currentPlayers, CleanStatus(currentStatus, name) ?? "")); + var roundTime = ExtractRoundTimeFromStatus(currentStatus); + list.Add(new ClassicServerStatusData(name, currentUrl, currentPlayers, CleanStatus(currentStatus, name) ?? "", roundTime ?? "In-Lobby")); } return list; } + private string? ExtractRoundTimeFromStatus(string? status) + { + if (string.IsNullOrEmpty(status)) return null; + + // Try to match "Round time: 00:07" or similar + var match = System.Text.RegularExpressions.Regex.Match(status, @"Round\s+time:\s+(?:)?(\d{1,2}:\d{2})(?:)?", System.Text.RegularExpressions.RegexOptions.IgnoreCase); + if (match.Success) + { + return match.Groups[1].Value; + } + return null; + } + private string? ExtractNameFromStatus(string? status) { if (string.IsNullOrEmpty(status)) return null; diff --git a/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs b/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs index a0f4525..62d2785 100644 --- a/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs +++ b/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs @@ -8,12 +8,14 @@ public class ClassicServerStatusData public string Address { get; set; } = string.Empty; public int PlayerCount { get; set; } public string Status { get; set; } = string.Empty; + public string RoundTime { get; set; } = string.Empty; - public ClassicServerStatusData(string name, string address, int playerCount, string status) + public ClassicServerStatusData(string name, string address, int playerCount, string status, string roundTime) { Name = name; Address = address; PlayerCount = playerCount; Status = status; + RoundTime = roundTime; } } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs index 814dd34..e1174cf 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs @@ -17,6 +17,7 @@ public class ClassicServerEntryViewModel : ViewModelBase public string Address => _server.Address; public string PlayerCount => _server.PlayerCount.ToString(); public string Status => _server.Status; + public string RoundTime => _server.RoundTime; private bool _isExpanded; public bool IsExpanded diff --git a/SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml b/SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml index 9211a83..6a72768 100644 --- a/SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml +++ b/SS14.Launcher/Views/MainWindowTabs/ClassicServerEntryView.xaml @@ -28,8 +28,8 @@ - - + + From b98b4736db1e0e61d5b03e3612672b01cec49b84 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Fri, 16 Jan 2026 15:12:21 -0500 Subject: [PATCH 6/7] Update ClassicServerListTabView.xaml --- .../ClassicServerListTabView.xaml | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/SS14.Launcher/Views/MainWindowTabs/ClassicServerListTabView.xaml b/SS14.Launcher/Views/MainWindowTabs/ClassicServerListTabView.xaml index 3c83ba1..b23fdb9 100644 --- a/SS14.Launcher/Views/MainWindowTabs/ClassicServerListTabView.xaml +++ b/SS14.Launcher/Views/MainWindowTabs/ClassicServerListTabView.xaml @@ -52,14 +52,16 @@ - - - - - - - - - + + + + + + + + + + + From c0fcbcb082c6d2ebc77e83de50779b69a3f32a68 Mon Sep 17 00:00:00 2001 From: DEATHB4DEFEAT Date: Thu, 22 Jan 2026 19:19:09 -0800 Subject: [PATCH 7/7] this shit is so code --- .vscode/launch.json | 7 -- SS14.Launcher.sln.DotSettings | 1 + SS14.Launcher/Assets/Locale/en-US/text.ftl | 6 +- .../ServerStatus/ClassicServerListCache.cs | 18 ++-- .../ServerStatus/ClassicServerStatusData.cs | 23 ++--- .../ClassicServerEntryViewModel.cs | 90 +++++++++---------- .../ClassicServerListTabViewModel.cs | 14 +-- .../MainWindowTabs/HomePageViewModel.cs | 1 - .../MainWindowTabs/ServerEntryViewModel.cs | 1 - .../ViewModels/MainWindowViewModel.cs | 2 +- 10 files changed, 68 insertions(+), 95 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 5c7247b..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [] -} \ No newline at end of file diff --git a/SS14.Launcher.sln.DotSettings b/SS14.Launcher.sln.DotSettings index 6c2ee26..238b512 100644 --- a/SS14.Launcher.sln.DotSettings +++ b/SS14.Launcher.sln.DotSettings @@ -3,6 +3,7 @@ OS VM True + True True True True \ No newline at end of file diff --git a/SS14.Launcher/Assets/Locale/en-US/text.ftl b/SS14.Launcher/Assets/Locale/en-US/text.ftl index 5f46a0d..869b075 100644 --- a/SS14.Launcher/Assets/Locale/en-US/text.ftl +++ b/SS14.Launcher/Assets/Locale/en-US/text.ftl @@ -234,8 +234,10 @@ region-short-south-america-west = SA West ## Strings for the "servers" tab tab-servers-title = Servers -tab-servers-classic-title = Classic Servers -tab-servers-classic-desc = All servers listed here connect via the BYOND client. +tab-servers-byond-title = BYOND Servers +tab-servers-byond-error-msg = BYOND not installed or found +tab-servers-byond-error-desc = To connect to BYOND servers, please install BYOND from https://www.byond.com/download/ and ensure it is set as the default program for handling byond:// links. +tab-servers-byond-error-link-text = Download BYOND tab-servers-refresh = Refresh filters = Filters ({ $filteredServers } / { $totalServers }) tab-servers-search-watermark = Search For Servers… diff --git a/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs b/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs index 1bb005c..47a3dc4 100644 --- a/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs +++ b/SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs @@ -29,8 +29,6 @@ public async Task Refresh() try { var response = await _http.GetStringAsync("http://www.byond.com/games/exadv1/spacestation13?format=text"); - // Log.Information("BYOND Response: {Response}", response); - await File.WriteAllTextAsync("byond_dump.txt", response); var servers = ParseByondResponse(response); Avalonia.Threading.Dispatcher.UIThread.Post(() => @@ -79,7 +77,7 @@ private List ParseByondResponse(string response) var roundTime = ExtractRoundTimeFromStatus(currentStatus); list.Add(new ClassicServerStatusData(name, currentUrl, currentPlayers, CleanStatus(currentStatus, name) ?? "", roundTime ?? "In-Lobby")); } - + // Reset for new server inServerBlock = true; currentName = null; @@ -149,7 +147,7 @@ private List ParseByondResponse(string response) private string? ExtractRoundTimeFromStatus(string? status) { if (string.IsNullOrEmpty(status)) return null; - + // Try to match "Round time: 00:07" or similar var match = System.Text.RegularExpressions.Regex.Match(status, @"Round\s+time:\s+(?:)?(\d{1,2}:\d{2})(?:)?", System.Text.RegularExpressions.RegexOptions.IgnoreCase); if (match.Success) @@ -171,20 +169,20 @@ private List ParseByondResponse(string response) var clean = System.Text.RegularExpressions.Regex.Replace(raw, "<.*?>", String.Empty); return System.Net.WebUtility.HtmlDecode(clean); } - return null; + return null; } private string? CleanStatus(string? status, string? nameToRemove) { if (string.IsNullOrEmpty(status)) return null; - + var s = status.Replace("
", "\n").Replace("
", "\n").Replace("
", "\n"); // Remove tags s = System.Text.RegularExpressions.Regex.Replace(s, "<.*?>", String.Empty); - + // Decode HTML s = System.Net.WebUtility.HtmlDecode(s); - + if (nameToRemove != null && s.StartsWith(nameToRemove)) { s = s.Substring(nameToRemove.Length); @@ -211,13 +209,13 @@ private string ParseStringValue(string line) // Extract content inside quotes var inner = line.Substring(idx + 1, lastIdx - idx - 1); - + // Unescape BYOND/C string escapes // \" -> " // \n -> newline // \\ -> \ // The most critical one is \n showing up as literal \n in UI. - + // Simple manual unescape for common sequences return inner.Replace("\\\"", "\"") .Replace("\\n", "\n") diff --git a/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs b/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs index 62d2785..51ac0b7 100644 --- a/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs +++ b/SS14.Launcher/Models/ServerStatus/ClassicServerStatusData.cs @@ -1,21 +1,10 @@ -using System.Collections.Generic; - namespace SS14.Launcher.Models.ServerStatus; -public class ClassicServerStatusData +public class ClassicServerStatusData(string name, string address, int playerCount, string status, string roundTime) { - public string Name { get; set; } = string.Empty; - public string Address { get; set; } = string.Empty; - public int PlayerCount { get; set; } - public string Status { get; set; } = string.Empty; - public string RoundTime { get; set; } = string.Empty; - - public ClassicServerStatusData(string name, string address, int playerCount, string status, string roundTime) - { - Name = name; - Address = address; - PlayerCount = playerCount; - Status = status; - RoundTime = roundTime; - } + public string Name { get; } = name; + public string Address { get; } = address; + public int PlayerCount { get; } = playerCount; + public string Status { get; } = status; + public string RoundTime { get; } = roundTime; } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs index e1174cf..ce49746 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerEntryViewModel.cs @@ -1,16 +1,18 @@ using System; using System.Diagnostics; -using Microsoft.Win32; using ReactiveUI; -using SS14.Launcher; +using Serilog; +using Splat; using SS14.Launcher.Localization; +using SS14.Launcher.Models; using SS14.Launcher.Models.ServerStatus; -using SS14.Launcher.ViewModels; +using SS14.Launcher.Utility; namespace SS14.Launcher.ViewModels.MainWindowTabs; public class ClassicServerEntryViewModel : ViewModelBase { + private readonly MainWindowViewModel _mainWindow; private readonly ClassicServerStatusData _server; public string Name => _server.Name; @@ -20,6 +22,7 @@ public class ClassicServerEntryViewModel : ViewModelBase public string RoundTime => _server.RoundTime; private bool _isExpanded; + public bool IsExpanded { get => _isExpanded; @@ -28,8 +31,9 @@ public bool IsExpanded public ReactiveCommand ConnectCommand { get; } - public ClassicServerEntryViewModel(ClassicServerStatusData server) + public ClassicServerEntryViewModel(MainWindowViewModel mainWindow, ClassicServerStatusData server) { + _mainWindow = mainWindow; _server = server; ConnectCommand = ReactiveCommand.Create(Connect); @@ -38,61 +42,47 @@ public ClassicServerEntryViewModel(ClassicServerStatusData server) private void Connect() { if (IsByondInstalled()) - { - Helpers.OpenUri(new Uri(_server.Address)); - } + Helpers.OpenUri(new Uri(Address)); else { - // Prompt to download - // We can use the native MessageBox helper from Helpers if available or just open the link. - // Following the prompt instructions mostly literally: "prompted to install it first by going to this link" - - // On Windows we can use the MessageBox to be nicer. - // NOTE: Helper MessageBox returns int, 1 is usually OK. - if (OperatingSystem.IsWindows()) + Log.Information("User attempted to connect to BYOND server but BYOND is not installed."); + // Set the MainWindowViewModel's CustomInfo to show the BYOND not installed message + // I didn't wanna make another dialog, reuse the generic thing :) + _mainWindow.CustomInfo = new LauncherInfoManager.CustomInfo() { - var res = Helpers.MessageBoxHelper( - "BYOND not detected. You need the BYOND client to play Space Station 13. Go to download page?", - "BYOND Missing", - 0x00000004 | 0x00000030); // MB_YESNO | MB_ICONWARNING - - if (res == 6) // IDYES - { - Helpers.OpenUri(new Uri("https://www.byond.com/download/")); - } - } - else - { - // Non-windows, just open the link? Or maybe they have it via Wine? - // For now, let's open the link if we can't be sure, or maybe just try launching it? - // The prompt was "Check if they have BYOND... If not... prompt". - // Since I can't check on Linux easily, I'll assume they might not have it if I can't check. - // But actually, opening the URI is the best 'try'. - // Let's just try to open it on non-windows. - Helpers.OpenUri(new Uri(_server.Address)); - } + Message = LocalizationManager.Instance.GetString("tab-servers-byond-error-msg"), + Description = LocalizationManager.Instance.GetString("tab-servers-byond-error-desc"), + LinkText = LocalizationManager.Instance.GetString("tab-servers-byond-error-link-text"), + Link = "https://www.byond.com/download/", + }; } } private bool IsByondInstalled() { - if (!OperatingSystem.IsWindows()) + #if WINDOWS + using var key = Registry.CurrentUser.OpenSubKey(@"Software\Dantom\BYOND"); + return key != null; + #elif LINUX + // Ask xdg-mime if BYOND is registered + var process = new Process { - // On Linux/Mac, we can't easily check for BYOND (usually running under Wine). - // We'll return true to let the OS/Wine try to handle the protocol. - return true; - } + StartInfo = new ProcessStartInfo + { + FileName = "xdg-mime", + Arguments = "query default x-scheme-handler/byond", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + }; + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); - try - { - // Check for BYOND in Registry - // HKCU\Software\Dantom\BYOND is the standard key. - using var key = Registry.CurrentUser.OpenSubKey(@"Software\Dantom\BYOND"); - return key != null; - } - catch - { - return false; - } + return !string.IsNullOrWhiteSpace(output); + #elif MACOS + return true; // No idea, they might have it, might not + #endif } } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerListTabViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerListTabViewModel.cs index ba4f56f..0895870 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerListTabViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ClassicServerListTabViewModel.cs @@ -11,11 +11,12 @@ namespace SS14.Launcher.ViewModels.MainWindowTabs; public class ClassicServerListTabViewModel : MainWindowTabViewModel { + private readonly MainWindowViewModel _mainWindow; private readonly ClassicServerListCache _cache; private readonly LocalizationManager _loc = LocalizationManager.Instance; - public override string Name => _loc.GetString("tab-servers-classic-title"); + public override string Name => _loc.GetString("tab-servers-byond-title"); private string? _searchString; @@ -32,11 +33,12 @@ public string? SearchString public ObservableCollection AllServers { get; } = new(); public ReactiveCommand RefreshPressed { get; } - public ClassicServerListTabViewModel() + public ClassicServerListTabViewModel(MainWindowViewModel mainWindow) { + _mainWindow = mainWindow; _cache = Locator.Current.GetRequiredService(); RefreshPressed = ReactiveCommand.CreateFromTask(_cache.Refresh); - + // Initial populate if any UpdateList(); @@ -54,10 +56,10 @@ private void UpdateList() // Filter then Sort by Players descending var filtered = _cache.AllServers.Where(DoesSearchMatch); var sorted = filtered.OrderByDescending(s => s.PlayerCount).ToList(); - + foreach (var s in sorted) { - AllServers.Add(new ClassicServerEntryViewModel(s)); + AllServers.Add(new ClassicServerEntryViewModel(_mainWindow, s)); } } @@ -65,7 +67,7 @@ private bool DoesSearchMatch(ClassicServerStatusData data) { if (string.IsNullOrWhiteSpace(_searchString)) return true; - + return data.Name.Contains(_searchString, System.StringComparison.CurrentCultureIgnoreCase); } diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs index c4c44c1..ab5a312 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/HomePageViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; diff --git a/SS14.Launcher/ViewModels/MainWindowTabs/ServerEntryViewModel.cs b/SS14.Launcher/ViewModels/MainWindowTabs/ServerEntryViewModel.cs index 904e944..450b547 100644 --- a/SS14.Launcher/ViewModels/MainWindowTabs/ServerEntryViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowTabs/ServerEntryViewModel.cs @@ -3,7 +3,6 @@ using System.Linq; using Avalonia.Controls; using Avalonia.VisualTree; -using DynamicData; using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.Messaging; using SS14.Launcher.Api; diff --git a/SS14.Launcher/ViewModels/MainWindowViewModel.cs b/SS14.Launcher/ViewModels/MainWindowViewModel.cs index 987c71d..044d277 100644 --- a/SS14.Launcher/ViewModels/MainWindowViewModel.cs +++ b/SS14.Launcher/ViewModels/MainWindowViewModel.cs @@ -52,7 +52,7 @@ public MainWindowViewModel() _loc = LocalizationManager.Instance; ServersTab = new ServerListTabViewModel(this); - ClassicServersTab = new ClassicServerListTabViewModel(); + ClassicServersTab = new ClassicServerListTabViewModel(this); NewsTab = new NewsTabViewModel(); HomeTab = new HomePageViewModel(this); OptionsTab = new OptionsTabViewModel();