neon-sprawl/server/NeonSprawl.Server/Game/Rewards/RewardRouterOperations.cs

198 lines
7.4 KiB
C#

using NeonSprawl.Server.Game.Encounters;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Quests;
using NeonSprawl.Server.Game.Skills;
namespace NeonSprawl.Server.Game.Rewards;
/// <summary>
/// Applies quest <see cref="QuestRewardBundleRow"/> completion grants idempotently (NEO-127).
/// Quest-state wiring: <see cref="QuestStateOperations"/> (NEO-128).
/// </summary>
public static class RewardRouterOperations
{
private static readonly RewardItemGrantApplied[] EmptyItemGrants = [];
private static readonly RewardSkillXpGrantApplied[] EmptySkillXpGrants = [];
/// <summary>
/// Applies <paramref name="bundle"/> item + skill XP rows for a quest completion and records
/// <see cref="RewardDeliveryEvent"/> when grants succeed.
/// </summary>
public static RewardDeliveryResult TryDeliverQuestCompletion(
string playerId,
string questId,
QuestRewardBundleRow? bundle,
IItemDefinitionRegistry itemRegistry,
IPlayerInventoryStore inventoryStore,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore skillProgressionStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IRewardDeliveryStore deliveryStore,
TimeProvider timeProvider)
{
var normalizedPlayerId = RewardDeliveryIds.NormalizePlayerId(playerId);
if (normalizedPlayerId.Length == 0)
{
return Deny(RewardDeliveryReasonCodes.InvalidPlayerId);
}
var normalizedQuestId = RewardDeliveryIds.NormalizeQuestId(questId);
if (normalizedQuestId.Length == 0)
{
return Deny(RewardDeliveryReasonCodes.InvalidQuestId);
}
if (deliveryStore.TryGet(normalizedPlayerId, normalizedQuestId, out var existingEvent))
{
return SuccessFromEvent(existingEvent, RewardDeliveryReasonCodes.AlreadyDelivered);
}
var itemGrants = bundle?.ItemGrants ?? Array.Empty<RewardGrantRow>();
var skillGrants = bundle?.SkillXpGrants ?? Array.Empty<QuestSkillXpGrantRow>();
if (itemGrants.Count > 0)
{
if (!inventoryStore.TryGetSnapshot(playerId, out var snapshot))
{
return Deny(RewardDeliveryReasonCodes.InventoryStoreMissing);
}
var simulated = snapshot.Clone();
foreach (var grant in itemGrants)
{
if (!PlayerInventoryOperations.TrySimulateAddStack(
ref simulated,
grant.ItemId,
grant.Quantity,
itemRegistry))
{
return Deny(RewardDeliveryReasonCodes.InventoryFull);
}
}
}
var appliedItems = new List<RewardItemGrantApplied>(itemGrants.Count);
foreach (var grant in itemGrants)
{
var addOutcome = PlayerInventoryOperations.TryAddStack(
playerId,
grant.ItemId,
grant.Quantity,
itemRegistry,
inventoryStore);
if (addOutcome.Kind == PlayerInventoryMutationKind.StoreMissing)
{
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
return Deny(RewardDeliveryReasonCodes.InventoryStoreMissing);
}
if (addOutcome.Kind == PlayerInventoryMutationKind.Denied)
{
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
return Deny(MapInventoryReason(addOutcome.ReasonCode));
}
appliedItems.Add(new RewardItemGrantApplied(grant.ItemId, grant.Quantity));
}
var appliedSkillXp = new List<RewardSkillXpGrantApplied>(skillGrants.Count);
foreach (var grant in skillGrants)
{
var skillOutcome = SkillProgressionGrantOperations.TryApplyGrant(
playerId,
grant.SkillId,
grant.Amount,
MissionRewardSkillXpConstants.MissionRewardSourceKind,
skillRegistry,
skillProgressionStore,
levelCurve,
perkUnlockEngine);
if (skillOutcome.Kind == SkillProgressionGrantApplyKind.ProgressionStoreMissing)
{
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
return Deny(RewardDeliveryReasonCodes.ProgressionStoreMissing);
}
if (skillOutcome.Kind == SkillProgressionGrantApplyKind.Denied)
{
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
var reasonCode = skillOutcome.Response?.ReasonCode ?? RewardDeliveryReasonCodes.UnknownSkill;
return Deny(reasonCode);
}
appliedSkillXp.Add(new RewardSkillXpGrantApplied(grant.SkillId, grant.Amount));
}
var deliveredAt = timeProvider.GetUtcNow();
var deliveryEvent = new RewardDeliveryEvent(
normalizedPlayerId,
normalizedQuestId,
deliveredAt,
appliedItems,
appliedSkillXp,
RewardDeliveryIds.MakeIdempotencyKey(normalizedPlayerId, normalizedQuestId));
if (deliveryStore.TryRecord(deliveryEvent))
{
return SuccessFromEvent(deliveryEvent, reasonCode: null);
}
if (deliveryStore.TryGet(normalizedPlayerId, normalizedQuestId, out var racedEvent))
{
return SuccessFromEvent(racedEvent, RewardDeliveryReasonCodes.AlreadyDelivered);
}
return SuccessFromEvent(deliveryEvent, RewardDeliveryReasonCodes.AlreadyDelivered);
}
private static string MapInventoryReason(string? reasonCode) =>
reasonCode switch
{
PlayerInventoryReasonCodes.InventoryFull => RewardDeliveryReasonCodes.InventoryFull,
PlayerInventoryReasonCodes.InvalidItem => RewardDeliveryReasonCodes.InvalidItem,
PlayerInventoryReasonCodes.InvalidQuantity => RewardDeliveryReasonCodes.InvalidQuantity,
null => RewardDeliveryReasonCodes.InventoryFull,
_ => reasonCode,
};
private static void CompensatingRemoveItems(
string playerId,
IReadOnlyList<RewardItemGrantApplied> grants,
IItemDefinitionRegistry itemRegistry,
IPlayerInventoryStore inventoryStore)
{
for (var i = grants.Count - 1; i >= 0; i--)
{
var grant = grants[i];
_ = PlayerInventoryOperations.TryRemoveStack(
playerId,
grant.ItemId,
grant.Quantity,
itemRegistry,
inventoryStore);
}
}
private static RewardDeliveryResult Deny(string reasonCode) =>
new(
Success: false,
ReasonCode: reasonCode,
GrantedItems: EmptyItemGrants,
GrantedSkillXp: EmptySkillXpGrants,
DeliveryEvent: null,
DeliveredAt: null);
private static RewardDeliveryResult SuccessFromEvent(RewardDeliveryEvent deliveryEvent, string? reasonCode) =>
new(
Success: true,
ReasonCode: reasonCode,
GrantedItems: deliveryEvent.GrantedItems,
GrantedSkillXp: deliveryEvent.GrantedSkillXp,
DeliveryEvent: deliveryEvent,
DeliveredAt: deliveryEvent.DeliveredAt);
}