using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using RadicadeServer.DB; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums.Players.Avatar; using RadicadeServer.RecNet.Enums.Store; using RadicadeServer.RecNet.Store; namespace RadicadeServer.RecNet.Storefront { public static class StorefrontCatalog { public const string ResourcePrefix = "StorefrontCaptures/"; public const string FileSearchPattern = "sf*.json"; private static readonly object Gate = new(); private static readonly Dictionary Captures = new(); private static readonly Dictionary ByPurchasableItemId = new(); private static bool _loaded; public static Func> FileSource { get; set; } = DiscoverFiles; public static bool IsLoaded { get { lock (Gate) return _loaded; } } public static IReadOnlyCollection StorefrontIds { get { Load(); lock (Gate) return Captures.Keys.OrderBy(id => id).ToArray(); } } public static int Load() { lock (Gate) { if (_loaded) return Captures.Count; var captures = ReadCaptures(); foreach (var capture in captures) { Captures[capture.StorefrontId] = capture.Capture; for (var index = 0; index < capture.Capture.StoreItems.Count; index++) { var item = capture.Capture.StoreItems[index]; if (item.PurchasableItemId <= 0) continue; ByPurchasableItemId.TryAdd(item.PurchasableItemId, (capture.StorefrontId, index)); } } _loaded = true; if (captures.Count == 0) { return Captures.Count; } var fromFiles = captures.Where(capture => capture.FromFile).Select(capture => capture.StorefrontId).OrderBy(id => id).ToList(); return Captures.Count; } } public static List GetItems(long storefrontId) { Load(); lock (Gate) { return Captures.TryGetValue(storefrontId, out var capture) ? ToItems(capture) : new List(); } } public static PurchasableGiftDrop? FindItem(long storefrontId, int purchasableItemId) { Load(); lock (Gate) { if (!Captures.TryGetValue(storefrontId, out var capture)) return null; var item = capture.StoreItems.FirstOrDefault(x => x.PurchasableItemId == purchasableItemId); return item == null ? null : ToItem(item, capture); } } public static PurchasableGiftDrop? FindItem(int purchasableItemId) { Load(); lock (Gate) { if (!ByPurchasableItemId.TryGetValue(purchasableItemId, out var location)) return null; if (!Captures.TryGetValue(location.StorefrontId, out var capture)) return null; if (location.Index >= capture.StoreItems.Count) return null; return ToItem(capture.StoreItems[location.Index], capture); } } public sealed record LoadedCapture(long StorefrontId, string Source, bool FromFile, StorefrontCapture Capture); public static List ReadCaptures() { var loaded = new List(); foreach (var (name, json) in EmbeddedCaptures()) { var capture = ParseCapture(json, name); if (capture == null) continue; var storefrontId = ResolveStorefrontId(capture, name); if (storefrontId == null) { continue; } loaded.Add(new LoadedCapture(storefrontId.Value, name, FromFile: false, capture)); } foreach (var path in FileSource()) { var name = Path.GetFileName(path); var capture = ReadCapture(path); if (capture == null) continue; var storefrontId = ResolveStorefrontId(capture, name); if (storefrontId == null) { continue; } var replaced = loaded.FindIndex(existing => existing.StorefrontId == storefrontId.Value); if (replaced >= 0) { loaded[replaced] = new LoadedCapture(storefrontId.Value, path, FromFile: true, capture); } else { loaded.Add(new LoadedCapture(storefrontId.Value, path, FromFile: true, capture)); } } return loaded.OrderBy(capture => capture.StorefrontId).ToList(); } public static IReadOnlyList<(string Name, string Json)> EmbeddedCaptures() { var assembly = typeof(StorefrontCatalog).Assembly; var payloads = new List<(string Name, string Json)>(); foreach (var resource in assembly.GetManifestResourceNames() .Where(name => name.StartsWith(ResourcePrefix, StringComparison.Ordinal)) .OrderBy(name => name, StringComparer.Ordinal)) { using var stream = assembly.GetManifestResourceStream(resource); if (stream == null) { continue; } using var reader = new StreamReader(stream); payloads.Add((resource[ResourcePrefix.Length..], reader.ReadToEnd())); } return payloads; } public static StorefrontCapture? ReadCapture(string path) { try { return ParseCapture(File.ReadAllText(path), Path.GetFileName(path)); } catch (Exception) { return null; } } public static StorefrontCapture? ParseCapture(string json, string sourceName) { try { var capture = JsonSerializer.Deserialize(json, JsonOptions); if (capture == null) { return null; } return capture; } catch (Exception) { return null; } } public static long? ResolveStorefrontId(StorefrontCapture capture, string path) { if (capture.StorefrontType is > 0) return capture.StorefrontType.Value; return StorefrontIdFromFileName(path); } public static long? StorefrontIdFromFileName(string path) { var name = Path.GetFileNameWithoutExtension(path); if (!name.StartsWith("sf", StringComparison.OrdinalIgnoreCase)) return null; var digits = name[2..].Trim(); return int.TryParse(digits, out var id) && id > 0 ? id : null; } public sealed class StorefrontCapture { [JsonPropertyName("StorefrontType")] public int? StorefrontType { get; set; } [JsonPropertyName("NextUpdate")] public DateTime? NextUpdate { get; set; } [JsonPropertyName("NewUntil")] public DateTime? NewUntil { get; set; } [JsonPropertyName("SubscriberDiscountPercent")] public int SubscriberDiscountPercent { get; set; } [JsonPropertyName("StoreItems")] public List StoreItems { get; set; } = new(); } public sealed class StorefrontCaptureItem { [JsonPropertyName("PurchasableItemId")] public int PurchasableItemId { get; set; } [JsonPropertyName("Type")] public int Type { get; set; } [JsonPropertyName("IsFeatured")] public bool IsFeatured { get; set; } [JsonPropertyName("NewUntil")] public DateTime? NewUntil { get; set; } [JsonPropertyName("Prices")] public List Prices { get; set; } = new(); [JsonPropertyName("SubscriberPrices")] public List SubscriberPrices { get; set; } = new(); [JsonPropertyName("GiftDrop")] public StorefrontCaptureGift? GiftDrop { get; set; } } public sealed class StorefrontCapturePrice { [JsonPropertyName("CurrencyType")] public int CurrencyType { get; set; } [JsonPropertyName("Price")] public int Price { get; set; } } public sealed class StorefrontCaptureGift { [JsonPropertyName("GiftDropId")] public long GiftDropId { get; set; } [JsonPropertyName("FriendlyName")] public string? FriendlyName { get; set; } [JsonPropertyName("Tooltip")] public string? Tooltip { get; set; } [JsonPropertyName("ConsumableItemDesc")] public string? ConsumableItemDesc { get; set; } [JsonPropertyName("AvatarItemDesc")] public string? AvatarItemDesc { get; set; } [JsonPropertyName("AvatarItemType")] public int? AvatarItemType { get; set; } [JsonPropertyName("EquipmentPrefabName")] public string? EquipmentPrefabName { get; set; } [JsonPropertyName("EquipmentModificationGuid")] public string? EquipmentModificationGuid { get; set; } [JsonPropertyName("IsQuery")] public bool IsQuery { get; set; } [JsonPropertyName("Unique")] public bool Unique { get; set; } [JsonPropertyName("SubscribersOnly")] public bool SubscribersOnly { get; set; } [JsonPropertyName("Rarity")] public int? Rarity { get; set; } [JsonPropertyName("Context")] public int? Context { get; set; } [JsonPropertyName("Currency")] public int Currency { get; set; } [JsonPropertyName("CurrencyType")] public int? CurrencyType { get; set; } [JsonPropertyName("ItemSetId")] public long? ItemSetId { get; set; } [JsonPropertyName("ItemSetFriendlyName")] public string? ItemSetFriendlyName { get; set; } } public static List ToItems(StorefrontCapture capture) { var items = new List(capture.StoreItems.Count); foreach (var item in capture.StoreItems) { var mapped = ToItem(item, capture); if (mapped != null) items.Add(mapped); } return items; } public static PurchasableGiftDrop? ToItem(StorefrontCaptureItem item, StorefrontCapture capture) { var gift = item.GiftDrop; if (gift == null || !HasContent(gift)) return null; return new PurchasableGiftDrop { PurchasableItemId = item.PurchasableItemId, Type = Enum.IsDefined(typeof(PurchasableItem.PurchasableItemType), item.Type) ? (PurchasableItem.PurchasableItemType)item.Type : PurchasableItem.PurchasableItemType.GiftDrop, IsFeatured = item.IsFeatured, Prices = item.Prices.Select(ToPrice).ToList(), SubscriberPrices = item.SubscriberPrices.Select(ToPrice).ToList(), EndDate = null, NewUntil = item.NewUntil, GiftDrops = new List { new() { GiftDropId = gift.GiftDropId, FriendlyName = gift.FriendlyName?.Trim() ?? string.Empty, Tooltip = gift.Tooltip?.Trim() ?? string.Empty, ConsumableItemDesc = gift.ConsumableItemDesc ?? string.Empty, AvatarItemDesc = gift.AvatarItemDesc ?? string.Empty, AvatarItemType = gift.AvatarItemType is null ? AvatarItemType.Outfit : (AvatarItemType)gift.AvatarItemType, EquipmentPrefabName = gift.EquipmentPrefabName ?? string.Empty, EquipmentModificationGuid = gift.EquipmentModificationGuid ?? string.Empty, IsQuery = gift.IsQuery, Unique = gift.Unique, SubscribersOnly = gift.SubscribersOnly, Rarity = (Rarity)(gift.Rarity ?? (int)Rarity.Common), Context = (GiftContext)(gift.Context ?? (int)GiftContext.Commerce_Purchase), Currency = gift.Currency, CurrencyType = (CurrencyType)(gift.CurrencyType ?? (int)CurrencyType.RecCenterTokens), ItemSetId = gift.ItemSetId ?? 0, ItemSetFriendlyName = gift.ItemSetFriendlyName ?? string.Empty } } }; } private static PurchasablePrice ToPrice(StorefrontCapturePrice price) => new() { CurrencyType = (CurrencyType)price.CurrencyType, Price = price.Price }; private static bool HasContent(StorefrontCaptureGift gift) => !string.IsNullOrWhiteSpace(gift.FriendlyName) || !string.IsNullOrWhiteSpace(gift.AvatarItemDesc) || !string.IsNullOrWhiteSpace(gift.ConsumableItemDesc) || !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName); private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; private static IReadOnlyList DiscoverFiles() { var files = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var directory in DBConfig.StorefrontDirectories()) { if (!Directory.Exists(directory)) continue; foreach (var path in Directory.GetFiles(directory, FileSearchPattern)) { if (!seen.Add(Path.GetFileName(path))) continue; files.Add(path); } } return files.OrderBy(path => StorefrontIdFromFileName(path) ?? long.MaxValue).ToList(); } } }