Files
Radicade/Server/Gift/GameRewardRedemption.cs
T
2026-10-16 23:33:13 -07:00

214 lines
9.4 KiB
C#

using Microsoft.EntityFrameworkCore;
using RadicadeServer.API.Storefront;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Avatar;
using RadicadeServer.RecNet.Enums.Store;
using RadicadeServer.RecNet.Progression;
namespace RadicadeServer.Utils
{
public static class GameRewardRedemption
{
public static async Task<Gift?> TakeOutstandingAsync(
RecDBContext db, long playerId, GiftContext context, string? message = null, Action<string>? trace = null)
{
var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId);
var json = data?.GetProgressionJson();
var pending = json?.PendingGameRewards;
var outstanding = pending?.Where(r => !r.Consumed).ToList() ?? new List<RecNet.Storefront.GameReward>();
trace?.Invoke($"stage=OutstandingReward claimed={RewardTrace.Context(context)} " +
$"rewards={pending?.Count ?? 0} outstanding={outstanding.Count}");
if (data == null || json == null || outstanding.Count == 0) return null;
foreach (var candidate in outstanding)
{
var held = candidate.GiftDrop1 ?? candidate.GiftDrop2 ?? candidate.GiftDrop3;
trace?.Invoke($"stage=OutstandingCandidate selectionId={candidate.RewardSelectionId} " +
$"rewardType={candidate.RewardType} context={RewardTrace.Context(candidate.GiftContext)} " +
$"box={RewardTrace.Box(held)}");
}
var reward = outstanding.FirstOrDefault(r => r.GiftContext == context);
var matchedBy = reward == null ? null : "context";
if (reward == null && RroRewardPools.RoomForContext(context) is { } room)
{
reward = outstanding.FirstOrDefault(r => RroRewardPools.RoomForContext(r.GiftContext) == room);
matchedBy = reward == null ? null : $"room={room}";
}
if (reward == null)
{
reward = outstanding[0];
matchedBy = "oldest outstanding";
}
var drop = reward.GiftDrop1 ?? reward.GiftDrop2 ?? reward.GiftDrop3;
if (drop == null)
{
trace?.Invoke($"stage=OutstandingRewardUnusable selectionId={reward.RewardSelectionId} " +
"has no gift drop at all; nothing can be handed over from it.");
return null;
}
var gift = drop.Clone();
if (!string.IsNullOrWhiteSpace(message)) gift.Tooltip = message;
if (gift.GiftDropId == 0) gift.GiftDropId = StorefrontShared.NewGiftDropId();
reward.Consumed = true;
reward.ConsumedGiftDropId = gift.GiftDropId;
reward.ConsumedAt = DateTime.UtcNow;
data.Progress = json.ToJson();
await db.SaveChangesAsync();
trace?.Invoke($"stage=OutstandingRewardMatched by={matchedBy} selectionId={reward.RewardSelectionId} " +
$"rewardType={reward.RewardType} context={RewardTrace.Context(reward.GiftContext)} " +
$"box={RewardTrace.Box(gift)}");
return gift;
}
public static async Task<Gift?> BuildRoomRewardAsync(
RecDBContext db, long playerId, GiftContext context, string? message = null,
Action<string>? trace = null, string? catalogueJson = null)
{
var room = RroRewardPools.RoomForContext(context);
if (room == null) return null;
var owned = await LevelUpGiftService.CollectOwnedKeysAsync(db, playerId, context);
Gift? box = RroRewardPools.BuildGift(context, owned, catalogueJson: catalogueJson);
var paidFrom = context;
if (box == null)
{
foreach (var weaker in RroRewardPools.LowerRanksOf(context))
{
box = RroRewardPools.BuildGift(weaker, owned, catalogueJson: catalogueJson);
if (box == null) continue;
paidFrom = weaker;
trace?.Invoke($"stage=RoomBoxFallbackRank context={RewardTrace.Context(context)} " +
$"nothing left unowned, so the same quest's {RewardTrace.Context(weaker)} is what it pays.");
break;
}
}
if (box == null)
{
box = RroRewardPools.BuildFallbackDrops(selection: false, owned: owned).FirstOrDefault();
if (box != null)
{
paidFrom = RroRewardPools.ReacquisitionContext;
trace?.Invoke($"stage=RoomBoxFallbackItem context={RewardTrace.Context(context)} " +
"every reward the activity pays is owned; paying the non-exclusive item pool.");
}
}
if (box == null) return null;
box.GiftDropId = StorefrontShared.NewGiftDropId();
if (!string.IsNullOrWhiteSpace(message)) box.Tooltip = message;
trace?.Invoke($"stage=RoomBox activity={room} context={RewardTrace.Context(context)} " +
$"paidFrom={RewardTrace.Context(paidFrom)} box={RewardTrace.Box(box)}");
await RememberHandoverAsync(db, playerId, context, box, trace);
return box;
}
public static async Task RememberHandoverAsync(
RecDBContext db, long playerId, GiftContext context, Gift box, Action<string>? trace = null)
{
var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId);
if (data == null)
{
data = new ProgressionData { PlayerId = playerId };
db.Progression.Add(data);
}
var json = data.GetProgressionJson() ?? new ProgressionJson();
json.PendingGameRewards ??= new List<RecNet.Storefront.GameReward>();
json.PendingGameRewards.Add(new RecNet.Storefront.GameReward
{
RewardSelectionId = StorefrontShared.NewGiftDropId(),
Message = box.Tooltip ?? string.Empty,
GiftContext = context,
RewardType = RroRewardPools.IsQuestContext(context)
? RewardType.PostQuestActivity
: RewardType.PostGameActivity,
GiftDrop1 = box.Clone(),
CreatedAt = DateTime.UtcNow,
Consumed = true,
ConsumedGiftDropId = box.GiftDropId,
ConsumedAt = DateTime.UtcNow
});
data.Progress = json.ToJson();
await db.SaveChangesAsync();
trace?.Invoke($"stage=HandoverRecorded context={RewardTrace.Context(context)} " +
$"box={RewardTrace.Box(box)} the completion is recorded as delivered, so a later request " +
"for it cannot pay a second box.");
}
public static async Task<Gift?> TakePendingBoxAsync(
RecDBContext db, long playerId, GiftContext context, Action<string>? trace = null)
{
var room = RroRewardPools.RoomForContext(context);
if (room == null || !RroRewardPools.IsRewardActivity(room)) return null;
var boxes = await StorefrontShared.GetPendingGiftsAsync(db, playerId);
var box = boxes.FirstOrDefault(b => b.Context == context)
?? boxes.FirstOrDefault(b => RroRewardPools.RoomForContext(b.Context) == room);
trace?.Invoke($"stage=PendingBox claimed={RewardTrace.Context(context)} room={room} " +
$"held={boxes.Count} match={RewardTrace.Box(box)}");
if (box == null) return null;
var gift = box.Clone();
if (gift.GiftDropId == 0) gift.GiftDropId = StorefrontShared.NewGiftDropId();
return gift;
}
public static async Task<Gift?> TakeRecentlyHandedOverAsync(
RecDBContext db, long playerId, GiftContext context, TimeSpan within, Action<string>? trace = null)
{
var data = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId);
var json = data?.GetProgressionJson();
var pending = json?.PendingGameRewards;
if (pending == null || pending.Count == 0) return null;
var cutoff = DateTime.UtcNow - within;
var taken = pending
.Where(r => r.Consumed && r.ConsumedAt is { } at && at >= cutoff)
.Where(r => r.GiftContext == context
|| (RroRewardPools.RoomForContext(context) is { } room
&& RroRewardPools.RoomForContext(r.GiftContext) == room))
.OrderByDescending(r => r.ConsumedAt)
.FirstOrDefault();
if (taken == null)
{
trace?.Invoke($"stage=HandedOver claimed={RewardTrace.Context(context)} none within {within}.");
return null;
}
var drop = taken.GiftDrop1 ?? taken.GiftDrop2 ?? taken.GiftDrop3;
if (drop == null) return null;
var gift = drop.Clone();
gift.GiftDropId = taken.ConsumedGiftDropId != 0 ? taken.ConsumedGiftDropId : gift.GiftDropId;
trace?.Invoke($"stage=HandedOver claimed={RewardTrace.Context(context)} " +
$"selectionId={taken.RewardSelectionId} box={RewardTrace.Box(gift)} - answering the retry " +
"with the box it was given.");
return gift;
}
}
}