neon-sprawl/server/NeonSprawl.Server/Game/Quests/QuestStateOperations.cs

257 lines
8.8 KiB
C#

using NeonSprawl.Server.Game.Items;
namespace NeonSprawl.Server.Game.Quests;
/// <summary>
/// Server-authoritative quest accept, step advance, and complete with reason codes (NEO-117).
/// Objective wiring: E7M1-07 (NEO-118). HTTP: E7M1-08/09 (NEO-119/NEO-120).
/// </summary>
public static class QuestStateOperations
{
/// <summary>
/// Validates quest id and prerequisites, then activates at step 0 when not started.
/// </summary>
public static QuestStateOperationResult TryAccept(
string playerId,
string questId,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry,
TimeProvider timeProvider)
{
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
!questRegistry.TryGetDefinition(normalizedQuestId, out var definition))
{
return Deny(QuestStateReasonCodes.UnknownQuest);
}
if (!progressStore.CanWritePlayer(playerId))
{
return Deny(QuestStateReasonCodes.UnknownPlayer);
}
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing))
{
if (existing.Status == QuestProgressStatus.Completed)
{
return Deny(QuestStateReasonCodes.AlreadyCompleted, existing);
}
return Deny(QuestStateReasonCodes.AlreadyActive, existing);
}
foreach (var prerequisiteId in definition.PrerequisiteQuestIds)
{
if (!progressStore.TryGetProgress(playerId, prerequisiteId, out var prerequisite) ||
prerequisite.Status != QuestProgressStatus.Completed)
{
return Deny(QuestStateReasonCodes.PrerequisiteIncomplete);
}
}
if (progressStore.TryActivate(playerId, normalizedQuestId, out var snapshot))
{
QuestObjectiveWiring.TryProcessInventoryHasItemForQuest(
playerId,
normalizedQuestId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry);
QuestObjectiveWiring.TryCompleteStepIfSatisfied(
playerId,
normalizedQuestId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
timeProvider);
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
{
return Success(refreshed);
}
return Success(snapshot);
}
return DenyActivateFailure(progressStore, playerId, normalizedQuestId, snapshot);
}
/// <summary>
/// Advances an active quest to <paramref name="newStepIndex"/> when within catalog bounds.
/// </summary>
public static QuestStateOperationResult TryAdvanceStep(
string playerId,
string questId,
int newStepIndex,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry,
TimeProvider timeProvider)
{
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
!questRegistry.TryGetDefinition(normalizedQuestId, out var definition))
{
return Deny(QuestStateReasonCodes.UnknownQuest);
}
if (!progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing))
{
return Deny(QuestStateReasonCodes.NotActive);
}
if (existing.Status == QuestProgressStatus.Completed)
{
return Deny(QuestStateReasonCodes.AlreadyCompleted, existing);
}
if (existing.Status != QuestProgressStatus.Active)
{
return Deny(QuestStateReasonCodes.NotActive);
}
if (newStepIndex <= existing.CurrentStepIndex || newStepIndex >= definition.Steps.Count)
{
return Deny(QuestStateReasonCodes.InvalidStepIndex, existing);
}
if (progressStore.TryAdvanceStep(playerId, normalizedQuestId, newStepIndex, out var snapshot))
{
QuestObjectiveWiring.TryProcessInventoryHasItemForQuest(
playerId,
normalizedQuestId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry);
QuestObjectiveWiring.TryCompleteStepIfSatisfied(
playerId,
normalizedQuestId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
timeProvider);
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
{
return Success(refreshed);
}
return Success(snapshot);
}
return DenyAdvanceFailure(progressStore, playerId, normalizedQuestId, snapshot, existing);
}
/// <summary>
/// Marks a quest complete. Idempotent: replays return success with the existing snapshot.
/// </summary>
public static QuestStateOperationResult TryMarkComplete(
string playerId,
string questId,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
TimeProvider timeProvider)
{
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
!questRegistry.TryGetDefinition(normalizedQuestId, out _))
{
return Deny(QuestStateReasonCodes.UnknownQuest);
}
if (!progressStore.TryGetProgress(playerId, normalizedQuestId, out var progress))
{
return Deny(QuestStateReasonCodes.NotActive);
}
if (progress.Status == QuestProgressStatus.Completed)
{
return Success(progress);
}
if (progress.Status != QuestProgressStatus.Active)
{
return Deny(QuestStateReasonCodes.NotActive);
}
if (progressStore.TryMarkComplete(playerId, normalizedQuestId, timeProvider.GetUtcNow(), out var snapshot))
{
return Success(snapshot);
}
if (snapshot?.Status == QuestProgressStatus.Completed)
{
return Success(snapshot);
}
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var afterAttempt) &&
afterAttempt.Status == QuestProgressStatus.Completed)
{
return Success(afterAttempt);
}
if (!progressStore.CanWritePlayer(playerId))
{
return Deny(QuestStateReasonCodes.UnknownPlayer);
}
return DenyFromProgressSnapshot(snapshot, QuestStateReasonCodes.NotActive)
?? Deny(QuestStateReasonCodes.UnknownPlayer, progress);
}
private static QuestStateOperationResult DenyActivateFailure(
IPlayerQuestStateStore progressStore,
string playerId,
string questId,
QuestStepState attemptSnapshot) =>
DenyFromProgressSnapshot(attemptSnapshot, QuestStateReasonCodes.AlreadyActive)
?? (progressStore.TryGetProgress(playerId, questId, out var reread)
? DenyFromProgressSnapshot(reread, QuestStateReasonCodes.AlreadyActive)
: null)
?? Deny(QuestStateReasonCodes.UnknownPlayer);
private static QuestStateOperationResult DenyAdvanceFailure(
IPlayerQuestStateStore progressStore,
string playerId,
string questId,
QuestStepState attemptSnapshot,
QuestStepState priorRead) =>
DenyFromProgressSnapshot(attemptSnapshot, QuestStateReasonCodes.InvalidStepIndex)
?? (progressStore.TryGetProgress(playerId, questId, out var reread)
? DenyFromProgressSnapshot(reread, QuestStateReasonCodes.InvalidStepIndex)
: null)
?? Deny(QuestStateReasonCodes.UnknownPlayer, priorRead);
private static QuestStateOperationResult? DenyFromProgressSnapshot(
QuestStepState? snapshot,
string activeOrIntermediateReasonCode)
{
if (snapshot is null)
{
return null;
}
if (snapshot.Status == QuestProgressStatus.Completed)
{
return Deny(QuestStateReasonCodes.AlreadyCompleted, snapshot);
}
if (snapshot.Status == QuestProgressStatus.Active)
{
return Deny(activeOrIntermediateReasonCode, snapshot);
}
return null;
}
private static QuestStateOperationResult Success(QuestStepState snapshot) =>
new(Success: true, ReasonCode: null, Snapshot: snapshot);
private static QuestStateOperationResult Deny(string reasonCode, QuestStepState? snapshot = null) =>
new(Success: false, ReasonCode: reasonCode, Snapshot: snapshot);
}