using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Npgsql; namespace Yuanshen.SDK.Handlers; public sealed class GachaLogRow { [JsonPropertyName("gachaType")] public uint GachaType { get; set; } [JsonPropertyName("itemId")] public uint ItemId { get; set; } [JsonPropertyName("rankLevel")] public uint RankLevel { get; set; } [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; [JsonPropertyName("kind")] public string Kind { get; set; } = string.Empty; [JsonPropertyName("time")] public long Time { get; set; } } public sealed class PlayerSnapshot { [JsonPropertyName("gachaLog")] public List GachaLog { get; set; } = new(); [JsonPropertyName("gachaPity")] public Dictionary GachaPity { get; set; } = new(); [JsonPropertyName("nickname")] public string Nickname { get; set; } = string.Empty; } public static class GachaHistoryHandler { private const string SelectSql = "SELECT data FROM players ORDER BY updated_at DESC LIMIT 1"; private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; private static readonly Dictionary BannerNames = new() { [100] = "Novice", [200] = "Standard", [301] = "Character Event", [302] = "Weapon Event" }; private static readonly string[] WeaponTypes = { "Weapon", "Sword", "Claymore", "Polearm", "Catalyst", "Bow" }; private static string? _connectionString; public static void Use(string connectionString) => _connectionString = connectionString; public static async Task OnHistory(HttpRequest request) { PlayerSnapshot snapshot; try { snapshot = await LoadAsync(); } catch (Exception exception) { Log.Warn($"gacha history unavailable: {exception.Message}"); return Html(Page("Wish History", $"

Could not read the database.
{WebUtility.HtmlEncode(exception.Message)}

")); } List log = snapshot.GachaLog; if (log.Count == 0) return Html(Page("Wish History", "

No wishes yet.
Pull on a banner and it will show up here.

")); uint filter = request.Query.TryGetValue("banner", out Microsoft.Extensions.Primitives.StringValues raw) && uint.TryParse(raw, out uint parsed) ? parsed : 0; List shown = filter == 0 ? log : log.Where(entry => entry.GachaType == filter).ToList(); shown = Enumerable.Reverse(shown).ToList(); StringBuilder body = new(); body.Append(Stats(shown, snapshot.GachaPity, filter)); body.Append(Tabs(log, filter)); if (shown.Count == 0) { body.Append("

Nothing on this banner yet.

"); return Html(Page("Wish History", body.ToString())); } body.Append("
"); string lastDate = string.Empty; int index = shown.Count; foreach (GachaLogRow entry in shown) { DateTime when = DateTimeOffset.FromUnixTimeSeconds(entry.Time).LocalDateTime; string date = when.ToString("dd MMM yyyy"); if (date != lastDate) { body.Append($"
{date}
"); lastDate = date; } uint rank = Math.Clamp(entry.RankLevel, 3, 5); string label = WebUtility.HtmlEncode(Label(entry)); string banner = WebUtility.HtmlEncode(BannerNames.GetValueOrDefault(entry.GachaType, $"Type {entry.GachaType}")); body.Append( $"
" + Icon(entry) + $"{rank}★" + $"{label}" + $"{entry.ItemId}" + $"{banner}" + $"{when:HH:mm}" + $"#{index--}" + "
"); } body.Append("
"); return Html(Page("Wish History", body.ToString())); } private static uint WeaponSlot(GachaLogRow entry) => entry.ItemId >= 10000 ? entry.ItemId / 1000 % 10 : 0; private static string Icon(GachaLogRow entry) { string glyph = WeaponSlot(entry) switch { 1 => "", 2 => "", 3 => "", 4 => "", 5 => "", _ => "" }; return $"{glyph}"; } private static string Label(GachaLogRow entry) { if (!string.IsNullOrEmpty(entry.Name)) return entry.Name; if (entry.ItemId >= 10000) { uint type = entry.ItemId / 1000 % 10; return type < WeaponTypes.Length ? WeaponTypes[type] : "Weapon"; } return $"Character {entry.ItemId}"; } private static string Stats(List shown, Dictionary pity, uint filter) { int five = shown.Count(entry => entry.RankLevel >= 5); int four = shown.Count(entry => entry.RankLevel == 4); string pityChip = string.Empty; if (filter != 0 && pity.TryGetValue(filter.ToString(), out uint counter)) pityChip = Chip("Pity", counter.ToString(), "c-pity"); return "
" + Chip("Wishes", shown.Count.ToString(), "c-all") + Chip("5★", five.ToString(), "c5") + Chip("4★", four.ToString(), "c4") + pityChip + "
"; } private static string Chip(string label, string value, string cls) => $"
{value}{WebUtility.HtmlEncode(label)}
"; private static string Tabs(List log, uint filter) { StringBuilder tabs = new("
"); tabs.Append(Tab("All", 0, filter)); foreach (uint type in log.Select(entry => entry.GachaType).Distinct().OrderBy(type => type)) tabs.Append(Tab(BannerNames.GetValueOrDefault(type, $"Type {type}"), type, filter)); tabs.Append("
"); return tabs.ToString(); } private static string Tab(string label, uint type, uint active) => $"{WebUtility.HtmlEncode(label)}"; private static async Task LoadAsync() { if (string.IsNullOrEmpty(_connectionString)) throw new InvalidOperationException("database connection string is not configured"); await using NpgsqlConnection connection = new(_connectionString); await connection.OpenAsync(); await using NpgsqlCommand command = new(SelectSql, connection); object? result = await command.ExecuteScalarAsync(); if (result is not string json) return new PlayerSnapshot(); return JsonSerializer.Deserialize(json, Options) ?? new PlayerSnapshot(); } private static IResult Html(string html) => TypedResults.Text(html, "text/html; charset=utf-8"); private static string Page(string title, string body) => $$""" {{title}}

{{title}}

{{body}}
"""; }