using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; namespace DNFLoadout; internal sealed class ServerClient { private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(10) }; private readonly Config _config; public ServerClient(Config config) => _config = config; private HttpRequestMessage Request(HttpMethod method, string path) { var req = new HttpRequestMessage(method, _config.ServerUrl.TrimEnd('/') + path); if (!string.IsNullOrWhiteSpace(_config.AdminToken)) req.Headers.TryAddWithoutValidation("X-Admin-Token", _config.AdminToken.Trim()); return req; } public async Task> GetEquipmentAsync(string characterName) { using var req = Request(HttpMethod.Get, "/local/dnf-gm/loadout/equipment?character_id=" + Uri.EscapeDataString(characterName.Trim())); using var resp = await _http.SendAsync(req); var text = await resp.Content.ReadAsStringAsync(); if (!resp.IsSuccessStatusCode) throw new Exception(text); var envelope = JsonSerializer.Deserialize>(text) ?? throw new Exception("empty response"); if (!envelope.Ok) throw new Exception(envelope.Error ?? "server returned failure"); return envelope.Data?.Slots ?? new List(); } public async Task> GetCharactersAsync() { using var req = Request(HttpMethod.Get, "/local/dnf-gm/characters"); using var resp = await _http.SendAsync(req); var text = await resp.Content.ReadAsStringAsync(); if (!resp.IsSuccessStatusCode) throw new Exception(text); var envelope = JsonSerializer.Deserialize>>(text) ?? throw new Exception("empty response"); if (!envelope.Ok) throw new Exception(envelope.Error ?? "server returned failure"); return envelope.Data ?? new List(); } internal sealed class CharacterInfo { [JsonPropertyName("character_id")] public string CharacterId { get; set; } = ""; [JsonPropertyName("name")] public string Name { get; set; } = ""; [JsonPropertyName("online")] public bool Online { get; set; } } public async Task ApplyAsync(string characterName, IEnumerable slots) { var body = new ApplyRequest { CharacterId = characterName.Trim(), Slots = slots.ToList(), }; using var req = Request(HttpMethod.Post, "/local/dnf-gm/loadout/apply"); req.Content = JsonContent.Create(body); using var resp = await _http.SendAsync(req); var text = await resp.Content.ReadAsStringAsync(); if (!resp.IsSuccessStatusCode) throw new Exception(text); var envelope = JsonSerializer.Deserialize>(text) ?? throw new Exception("empty response"); if (!envelope.Ok) throw new Exception(envelope.Error ?? "server returned failure"); return envelope.Data ?? new ApplyData(); } private sealed class Envelope { [JsonPropertyName("ok")] public bool Ok { get; set; } [JsonPropertyName("error")] public string? Error { get; set; } [JsonPropertyName("data")] public T? Data { get; set; } } private sealed class SnapshotData { [JsonPropertyName("character_id")] public string CharacterId { get; set; } = ""; [JsonPropertyName("online")] public bool Online { get; set; } [JsonPropertyName("slots")] public List Slots { get; set; } = new(); } private sealed class ApplyRequest { [JsonPropertyName("character_id")] public string CharacterId { get; set; } = ""; [JsonPropertyName("slots")] public List Slots { get; set; } = new(); } internal sealed class ApplyData { [JsonPropertyName("applied")] public List? Applied { get; set; } = new(); [JsonPropertyName("skipped")] public List? Skipped { get; set; } = new(); [JsonPropertyName("refresh_sent")] public bool RefreshSent { get; set; } [JsonPropertyName("refresh_error")] public string? RefreshError { get; set; } [JsonPropertyName("cooldown_seconds")] public int CooldownSeconds { get; set; } [JsonPropertyName("cooldown_remaining")] public int CooldownRemaining { get; set; } } internal sealed class SkipData { [JsonPropertyName("slot")] public int Slot { get; set; } [JsonPropertyName("item_id")] public long ItemId { get; set; } [JsonPropertyName("reason")] public string Reason { get; set; } = ""; } }