Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+60
View File
@@ -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; }
}
}
+23
View File
@@ -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; }
}
}
+80
View File
@@ -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;
}
}
+13
View File
@@ -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();
}
}
+10
View File
@@ -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();
}
}
+170
View File
@@ -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);
}
}
}
+11
View File
@@ -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>());
}
+40
View File
@@ -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; }
}
}
+16
View File
@@ -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
{
}
}
+355
View File
@@ -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();
}
}
}
+53
View File
@@ -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; }
}
}
+169
View File
@@ -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!;
}
}
+12
View File
@@ -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; }
}
}
+926
View File
@@ -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
};
}
}
+166
View File
@@ -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>());
}
}
+122
View File
@@ -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 })
};
}
}
+98
View File
@@ -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;
}
+44
View File
@@ -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 } };
}
}
+68
View File
@@ -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 });
}
}