mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
926 lines
37 KiB
C#
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
|
|
};
|
|
}
|
|
} |