mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
namespace RadicadeServer.RecNet.Store
|
||||
{
|
||||
public enum StorefrontTypes
|
||||
{
|
||||
None = 0,
|
||||
LaserTag = 1,
|
||||
RecCenter = 2,
|
||||
Watch = 3,
|
||||
MitMShop = 4,
|
||||
Quest_LostSkulls = 100,
|
||||
Quest_Dracula = 101,
|
||||
Quest_GoldenTrophy = 102,
|
||||
Quest_CrimsonCauldron = 103,
|
||||
RecRoyale = 200,
|
||||
Cafe = 300,
|
||||
Paintball = 400,
|
||||
Paintball_River = 401,
|
||||
Paintball_Homestead = 402,
|
||||
Paintball_Quarry = 403,
|
||||
Paintball_ClearCut = 404,
|
||||
Paintball_Spillway = 405,
|
||||
Paintball_SunsetDriveIn = 406,
|
||||
Bowling = 500,
|
||||
StuntRunner = 600,
|
||||
DormMirror = 700,
|
||||
InventionStore = 800,
|
||||
RoomKeys = 900,
|
||||
Player_Profile = 1000,
|
||||
Room_Save = 1100,
|
||||
RoomCurrency = 1200,
|
||||
Wishlist = 1300,
|
||||
RoomConsumable = 1400,
|
||||
RecRally = 1600,
|
||||
PopUpShop_1 = 1700,
|
||||
PopUpShop_2 = 1701,
|
||||
PopUpShop_3 = 1702,
|
||||
PopUpShop_4 = 1703,
|
||||
PopUpShop_5 = 1704,
|
||||
PopUpShop_6 = 1705,
|
||||
PopUpShop_7 = 1706,
|
||||
PopUpShop_8 = 1707,
|
||||
PopUpShop_9 = 1708,
|
||||
PopUpShop_10 = 1709,
|
||||
CustomAvatarItems = 1800,
|
||||
ProgressionEventPurchasableXpBoosts = 1900,
|
||||
PopUpShop_ProgressionEvent_1 = 2000,
|
||||
PopUpShop_ProgressionEvent_2 = 2001,
|
||||
PopUpShop_ProgressionEvent_3 = 2002,
|
||||
PopUpShop_ProgressionEvent_4 = 2003,
|
||||
PopUpShop_ProgressionEvent_5 = 2004
|
||||
}
|
||||
public class BaseStorefrontDTO
|
||||
{
|
||||
public StorefrontTypes StorefrontType { get; set; }
|
||||
|
||||
public DateTime NextUpdate { get; set; }
|
||||
|
||||
public DateTime? EndDate { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
|
||||
namespace RadicadeServer.RecNet.Storefront
|
||||
{
|
||||
public class GameReward
|
||||
{
|
||||
public long RewardSelectionId { get; set; }
|
||||
public string Message { get; set; } = null!;
|
||||
public GiftContext GiftContext { get; set; }
|
||||
public RewardType RewardType { get; set; }
|
||||
public Gift GiftDrop1 { get; set; } = null!;
|
||||
public Gift GiftDrop2 { get; set; } = null!;
|
||||
public Gift GiftDrop3 { get; set; } = null!;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public bool Consumed { get; set; }
|
||||
|
||||
public long ConsumedGiftDropId { get; set; }
|
||||
|
||||
public DateTime? ConsumedAt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using RadicadeServer.RecNet.Enums.Players.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
|
||||
namespace RadicadeServer.RecNet.Store
|
||||
{
|
||||
public class GiftDropDB
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long GiftDropId { get; set; }
|
||||
|
||||
public string Name { get; set; } = "";
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public long StorefrontId { get; set; }
|
||||
|
||||
|
||||
public string Description { get; set; } = "";
|
||||
|
||||
public int CurrencyAmount { get; set; }
|
||||
public int XPAmount { get; set; }
|
||||
|
||||
|
||||
public string AvatarItems { get; set; } = "";
|
||||
|
||||
|
||||
public string EquipmentItems { get; set; } = "";
|
||||
|
||||
|
||||
public string ConsumableItems { get; set; } = "";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public int PurchasableItemId { get; set; }
|
||||
|
||||
public bool IsFeatured { get; set; }
|
||||
|
||||
|
||||
public int Price { get; set; }
|
||||
|
||||
public CurrencyType CurrencyType { get; set; } = CurrencyType.RecCenterTokens;
|
||||
|
||||
public Rarity Rarity { get; set; } = Rarity.Common;
|
||||
|
||||
public GiftContext Context { get; set; } = GiftContext.Game_Drop;
|
||||
|
||||
public AvatarItemType AvatarItemType { get; set; } = AvatarItemType.Outfit;
|
||||
|
||||
public string EquipmentModificationGuid { get; set; } = "";
|
||||
|
||||
public string Tooltip { get; set; } = "";
|
||||
|
||||
public bool IsQuery { get; set; }
|
||||
public bool Unique { get; set; }
|
||||
public bool SubscribersOnly { get; set; }
|
||||
public long ItemSetId { get; set; }
|
||||
public string ItemSetFriendlyName { get; set; } = "";
|
||||
}
|
||||
|
||||
public class ItemSetDB
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long ItemSetId { get; set; }
|
||||
|
||||
public string FriendlyName { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Store;
|
||||
|
||||
namespace RadicadeServer.RecNet.Storefront
|
||||
{
|
||||
public class GiftDropStorefront : BaseStorefrontDTO
|
||||
{
|
||||
public DateTime? NewUntil { get; set; }
|
||||
|
||||
public List<PurchasableGiftDrop> StoreItems { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
|
||||
namespace RadicadeServer.RecNet.Storefront
|
||||
{
|
||||
public class GiftPackage
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public List<Gift> Gifts { get; set; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API.Notifications;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Enums.Messages;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.RecNet.Storefront;
|
||||
|
||||
namespace RadicadeServer.API.Storefront
|
||||
{
|
||||
[Route("api/storefronts/")]
|
||||
[Route("econ/api/storefronts/")]
|
||||
public class PurchaseAPI(RecDBContext _db) : Controller
|
||||
{
|
||||
[HttpPost("v1/buyItem"), Authorize]
|
||||
public async Task<IActionResult> BuyItem([FromBody] RequestPurchaseItemDTO? request)
|
||||
=> await ProcessPurchase(request);
|
||||
|
||||
[HttpPost("v1/buyTier"), Authorize]
|
||||
public async Task<IActionResult> BuyTier([FromBody] RequestPurchaseItemDTO? request)
|
||||
=> await ProcessPurchase(request);
|
||||
|
||||
[HttpPost("v1/buyElite"), Authorize]
|
||||
public async Task<IActionResult> BuyElite([FromBody] RequestPurchaseItemDTO? request)
|
||||
=> await ProcessPurchase(request);
|
||||
|
||||
[HttpGet("v2/buyInvention"), Authorize]
|
||||
public async Task<IActionResult> BuyInvention([FromQuery] long inventionId, [FromQuery] long requestedPrice)
|
||||
=> await ProcessInventionPurchase(inventionId, requestedPrice);
|
||||
|
||||
private async Task<IActionResult> ProcessPurchase(RequestPurchaseItemDTO? request)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
return BadRequest(new { error = "invalid_request" });
|
||||
}
|
||||
|
||||
var outcome = await StorefrontShared.ExecutePurchaseAsync(
|
||||
_db,
|
||||
userId.Value,
|
||||
request.StorefrontType,
|
||||
request.PurchasableItemId,
|
||||
request.CurrencyType,
|
||||
request.RequestedPrice,
|
||||
request.Gift);
|
||||
|
||||
if (outcome.Response != null)
|
||||
return Ok(outcome.Response);
|
||||
return outcome.Error switch
|
||||
{
|
||||
"cannot_gift_self" or "invalid_recipient" => BadRequest(new { error = outcome.Error, message = outcome.ErrorDetail }),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, new { error = outcome.Error, message = outcome.ErrorDetail })
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ProcessInventionPurchase(long inventionId, long requestedPrice)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var invention = await _db.Inventions.FirstOrDefaultAsync(x => x.InventionId == inventionId);
|
||||
if (invention == null) return NotFound();
|
||||
|
||||
var currency = await _db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId);
|
||||
if (currency == null) return NotFound();
|
||||
|
||||
var unlocks = await _db.Unlocks.FirstOrDefaultAsync(x => x.AccountId == userId);
|
||||
if (unlocks == null)
|
||||
{
|
||||
unlocks = new PlayerUnlocks { AccountId = userId.Value };
|
||||
await _db.Unlocks.AddAsync(unlocks);
|
||||
}
|
||||
|
||||
var owned = ReadOwnedInventions(unlocks);
|
||||
if (owned.Contains(inventionId)) return Ok(new Success(false, "Invention is already owned.", null));
|
||||
|
||||
var price = invention.Price ?? 0;
|
||||
if (requestedPrice != price)
|
||||
{
|
||||
return Ok(new Success(false, "Invalid requested price.", null));
|
||||
}
|
||||
|
||||
var (balance, delta, sufficient) = TryDeductCurrency(currency, CurrencyType.RecCenterTokens, price);
|
||||
|
||||
if (!sufficient)
|
||||
{
|
||||
return Ok(new Success(false, "You do not have enough tokens for this invention.", null));
|
||||
}
|
||||
|
||||
owned.Add(inventionId);
|
||||
unlocks.Inventions = owned.ToJson();
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var response = new InventionPurchaseResponseDTO.BalanceResponse
|
||||
{
|
||||
Balance = balance,
|
||||
BalanceAddType = StorefrontBalanceAddTypes.CommercePurchase,
|
||||
CurrencyType = CurrencyType.RecCenterTokens,
|
||||
Delta = delta,
|
||||
Result = new List<InventionPurchaseResponseDTO.BalanceResponse.InventionData>
|
||||
{
|
||||
new InventionPurchaseResponseDTO.BalanceResponse.InventionData{ Invention = invention.ToInvention(), Status = PurchaseStatus.OK }
|
||||
}
|
||||
};
|
||||
var purchased = new InventionPurchaseResponseDTO
|
||||
{
|
||||
InventionResponse = invention.ToInvention(),
|
||||
BalanceUpdateResponse = response
|
||||
};
|
||||
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = NotificationType.StorefrontBalancePurchase, Msg = purchased });
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = NotificationType.StorefrontBalanceUpdate, Msg = response });
|
||||
|
||||
var buyerProfile = await _db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == userId);
|
||||
if (buyerProfile != null)
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = NotificationType.RefreshLogin, Msg = buyerProfile });
|
||||
|
||||
return Ok(new Success(true, null, purchased));
|
||||
}
|
||||
|
||||
private static List<long> ReadOwnedInventions(PlayerUnlocks unlocks)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(unlocks.Inventions) || unlocks.Inventions == "null")
|
||||
return new List<long>();
|
||||
|
||||
try
|
||||
{
|
||||
return unlocks.Inventions.FromJson<List<long>>() ?? new List<long>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<long>();
|
||||
}
|
||||
}
|
||||
|
||||
private static (long balance, long delta, bool sufficient) TryDeductCurrency(
|
||||
RecNet.Accounts.Currency currency, CurrencyType type, long cost)
|
||||
{ long balance = type switch
|
||||
{
|
||||
CurrencyType.RecCenterTokens => currency.RecTokens,
|
||||
CurrencyType.LaserTagTickets => currency.LaserTagTickets,
|
||||
CurrencyType.LostSkullsGold => currency.Gold,
|
||||
CurrencyType.DraculaSilver => currency.DraculaSilver,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
if (balance < cost) return (balance, 0, false);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case CurrencyType.RecCenterTokens: currency.RecTokens -= cost; break;
|
||||
case CurrencyType.LaserTagTickets: currency.LaserTagTickets -= cost; break;
|
||||
case CurrencyType.LostSkullsGold: currency.Gold -= cost; break;
|
||||
case CurrencyType.DraculaSilver: currency.DraculaSilver -= cost; break;
|
||||
}
|
||||
|
||||
return (balance - cost, cost, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace RadicadeServer.API.Storefront;
|
||||
|
||||
[ApiController]
|
||||
[Route("econ/econ/roomGiftDropShops")]
|
||||
public class RoomGiftDropShopAPI : ControllerBase
|
||||
{
|
||||
[HttpGet("room/{roomId:long}")]
|
||||
public IActionResult GetRoomGiftDropShops(long roomId) => Ok(Array.Empty<object>());
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Store;
|
||||
|
||||
namespace RadicadeServer.RecNet.Storefront
|
||||
{
|
||||
public class SeasonRewardItemDTO
|
||||
{
|
||||
public bool RequiresEliteUpgrade { get; set; }
|
||||
public Gift? GiftDrop { get; set; }
|
||||
public float SeasonXpBoost { get; set; }
|
||||
}
|
||||
|
||||
public class SeasonTierDTO : PurchasableGiftDrop
|
||||
{
|
||||
public int Tier { get; set; }
|
||||
public List<SeasonRewardItemDTO> Rewards { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SeasonPersonalDetailsDTO
|
||||
{
|
||||
public bool HasEliteUpgrade { get; set; }
|
||||
public int HasEliteUpgradePlatformMask { get; set; }
|
||||
public int CurrentSeasonTier { get; set; }
|
||||
public DateTime ModifiedAt { get; set; }
|
||||
}
|
||||
|
||||
public class SeasonStorefrontDTO : BaseStorefrontDTO
|
||||
{
|
||||
public int Season { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public DateTime StartAt { get; set; }
|
||||
public DateTime EndAt { get; set; }
|
||||
public CurrencyType CurrencyType { get; set; }
|
||||
public PurchasableGiftDrop? EliteUpgrade { get; set; }
|
||||
public List<SeasonTierDTO> Tiers { get; set; } = new();
|
||||
public SeasonPersonalDetailsDTO PersonalDetails { get; set; } = new();
|
||||
public DateTime? NewUntil { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Store;
|
||||
using RadicadeServer.RecNet.Storefront;
|
||||
|
||||
namespace RadicadeServer.API.Storefront
|
||||
{
|
||||
[Route("api/storefronts/")]
|
||||
public class StorefrontAPI : Controller
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Players.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Store;
|
||||
|
||||
namespace RadicadeServer.RecNet.Storefront
|
||||
{
|
||||
public static class StorefrontCatalog
|
||||
{
|
||||
public const string ResourcePrefix = "StorefrontCaptures/";
|
||||
|
||||
public const string FileSearchPattern = "sf*.json";
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static readonly Dictionary<long, StorefrontCapture> Captures = new();
|
||||
private static readonly Dictionary<int, (long StorefrontId, int Index)> ByPurchasableItemId = new();
|
||||
private static bool _loaded;
|
||||
|
||||
public static Func<IReadOnlyList<string>> FileSource { get; set; } = DiscoverFiles;
|
||||
|
||||
public static bool IsLoaded
|
||||
{
|
||||
get { lock (Gate) return _loaded; }
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<long> StorefrontIds
|
||||
{
|
||||
get
|
||||
{
|
||||
Load();
|
||||
lock (Gate) return Captures.Keys.OrderBy(id => id).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static int Load()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (_loaded) return Captures.Count;
|
||||
|
||||
var captures = ReadCaptures();
|
||||
foreach (var capture in captures)
|
||||
{
|
||||
Captures[capture.StorefrontId] = capture.Capture;
|
||||
for (var index = 0; index < capture.Capture.StoreItems.Count; index++)
|
||||
{
|
||||
var item = capture.Capture.StoreItems[index];
|
||||
if (item.PurchasableItemId <= 0) continue;
|
||||
ByPurchasableItemId.TryAdd(item.PurchasableItemId, (capture.StorefrontId, index));
|
||||
}
|
||||
}
|
||||
|
||||
_loaded = true;
|
||||
|
||||
if (captures.Count == 0)
|
||||
{
|
||||
return Captures.Count;
|
||||
}
|
||||
|
||||
var fromFiles = captures.Where(capture => capture.FromFile).Select(capture => capture.StorefrontId).OrderBy(id => id).ToList();
|
||||
return Captures.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<PurchasableGiftDrop> GetItems(long storefrontId)
|
||||
{
|
||||
Load();
|
||||
lock (Gate)
|
||||
{
|
||||
return Captures.TryGetValue(storefrontId, out var capture)
|
||||
? ToItems(capture)
|
||||
: new List<PurchasableGiftDrop>();
|
||||
}
|
||||
}
|
||||
|
||||
public static PurchasableGiftDrop? FindItem(long storefrontId, int purchasableItemId)
|
||||
{
|
||||
Load();
|
||||
lock (Gate)
|
||||
{
|
||||
if (!Captures.TryGetValue(storefrontId, out var capture)) return null;
|
||||
var item = capture.StoreItems.FirstOrDefault(x => x.PurchasableItemId == purchasableItemId);
|
||||
return item == null ? null : ToItem(item, capture);
|
||||
}
|
||||
}
|
||||
|
||||
public static PurchasableGiftDrop? FindItem(int purchasableItemId)
|
||||
{
|
||||
Load();
|
||||
lock (Gate)
|
||||
{
|
||||
if (!ByPurchasableItemId.TryGetValue(purchasableItemId, out var location)) return null;
|
||||
if (!Captures.TryGetValue(location.StorefrontId, out var capture)) return null;
|
||||
if (location.Index >= capture.StoreItems.Count) return null;
|
||||
return ToItem(capture.StoreItems[location.Index], capture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public sealed record LoadedCapture(long StorefrontId, string Source, bool FromFile, StorefrontCapture Capture);
|
||||
|
||||
public static List<LoadedCapture> ReadCaptures()
|
||||
{
|
||||
var loaded = new List<LoadedCapture>();
|
||||
|
||||
foreach (var (name, json) in EmbeddedCaptures())
|
||||
{
|
||||
var capture = ParseCapture(json, name);
|
||||
if (capture == null) continue;
|
||||
var storefrontId = ResolveStorefrontId(capture, name);
|
||||
if (storefrontId == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
loaded.Add(new LoadedCapture(storefrontId.Value, name, FromFile: false, capture));
|
||||
}
|
||||
|
||||
foreach (var path in FileSource())
|
||||
{
|
||||
var name = Path.GetFileName(path);
|
||||
var capture = ReadCapture(path);
|
||||
if (capture == null) continue;
|
||||
|
||||
var storefrontId = ResolveStorefrontId(capture, name);
|
||||
if (storefrontId == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var replaced = loaded.FindIndex(existing => existing.StorefrontId == storefrontId.Value);
|
||||
if (replaced >= 0)
|
||||
{
|
||||
loaded[replaced] = new LoadedCapture(storefrontId.Value, path, FromFile: true, capture);
|
||||
}
|
||||
else
|
||||
{
|
||||
loaded.Add(new LoadedCapture(storefrontId.Value, path, FromFile: true, capture));
|
||||
}
|
||||
}
|
||||
|
||||
return loaded.OrderBy(capture => capture.StorefrontId).ToList();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<(string Name, string Json)> EmbeddedCaptures()
|
||||
{
|
||||
var assembly = typeof(StorefrontCatalog).Assembly;
|
||||
var payloads = new List<(string Name, string Json)>();
|
||||
|
||||
foreach (var resource in assembly.GetManifestResourceNames()
|
||||
.Where(name => name.StartsWith(ResourcePrefix, StringComparison.Ordinal))
|
||||
.OrderBy(name => name, StringComparer.Ordinal))
|
||||
{
|
||||
using var stream = assembly.GetManifestResourceStream(resource);
|
||||
if (stream == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
payloads.Add((resource[ResourcePrefix.Length..], reader.ReadToEnd()));
|
||||
}
|
||||
|
||||
return payloads;
|
||||
}
|
||||
|
||||
public static StorefrontCapture? ReadCapture(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ParseCapture(File.ReadAllText(path), Path.GetFileName(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static StorefrontCapture? ParseCapture(string json, string sourceName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var capture = JsonSerializer.Deserialize<StorefrontCapture>(json, JsonOptions);
|
||||
if (capture == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return capture;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static long? ResolveStorefrontId(StorefrontCapture capture, string path)
|
||||
{
|
||||
if (capture.StorefrontType is > 0) return capture.StorefrontType.Value;
|
||||
return StorefrontIdFromFileName(path);
|
||||
}
|
||||
|
||||
public static long? StorefrontIdFromFileName(string path)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
if (!name.StartsWith("sf", StringComparison.OrdinalIgnoreCase)) return null;
|
||||
var digits = name[2..].Trim();
|
||||
return int.TryParse(digits, out var id) && id > 0 ? id : null;
|
||||
}
|
||||
|
||||
|
||||
public sealed class StorefrontCapture
|
||||
{
|
||||
[JsonPropertyName("StorefrontType")] public int? StorefrontType { get; set; }
|
||||
[JsonPropertyName("NextUpdate")] public DateTime? NextUpdate { get; set; }
|
||||
[JsonPropertyName("NewUntil")] public DateTime? NewUntil { get; set; }
|
||||
[JsonPropertyName("SubscriberDiscountPercent")] public int SubscriberDiscountPercent { get; set; }
|
||||
[JsonPropertyName("StoreItems")] public List<StorefrontCaptureItem> StoreItems { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class StorefrontCaptureItem
|
||||
{
|
||||
[JsonPropertyName("PurchasableItemId")] public int PurchasableItemId { get; set; }
|
||||
[JsonPropertyName("Type")] public int Type { get; set; }
|
||||
[JsonPropertyName("IsFeatured")] public bool IsFeatured { get; set; }
|
||||
[JsonPropertyName("NewUntil")] public DateTime? NewUntil { get; set; }
|
||||
[JsonPropertyName("Prices")] public List<StorefrontCapturePrice> Prices { get; set; } = new();
|
||||
[JsonPropertyName("SubscriberPrices")] public List<StorefrontCapturePrice> SubscriberPrices { get; set; } = new();
|
||||
[JsonPropertyName("GiftDrop")] public StorefrontCaptureGift? GiftDrop { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StorefrontCapturePrice
|
||||
{
|
||||
[JsonPropertyName("CurrencyType")] public int CurrencyType { get; set; }
|
||||
[JsonPropertyName("Price")] public int Price { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StorefrontCaptureGift
|
||||
{
|
||||
[JsonPropertyName("GiftDropId")] public long GiftDropId { get; set; }
|
||||
[JsonPropertyName("FriendlyName")] public string? FriendlyName { get; set; }
|
||||
[JsonPropertyName("Tooltip")] public string? Tooltip { get; set; }
|
||||
[JsonPropertyName("ConsumableItemDesc")] public string? ConsumableItemDesc { get; set; }
|
||||
[JsonPropertyName("AvatarItemDesc")] public string? AvatarItemDesc { get; set; }
|
||||
[JsonPropertyName("AvatarItemType")] public int? AvatarItemType { get; set; }
|
||||
[JsonPropertyName("EquipmentPrefabName")] public string? EquipmentPrefabName { get; set; }
|
||||
[JsonPropertyName("EquipmentModificationGuid")] public string? EquipmentModificationGuid { get; set; }
|
||||
[JsonPropertyName("IsQuery")] public bool IsQuery { get; set; }
|
||||
[JsonPropertyName("Unique")] public bool Unique { get; set; }
|
||||
[JsonPropertyName("SubscribersOnly")] public bool SubscribersOnly { get; set; }
|
||||
[JsonPropertyName("Rarity")] public int? Rarity { get; set; }
|
||||
[JsonPropertyName("Context")] public int? Context { get; set; }
|
||||
[JsonPropertyName("Currency")] public int Currency { get; set; }
|
||||
[JsonPropertyName("CurrencyType")] public int? CurrencyType { get; set; }
|
||||
[JsonPropertyName("ItemSetId")] public long? ItemSetId { get; set; }
|
||||
[JsonPropertyName("ItemSetFriendlyName")] public string? ItemSetFriendlyName { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public static List<PurchasableGiftDrop> ToItems(StorefrontCapture capture)
|
||||
{
|
||||
var items = new List<PurchasableGiftDrop>(capture.StoreItems.Count);
|
||||
foreach (var item in capture.StoreItems)
|
||||
{
|
||||
var mapped = ToItem(item, capture);
|
||||
if (mapped != null) items.Add(mapped);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public static PurchasableGiftDrop? ToItem(StorefrontCaptureItem item, StorefrontCapture capture)
|
||||
{
|
||||
var gift = item.GiftDrop;
|
||||
if (gift == null || !HasContent(gift)) return null;
|
||||
|
||||
return new PurchasableGiftDrop
|
||||
{
|
||||
PurchasableItemId = item.PurchasableItemId,
|
||||
Type = Enum.IsDefined(typeof(PurchasableItem.PurchasableItemType), item.Type)
|
||||
? (PurchasableItem.PurchasableItemType)item.Type
|
||||
: PurchasableItem.PurchasableItemType.GiftDrop,
|
||||
IsFeatured = item.IsFeatured,
|
||||
Prices = item.Prices.Select(ToPrice).ToList(),
|
||||
SubscriberPrices = item.SubscriberPrices.Select(ToPrice).ToList(),
|
||||
EndDate = null,
|
||||
NewUntil = item.NewUntil,
|
||||
GiftDrops = new List<Gift>
|
||||
{
|
||||
new()
|
||||
{
|
||||
GiftDropId = gift.GiftDropId,
|
||||
FriendlyName = gift.FriendlyName?.Trim() ?? string.Empty,
|
||||
Tooltip = gift.Tooltip?.Trim() ?? string.Empty,
|
||||
ConsumableItemDesc = gift.ConsumableItemDesc ?? string.Empty,
|
||||
AvatarItemDesc = gift.AvatarItemDesc ?? string.Empty,
|
||||
AvatarItemType = gift.AvatarItemType is null
|
||||
? AvatarItemType.Outfit
|
||||
: (AvatarItemType)gift.AvatarItemType,
|
||||
EquipmentPrefabName = gift.EquipmentPrefabName ?? string.Empty,
|
||||
EquipmentModificationGuid = gift.EquipmentModificationGuid ?? string.Empty,
|
||||
IsQuery = gift.IsQuery,
|
||||
Unique = gift.Unique,
|
||||
SubscribersOnly = gift.SubscribersOnly,
|
||||
Rarity = (Rarity)(gift.Rarity ?? (int)Rarity.Common),
|
||||
Context = (GiftContext)(gift.Context ?? (int)GiftContext.Commerce_Purchase),
|
||||
Currency = gift.Currency,
|
||||
CurrencyType = (CurrencyType)(gift.CurrencyType ?? (int)CurrencyType.RecCenterTokens),
|
||||
ItemSetId = gift.ItemSetId ?? 0,
|
||||
ItemSetFriendlyName = gift.ItemSetFriendlyName ?? string.Empty
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static PurchasablePrice ToPrice(StorefrontCapturePrice price) => new()
|
||||
{
|
||||
CurrencyType = (CurrencyType)price.CurrencyType,
|
||||
Price = price.Price
|
||||
};
|
||||
|
||||
private static bool HasContent(StorefrontCaptureGift gift) =>
|
||||
!string.IsNullOrWhiteSpace(gift.FriendlyName)
|
||||
|| !string.IsNullOrWhiteSpace(gift.AvatarItemDesc)
|
||||
|| !string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)
|
||||
|| !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName);
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true
|
||||
};
|
||||
|
||||
private static IReadOnlyList<string> DiscoverFiles()
|
||||
{
|
||||
var files = new List<string>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var directory in DBConfig.StorefrontDirectories())
|
||||
{
|
||||
if (!Directory.Exists(directory)) continue;
|
||||
|
||||
foreach (var path in Directory.GetFiles(directory, FileSearchPattern))
|
||||
{
|
||||
if (!seen.Add(Path.GetFileName(path))) continue;
|
||||
files.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
return files.OrderBy(path => StorefrontIdFromFileName(path) ?? long.MaxValue).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Store;
|
||||
|
||||
namespace RadicadeServer.RecNet.Storefront
|
||||
{
|
||||
public class RequestPurchaseItemDTO
|
||||
{
|
||||
public StorefrontTypes StorefrontType { get; set; }
|
||||
public int PurchasableItemId { get; set; }
|
||||
public CurrencyType CurrencyType { get; set; }
|
||||
|
||||
public long? RequestedPrice { get; set; }
|
||||
|
||||
public long? CouponConsumablePlayerMappingId { get; set; }
|
||||
|
||||
public GiftRequestItemDTO? Gift { get; set; }
|
||||
}
|
||||
|
||||
public class GiftRequestItemDTO
|
||||
{
|
||||
public int ToPlayerId { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public bool Anonymous { get; set; }
|
||||
public GiftContext GiftContext { get; set; }
|
||||
}
|
||||
|
||||
public class GrantBalanceRequest
|
||||
{
|
||||
public float Multiplier { get; set; }
|
||||
public StorefrontBalanceAddTypes BalanceAddType { get; set; }
|
||||
}
|
||||
|
||||
public class GrantBalanceRewardDTO
|
||||
{
|
||||
public CurrencyType CurrencyType { get; set; }
|
||||
public List<GrantBalanceRequest> BalanceAdds { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CompleteObjectiveDTO
|
||||
{
|
||||
public int ObjectiveType { get; set; }
|
||||
public float CompletionPercentage { get; set; }
|
||||
public long? RoomId { get; set; }
|
||||
}
|
||||
|
||||
public class StorefrontPurchaseRequestDTO
|
||||
{
|
||||
public StorefrontTypes Storefront { get; set; }
|
||||
public int ItemId { get; set; }
|
||||
public CurrencyType Currency { get; set; }
|
||||
public long Price { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Players;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Inventions;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.RecNet.Store;
|
||||
|
||||
namespace RadicadeServer.RecNet.Storefront
|
||||
{
|
||||
public enum BalanceType
|
||||
{
|
||||
Free = 0,
|
||||
Paid = 1,
|
||||
Promotional = 2
|
||||
}
|
||||
|
||||
public class StorefrontBalanceDTO
|
||||
{
|
||||
public CurrencyType Currency { get; set; }
|
||||
public BalanceType Type { get; set; }
|
||||
public long Amount { get; set; }
|
||||
}
|
||||
|
||||
public class StorefrontItemDTO
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public long Price { get; set; }
|
||||
}
|
||||
|
||||
public class StorefrontResponseDTO
|
||||
{
|
||||
public List<StorefrontItemDTO> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AdCarouselItemDTO
|
||||
{
|
||||
public int AdCarouselItemId { get; set; }
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<int> PurchasableItemIds { get; set; } = new();
|
||||
|
||||
public int Id { get; set; }
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class StorefrontPurchaseResponseDTO
|
||||
{
|
||||
public StorefrontBalanceDTO BalanceUpdate { get; set; } = null!;
|
||||
}
|
||||
|
||||
|
||||
public class RewardBalanceModificationDTO
|
||||
{
|
||||
public StorefrontBalanceAddTypes BalanceAddType { get; set; }
|
||||
public int BaseAward { get; set; }
|
||||
public int BonusAward { get; set; }
|
||||
public int RateLimit { get; set; }
|
||||
public int CurrentCount { get; set; }
|
||||
public int Total { get; set; }
|
||||
public Platforms Platform { get; set; } = Platforms.All;
|
||||
public bool BalanceInGiftBox { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public enum StorefrontRateLimitTypes
|
||||
{
|
||||
Interval = 0,
|
||||
PerDay = 1
|
||||
}
|
||||
|
||||
public class StorefrontBalanceAddConfigDTO
|
||||
{
|
||||
public StorefrontBalanceAddTypes BalanceAddType { get; set; }
|
||||
|
||||
public CurrencyType CurrencyType { get; set; }
|
||||
|
||||
public int BaseAward { get; set; }
|
||||
|
||||
public int BonusAwardMin { get; set; }
|
||||
|
||||
public int BonusAwardMax { get; set; }
|
||||
|
||||
public StorefrontRateLimitTypes RateLimitType { get; set; } = StorefrontRateLimitTypes.PerDay;
|
||||
|
||||
public float MaxPartialMultiplier { get; set; } = 1f;
|
||||
|
||||
public float Multiplier { get; set; } = 1f;
|
||||
|
||||
public int RateLimit { get; set; }
|
||||
|
||||
public int CurrentCount { get; set; }
|
||||
|
||||
public int Total { get; set; }
|
||||
|
||||
public bool BalanceInGiftBox { get; set; }
|
||||
|
||||
public bool IgnorePartialMultiplier { get; set; }
|
||||
}
|
||||
|
||||
public class BalanceUpdateEntry<T>
|
||||
{
|
||||
public UpdateResponseTypes UpdateResponse { get; set; }
|
||||
public T? Data { get; set; }
|
||||
}
|
||||
|
||||
public class BalanceUpdateResponse<T> : BalanceResponse
|
||||
{
|
||||
public List<BalanceUpdateEntry<T>> BalanceUpdates { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PurchaseBalanceUpdateEntry<T>
|
||||
{
|
||||
public UpdateResponseTypes UpdateResponse { get; set; }
|
||||
public List<T> Data { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PurchaseBalanceUpdateResponse<T> : BalanceResponse
|
||||
{
|
||||
public List<PurchaseBalanceUpdateEntry<T>> BalanceUpdates { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BalanceResponseDTO
|
||||
{
|
||||
public StorefrontBalanceAddTypes BalanceAddType { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public long? Delta { get; set; }
|
||||
|
||||
public long Balance { get; set; }
|
||||
public CurrencyType CurrencyType { get; set; }
|
||||
}
|
||||
|
||||
public enum PurchaseStatus
|
||||
{
|
||||
OK,
|
||||
TooManyRequests,
|
||||
NotEnoughCredit,
|
||||
AlreadyOwned,
|
||||
NoItemAvailable,
|
||||
CouponNotApplicable,
|
||||
RequestedPriceDoesNotMatch
|
||||
}
|
||||
|
||||
public class InventionPurchaseResponseDTO
|
||||
{
|
||||
public class BalanceResponse : BalanceResponseDTO
|
||||
{
|
||||
public class InventionData
|
||||
{
|
||||
public PurchaseStatus Status { get; set; }
|
||||
public Invention Invention { get; set; } = null!;
|
||||
}
|
||||
public List<InventionData> Result { get; set; } = null!;
|
||||
}
|
||||
|
||||
public Invention InventionResponse { get; set; } = null!;
|
||||
public BalanceResponse BalanceUpdateResponse { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace RadicadeServer.RecNet.Storefront
|
||||
{
|
||||
public class StorefrontSeasonDTO
|
||||
{
|
||||
public int SeasonNumber { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public int MaxTier { get; set; }
|
||||
public bool HasEliteTrack { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Store;
|
||||
using RadicadeServer.RecNet.Storefront;
|
||||
using RadicadeServer.API.Storefront;
|
||||
|
||||
namespace RadicadeServer.API.Storefront.V1;
|
||||
|
||||
[Route("api/storefronts/v1")]
|
||||
[Route("econ/api/storefronts/v1")]
|
||||
public class StorefrontsV1(RecDBContext db) : Controller
|
||||
{
|
||||
[HttpGet("season/{type}")]
|
||||
public IActionResult GetSeason(StorefrontTypes type)
|
||||
{
|
||||
if (type != StorefrontTypes.RecRoyale)
|
||||
return NotFound();
|
||||
|
||||
const int maxTiers = 20;
|
||||
const int elitePurchasableItemId = 200100;
|
||||
const int tierPriceTokens = 500;
|
||||
|
||||
var tiers = new List<SeasonTierDTO>();
|
||||
for (int tier = 1; tier <= maxTiers; tier++)
|
||||
{
|
||||
var gift = new Gift
|
||||
{
|
||||
GiftDropId = StorefrontShared.NewGiftDropId(),
|
||||
FriendlyName = $"Season 1 Tier {tier} Reward",
|
||||
Tooltip = "Rec Royale Season 1 reward.",
|
||||
CurrencyType = CurrencyType.RecRoyale_Season1,
|
||||
Currency = 100 + tier * 50,
|
||||
Context = GiftContext.RecRoyale,
|
||||
Rarity = tier >= 15 ? Rarity.Epic : tier >= 8 ? Rarity.Rare : Rarity.Common
|
||||
};
|
||||
tiers.Add(new SeasonTierDTO
|
||||
{
|
||||
PurchasableItemId = 200000 + tier,
|
||||
Type = PurchasableItem.PurchasableItemType.SeasonTier,
|
||||
IsFeatured = tier == 1,
|
||||
Prices = new List<PurchasablePrice>
|
||||
{
|
||||
new() { CurrencyType = CurrencyType.RecCenterTokens, Price = tierPriceTokens + (tier - 1) * 250 }
|
||||
},
|
||||
SubscriberPrices = new List<PurchasablePrice>(),
|
||||
EndDate = null,
|
||||
NewUntil = DateTime.MaxValue,
|
||||
GiftDrops = new List<Gift> { gift },
|
||||
Tier = tier,
|
||||
Rewards = new List<SeasonRewardItemDTO>
|
||||
{
|
||||
new()
|
||||
{
|
||||
RequiresEliteUpgrade = false,
|
||||
GiftDrop = gift,
|
||||
SeasonXpBoost = 0f
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var eliteGift = new Gift
|
||||
{
|
||||
GiftDropId = StorefrontShared.NewGiftDropId(),
|
||||
FriendlyName = "Rec Royale Season 1 Elite Upgrade",
|
||||
Tooltip = "Unlocks the elite track for every tier.",
|
||||
CurrencyType = CurrencyType.RecRoyale_Season1,
|
||||
Currency = 500,
|
||||
Context = GiftContext.RecRoyale,
|
||||
Rarity = Rarity.Epic
|
||||
};
|
||||
var eliteUpgrade = new PurchasableGiftDrop
|
||||
{
|
||||
PurchasableItemId = elitePurchasableItemId,
|
||||
Type = PurchasableItem.PurchasableItemType.SeasonEliteUpgrade,
|
||||
IsFeatured = true,
|
||||
Prices = new List<PurchasablePrice>
|
||||
{
|
||||
new() { CurrencyType = CurrencyType.RecCenterTokens, Price = 2000 }
|
||||
},
|
||||
SubscriberPrices = new List<PurchasablePrice>(),
|
||||
EndDate = null,
|
||||
GiftDrops = new List<Gift> { eliteGift }
|
||||
};
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var season = new SeasonStorefrontDTO
|
||||
{
|
||||
StorefrontType = StorefrontTypes.RecRoyale,
|
||||
NextUpdate = DateTime.MaxValue,
|
||||
NewUntil = DateTime.MaxValue,
|
||||
EndDate = DateTime.MaxValue,
|
||||
Season = 1,
|
||||
Name = "Rec Royale Season 1",
|
||||
StartAt = now.AddDays(-30),
|
||||
EndAt = now.AddDays(30),
|
||||
CurrencyType = CurrencyType.RecRoyale_Season1,
|
||||
EliteUpgrade = eliteUpgrade,
|
||||
Tiers = tiers,
|
||||
PersonalDetails = new SeasonPersonalDetailsDTO
|
||||
{
|
||||
HasEliteUpgrade = false,
|
||||
HasEliteUpgradePlatformMask = 0,
|
||||
CurrentSeasonTier = 0,
|
||||
ModifiedAt = now
|
||||
}
|
||||
};
|
||||
return Ok(season);
|
||||
}
|
||||
|
||||
[HttpGet("balanceAddType/{type}/{addType}"), Authorize]
|
||||
public IActionResult GetBalanceAddConfig(CurrencyType type, StorefrontBalanceAddTypes addType)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
return Ok(new StorefrontBalanceAddConfigDTO
|
||||
{
|
||||
BalanceAddType = addType,
|
||||
CurrencyType = type,
|
||||
BaseAward = 10,
|
||||
RateLimit = 0,
|
||||
Multiplier = 1f
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{type}")]
|
||||
public async Task<IActionResult> GetStorefront(StorefrontTypes type)
|
||||
{
|
||||
var items = await StorefrontShared.GetStoreItems(db, (long)type);
|
||||
if (items.Count == 0 && !Enum.IsDefined(typeof(StorefrontTypes), type))
|
||||
return NotFound();
|
||||
return Ok(new GiftDropStorefront
|
||||
{
|
||||
NextUpdate = DateTime.MaxValue,
|
||||
NewUntil = null,
|
||||
StorefrontType = type,
|
||||
StoreItems = items
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("toptoday")]
|
||||
public async Task<IActionResult> GetTopItems()
|
||||
{
|
||||
var items = await StorefrontShared.GetStoreItems(db, (long)StorefrontTypes.Watch);
|
||||
|
||||
return Ok(items.Where(x => x.IsFeatured).Select(x => x.PurchasableItemId).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("objectives"), Authorize]
|
||||
public IActionResult CompleteObjectives([FromBody] List<CompleteObjectiveDTO> objectives)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("adcarouselitems")]
|
||||
public IActionResult GetAdCarouselItems()
|
||||
{
|
||||
return Ok(Array.Empty<AdCarouselItemDTO>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Storefront;
|
||||
|
||||
namespace RadicadeServer.API.Storefront.V2;
|
||||
|
||||
[Route("api/storefronts/v2")]
|
||||
[Route("econ/api/storefronts/v2")]
|
||||
public class StorefrontsV2(RecDBContext db) : Controller
|
||||
{
|
||||
[HttpGet("balance"), Authorize]
|
||||
public async Task<IActionResult> GetBalances()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var data = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId.Value);
|
||||
if (data == null)
|
||||
return Ok(new List<RadicadeServer.RecNet.Responses.BalanceResponse>());
|
||||
|
||||
return Ok(new List<RadicadeServer.RecNet.Responses.BalanceResponse>
|
||||
{
|
||||
new() { CurrencyType = CurrencyType.RecCenterTokens, Balance = data.RecTokens },
|
||||
new() { CurrencyType = CurrencyType.LaserTagTickets, Balance = data.LaserTagTickets },
|
||||
new() { CurrencyType = CurrencyType.LostSkullsGold, Balance = data.Gold },
|
||||
new() { CurrencyType = CurrencyType.DraculaSilver, Balance = data.DraculaSilver }
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("balance"), Authorize]
|
||||
public async Task<IActionResult> ModifyBalance([FromBody] GrantBalanceRewardDTO? request)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null || request == null || request.BalanceAdds is not { Count: > 0 })
|
||||
return BadRequest();
|
||||
|
||||
var currency = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId.Value);
|
||||
if (currency == null) return NotFound();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const int baseAward = 10;
|
||||
long awarded = 0;
|
||||
var updates = new List<BalanceUpdateEntry<RewardBalanceModificationDTO>>();
|
||||
|
||||
foreach (var grant in request.BalanceAdds)
|
||||
{
|
||||
if (grant == null) continue;
|
||||
|
||||
long total = (long)(baseAward * (double)grant.Multiplier);
|
||||
if (total <= 0) total = baseAward;
|
||||
|
||||
StorefrontShared.AddCurrency(currency, request.CurrencyType, total);
|
||||
awarded += total;
|
||||
|
||||
updates.Add(new()
|
||||
{
|
||||
UpdateResponse = UpdateResponseTypes.OK,
|
||||
Data = new RewardBalanceModificationDTO
|
||||
{
|
||||
BalanceAddType = grant.BalanceAddType,
|
||||
BaseAward = baseAward,
|
||||
BonusAward = 0,
|
||||
Total = (int)total,
|
||||
CurrentCount = 1,
|
||||
RateLimit = 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
long newBalance = StorefrontShared.GetBalance(currency, request.CurrencyType);
|
||||
|
||||
return Ok(new BalanceUpdateResponse<RewardBalanceModificationDTO>
|
||||
{
|
||||
CurrencyType = request.CurrencyType,
|
||||
Balance = newBalance,
|
||||
BalanceUpdates = updates
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost("buyItem"), Authorize]
|
||||
public async Task<IActionResult> BuyItem([FromBody] RequestPurchaseItemDTO? request)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
if (request == null)
|
||||
{
|
||||
return BadRequest(new { error = "invalid_request" });
|
||||
}
|
||||
|
||||
var outcome = await StorefrontShared.ExecutePurchaseAsync(
|
||||
db,
|
||||
userId.Value,
|
||||
request.StorefrontType,
|
||||
request.PurchasableItemId,
|
||||
request.CurrencyType,
|
||||
request.RequestedPrice,
|
||||
request.Gift);
|
||||
|
||||
if (outcome.Response != null)
|
||||
return Ok(outcome.Response);
|
||||
return outcome.Error switch
|
||||
{
|
||||
"cannot_gift_self" or "invalid_recipient" => BadRequest(new { error = outcome.Error, message = outcome.ErrorDetail }),
|
||||
_ => StatusCode(StatusCodes.Status500InternalServerError, new { error = outcome.Error, message = outcome.ErrorDetail })
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Players;
|
||||
using RadicadeServer.RecNet.Store;
|
||||
using RadicadeServer.RecNet.Storefront;
|
||||
|
||||
namespace RadicadeServer.API.Storefront.V3;
|
||||
|
||||
[Route("api/storefronts/v3")]
|
||||
[Route("econ/api/storefronts/v3")]
|
||||
public class StorefrontsV3(RecDBContext db) : Controller
|
||||
{
|
||||
[HttpGet("giftdropstore/{type}")]
|
||||
public async Task<IActionResult> GetGiftDropStore(StorefrontTypes type)
|
||||
{
|
||||
var items = await StorefrontShared.GetStoreItems(db, (long)type);
|
||||
items = ProjectStoreItemsForClient(
|
||||
items,
|
||||
User.FindFirst("rn.plat")?.Value,
|
||||
AvatarUnlockCatalog.IsClientResolvableDescription);
|
||||
Console.WriteLine($"[Storefront v3] GET giftdropstore {type} ({(long)type}): returning {items.Count} item(s).");
|
||||
|
||||
return Ok(new GiftDropStorefront
|
||||
{
|
||||
StorefrontType = type,
|
||||
NextUpdate = DateTime.MaxValue,
|
||||
NewUntil = null,
|
||||
StoreItems = items
|
||||
});
|
||||
}
|
||||
|
||||
public static List<PurchasableGiftDrop> ProjectStoreItemsForClient(
|
||||
IEnumerable<PurchasableGiftDrop> items,
|
||||
string? platformClaim,
|
||||
Func<string?, bool> canResolveAvatarItem)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
ArgumentNullException.ThrowIfNull(canResolveAvatarItem);
|
||||
|
||||
if (!string.Equals(platformClaim, ((int)Platforms.Standalone).ToString(), StringComparison.Ordinal))
|
||||
return items.ToList();
|
||||
|
||||
var projected = new List<PurchasableGiftDrop>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var projectedGifts = new List<Gift>();
|
||||
var changed = false;
|
||||
|
||||
foreach (var gift in item.GiftDrops)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(gift.AvatarItemDesc) || canResolveAvatarItem(gift.AvatarItemDesc))
|
||||
{
|
||||
projectedGifts.Add(gift);
|
||||
continue;
|
||||
}
|
||||
|
||||
changed = true;
|
||||
if (HasNonAvatarReward(gift))
|
||||
{
|
||||
var giftCopy = gift.Clone();
|
||||
giftCopy.AvatarItemDesc = string.Empty;
|
||||
projectedGifts.Add(giftCopy);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
projected.Add(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (projectedGifts.Count == 0) continue;
|
||||
|
||||
projected.Add(new PurchasableGiftDrop
|
||||
{
|
||||
PurchasableItemId = item.PurchasableItemId,
|
||||
Type = item.Type,
|
||||
Prices = item.Prices.ToList(),
|
||||
SubscriberPrices = item.SubscriberPrices.ToList(),
|
||||
IsFeatured = item.IsFeatured,
|
||||
EndDate = item.EndDate,
|
||||
NewUntil = item.NewUntil,
|
||||
GiftDrops = projectedGifts
|
||||
});
|
||||
}
|
||||
|
||||
return projected;
|
||||
}
|
||||
|
||||
private static bool HasNonAvatarReward(Gift gift) =>
|
||||
!string.IsNullOrWhiteSpace(gift.ConsumableItemDesc)
|
||||
|| !string.IsNullOrWhiteSpace(gift.EquipmentPrefabName)
|
||||
|| gift.Currency > 0
|
||||
|| gift.Xp > 0;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.Utils;
|
||||
|
||||
namespace RadicadeServer.API.Storefront.V4;
|
||||
|
||||
[Route("api/storefronts/v4")]
|
||||
[Route("econ/api/storefronts/v4")]
|
||||
public class StorefrontsV4(RecDBContext db) : Controller
|
||||
{
|
||||
[HttpGet("balance/{type}"), Authorize]
|
||||
public async Task<List<BalanceResponse>?> GetBalance(CurrencyType type)
|
||||
{
|
||||
var userId = User.GetUserId(); var data = await db.Currency.FirstOrDefaultAsync(x => x.AccountId == userId);
|
||||
|
||||
long balance;
|
||||
if (type == CurrencyType.RecRoyale_Season1)
|
||||
{
|
||||
|
||||
|
||||
var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == userId);
|
||||
balance = progression == null ? 0 : LevelUpGiftService.XpIntoCurrentLevel(progression);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data == null) return [];
|
||||
balance = type switch
|
||||
{
|
||||
CurrencyType.RecCenterTokens => data.RecTokens,
|
||||
CurrencyType.LaserTagTickets => data.LaserTagTickets,
|
||||
CurrencyType.LostSkullsGold => data.Gold,
|
||||
CurrencyType.DraculaSilver => data.DraculaSilver,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
return new List<BalanceResponse> { new() { CurrencyType = type, Balance = balance } };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.Extensions;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using RadicadeServer.DB;
|
||||
|
||||
namespace RadicadeServer.API.Wishlists;
|
||||
|
||||
public class WishlistItemDB
|
||||
{
|
||||
[Key] public string WishlistItemId { get; set; } = Guid.NewGuid().ToString();
|
||||
public long AccountId { get; set; }
|
||||
public long PurchasableItemId { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
[Route("api/itemWishlists/v1")]
|
||||
[Route("econ/api/itemWishlists/v1")]
|
||||
public class WishlistAPI : Controller
|
||||
{
|
||||
private readonly RecDBContext db;
|
||||
public WishlistAPI(RecDBContext db) => this.db = db;
|
||||
|
||||
[HttpGet("wishlist/me"), Authorize]
|
||||
public async Task<IActionResult> GetMyWishlist()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
return Ok(await db.Set<WishlistItemDB>().AsNoTracking()
|
||||
.Where(x => x.AccountId == userId.Value).OrderBy(x => x.CreatedAt).ToListAsync());
|
||||
}
|
||||
|
||||
[HttpGet("wishlist/{accountId}"), Authorize]
|
||||
public async Task<IActionResult> GetWishlistByAccountId(long accountId)
|
||||
{
|
||||
return Ok(await db.Set<WishlistItemDB>().AsNoTracking()
|
||||
.Where(x => x.AccountId == accountId).OrderBy(x => x.CreatedAt).ToListAsync());
|
||||
}
|
||||
|
||||
[HttpPut("wishlist/me/{purchasableItemId}"), Authorize]
|
||||
[HttpPost("wishlist/me/{purchasableItemId}"), Authorize]
|
||||
public async Task<IActionResult> AddToWishlist(long purchasableItemId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null || purchasableItemId <= 0) return BadRequest(new { error = "invalid_request" });
|
||||
var set = db.Set<WishlistItemDB>();
|
||||
var item = await set.FirstOrDefaultAsync(x => x.AccountId == userId.Value && x.PurchasableItemId == purchasableItemId);
|
||||
if (item == null)
|
||||
{
|
||||
item = new WishlistItemDB { WishlistItemId = Guid.NewGuid().ToString(), AccountId = userId.Value, PurchasableItemId = purchasableItemId, CreatedAt = DateTime.UtcNow };
|
||||
await set.AddAsync(item);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
return Ok(new { error = "", success = true, value = item });
|
||||
}
|
||||
|
||||
[HttpDelete("wishlist/me/{purchasableItemId}"), Authorize]
|
||||
public async Task<IActionResult> RemoveFromWishlist(long purchasableItemId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
var item = await db.Set<WishlistItemDB>().FirstOrDefaultAsync(x => x.AccountId == userId.Value && x.PurchasableItemId == purchasableItemId);
|
||||
if (item != null) { db.Remove(item); await db.SaveChangesAsync(); }
|
||||
return Ok(new { Success = true });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user