using RadicadeServer.Extensions; using System.Text.Json; using System.Text.Json.Serialization; namespace RadicadeServer.RecNet.Avatar; public static class AvatarUnlockCatalog { public static string LoadStarterJson() { using var stream = typeof(AvatarUnlockCatalog).Assembly.GetManifestResourceStream("StarterAvatarItems.json") ?? throw new InvalidOperationException("The starter avatar inventory resource is missing."); using var reader = new StreamReader(stream); return reader.ReadToEnd(); } public static List GetStarterItems() { var starters = EnrichWithMetadata(ParseCatalog(LoadStarterJson()), DBConfig.DefaultAvatarCatalog); starters.AddRange(ParseCatalog(DBConfig.DefaultAvatarCatalog).Where(item => item.IsBaseAvatarItem)); return KeepClientAvailableItems(KeepResolvableItems(starters)) .DistinctBy(item => item.AvatarItemDesc, StringComparer.OrdinalIgnoreCase).ToList(); } private static readonly JsonSerializerOptions CatalogOptions = new() { PropertyNameCaseInsensitive = true, AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, NumberHandling = JsonNumberHandling.AllowReadingFromString }; public static List ParseCatalog(string? json) { var items = new List(); if (string.IsNullOrWhiteSpace(json)) return items; try { using var document = JsonDocument.Parse(json, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); if (document.RootElement.ValueKind != JsonValueKind.Array) { return items; } var index = 0; foreach (var entry in document.RootElement.EnumerateArray()) { try { var item = entry.Deserialize(CatalogOptions); if (item != null && !string.IsNullOrWhiteSpace(item.AvatarItemDesc)) items.Add(item); } catch (JsonException) { } index++; } } catch (JsonException) { } return KeepSupportedItems(items); } public static bool NeedsMetadataEnrichment(IEnumerable? items) { return items != null && items.Any(item => item == null || item.AvatarItemId <= 0 || string.IsNullOrWhiteSpace(item.ThumbnailImage)); } public static List EnrichWithMetadata(IEnumerable items, string? metadataJson) { var metadata = ParseCatalog(metadataJson).ToDictionary(x => x.AvatarItemDesc, StringComparer.OrdinalIgnoreCase); return items.Select(item => metadata.TryGetValue(item.AvatarItemDesc, out var full) ? new AvatarItem { AvatarItemDesc = item.AvatarItemDesc, UnlockedLevel = item.UnlockedLevel, PlatformMask = item.PlatformMask, FriendlyName = full.FriendlyName, Tooltip = full.Tooltip, Rarity = full.Rarity, AvatarItemType = full.AvatarItemType, AvatarItemId = full.AvatarItemId, IsBaseAvatarItem = full.IsBaseAvatarItem, TagList = full.TagList, CreatedAt = full.CreatedAt, ThumbnailImage = full.ThumbnailImage } : item).ToList(); } private static readonly string[] KnownUnsafeDescriptionParts = [ "07116e9b-b182-40b4-90c2-99738e7aedd6", "11b3413f-2c18-41cb-8ef2-55fb825ed476", "UjSxxUK7z0GGdjvI_NsJ3Q", "X0nzvhAtgEKqzjQPb9d3qQ", "6taO0KRM_kuKoaTnOIsHRg", "KUAMuM41hk-YLZoqTiKncA,LjQ1UKN2xkqJkok40Dxaew", "5aa7be74-19a9-4dd8-b525-46c5aef34866", ]; private static readonly Lazy> SafeDefaultItems = new(() => DBConfig.SafeDefaultAvatarUnlocks2023.FromJson>() ?? new List()); private static readonly Lazy> SafeDefaultDescriptions = new(() => SafeDefaultItems.Value .Where(item => !string.IsNullOrWhiteSpace(item.AvatarItemDesc)) .Select(item => item.AvatarItemDesc) .ToHashSet(StringComparer.OrdinalIgnoreCase)); public static List GetSafeDefaultItems() { var catalogJson = string.Equals(DBConfig.DefaultAvatarCatalog, DBConfig.SafeDefaultAvatarUnlocks2023, StringComparison.Ordinal) ? DBConfig.DefaultAvatarUnlocks : DBConfig.DefaultAvatarCatalog; var items = ParseCatalog(catalogJson); if (items.Count == 0) items = ParseCatalog(DBConfig.DefaultAvatarUnlocks); var supportedItems = KeepResolvableItems(items); var candidates = supportedItems.Count > 0 ? supportedItems : SafeDefaultItems.Value.Select(Clone).ToList(); var available = KeepClientAvailableItems(candidates); return available.Count > 0 ? available : candidates; } public static List KeepClientAvailableItems(IEnumerable? items) { if (items == null) { return new List(); } var cutoff = DBConfig.AvatarItemReleaseCutoffUtc; if (cutoff == default) { return items.Where(item => item != null).ToList(); } return items.Where(item => item != null && IsAvailableToClient(item, cutoff)).ToList(); } public static bool IsAvailableToClient(AvatarItem item) { return IsAvailableToClient(item, DBConfig.AvatarItemReleaseCutoffUtc); } private static bool IsAvailableToClient(AvatarItem? item, DateTime cutoffUtc) { if (item == null || item.CreatedAt == default) { return true; } var createdAt = item.CreatedAt.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(item.CreatedAt, DateTimeKind.Utc) : item.CreatedAt.ToUniversalTime(); return createdAt < cutoffUtc; } public static List KeepSupportedItems(IEnumerable? items) { if (items == null) { return new List(); } return items .Where(item => item != null && !string.IsNullOrWhiteSpace(item.AvatarItemDesc) && !IsKnownUnsafe(item.AvatarItemDesc)) .Select(Clone) .ToList(); } public static List KeepResolvableItems(IEnumerable? items) { var supportedItems = KeepSupportedItems(items); var resolvable = DBConfig.ResolvableAvatarItemDescs; if (resolvable == null || resolvable.Count == 0) { return supportedItems; } return supportedItems.Where(item => IsClientResolvableDescription(item.AvatarItemDesc)).ToList(); } public static bool IsClientResolvableDescription(string? description) { if (string.IsNullOrWhiteSpace(description)) return false; var resolvable = DBConfig.ResolvableAvatarItemDescs; return resolvable == null || resolvable.Count == 0 || resolvable.Contains(AvatarItemDescription.Normalize(description)); } public static AvatarItem? FindItem(string? avatarItemDesc, string? catalogueJson = null) { if (string.IsNullOrWhiteSpace(avatarItemDesc)) return null; var wanted = AvatarItemDescription.Normalize(avatarItemDesc); if (string.IsNullOrWhiteSpace(wanted)) return null; var json = string.IsNullOrWhiteSpace(catalogueJson) ? DBConfig.DefaultAvatarCatalog : catalogueJson; return CatalogueByDescription(json).TryGetValue(wanted, out var item) ? Clone(item) : null; } private static string? _byDescriptionCacheKey; private static Dictionary? _byDescriptionCache; private static Dictionary CatalogueByDescription(string? json) { if (_byDescriptionCache != null && string.Equals(_byDescriptionCacheKey, json, StringComparison.Ordinal)) return _byDescriptionCache; var byDescription = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var item in ParseCatalog(json)) { if (item == null || string.IsNullOrWhiteSpace(item.AvatarItemDesc)) continue; if (!byDescription.ContainsKey(item.AvatarItemDesc)) byDescription[item.AvatarItemDesc] = item; } _byDescriptionCache = byDescription; _byDescriptionCacheKey = json; return byDescription; } public static bool IsKnownSafe(string avatarItemDesc) { return SafeDefaultDescriptions.Value.Contains(avatarItemDesc); } public static bool IsKnownUnsafe(string avatarItemDesc) { return KnownUnsafeDescriptionParts.Any(part => avatarItemDesc.Contains(part, StringComparison.OrdinalIgnoreCase)); } private static AvatarItem Clone(AvatarItem item) { return new AvatarItem { AvatarItemDesc = AvatarItemDescription.Normalize(item.AvatarItemDesc), UnlockedLevel = item.UnlockedLevel, PlatformMask = item.PlatformMask, FriendlyName = item.FriendlyName, Tooltip = item.Tooltip, Rarity = item.Rarity, AvatarItemType = item.AvatarItemType, AvatarItemId = item.AvatarItemId, IsBaseAvatarItem = item.IsBaseAvatarItem, TagList = item.TagList, CreatedAt = item.CreatedAt, ThumbnailImage = item.ThumbnailImage }; } }