198 lines
7.4 KiB
C#
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);
|
|
}
|