using System.Text.Json; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet; using RadicadeServer.RecNet.Accounts; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Config; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Enums.Players.Avatar; using RadicadeServer.RecNet.Enums.Store; using RadicadeServer.RecNet.Progression; using RadicadeServer.RecNet.Responses; using RadicadeServer.RecNet.Store; using RadicadeServer.RecNet.Storefront; using static RadicadeServer.RecNet.Storefront.BalanceType; namespace RadicadeServer.API.Storefront { public static class StorefrontShared { public static long NewGiftDropId() { int value = Random.Shared.Next(1, int.MaxValue); return value; } public static List ParseGiftList(string? data) { if (string.IsNullOrWhiteSpace(data)) return new List(); try { using var doc = JsonDocument.Parse(data); if (doc.RootElement.ValueKind == JsonValueKind.Array) { var gifts = doc.RootElement.Deserialize>(JsonOptions); return gifts ?? new List(); } if (doc.RootElement.ValueKind == JsonValueKind.Object) { var gift = doc.RootElement.Deserialize(JsonOptions); return gift == null ? new List() : new List { gift }; } } catch (Exception) { } return new List(); } public static async Task> GetPendingGiftsAsync(RecDBContext db, long playerId) { var row = await db.Gifts.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == playerId); if (row == null) return new List(); return ParseGiftList(row.Data); } public static Gift ToGift(this GiftDropDB drop) { return new Gift { GiftDropId = drop.GiftDropId, ItemSetId = drop.ItemSetId, ItemSetFriendlyName = drop.ItemSetFriendlyName, FriendlyName = drop.Name ?? "Gift Drop", Tooltip = !string.IsNullOrWhiteSpace(drop.Tooltip) ? drop.Tooltip : (drop.Description ?? ""), IsQuery = drop.IsQuery, Unique = drop.Unique, SubscribersOnly = drop.SubscribersOnly, ConsumableItemDesc = drop.ConsumableItems ?? "", AvatarItemDesc = drop.AvatarItems ?? "", AvatarItemType = drop.AvatarItemType, EquipmentPrefabName = drop.EquipmentItems ?? "", EquipmentModificationGuid = drop.EquipmentModificationGuid ?? "", CurrencyType = drop.CurrencyType, Currency = drop.CurrencyAmount, Xp = drop.XPAmount, Context = drop.Context, Rarity = drop.Rarity }; } public static PurchasableGiftDrop ToPurchasableGiftDrop(this GiftDropDB drop) { var gift = drop.ToGift(); return new PurchasableGiftDrop { PurchasableItemId = drop.PurchasableItemId > 0 ? drop.PurchasableItemId : (int)(drop.GiftDropId % int.MaxValue), Type = PurchasableItem.PurchasableItemType.GiftDrop, Prices = new List { new PurchasablePrice { CurrencyType = drop.CurrencyType, Price = drop.Price } }, SubscriberPrices = new List(), IsFeatured = drop.IsFeatured, GiftDrops = new List { gift } }; } public static async Task> GetStoreItems(RecDBContext db, long storefrontId) { var shipped = StorefrontCatalog.GetItems(storefrontId); if (shipped.Count > 0) { return shipped; } var drops = await db.GiftItems .Where(x => x.StorefrontId == storefrontId) .OrderBy(x => x.PurchasableItemId) .ToListAsync(); if (drops.Count > 0) { var result = drops.Select(x => x.ToPurchasableGiftDrop()).ToList(); return result; } if (storefrontId != 0) { var shared = await db.GiftItems .Where(x => x.StorefrontId == 0) .OrderBy(x => x.PurchasableItemId) .ToListAsync(); if (shared.Count > 0) { var sharedItems = shared.Select(x => x.ToPurchasableGiftDrop()).ToList(); return sharedItems; } } return new List(); } public static async Task<(GiftDropDB? dbDrop, PurchasableGiftDrop? item)> FindStoreItem(RecDBContext db, StorefrontTypes type, int itemId) { var shipped = StorefrontCatalog.FindItem((long)type, itemId); if (shipped != null) return (null, shipped); var dbDrop = await db.GiftItems .FirstOrDefaultAsync(x => x.StorefrontId == (long)type && x.PurchasableItemId == itemId); if (dbDrop != null) return (dbDrop, dbDrop.ToPurchasableGiftDrop()); return (null, null); } public static async Task PersistGiftBoxAsync(RecDBContext db, long playerId, Gift giftBox, bool notify = true) { if (giftBox == null) throw new InvalidOperationException("PersistGiftBoxAsync called with a null gift."); if (giftBox.GiftDropId == 0) giftBox.GiftDropId = NewGiftDropId(); long overwritten = 0; var row = await db.Gifts.FirstOrDefaultAsync(x => x.AccountId == playerId); var gifts = row == null ? new List() : ParseGiftList(row.Data); var existing = gifts.FirstOrDefault(g => g.GiftDropId == giftBox.GiftDropId); if (existing != null) { overwritten = existing.GiftDropId; gifts[gifts.IndexOf(existing)] = giftBox; } else { gifts.Add(giftBox); } if (row == null) db.Gifts.Add(new GenericData { AccountId = playerId, Data = gifts.ToJson() }); else row.Data = gifts.ToJson(); await db.SaveChangesAsync(); if (notify) await SendGiftPackageNotificationAsync(playerId, giftBox); return overwritten; } public static async Task RemovePendingGiftAsync(RecDBContext db, long playerId, long giftDropId) { var row = await db.Gifts.FirstOrDefaultAsync(x => x.AccountId == playerId); if (row == null) return null; var gifts = ParseGiftList(row.Data); var match = gifts.FirstOrDefault(g => g.GiftDropId == giftDropId); if (match == null) return null; gifts.Remove(match); if (gifts.Count == 0) db.Gifts.Remove(row); else row.Data = gifts.ToJson(); await db.SaveChangesAsync(); return match; } public static async Task SendPendingGiftBoxNotificationAsync(RecDBContext db, long playerId) { var gifts = await GetPendingGiftsAsync(db, playerId); if (gifts.Count == 0) return false; foreach (var gift in gifts) { await SendGiftPackageNotificationAsync(playerId, gift); } return true; } public sealed class GiftSendResult { public bool Success { get; init; } public string? Error { get; init; } public string? ErrorDetail { get; init; } public Gift? Gift { get; init; } public long SenderBalance { get; init; } } public static async Task GeneratePlayerGiftAsync( RecDBContext db, long senderId, long? recipientId, int? purchasableItemId, long? giftDropId, StorefrontTypes storefront, string? message, CurrencyType? requestedCurrency, long? requestedPrice) { if (recipientId is not > 0) return new GiftSendResult { Success = false, Error = "invalid_recipient", ErrorDetail = "A valid recipientPlayerId is required." }; if (recipientId.Value == senderId) return new GiftSendResult { Success = false, Error = "cannot_gift_self", ErrorDetail = "You cannot gift yourself." }; var recipientExists = await db.Players.AnyAsync(x => x.AccountId == recipientId.Value); if (!recipientExists) return new GiftSendResult { Success = false, Error = "invalid_recipient", ErrorDetail = $"Recipient account {recipientId.Value} does not exist." }; int itemId = purchasableItemId ?? 0; var shippedItem = itemId > 0 ? StorefrontCatalog.FindItem(itemId) : null; GiftDropDB? dbDrop = null; if (shippedItem == null) { if (itemId > 0) dbDrop = await db.GiftItems.FirstOrDefaultAsync(x => x.PurchasableItemId == itemId); else if (giftDropId is > 0) dbDrop = await db.GiftItems.FirstOrDefaultAsync(x => x.GiftDropId == giftDropId.Value); } PurchasableGiftDrop? configItem = null; if (shippedItem == null && dbDrop == null) { var configItems = (await db.Config.FirstOrDefaultAsync())?.GiftDropStoreItems?.FromJson>() ?? new List(); if (itemId > 0) configItem = configItems.FirstOrDefault(x => x.PurchasableItemId == itemId); else if (giftDropId is > 0) configItem = configItems.FirstOrDefault(x => x.GiftDrops.Any(g => g.GiftDropId == giftDropId.Value)); } Gift gift; PurchasablePrice? price; if (shippedItem != null) { gift = shippedItem.GiftDrop?.Clone() ?? new Gift { FriendlyName = "Gift", Tooltip = "A gift from a player!", Context = GiftContext.Purchased_Gift_A, Rarity = Rarity.Common }; price = shippedItem.Prices.FirstOrDefault(); } else if (dbDrop != null) { gift = dbDrop.ToGift(); price = new PurchasablePrice { CurrencyType = dbDrop.CurrencyType, Price = dbDrop.Price }; } else if (configItem != null) { gift = configItem.GiftDrops?.FirstOrDefault()?.Clone() ?? new Gift { FriendlyName = "Gift", Tooltip = "A gift from a player!", Context = GiftContext.Purchased_Gift_A, Rarity = Rarity.Common }; price = configItem.Prices?.FirstOrDefault(); } else { return new GiftSendResult { Success = false, Error = "item_not_found", ErrorDetail = $"No store item matches purchasableItemId={itemId} giftDropId={giftDropId}." }; } gift.GiftDropId = NewGiftDropId(); gift.FromPlayerId = senderId; if (!string.IsNullOrWhiteSpace(message)) gift.Tooltip = message; if (gift.Context == GiftContext.None || gift.Context == GiftContext.Default) gift.Context = GiftContext.Purchased_Gift_A; var currencyType = requestedCurrency ?? price?.CurrencyType ?? CurrencyType.RecCenterTokens; long priceAmount = price?.Price ?? 0; if (requestedPrice is > 0 && requestedPrice.Value != priceAmount) return new GiftSendResult { Success = false, Error = "requested_price_mismatch", ErrorDetail = $"Item costs {priceAmount}, request quoted {requestedPrice}." }; var strategy = db.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync(async () => { await using var tx = await db.Database.BeginTransactionAsync(); try { var senderCurrency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == senderId); if (senderCurrency == null) { senderCurrency = new Currency { AccountId = senderId }; db.Currency.Add(senderCurrency); } long balance = GetBalance(senderCurrency, currencyType); if (priceAmount > 0 && balance < priceAmount) return new GiftSendResult { Success = false, Error = "not_enough_credit", ErrorDetail = $"Balance {balance} is below price {priceAmount}.", SenderBalance = balance }; if (priceAmount > 0) AddCurrency(senderCurrency, currencyType, -priceAmount); await db.SaveChangesAsync(); await PersistGiftBoxAsync(db, recipientId.Value, gift, notify: false); await tx.CommitAsync(); await SendGiftPackageNotificationAsync(recipientId.Value, gift); return new GiftSendResult { Success = true, Gift = gift, SenderBalance = GetBalance(senderCurrency, currencyType) }; } catch (Exception ex) { await tx.RollbackAsync(); return new GiftSendResult { Success = false, Error = "gift_creation_failed", ErrorDetail = ex.Message }; } }); } public sealed class BalanceGrant { public CurrencyType CurrencyType { get; init; } public long Granted { get; init; } public long Balance { get; init; } } public static async Task SendBalanceUpdateAsync(long playerId, CurrencyType currencyType, long balance, long granted, StorefrontBalanceAddTypes addType) { if (currencyType == CurrencyType.Invalid) return; try { await RadicadeServer.API.Notifications.Notify.SendNotify(playerId, new RadicadeServer.RecNet.Notifications.Notification { Id = RadicadeServer.RecNet.Enums.Messages.NotificationType.StorefrontBalanceUpdate, Msg = new BalanceResponseDTO { BalanceAddType = addType, Delta = granted, Balance = balance, CurrencyType = currencyType } }); } catch (Exception) { } } public static Task SendXpBalanceUpdateAsync(long playerId, long xp, long granted, StorefrontBalanceAddTypes addType) => SendBalanceUpdateAsync(playerId, CurrencyType.RecRoyale_Season1, xp, granted, addType); public static async Task SendGiftPackageNotificationAsync(long playerId, Gift giftBox) { await RadicadeServer.API.Notifications.Notify.SendNotify(playerId, new RadicadeServer.RecNet.Notifications.Notification { Id = RadicadeServer.RecNet.Enums.Messages.NotificationType.GiftPackageReceivedImmediate, Msg = giftBox.ForClient() }); } internal static long GetBalance(RecNet.Accounts.Currency currency, CurrencyType type) => type switch { CurrencyType.RecCenterTokens => currency.RecTokens, CurrencyType.LaserTagTickets => currency.LaserTagTickets, CurrencyType.LostSkullsGold => currency.Gold, CurrencyType.DraculaSilver => currency.DraculaSilver, _ => 0 }; internal static void AddCurrency(RecNet.Accounts.Currency currency, CurrencyType type, long amount) { switch (type) { case CurrencyType.RecCenterTokens: currency.RecTokens += amount; break; case CurrencyType.LaserTagTickets: currency.LaserTagTickets += amount; break; case CurrencyType.LostSkullsGold: currency.Gold += amount; break; case CurrencyType.DraculaSilver: currency.DraculaSilver += amount; break; } } internal static bool TryParseCurrencyType(string? raw, out CurrencyType type) { type = CurrencyType.Invalid; if (string.IsNullOrWhiteSpace(raw)) return false; var trimmed = raw.Trim(); if (int.TryParse(trimmed, out var numeric) && Enum.IsDefined(typeof(CurrencyType), numeric) && (CurrencyType)numeric != CurrencyType.Invalid) { type = (CurrencyType)numeric; return true; } foreach (var name in Enum.GetNames()) { if (name == nameof(CurrencyType.Invalid)) continue; if (string.Equals(name, trimmed, StringComparison.OrdinalIgnoreCase) || string.Equals(name.Replace("_", ""), trimmed.Replace("_", ""), StringComparison.OrdinalIgnoreCase)) { type = Enum.Parse(name); return true; } } return false; } public sealed class PurchaseOutcome { public bool Success { get; init; } public PurchaseBalanceUpdateResponse? Response { get; init; } public string? Error { get; init; } public string? ErrorDetail { get; init; } } public static async Task ExecutePurchaseAsync( RecDBContext db, long userId, StorefrontTypes storefrontType, int purchasableItemId, CurrencyType currencyType, long? requestedPrice, GiftRequestItemDTO? giftRequest) { if (purchasableItemId <= 0) return new PurchaseOutcome { Success = false, Error = "invalid_request", ErrorDetail = $"purchasableItemId={purchasableItemId} is not valid." }; var (dbDrop, item) = await FindStoreItem(db, storefrontType, purchasableItemId); if (item == null) { return new PurchaseOutcome { Success = true, Response = BuildPurchaseResponse(currencyType, 0, UpdateResponseTypes.NoItemAvailable, null) }; } var price = item.Prices.FirstOrDefault(p => p.CurrencyType == currencyType) ?? item.Prices.FirstOrDefault(); long cost = price?.Price ?? 0; if (requestedPrice is > 0 && requestedPrice.Value != cost) { return new PurchaseOutcome { Success = true, Response = BuildPurchaseResponse(currencyType, await BalanceFor(db, userId, currencyType), UpdateResponseTypes.RequestedPriceDoesNotMatch, null) }; } var gift = (dbDrop?.ToGift()) ?? item.GiftDrop?.Clone() ?? new Gift { FriendlyName = "Purchased Item", Tooltip = "Purchased from the store.", CurrencyType = currencyType, Context = GiftContext.Commerce_Purchase, Rarity = Rarity.Common }; gift.GiftDropId = NewGiftDropId(); if (gift.Context == GiftContext.None || gift.Context == GiftContext.Default) gift.Context = GiftContext.Commerce_Purchase; if (item.GiftDrop?.IsQuery == true || gift.IsQuery) { var queryOwner = giftRequest is { ToPlayerId: > 0 } ? giftRequest.ToPlayerId : userId; var pick = await PickQueryDropAsync(db, queryOwner, gift.Rarity); if (pick != null) { gift.AvatarItemDesc = pick.AvatarItemDesc; gift.AvatarItemType = AvatarItemType.Outfit; gift.IsQuery = false; if (!string.IsNullOrWhiteSpace(pick.FriendlyName)) gift.FriendlyName = pick.FriendlyName; if (!string.IsNullOrWhiteSpace(pick.Tooltip)) gift.Tooltip = pick.Tooltip; } } if (giftRequest is { ToPlayerId: > 0 }) { var recipient = giftRequest.ToPlayerId; if (recipient == userId) return new PurchaseOutcome { Success = false, Error = "cannot_gift_self", ErrorDetail = "You cannot gift an item to yourself." }; if (!await db.Players.AnyAsync(x => x.AccountId == recipient)) return new PurchaseOutcome { Success = false, Error = "invalid_recipient", ErrorDetail = $"Recipient account {recipient} does not exist." }; if (!string.IsNullOrWhiteSpace(giftRequest.Message)) gift.Tooltip = giftRequest.Message; gift.Context = giftRequest.GiftContext != GiftContext.None ? giftRequest.GiftContext : GiftContext.Purchased_Gift_A; gift.FromPlayerId = userId; var strategy = db.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync(async () => { await using var tx = await db.Database.BeginTransactionAsync(); try { var senderCurrency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId); if (senderCurrency == null) { senderCurrency = new Currency { AccountId = userId }; db.Currency.Add(senderCurrency); } long balance = GetBalance(senderCurrency, currencyType); if (balance < cost) { await tx.RollbackAsync(); return new PurchaseOutcome { Success = true, Response = BuildPurchaseResponse(currencyType, balance, UpdateResponseTypes.NotEnoughCredit, null) }; } if (cost > 0) AddCurrency(senderCurrency, currencyType, -cost); await db.SaveChangesAsync(); await PersistGiftBoxAsync(db, recipient, gift, notify: false); await tx.CommitAsync(); await SendGiftPackageNotificationAsync(recipient, gift); var newBalance = GetBalance(senderCurrency, currencyType); return new PurchaseOutcome { Success = true, Response = BuildPurchaseResponse(currencyType, newBalance, UpdateResponseTypes.OK, gift) }; } catch (Exception ex) { await tx.RollbackAsync(); return new PurchaseOutcome { Success = false, Error = "purchase_failed", ErrorDetail = ex.Message }; } }); } var strategy2 = db.Database.CreateExecutionStrategy(); return await strategy2.ExecuteAsync(async () => { await using var tx = await db.Database.BeginTransactionAsync(); try { var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId); if (currency == null) { currency = new Currency { AccountId = userId }; db.Currency.Add(currency); } long balance = GetBalance(currency, currencyType); if (balance < cost) { await tx.RollbackAsync(); return new PurchaseOutcome { Success = true, Response = BuildPurchaseResponse(currencyType, balance, UpdateResponseTypes.NotEnoughCredit, null) }; } if (cost > 0) AddCurrency(currency, currencyType, -cost); await db.SaveChangesAsync(); await PersistGiftBoxAsync(db, userId, gift, notify: false); await tx.CommitAsync(); var newBalance = GetBalance(currency, currencyType); return new PurchaseOutcome { Success = true, Response = BuildPurchaseResponse(currencyType, newBalance, UpdateResponseTypes.OK, gift) }; } catch (Exception ex) { await tx.RollbackAsync(); return new PurchaseOutcome { Success = false, Error = "purchase_failed", ErrorDetail = ex.Message }; } }); } public static PurchaseBalanceUpdateResponse BuildPurchaseResponse( CurrencyType currencyType, long balance, UpdateResponseTypes status, Gift? gift) { return new PurchaseBalanceUpdateResponse { Balance = balance, CurrencyType = currencyType, BalanceUpdates = new List> { new() { UpdateResponse = status, Data = gift == null ? new List() : new List { gift.ForClient() } } } }; } private static async Task BalanceFor(RecDBContext db, long playerId, CurrencyType currencyType) { var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == playerId); return currency == null ? 0 : GetBalance(currency, currencyType); } private static bool AlreadyOwnsEquipment(IEnumerable owned, Gift gift) { var prefab = (gift.EquipmentPrefabName ?? "").Trim(); var guid = (gift.EquipmentModificationGuid ?? "").Trim(); return owned.Any(item => (item.PrefabName ?? "").Trim() == prefab && (item.ModificationGuid ?? "").Trim() == guid); } public static async Task GrantGiftAsync(RecDBContext db, long playerId, Gift gift) { BalanceGrant? balanceGrant = null; if (gift.Currency > 0) { if (gift.CurrencyType == CurrencyType.RecRoyale_Season1) { var progression = await GetOrCreateProgression(db, playerId); progression.XP = (int)Math.Min(progression.XP + gift.Currency, int.MaxValue); progression.RecordXpGrant((int)Math.Min(gift.Currency, int.MaxValue)); } else if (gift.CurrencyType != CurrencyType.Invalid) { var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == playerId); if (currency == null) { currency = new Currency { AccountId = playerId }; db.Currency.Add(currency); } AddCurrency(currency, gift.CurrencyType, gift.Currency); balanceGrant = new BalanceGrant { CurrencyType = gift.CurrencyType, Granted = gift.Currency, Balance = GetBalance(currency, gift.CurrencyType) }; } } if (gift.Xp > 0) { var progression = await GetOrCreateProgression(db, playerId); progression.XP = (int)Math.Min(progression.XP + gift.Xp, int.MaxValue); progression.RecordXpGrant((int)Math.Min(gift.Xp, int.MaxValue)); } var unlocks = await db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == playerId); bool hasItems = !string.IsNullOrWhiteSpace(gift.AvatarItemDesc) || !string.IsNullOrWhiteSpace(gift.ConsumableItemDesc) || !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName); bool allowItems = true; if (hasItems) { var ownedItems = unlocks?.GetAvatarItems() ?? new List(); var ownedConsumables = unlocks?.GetConsumables() ?? new List(); var ownedEquipment = unlocks?.GetEquipment() ?? new List(); int itemCount = 0; var avatarDesc = NormalizeAvatarGiftDesc(gift.AvatarItemDesc); if (!string.IsNullOrWhiteSpace(avatarDesc) && !ownedItems.Any(x => x.AvatarItemDesc == avatarDesc)) itemCount++; if (!string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)) { foreach (var item in gift.ConsumableItemDesc.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) if (!ownedConsumables.Any(x => x.ConsumableItemDesc == item)) itemCount++; } if (!string.IsNullOrWhiteSpace(gift.EquipmentPrefabName) && !AlreadyOwnsEquipment(ownedEquipment, gift)) itemCount++; } if (allowItems && !string.IsNullOrWhiteSpace(gift.AvatarItemDesc)) { if (unlocks == null) { unlocks = new PlayerUnlocks { AccountId = playerId, AvatarItems = DBConfig.DefaultAvatarUnlocks }; db.Unlocks.Add(unlocks); } var existingItems = unlocks.GetAvatarItems(); var clean = NormalizeAvatarGiftDesc(gift.AvatarItemDesc); if (!string.IsNullOrWhiteSpace(clean) && !existingItems.Any(x => x.AvatarItemDesc == clean)) { unlocks.AddAvatarItem(UnlockFor(gift, clean)); } } if (allowItems && !string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)) { if (unlocks == null) { unlocks = new PlayerUnlocks { AccountId = playerId, AvatarItems = DBConfig.DefaultAvatarUnlocks }; db.Unlocks.Add(unlocks); } var existingConsumables = unlocks.GetConsumables() ?? new List(); foreach (var item in gift.ConsumableItemDesc.Split(',', StringSplitOptions.RemoveEmptyEntries)) { var clean = item.Trim(); if (!string.IsNullOrEmpty(clean) && !existingConsumables.Any(x => x.ConsumableItemDesc == clean)) unlocks.AddConsumable(new Consumable { Id = StableId(clean), ConsumableItemDesc = clean, Count = 1, InitialCount = 1, CreatedAt = DateTime.UtcNow, IsActive = true }); } } if (allowItems && !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName)) { if (unlocks == null) { unlocks = new PlayerUnlocks { AccountId = playerId, AvatarItems = DBConfig.DefaultAvatarUnlocks }; db.Unlocks.Add(unlocks); } var existingEquipment = unlocks.GetEquipment() ?? new List(); var prefab = gift.EquipmentPrefabName.Trim(); if (!string.IsNullOrEmpty(prefab) && !AlreadyOwnsEquipment(existingEquipment, gift)) unlocks.AddEquipment(new Equipment { PrefabName = prefab, ModificationGuid = gift.EquipmentModificationGuid ?? "", FriendlyName = gift.FriendlyName ?? "", Tooltip = gift.Tooltip ?? "", Rarity = gift.Rarity }); } return balanceGrant; } private static async Task PickQueryDropAsync(RecDBContext db, long playerId, Rarity rarity) { var catalogue = RadicadeServer.Utils.LevelUpGiftService.ClothingCatalogue(DBConfig.DefaultAvatarCatalog); var pool = new RadicadeServer.Utils.LevelUpRewardPool.Pool { AvatarItems = catalogue .Where(item => !string.IsNullOrWhiteSpace(item.AvatarItemDesc)) .Select(item => item.AvatarItemDesc) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList() }; var owned = await RadicadeServer.Utils.LevelUpGiftService.CollectOwnedKeysAsync(db, playerId); return RadicadeServer.Utils.LevelUpRewardPool.PickAvatarItem( catalogue, rarity, owned, Random.Shared, pool); } private static AvatarItem UnlockFor(Gift gift, string avatarItemDesc) { var catalogueItem = AvatarUnlockCatalog.FindItem(avatarItemDesc) ?? RadicadeServer.Utils.LevelUpGiftService.ClothingCatalogue() .FirstOrDefault(x => string.Equals(x.AvatarItemDesc, avatarItemDesc, StringComparison.OrdinalIgnoreCase)); if (catalogueItem != null) { if (string.IsNullOrWhiteSpace(catalogueItem.FriendlyName) && !string.IsNullOrWhiteSpace(gift.FriendlyName)) catalogueItem.FriendlyName = gift.FriendlyName; if (catalogueItem.Rarity == 0 && gift.Rarity != 0) catalogueItem.Rarity = (int)gift.Rarity; if (catalogueItem.CreatedAt == default) catalogueItem.CreatedAt = DateTime.UtcNow; catalogueItem.AvatarItemDesc = avatarItemDesc; return catalogueItem; } return new AvatarItem { AvatarItemDesc = avatarItemDesc, FriendlyName = gift.FriendlyName ?? string.Empty, Tooltip = gift.Tooltip ?? string.Empty, Rarity = (int)gift.Rarity, AvatarItemType = (int)gift.AvatarItemType, CreatedAt = DateTime.UtcNow }; } private static async Task GetOrCreateProgression(RecDBContext db, long playerId) { var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId); if (progression != null) return progression; progression = new ProgressionData { PlayerId = playerId }; db.Progression.Add(progression); return progression; } private static long StableId(string value) { uint hash = 2166136261; foreach (var c in value) { hash ^= c; hash *= 16777619; } return (hash & 0x7FFFFFFF) + 1; } private static string NormalizeAvatarGiftDesc(string? value) { return AvatarItemDescription.Normalize(value); } private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; } }