Files
Radicade/Server/Storefront/StorefrontCatalog.cs
T
2026-10-16 23:33:13 -07:00

356 lines
14 KiB
C#

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<long, StorefrontCapture> Captures = new();
private static readonly Dictionary<int, (long StorefrontId, int Index)> ByPurchasableItemId = new();
private static bool _loaded;
public static Func<IReadOnlyList<string>> FileSource { get; set; } = DiscoverFiles;
public static bool IsLoaded
{
get { lock (Gate) return _loaded; }
}
public static IReadOnlyCollection<long> 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<PurchasableGiftDrop> GetItems(long storefrontId)
{
Load();
lock (Gate)
{
return Captures.TryGetValue(storefrontId, out var capture)
? ToItems(capture)
: new List<PurchasableGiftDrop>();
}
}
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<LoadedCapture> ReadCaptures()
{
var loaded = new List<LoadedCapture>();
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<StorefrontCapture>(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<StorefrontCaptureItem> 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<StorefrontCapturePrice> Prices { get; set; } = new();
[JsonPropertyName("SubscriberPrices")] public List<StorefrontCapturePrice> 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<PurchasableGiftDrop> ToItems(StorefrontCapture capture)
{
var items = new List<PurchasableGiftDrop>(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<Gift>
{
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<string> DiscoverFiles()
{
var files = new List<string>();
var seen = new HashSet<string>(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();
}
}
}