Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 22 additions & 114 deletions SS14.Launcher/Models/ServerStatus/ClassicServerListCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Serilog;
using Splat;
using SS14.Launcher.Utility;
Expand All @@ -28,8 +30,10 @@ public async Task Refresh()
{
try
{
var response = await _http.GetStringAsync("http://www.byond.com/games/exadv1/spacestation13?format=text");
var servers = ParseByondResponse(response);
var hubResponse = await _http.GetFromJsonAsync<GoonhubHubResponse>("https://node.goonhub.com/hub");
if (hubResponse == null) return;

var servers = ParseGoonhubResponse(hubResponse);

Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
Expand All @@ -46,101 +50,17 @@ public async Task Refresh()
}
}

private List<ClassicServerStatusData> ParseByondResponse(string response)
private List<ClassicServerStatusData> ParseGoonhubResponse(GoonhubHubResponse hubResponse)
{
var list = new List<ClassicServerStatusData>();
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";
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;
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)
foreach (var server in hubResponse.Response)
{
var name = currentName ?? ExtractNameFromStatus(currentStatus) ?? "Unknown Server";
var roundTime = ExtractRoundTimeFromStatus(currentStatus);
list.Add(new ClassicServerStatusData(name, currentUrl, currentPlayers, CleanStatus(currentStatus, name) ?? "", roundTime ?? "In-Lobby"));
var name = ExtractNameFromStatus(server.Status) ?? "Unknown Server";
var roundTime = ExtractRoundTimeFromStatus(server.Status) ?? "In-Lobby";
var address = $"byond://BYOND.world.{server.UrlId}";

list.Add(new ClassicServerStatusData(name, address, server.Players, CleanStatus(server.Status, name) ?? "", roundTime));
}

return list;
}

Expand Down Expand Up @@ -199,27 +119,15 @@ private List<ClassicServerStatusData> ParseByondResponse(string response)
return s;
}

private string ParseStringValue(string line)
private sealed class GoonhubHubResponse
{
[JsonPropertyName("response")] public List<GoonhubServerEntry> Response { get; set; } = new();
}

private sealed class GoonhubServerEntry
{
// format: key = "value"
var idx = line.IndexOf('"');
if (idx == -1) return string.Empty;
var lastIdx = line.LastIndexOf('"');
if (lastIdx <= idx) return string.Empty;

// 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")
.Replace("\\\\", "\\")
.Replace("\\t", "\t");
[JsonPropertyName("urlId")] public string UrlId { get; set; } = "";
[JsonPropertyName("players")] public int Players { get; set; }
[JsonPropertyName("status")] public string Status { get; set; } = "";
}
}
Loading