using System.Text; using System.Text.Json; using Discord; using Discord.Interactions; using TentacleBot.Services; namespace TentacleBot.Commands; [Group("palworld", "Palworld server status commands")] public class PalworldCommands : InteractionModuleBase { private readonly PalworldApiService _palworldApi; public PalworldCommands(PalworldApiService palworldApi) { _palworldApi = palworldApi; } [SlashCommand("status", "Show a combined server status dashboard")] public async Task HandleStatusAsync() { await DeferAsync(); var infoTask = _palworldApi.GetInfoAsync(); var metricsTask = _palworldApi.GetMetricsAsync(); var playersTask = _palworldApi.GetPlayersAsync(); var info = await infoTask; var metrics = await metricsTask; var players = await playersTask; if (info is null && metrics is null && players is null) { await FollowupAsync(embed: new EmbedBuilder() .WithTitle("Palworld Server Status") .WithDescription("Failed to connect to the Palworld server. Check that the server is running and the REST API is enabled.") .WithColor(Color.Red) .WithTimestamp(DateTimeOffset.UtcNow) .Build()); return; } var embed = new EmbedBuilder() .WithTitle("Palworld Server Status") .WithColor(Color.DarkGreen) .WithTimestamp(DateTimeOffset.UtcNow); if (info is not null) { var root = info.RootElement; var name = GetString(root, "servername"); var version = GetString(root, "version"); if (!string.IsNullOrWhiteSpace(name)) embed.AddField("Server Name", name, true); if (!string.IsNullOrWhiteSpace(version)) embed.AddField("Version", version, true); } if (metrics is not null) { var root = metrics.RootElement; embed.AddField("Players", $"{GetInt(root, "currentplayernum")} / {GetInt(root, "maxplayernum")}", true); embed.AddField("FPS", $"{GetDouble(root, "serverfps")}", true); var uptime = GetInt(root, "uptime"); embed.AddField("Uptime", FormatUptime(uptime), true); var days = GetInt(root, "days"); embed.AddField("In-Game Days", $"{days}", true); var camps = GetInt(root, "basecampnum"); embed.AddField("Base Camps", $"{camps}", true); } if (players is not null) { var playerList = players.RootElement.GetProperty("players"); var count = playerList.GetArrayLength(); if (count > 0) { var sb = new StringBuilder(); var limit = Math.Min(count, 15); for (var i = 0; i < limit; i++) { var p = playerList[i]; var name = GetString(p, "name"); var level = GetInt(p, "level"); sb.AppendLine($"**{name}** (Lv.{level})"); } if (count > 15) sb.AppendLine($"*...and {count - 15} more*"); embed.AddField($"Online Players ({count})", sb.ToString()); } else { embed.AddField("Online Players", "None"); } } await FollowupAsync(embed: embed.Build()); } [SlashCommand("info", "Get Palworld server information")] public async Task HandleInfoAsync() { await DeferAsync(); var data = await _palworldApi.GetInfoAsync(); if (data is null) { await FollowupAsync(embed: ErrorEmbed("info")); return; } var root = data.RootElement; var embed = new EmbedBuilder() .WithTitle("Server Info") .WithColor(Color.Blue) .AddField("Server Name", GetString(root, "servername") ?? "N/A", true) .AddField("Version", GetString(root, "version") ?? "N/A", true) .AddField("Description", GetString(root, "description") ?? "N/A") .AddField("World GUID", GetString(root, "worldguid") ?? "N/A") .WithTimestamp(DateTimeOffset.UtcNow) .Build(); await FollowupAsync(embed: embed); } [SlashCommand("players", "Get the list of online players")] public async Task HandlePlayersAsync() { await DeferAsync(); var data = await _palworldApi.GetPlayersAsync(); if (data is null) { await FollowupAsync(embed: ErrorEmbed("players")); return; } var players = data.RootElement.GetProperty("players"); var embed = new EmbedBuilder() .WithTitle("Online Players") .WithColor(Color.Green) .WithTimestamp(DateTimeOffset.UtcNow); var count = players.GetArrayLength(); if (count == 0) { embed.WithDescription("No players are currently online."); } else { var sb = new StringBuilder(); var limit = Math.Min(count, 20); for (var i = 0; i < limit; i++) { var p = players[i]; var name = GetString(p, "name"); var level = GetInt(p, "level"); var ping = GetDouble(p, "ping"); sb.AppendLine($"{i + 1}. **{name}** — Lv.{level} — {ping:F0}ms ping"); } if (count > 20) sb.AppendLine($"*...and {count - 20} more*"); embed.WithDescription(sb.ToString()); } await FollowupAsync(embed: embed.Build()); } [SlashCommand("metrics", "Get Palworld server performance metrics")] public async Task HandleMetricsAsync() { await DeferAsync(); var data = await _palworldApi.GetMetricsAsync(); if (data is null) { await FollowupAsync(embed: ErrorEmbed("metrics")); return; } var root = data.RootElement; var embed = new EmbedBuilder() .WithTitle("Server Metrics") .WithColor(Color.Orange) .AddField("FPS", $"{GetDouble(root, "serverfps"):F1}", true) .AddField("Frame Time", $"{GetDouble(root, "serverframetime"):F2}ms", true) .AddField("Players", $"{GetInt(root, "currentplayernum")} / {GetInt(root, "maxplayernum")}", true) .AddField("Uptime", FormatUptime(GetInt(root, "uptime")), true) .AddField("In-Game Days", $"{GetInt(root, "days")}", true) .AddField("Base Camps", $"{GetInt(root, "basecampnum")}", true) .WithTimestamp(DateTimeOffset.UtcNow) .Build(); await FollowupAsync(embed: embed); } private static string FormatUptime(int seconds) { if (seconds <= 0) return "N/A"; var ts = TimeSpan.FromSeconds(seconds); var parts = new List(); if (ts.Days > 0) parts.Add($"{ts.Days}d"); if (ts.Hours > 0) parts.Add($"{ts.Hours}h"); if (ts.Minutes > 0) parts.Add($"{ts.Minutes}m"); if (parts.Count == 0) parts.Add($"{ts.Seconds}s"); return string.Join(" ", parts); } private static Embed ErrorEmbed(string endpoint) { return new EmbedBuilder() .WithTitle($"Failed to fetch {endpoint}") .WithDescription("Could not connect to the Palworld server. Verify the server is running and REST API is enabled.") .WithColor(Color.Red) .WithTimestamp(DateTimeOffset.UtcNow) .Build(); } private static string? GetString(JsonElement element, string property) { return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.String ? prop.GetString() : null; } private static int GetInt(JsonElement element, string property) { return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.Number ? prop.GetInt32() : 0; } private static double GetDouble(JsonElement element, string property) { return element.TryGetProperty(property, out var prop) && prop.ValueKind == JsonValueKind.Number ? prop.GetDouble() : 0; } }