Files
2026-10-16 23:33:13 -07:00

926 lines
37 KiB
C#

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<Gift> ParseGiftList(string? data)
{
if (string.IsNullOrWhiteSpace(data)) return new List<Gift>();
try
{
using var doc = JsonDocument.Parse(data);
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
var gifts = doc.RootElement.Deserialize<List<Gift>>(JsonOptions);
return gifts ?? new List<Gift>();
}
if (doc.RootElement.ValueKind == JsonValueKind.Object)
{
var gift = doc.RootElement.Deserialize<Gift>(JsonOptions);
return gift == null ? new List<Gift>() : new List<Gift> { gift };
}
}
catch (Exception)
{
}
return new List<Gift>();
}
public static async Task<List<Gift>> GetPendingGiftsAsync(RecDBContext db, long playerId)
{
var row = await db.Gifts.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == playerId);
if (row == null) return new List<Gift>();
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<PurchasablePrice>
{
new PurchasablePrice { CurrencyType = drop.CurrencyType, Price = drop.Price }
},
SubscriberPrices = new List<PurchasablePrice>(),
IsFeatured = drop.IsFeatured,
GiftDrops = new List<Gift> { gift }
};
}
public static async Task<List<PurchasableGiftDrop>> 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<PurchasableGiftDrop>();
}
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<long> 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<Gift>() : 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<Gift?> 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<bool> 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<GiftSendResult> 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<List<PurchasableGiftDrop>>() ?? new List<PurchasableGiftDrop>();
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<CurrencyType>())
{
if (name == nameof(CurrencyType.Invalid)) continue;
if (string.Equals(name, trimmed, StringComparison.OrdinalIgnoreCase) ||
string.Equals(name.Replace("_", ""), trimmed.Replace("_", ""), StringComparison.OrdinalIgnoreCase))
{
type = Enum.Parse<CurrencyType>(name);
return true;
}
}
return false;
}
public sealed class PurchaseOutcome
{
public bool Success { get; init; }
public PurchaseBalanceUpdateResponse<Gift>? Response { get; init; }
public string? Error { get; init; }
public string? ErrorDetail { get; init; }
}
public static async Task<PurchaseOutcome> 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<Gift> BuildPurchaseResponse(
CurrencyType currencyType, long balance, UpdateResponseTypes status, Gift? gift)
{
return new PurchaseBalanceUpdateResponse<Gift>
{
Balance = balance,
CurrencyType = currencyType,
BalanceUpdates = new List<PurchaseBalanceUpdateEntry<Gift>>
{
new()
{
UpdateResponse = status,
Data = gift == null ? new List<Gift>() : new List<Gift> { gift.ForClient() }
}
}
};
}
private static async Task<long> 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<Equipment> 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<BalanceGrant?> 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<AvatarItem>();
var ownedConsumables = unlocks?.GetConsumables() ?? new List<Consumable>();
var ownedEquipment = unlocks?.GetEquipment() ?? new List<Equipment>();
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<Consumable>();
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<Equipment>();
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<AvatarItem?> 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<ProgressionData> 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
};
}
}