mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
547 lines
21 KiB
C#
547 lines
21 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Text.Json;
|
|
using RadicadeServer.API.Storefront;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.Extensions;
|
|
using RadicadeServer.RecNet.Accounts;
|
|
using RadicadeServer.Utils;
|
|
using RadicadeServer.RecNet.Avatar;
|
|
using RadicadeServer.RecNet.Enums.Store;
|
|
using RadicadeServer.RecNet.Progression;
|
|
using RadicadeServer.RecNet.Store;
|
|
|
|
namespace RadicadeServer.API.Avatars.V2;
|
|
|
|
[Route("api/avatar/v2")]
|
|
[Route("econ/api/avatar/v2")]
|
|
public class AvatarV2(RecDBContext db) : Controller
|
|
{
|
|
[HttpGet(""), Authorize]
|
|
public async Task<Avatar?> GetLocalAvatar()
|
|
{
|
|
var userId = User.GetUserId();
|
|
if (userId == null) return null;
|
|
|
|
var avatar = db.Avatars.FirstOrDefault(x => x.AccountId == userId);
|
|
if (avatar != null)
|
|
{
|
|
if (NormalizeSavedAvatar(avatar))
|
|
{
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
return avatar.DeserializeAvatar();
|
|
}
|
|
|
|
avatar = new RadicadeServer.RecNet.Accounts.Local.PlayerAvatar
|
|
{
|
|
AccountId = userId.Value,
|
|
Avatar = DBConfig.DefaultAvatar,
|
|
Outfits = "[]"
|
|
};
|
|
await db.Avatars.AddAsync(avatar);
|
|
await db.SaveChangesAsync();
|
|
|
|
return avatar.DeserializeAvatar();
|
|
}
|
|
|
|
[HttpGet("{accountId:long}")]
|
|
public async Task<Avatar> GetAvatarForAccount(long accountId)
|
|
{
|
|
var saved = await db.Avatars.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == accountId);
|
|
var avatar = saved?.DeserializeAvatar();
|
|
if (avatar != null) return avatar;
|
|
|
|
return DBConfig.DefaultAvatar.FromJson<Avatar>() ?? new Avatar();
|
|
}
|
|
|
|
[HttpPost("set"), Authorize]
|
|
public async Task<Avatar?> SetLocalAvatar([FromBody] Avatar newAvatar)
|
|
{
|
|
var userId = User.GetUserId();
|
|
if (userId == null) return null;
|
|
|
|
var avatar = await db.Avatars.FirstOrDefaultAsync(x => x.AccountId == userId);
|
|
if (avatar == null)
|
|
{
|
|
avatar = new RadicadeServer.RecNet.Accounts.Local.PlayerAvatar
|
|
{
|
|
AccountId = userId.Value,
|
|
Avatar = DBConfig.DefaultAvatar,
|
|
Outfits = "[]"
|
|
};
|
|
await db.Avatars.AddAsync(avatar);
|
|
}
|
|
|
|
avatar.Avatar = newAvatar.ToJson();
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return newAvatar;
|
|
} [HttpGet("gifts"), Authorize]
|
|
public async Task<IActionResult> GetGifts()
|
|
{
|
|
var userId = User.GetUserId();
|
|
if (userId == null) return Unauthorized();
|
|
|
|
var gifts = await StorefrontShared.GetPendingGiftsAsync(db, userId.Value);
|
|
|
|
return Ok(gifts.Select(g => g.ForClient()).ToList());
|
|
}
|
|
|
|
[HttpPost("gifts/claim"), Authorize]
|
|
public async Task<IActionResult> ClaimGifts()
|
|
{
|
|
var userId = User.GetUserId();
|
|
if (userId == null) return Unauthorized();
|
|
|
|
var requestedIds = await ReadGiftIdsFromBodyAsync();
|
|
|
|
var gifts = await StorefrontShared.GetPendingGiftsAsync(db, userId.Value);
|
|
var toClaim = requestedIds.Count > 0
|
|
? gifts.Where(g => requestedIds.Contains(g.GiftDropId)).ToList()
|
|
: gifts;
|
|
|
|
if (toClaim.Count == 0)
|
|
{
|
|
return GiftClaimResponse(new List<Gift>(), await GetPlayerLevelAsync(userId.Value));
|
|
}
|
|
|
|
var claimed = new List<Gift>();
|
|
var levelUpGifts = new List<Gift>();
|
|
var balanceGrants = new List<StorefrontShared.BalanceGrant>();
|
|
var strategy = db.Database.CreateExecutionStrategy();
|
|
await strategy.ExecuteAsync(async () =>
|
|
{
|
|
await using var tx = await db.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
foreach (var gift in toClaim)
|
|
{
|
|
var grant = await GrantGift(userId.Value, gift);
|
|
if (grant != null) balanceGrants.Add(grant);
|
|
await StorefrontShared.RemovePendingGiftAsync(db, userId.Value, gift.GiftDropId);
|
|
claimed.Add(gift);
|
|
}
|
|
levelUpGifts.AddRange(await LevelUpGiftService.DeliverLevelUpGiftsAsync(db, userId.Value, recomputeFromXp: true, notify: false));
|
|
await tx.CommitAsync();
|
|
await AnnounceBalancesAsync(userId.Value, balanceGrants, StorefrontBalanceAddTypes.FromGiftBox);
|
|
}
|
|
catch
|
|
{
|
|
await tx.RollbackAsync();
|
|
throw;
|
|
}
|
|
});
|
|
|
|
foreach (var gift in levelUpGifts)
|
|
await StorefrontShared.SendGiftPackageNotificationAsync(userId.Value, gift);
|
|
return GiftClaimResponse(claimed, await GetPlayerLevelAsync(userId.Value));
|
|
}
|
|
|
|
[HttpPost("gifts/generate"), Authorize]
|
|
public async Task<IActionResult> GenerateGift()
|
|
{
|
|
var userId = User.GetUserId();
|
|
if (userId == null) return Unauthorized();
|
|
|
|
var fields = await ReadGiftGenerateFieldsAsync();
|
|
if (!TryReadGiftContext(fields, "MFPELEJLJLD", "GiftContext", out var giftContext) ||
|
|
!TryReadOptionalGiftContext(fields, "CIEHFJEIOJJ", "AlternateGiftContext", out var alternateContext))
|
|
return BadRequest(new { error = "invalid_gift_context" });
|
|
|
|
fields.TryGetValue("Message", out var message);
|
|
if (message == null && fields.TryGetValue("DPAJDIEBNKM", out var opaqueMessage))
|
|
message = opaqueMessage;
|
|
var xp = TryFindField(fields, out var rawXp, "Xp", "XP") && int.TryParse(rawXp, out var parsedXp)
|
|
? parsedXp
|
|
: 0;
|
|
|
|
var context = (GiftContext)giftContext;
|
|
if (context is GiftContext.All_Weekly_Challenge_Complete or GiftContext.Weekly_Challenge_Complete)
|
|
{
|
|
var mapId = WeeklyChallenges.CreateMap(DateTime.UtcNow).ChallengeMapId;
|
|
var state = await db.WeeklyChallenges.AsNoTracking().SingleOrDefaultAsync(x =>
|
|
x.PlayerId == userId.Value && x.ChallengeMapId == mapId);
|
|
var pendingWeekly = await StorefrontShared.GetPendingGiftsAsync(db, userId.Value);
|
|
var weeklyGift = pendingWeekly.FirstOrDefault(x => x.GiftDropId == state?.RewardGiftDropId);
|
|
return weeklyGift == null
|
|
? Conflict(new { error = "weekly_reward_not_pending" })
|
|
: Ok(weeklyGift.ForClient());
|
|
}
|
|
void Trace(string line) => RewardTrace.Log(userId.Value, line);
|
|
|
|
var room = RroRewardPools.RoomForContext(context);
|
|
var paysReward = room != null && RroRewardPools.IsRewardActivity(room);
|
|
RewardTrace.Log(userId.Value, $"stage=Generate claimed={RewardTrace.Context(context)} " +
|
|
$"room={(room ?? "<none>")} isActivityReward={paysReward} xp={xp} message=\"{message}\"");
|
|
|
|
var gift = await GameRewardRedemption.TakeOutstandingAsync(db, userId.Value, context, message, Trace);
|
|
var source = gift == null ? null : "outstanding reward";
|
|
|
|
if (gift == null)
|
|
{
|
|
gift = await GameRewardRedemption.TakePendingBoxAsync(db, userId.Value, context, Trace);
|
|
if (gift != null) source = "box the selection route already handed over";
|
|
}
|
|
|
|
if (gift == null)
|
|
{
|
|
gift = await GameRewardRedemption.TakeRecentlyHandedOverAsync(
|
|
db, userId.Value, context, TimeSpan.FromMinutes(10), Trace);
|
|
if (gift != null) source = "box the server already handed over for this reward";
|
|
}
|
|
|
|
if (gift == null && paysReward)
|
|
{
|
|
gift = await GameRewardRedemption.BuildRoomRewardAsync(db, userId.Value, context, message, Trace);
|
|
if (gift != null)
|
|
source = "the activity's own reward, selected by the server";
|
|
else
|
|
{
|
|
var sets = string.Join(", ", RroRewardPools.SetsFor(context)
|
|
.Select(x => string.IsNullOrEmpty(x.Colour) ? x.Family : $"{x.Family} {x.Colour}"));
|
|
RewardTrace.Error(userId.Value,
|
|
"Unable to resolve the reward for this context. " +
|
|
$"Activity={room} Context={RewardTrace.Context(context)} ExpectedPool={sets} " +
|
|
$"EligibleItems={RroRewardPools.Resolve(context).Count} " +
|
|
"Reason=the activity's own pool, its lower ranks and the non-exclusive pool all " +
|
|
"resolved to nothing.");
|
|
}
|
|
}
|
|
|
|
if (gift == null)
|
|
{
|
|
RewardTrace.Log(userId.Value, $"stage=Fallback claimed={RewardTrace.Context(context)} " +
|
|
"is not an activity this server pays a reward for; generating the ordinary box.");
|
|
gift = BuildGeneratedGift(userId.Value, context, message, xp);
|
|
source = "generated box (no activity reward)";
|
|
}
|
|
|
|
await StorefrontShared.PersistGiftBoxAsync(db, userId.Value, gift, notify: false);
|
|
RewardTrace.Log(userId.Value, $"stage=Grant source={source} box={RewardTrace.Box(gift)}");
|
|
return Ok(gift.ForClient());
|
|
}
|
|
|
|
private async Task<Dictionary<string, string>> ReadGiftGenerateFieldsAsync()
|
|
{
|
|
var fields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
if (Request.HasFormContentType)
|
|
{
|
|
var form = await Request.ReadFormAsync();
|
|
foreach (var field in form) fields[field.Key] = field.Value.FirstOrDefault() ?? string.Empty;
|
|
return fields;
|
|
}
|
|
|
|
Request.EnableBuffering();
|
|
using var reader = new StreamReader(Request.Body, leaveOpen: true);
|
|
var raw = (await reader.ReadToEndAsync()).Trim();
|
|
Request.Body.Position = 0;
|
|
if (raw.Length == 0) return fields;
|
|
|
|
if (raw.StartsWith('{'))
|
|
{
|
|
try
|
|
{
|
|
using var document = JsonDocument.Parse(raw);
|
|
foreach (var property in document.RootElement.EnumerateObject())
|
|
fields[property.Name] = property.Value.ValueKind == JsonValueKind.String
|
|
? property.Value.GetString() ?? string.Empty
|
|
: property.Value.ToString();
|
|
}
|
|
catch (JsonException) { }
|
|
}
|
|
else
|
|
{
|
|
foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var parts = pair.Split('=', 2);
|
|
fields[Uri.UnescapeDataString(parts[0].Replace('+', ' '))] =
|
|
parts.Length > 1 ? Uri.UnescapeDataString(parts[1].Replace('+', ' ')) : string.Empty;
|
|
}
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
private static bool TryReadGiftContext(IReadOnlyDictionary<string, string> fields, string opaqueName, string friendlyName, out int value)
|
|
{
|
|
value = 0;
|
|
if (!TryFindField(fields, out var raw, opaqueName, friendlyName)) return false;
|
|
if (!int.TryParse(raw, out value)) return false;
|
|
return Enum.IsDefined(typeof(GiftContext), value) && (GiftContext)value != GiftContext.None;
|
|
}
|
|
|
|
private static bool TryReadOptionalGiftContext(IReadOnlyDictionary<string, string> fields, string opaqueName, string friendlyName, out int? value)
|
|
{
|
|
value = null;
|
|
if (!TryFindField(fields, out var raw, opaqueName, friendlyName) || string.IsNullOrWhiteSpace(raw)) return true;
|
|
if (!int.TryParse(raw, out var parsed) || !Enum.IsDefined(typeof(GiftContext), parsed)) return false;
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryFindField(IReadOnlyDictionary<string, string> fields, out string raw, params string[] names)
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
if (fields.TryGetValue(name, out var value))
|
|
{
|
|
raw = value;
|
|
return true;
|
|
}
|
|
}
|
|
raw = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
private static Gift BuildGeneratedGift(long playerId, GiftContext context, string? message, int xp)
|
|
{
|
|
int[] tokenAmounts = [10, 25, 50, 100, 250, 500];
|
|
return new Gift
|
|
{
|
|
GiftDropId = StorefrontShared.NewGiftDropId(),
|
|
FromPlayerId = playerId,
|
|
Tooltip = message ?? string.Empty,
|
|
CurrencyType = CurrencyType.RecCenterTokens,
|
|
Currency = tokenAmounts[Random.Shared.Next(tokenAmounts.Length)],
|
|
Xp = xp,
|
|
Context = context,
|
|
BalanceType = 0,
|
|
Rarity = Rarity.Rare
|
|
};
|
|
}
|
|
|
|
[HttpPost("gifts/consume/{giftDropId:long}"), Authorize]
|
|
public async Task<IActionResult> ConsumeGift(long giftDropId)
|
|
=> await ConsumePlayerGift(giftDropId, await ReadGiftIdsFromBodyAsync());
|
|
|
|
[HttpPost("gifts/consume"), Authorize]
|
|
public async Task<IActionResult> ConsumeGiftBody()
|
|
=> await ConsumePlayerGift(null, await ReadGiftIdsFromBodyAsync());
|
|
|
|
private async Task<List<long>> ReadGiftIdsFromBodyAsync()
|
|
{
|
|
var ids = new List<long>();
|
|
try
|
|
{
|
|
Request.EnableBuffering();
|
|
using var reader = new StreamReader(Request.Body, leaveOpen: true);
|
|
var raw = (await reader.ReadToEndAsync()).Trim();
|
|
Request.Body.Position = 0;
|
|
if (raw.Length == 0) return ids;
|
|
|
|
if (raw.StartsWith('{'))
|
|
{
|
|
using var doc = JsonDocument.Parse(raw);
|
|
foreach (var prop in doc.RootElement.EnumerateObject())
|
|
{
|
|
if (!string.Equals(prop.Name, "Id", StringComparison.OrdinalIgnoreCase) &&
|
|
!string.Equals(prop.Name, "giftDropId", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
if (TryReadGiftId(prop.Value, out var v)) ids.Add(v);
|
|
}
|
|
foreach (var prop in doc.RootElement.EnumerateObject())
|
|
{
|
|
if (!string.Equals(prop.Name, "giftDropIds", StringComparison.OrdinalIgnoreCase) ||
|
|
prop.Value.ValueKind != JsonValueKind.Array)
|
|
continue;
|
|
foreach (var el in prop.Value.EnumerateArray())
|
|
if (TryReadGiftId(el, out var v)) ids.Add(v);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var pair in raw.Split('&'))
|
|
{
|
|
var kv = pair.Split('=', 2);
|
|
var key = Uri.UnescapeDataString(kv[0]).Trim();
|
|
if (!string.Equals(key, "Id", StringComparison.OrdinalIgnoreCase) &&
|
|
!string.Equals(key, "giftDropId", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
var val = kv.Length > 1 ? Uri.UnescapeDataString(kv[1]).Trim() : "";
|
|
if (long.TryParse(val, out var v)) ids.Add(v);
|
|
}
|
|
if (ids.Count == 0 && long.TryParse(raw, out var bare)) ids.Add(bare);
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
return ids.Distinct().ToList();
|
|
}
|
|
|
|
private static bool TryReadGiftId(JsonElement el, out long v)
|
|
{
|
|
if (el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out v)) return true;
|
|
if (el.ValueKind == JsonValueKind.String && long.TryParse(el.GetString(), out v)) return true;
|
|
v = 0;
|
|
return false;
|
|
}
|
|
|
|
private async Task<IActionResult> ConsumePlayerGift(long? giftDropId, List<long> bodyIds)
|
|
{
|
|
var userId = User.GetUserId();
|
|
if (userId == null) return Unauthorized();
|
|
|
|
var requestedIds = new List<long>();
|
|
if (giftDropId is > 0) requestedIds.Add(giftDropId.Value);
|
|
if (bodyIds is { Count: > 0 }) requestedIds.AddRange(bodyIds);
|
|
|
|
var gifts = await StorefrontShared.GetPendingGiftsAsync(db, userId.Value);
|
|
var toConsume = requestedIds.Count > 0
|
|
? gifts.Where(g => requestedIds.Contains(g.GiftDropId)).ToList()
|
|
: gifts;
|
|
if (toConsume.Count == 0)
|
|
{
|
|
var delivered = requestedIds.Count > 0
|
|
? await HandedOverBoxIdsAsync(userId.Value, requestedIds)
|
|
: new List<long>();
|
|
if (delivered.Count > 0)
|
|
{
|
|
return GiftClaimResponse(new List<Gift>(), await GetPlayerLevelAsync(userId.Value));
|
|
}
|
|
return NotFound(new { error = "unable_to_consume_missing_or_used_gift", giftDropId });
|
|
}
|
|
|
|
var consumed = new List<Gift>();
|
|
var levelUpGifts = new List<Gift>();
|
|
var balanceGrants = new List<StorefrontShared.BalanceGrant>();
|
|
var strategy = db.Database.CreateExecutionStrategy();
|
|
await strategy.ExecuteAsync(async () =>
|
|
{
|
|
await using var tx = await db.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
foreach (var gift in toConsume)
|
|
{
|
|
var grant = await GrantGift(userId.Value, gift);
|
|
if (grant != null) balanceGrants.Add(grant);
|
|
await StorefrontShared.RemovePendingGiftAsync(db, userId.Value, gift.GiftDropId);
|
|
consumed.Add(gift);
|
|
}
|
|
levelUpGifts.AddRange(await LevelUpGiftService.DeliverLevelUpGiftsAsync(db, userId.Value, recomputeFromXp: true, notify: false));
|
|
await tx.CommitAsync();
|
|
await AnnounceBalancesAsync(userId.Value, balanceGrants, StorefrontBalanceAddTypes.FromGiftBox);
|
|
}
|
|
catch
|
|
{
|
|
await tx.RollbackAsync();
|
|
throw;
|
|
}
|
|
});
|
|
|
|
foreach (var gift in levelUpGifts)
|
|
await StorefrontShared.SendGiftPackageNotificationAsync(userId.Value, gift);
|
|
|
|
if (consumed.Count > 0)
|
|
await ProgressionAwardService.AwardXpAsync(db, userId.Value,
|
|
consumed.Count * ProgressionAwardService.XpPerGiftBoxOpened, "gift box");
|
|
return GiftClaimResponse(consumed, await GetPlayerLevelAsync(userId.Value));
|
|
}
|
|
|
|
private async Task<int> GetPlayerLevelAsync(long playerId)
|
|
{
|
|
var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId);
|
|
return progression?.Level ?? 1;
|
|
}
|
|
|
|
private async Task<List<long>> HandedOverBoxIdsAsync(long playerId, List<long> requestedIds)
|
|
{
|
|
var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId);
|
|
var rewards = data?.GetProgressionJson()?.PendingGameRewards;
|
|
if (rewards == null) return new List<long>();
|
|
|
|
return rewards
|
|
.Where(r => r.ConsumedGiftDropId != 0 && requestedIds.Contains(r.ConsumedGiftDropId))
|
|
.Select(r => r.ConsumedGiftDropId)
|
|
.Distinct()
|
|
.ToList();
|
|
}
|
|
|
|
private IActionResult GiftClaimResponse(List<Gift> claimed, int unlockedLevel)
|
|
{
|
|
var body = new Dictionary<string, object?>
|
|
{
|
|
["consumed"] = claimed.Select(g => g.ForClient()).ToList(),
|
|
["UnlockedLevel"] = unlockedLevel
|
|
};
|
|
var first = claimed.FirstOrDefault();
|
|
if (first != null)
|
|
{
|
|
foreach (var kv in first.ForClient().ToJson().FromJson<Dictionary<string, object?>>() ?? new Dictionary<string, object?>())
|
|
body[kv.Key] = kv.Value;
|
|
}
|
|
return Ok(body);
|
|
}
|
|
|
|
private async Task<StorefrontShared.BalanceGrant?> GrantGift(long playerId, Gift gift)
|
|
{
|
|
return await StorefrontShared.GrantGiftAsync(db, playerId, gift);
|
|
}
|
|
|
|
private static async Task AnnounceBalancesAsync(long playerId, List<StorefrontShared.BalanceGrant> grants, StorefrontBalanceAddTypes addType)
|
|
{
|
|
foreach (var grant in grants)
|
|
await StorefrontShared.SendBalanceUpdateAsync(playerId, grant.CurrencyType, grant.Balance, grant.Granted, addType);
|
|
}
|
|
|
|
private static bool NormalizeSavedAvatar(RadicadeServer.RecNet.Accounts.Local.PlayerAvatar avatar)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(avatar.Avatar))
|
|
{
|
|
avatar.Avatar = DBConfig.DefaultAvatar;
|
|
return true;
|
|
}
|
|
|
|
var original = avatar.Avatar;
|
|
avatar.Avatar = RepairKnownUnsafeOutfitSelections(avatar.Avatar);
|
|
|
|
return avatar.Avatar != original;
|
|
}
|
|
|
|
private static string RepairKnownUnsafeOutfitSelections(string avatarJson)
|
|
{
|
|
const string body = "d0a9262f-5504-46a7-bb10-7507503db58e";
|
|
const string accessory = "95e4cc30-cb68-473d-a395-feadf5b51512";
|
|
const string repairedOutfitSelection = body + "," + accessory
|
|
+ ",0440f08f-ef1d-49d8-942b-523056e8bb45,,1";
|
|
|
|
string[] shortOutfitSelections =
|
|
[
|
|
body + ",,,,1",
|
|
body + "," + accessory + ",,,1"
|
|
];
|
|
|
|
foreach (var shortOutfitSelection in shortOutfitSelections)
|
|
{
|
|
avatarJson = avatarJson.Replace(
|
|
shortOutfitSelection,
|
|
repairedOutfitSelection,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
return avatarJson;
|
|
}
|
|
}
|
|
|
|
public class ClaimGiftsRequest
|
|
{
|
|
public List<long>? GiftDropIds { get; set; }
|
|
public long? GiftDropId { get; set; }
|
|
}
|
|
|
|
public class GenerateGiftRequest
|
|
{
|
|
public long? RecipientPlayerId { get; set; }
|
|
public long? RecipientId { get; set; }
|
|
public int? PurchasableItemId { get; set; }
|
|
public int? ItemId { get; set; }
|
|
public long? GiftDropId { get; set; }
|
|
public StorefrontTypes? Storefront { get; set; }
|
|
public CurrencyType? Currency { get; set; }
|
|
public long? Price { get; set; }
|
|
public string? Message { get; set; }
|
|
}
|