mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,926 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user