NEO-117: Address code review findings for QuestStateOperations.

Align test DI resolution with encounter pattern, add unknown_quest
advance/complete tests, catalog quest id constants, refactor
TryMarkComplete readability, and update decomposition docs.
pull/156/head
VinPropane 2026-06-06 19:30:21 -04:00
parent 84847e20f0
commit 025c3250d2
9 changed files with 96 additions and 43 deletions

View File

@ -7,7 +7,7 @@
| **Module ID** | E7.M1 |
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
| **Stage target** | Prototype |
| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) server load **landed**; **E7M1-03** [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) registry **landed**; **E7M1-04** [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) HTTP read **landed**; **E7M1-05** [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) player quest state store **landed**; runtime from **E7M1-06** [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) |
| **Status** | Planned — Slice 1 backlog [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md): **E7M1-01** [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) catalog **landed** (schemas + CI); **E7M1-02** [NEO-113](https://linear.app/neon-sprawl/issue/NEO-113) server load **landed**; **E7M1-03** [NEO-114](https://linear.app/neon-sprawl/issue/NEO-114) registry **landed**; **E7M1-04** [NEO-115](https://linear.app/neon-sprawl/issue/NEO-115) HTTP read **landed**; **E7M1-05** [NEO-116](https://linear.app/neon-sprawl/issue/NEO-116) player quest state store **landed**; **E7M1-06** [NEO-117](https://linear.app/neon-sprawl/issue/NEO-117) **`QuestStateOperations`** **landed**; objective wiring from **E7M1-07** [NEO-118](https://linear.app/neon-sprawl/issue/NEO-118) → capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123) |
| **Linear** | Label **`E7.M1`** · [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md) |
## Purpose
@ -82,7 +82,9 @@ The **first shipped quest spine** is **frozen** for prototype tuning until a del
**HTTP read model (NEO-115):** **`GET /game/world/quest-definitions`** — `QuestDefinitionsWorldApi` + DTOs in `Game/Quests/` ([NEO-115](../../plans/NEO-115-implementation-plan.md)); [server README — Quest definitions (NEO-115)](../../../server/README.md#quest-definitions-neo-115); Bruno `bruno/neon-sprawl-server/quest-definitions/`.
**Player quest state store (NEO-116):** **`IPlayerQuestStateStore`** — in-memory + Postgres (`V008__player_quest_progress.sql`) per-player rows keyed by `(playerId, questId)`; implicit `not_started` when missing ([NEO-116](../../plans/NEO-116-implementation-plan.md)); [server README — Quest progress store (NEO-116)](../../../server/README.md#quest-progress-store-neo-116). HTTP read/accept deferred to E7M1-08/09; **`QuestStateOperations`** in E7M1-06.
**Player quest state store (NEO-116):** **`IPlayerQuestStateStore`** — in-memory + Postgres (`V008__player_quest_progress.sql`) per-player rows keyed by `(playerId, questId)`; implicit `not_started` when missing ([NEO-116](../../plans/NEO-116-implementation-plan.md)); [server README — Quest progress store (NEO-116)](../../../server/README.md#quest-progress-store-neo-116).
**Quest state operations (NEO-117):** **`QuestStateOperations`** — accept, step advance, complete with **`QuestStateReasonCodes`**; prerequisite gate on accept; idempotent complete at operations layer ([NEO-117](../../plans/NEO-117-implementation-plan.md)); [server README — Quest state operations (NEO-117)](../../../server/README.md#quest-state-operations-neo-117). HTTP read/accept deferred to E7M1-08/09; objective wiring in E7M1-07.
**Reward policy (Slice 1):** Quest completion updates **`QuestStepState` only** — no duplicate item/XP grants. Gather/craft/encounter paths keep existing payouts; [E7.M2](E7_M2_RewardAndUnlockRouter.md) adds **`QuestRewardBundle`** apply in Slice 2.

File diff suppressed because one or more lines are too long

View File

@ -109,7 +109,7 @@ Gameplay **content and curves** default to **boot load** with optional **dev-onl
|---|---|---|---|---|---|
| E7.M1 | QuestStateMachine | E3.M2, E5.M1 | QuestDef, QuestStepState, QuestStateTransition | Prototype | Planned |
**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); **E7M1-02 / NEO-113** server load landed; **E7M1-03 / NEO-114** registry landed ([NEO-114 plan](../../plans/NEO-114-implementation-plan.md)); **E7M1-04 / NEO-115** HTTP read landed ([NEO-115 plan](../../plans/NEO-115-implementation-plan.md)); **E7M1-05 / NEO-116** player quest state store landed ([NEO-116 plan](../../plans/NEO-116-implementation-plan.md)); register row stays **Planned** until quest runtime (E7M1-06+) lands.
**E7.M1 note:** Epic 7 **Slice 1** backlog in Linear ([Epic 7 — Questing, Narrative, and Faction Reputation](https://linear.app/neon-sprawl/project/epic-7-questing-narrative-and-faction-reputation-a9416783ceee)): [NEO-112](https://linear.app/neon-sprawl/issue/NEO-112) → [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123); label **`E7.M1`**. See [E7M1-prototype-backlog.md](../../plans/E7M1-prototype-backlog.md), [E7_M1_QuestStateMachine.md](E7_M1_QuestStateMachine.md). Upstream: E3.M1 gather **Ready**, E3.M2 craft **Ready**, E5.M3 encounter **Ready**. Client capstone **E7M1-12** [NEO-123](https://linear.app/neon-sprawl/issue/NEO-123). **E7M1-01 / NEO-112** catalog landed (schemas + CI); **E7M1-02 / NEO-113** server load landed; **E7M1-03 / NEO-114** registry landed ([NEO-114 plan](../../plans/NEO-114-implementation-plan.md)); **E7M1-04 / NEO-115** HTTP read landed ([NEO-115 plan](../../plans/NEO-115-implementation-plan.md)); **E7M1-05 / NEO-116** player quest state store landed ([NEO-116 plan](../../plans/NEO-116-implementation-plan.md)); **E7M1-06 / NEO-117** **`QuestStateOperations`** landed ([NEO-117 plan](../../plans/NEO-117-implementation-plan.md)); register row stays **Planned** until E7M1-07+ objective wiring and client capstone.
| E7.M2 | RewardAndUnlockRouter | E2.M2, E3.M3, E7.M1 | QuestRewardBundle, UnlockGrant, RewardDeliveryEvent | Prototype | Planned |
| E7.M3 | FactionReputationLedger | E7.M1 | FactionStanding, ReputationDelta, FactionGateRule | Pre-production | Planned |
| E7.M4 | ContractMissionGenerator | E4.M1, E5.M3, E7.M3 | ContractTemplate, ContractSeed, ContractOutcome | Pre-production | Planned |

View File

@ -214,9 +214,9 @@ Working backlog for **Epic 7 — Slice 1** ([quest core and persistence](../deco
**Acceptance criteria**
- [ ] Accept fails when prerequisite quest not **completed**.
- [ ] Complete is idempotent per player+quest.
- [ ] Reason codes documented in `server/README.md`.
- [x] Accept fails when prerequisite quest not **completed**.
- [x] Complete is idempotent per player+quest.
- [x] Reason codes documented in `server/README.md`.
**Client counterpart:** [NEO-120](https://linear.app/neon-sprawl/issue/NEO-120) exposes accept POST to Godot.

View File

@ -58,7 +58,7 @@
- **Types:** `QuestStateReasonCodes`, `QuestStateOperationResult`, `QuestStateOperations` (`TryAccept`, `TryAdvanceStep`, `TryMarkComplete`).
- **Tests:** `QuestStateOperationsTests` — 15 AAA cases (accept prerequisites, denials, advance bounds, idempotent complete, host DI smoke).
- **Docs:** `server/README.md` quest operations + reason-code table.
- **Docs:** `server/README.md` quest operations + reason-code table; E7.M1 alignment register, module doc, dependency register note, E7M1-06 backlog checkboxes.
## Technical approach
@ -132,8 +132,11 @@ flowchart TD
| Path | Rationale |
|------|-----------|
| `server/README.md` | Document `QuestStateOperations`, reason codes, idempotent complete semantics; note HTTP deferred to E7M1-08/09. |
| `server/NeonSprawl.Server/Game/Quests/IPlayerQuestStateStore.cs` | Update XML `<see cref="QuestStateOperations"/>` now that the type will exist (optional nit from NEO-116 review). |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E7.M1 row — E7M1-06 / NEO-117 operations landed. |
| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | NEO-117 operations bullet + README link. |
| `docs/decomposition/modules/module_dependency_register.md` | E7.M1 note — NEO-117 landed. |
| `docs/plans/E7M1-prototype-backlog.md` | E7M1-06 acceptance checkboxes checked. |
| `server/NeonSprawl.Server/Game/Quests/PrototypeE7M1QuestCatalogRules.cs` | Add `RefineIntroQuestId` and `CombatIntroQuestId` test constants. |
## Tests
@ -147,5 +150,5 @@ flowchart TD
|-----------------|----------------------|--------|
| **Idempotent complete vs encounter deny pattern** | Operations return `Success: true` on replay (E7M1-06 backlog); store layer unchanged. | **adopted** |
| **Terminal step required before complete** | Defer to E7M1-07; operations do not validate objective satisfaction or terminal step index in NEO-117. | **adopted** |
| **Extra catalog test constants** | Optional `RefineIntroQuestId` / `CombatIntroQuestId` on `PrototypeE7M1QuestCatalogRules`; inline strings acceptable if only used in one test file. | **deferred** |
| **Extra catalog test constants** | Optional `RefineIntroQuestId` / `CombatIntroQuestId` on `PrototypeE7M1QuestCatalogRules`; inline strings acceptable if only used in one test file. | **adopted** |
| **Prerequisite check strictness** | Require `TryGetProgress` + `Completed` for each prerequisite id (implicit `not_started` fails gate). | **adopted** |

View File

@ -17,10 +17,10 @@ NEO-117 adds **`QuestStateOperations`** — static accept, step advance, and com
| Path | Result |
|------|--------|
| `docs/plans/NEO-117-implementation-plan.md` | **Matches** — kickoff decisions adopted; acceptance checklist checked; reconciliation accurate. |
| `docs/plans/E7M1-prototype-backlog.md` (E7M1-06) | **Partially matches** — implementation satisfies AC; backlog checkboxes still `[ ]` (expected until merge/story end). |
| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | **Partially matches** — module page still lists **`QuestStateOperations`** as future E7M1-06; should note NEO-117 landed after merge. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Partially matches** — E7.M1 row ends at E7M1-05 / NEO-116; plan defers register update until story lands — append E7M1-06 / NEO-117 on merge. |
| `docs/decomposition/modules/module_dependency_register.md` | **Partially matches** — no NEO-117 note yet; same deferred update as NEO-116 review. |
| `docs/plans/E7M1-prototype-backlog.md` (E7M1-06) | **Matches** — E7M1-06 acceptance checkboxes checked. |
| `docs/decomposition/modules/E7_M1_QuestStateMachine.md` | **Matches** — NEO-117 operations bullet + README link added. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E7M1-06 / NEO-117 landed note appended. |
| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — NEO-117 note appended. |
| `server/README.md` | **Matches** — quest operations section, reason-code table, idempotent complete semantics, HTTP deferral to NEO-119/NEO-120. |
| Full-stack epic decomposition | **N/A** — server-only; client counterpart explicitly NEO-120/NEO-122; plan does not claim prototype slice complete. |
@ -32,19 +32,19 @@ NEO-117 adds **`QuestStateOperations`** — static accept, step advance, and com
## Suggestions
1. **Test `ResolveDependencies` pattern**`QuestStateOperationsTests.ResolveDependencies` creates an `IServiceScope` that is never disposed. **`EncounterCompletionOperationsTests`** resolves from `factory.Services` root (singleton stores). Align to avoid undisposed scopes and match the established encounter test pattern.
1. ~~**Test `ResolveDependencies` pattern** — `QuestStateOperationsTests.ResolveDependencies` creates an `IServiceScope` that is never disposed. **`EncounterCompletionOperationsTests`** resolves from `factory.Services` root (singleton stores). Align to avoid undisposed scopes and match the established encounter test pattern.~~ **Done.** `ResolveDependencies` and host smoke test resolve from `factory.Services`.
2. **`unknown_quest` coverage on advance/complete** — Accept path has `TryAccept_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog`; advance and complete lack parity. Add two small `[Fact]` cases (mirror **`EncounterCompletionOperationsTests`** unknown-encounter test) for registry gate consistency.
2. ~~**`unknown_quest` coverage on advance/complete** — Accept path has `TryAccept_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog`; advance and complete lack parity. Add two small `[Fact]` cases (mirror **`EncounterCompletionOperationsTests`** unknown-encounter test) for registry gate consistency.~~ **Done.** Added `TryAdvanceStep_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog` and `TryMarkComplete_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog`.
3. **Post-merge decomposition docs** — Plan defers register/module updates; on story end append **E7M1-06 / NEO-117** to `documentation_and_implementation_alignment.md`, `E7_M1_QuestStateMachine.md` (operations bullet + README link), and check E7M1-06 AC boxes in `E7M1-prototype-backlog.md`.
3. ~~**Post-merge decomposition docs** — Plan defers register/module updates; on story end append **E7M1-06 / NEO-117** to `documentation_and_implementation_alignment.md`, `E7_M1_QuestStateMachine.md` (operations bullet + README link), and check E7M1-06 AC boxes in `E7M1-prototype-backlog.md`.~~ **Done.** Alignment register, module doc, dependency register note, and E7M1-06 backlog checkboxes updated.
## Nits
- Nit: **`TryAccept`** progress switch default `_ => Deny(UnknownQuest)` is unreachable while **`QuestProgressStatus`** is only `Active`/`Completed`; if kept for exhaustiveness, a more accurate code (or `NotActive`) would aid future enum extensions.
- ~~Nit: **`TryAccept`** progress switch default `_ => Deny(UnknownQuest)` is unreachable while **`QuestProgressStatus`** is only `Active`/`Completed`; if kept for exhaustiveness, a more accurate code (or `NotActive`) would aid future enum extensions.~~ **Done.** Replaced switch with explicit `Completed` / `AlreadyActive` branches.
- Nit: Chain-quest setup tests hardcode **`prototype_quest_combat_intro`** and **`prototype_quest_refine_intro`**; plan deferred **`RefineIntroQuestId`** / **`CombatIntroQuestId`** constants — acceptable for now, but NEO-118 wiring will reuse these ids.
- ~~Nit: Chain-quest setup tests hardcode **`prototype_quest_combat_intro`** and **`prototype_quest_refine_intro`**; plan deferred **`RefineIntroQuestId`** / **`CombatIntroQuestId`** constants — acceptable for now, but NEO-118 wiring will reuse these ids.~~ **Done.** Added **`RefineIntroQuestId`** and **`CombatIntroQuestId`** to **`PrototypeE7M1QuestCatalogRules`**; tests updated.
- Nit: **`TryMarkComplete`** re-calls **`TryGetProgress`** after the active-row read (lines 117118) where `active` could be reused on the `not_active` deny path — minor readability only.
- ~~Nit: **`TryMarkComplete`** re-calls **`TryGetProgress`** after the active-row read (lines 117118) where `active` could be reused on the `not_active` deny path — minor readability only.~~ **Done.** Single **`TryGetProgress`** read at start of **`TryMarkComplete`**.
## Verification

View File

@ -10,7 +10,8 @@ public sealed class QuestStateOperationsTests
private const string PlayerId = "dev-local-1";
private const string UnknownPlayerId = "unknown-player-xyz";
private const string GatherQuestId = PrototypeE7M1QuestCatalogRules.GatherIntroQuestId;
private const string RefineQuestId = "prototype_quest_refine_intro";
private const string RefineQuestId = PrototypeE7M1QuestCatalogRules.RefineIntroQuestId;
private const string CombatQuestId = PrototypeE7M1QuestCatalogRules.CombatIntroQuestId;
private const string ChainQuestId = PrototypeE7M1QuestCatalogRules.ChainQuestId;
private const string ChainObjectiveId = PrototypeE7M1QuestCatalogRules.ChainFirstObjectiveId;
@ -181,7 +182,7 @@ public sealed class QuestStateOperationsTests
var timeProvider = TimeProvider.System;
AcceptAndComplete(deps, GatherQuestId, timeProvider);
AcceptAndComplete(deps, RefineQuestId, timeProvider);
AcceptAndComplete(deps, "prototype_quest_combat_intro", timeProvider);
AcceptAndComplete(deps, CombatQuestId, timeProvider);
Assert.True(QuestStateOperations.TryAccept(PlayerId, ChainQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
Assert.True(deps.ProgressStore.TryUpdateObjectiveCounter(PlayerId, ChainQuestId, ChainObjectiveId, 5, out _));
@ -211,7 +212,7 @@ public sealed class QuestStateOperationsTests
var timeProvider = TimeProvider.System;
AcceptAndComplete(deps, GatherQuestId, timeProvider);
AcceptAndComplete(deps, RefineQuestId, timeProvider);
AcceptAndComplete(deps, "prototype_quest_combat_intro", timeProvider);
AcceptAndComplete(deps, CombatQuestId, timeProvider);
Assert.True(QuestStateOperations.TryAccept(PlayerId, ChainQuestId, deps.QuestRegistry, deps.ProgressStore).Success);
// Act
@ -227,6 +228,46 @@ public sealed class QuestStateOperationsTests
Assert.Equal(QuestStateReasonCodes.InvalidStepIndex, result.ReasonCode);
}
[Fact]
public async Task TryAdvanceStep_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
// Act
var result = QuestStateOperations.TryAdvanceStep(
PlayerId,
"prototype_quest_missing",
1,
deps.QuestRegistry,
deps.ProgressStore);
// Assert
Assert.False(result.Success);
Assert.Equal(QuestStateReasonCodes.UnknownQuest, result.ReasonCode);
}
[Fact]
public async Task TryMarkComplete_ShouldDenyUnknownQuest_WhenQuestIdNotInCatalog()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var deps = ResolveDependencies(factory);
// Act
var result = QuestStateOperations.TryMarkComplete(
PlayerId,
"prototype_quest_missing",
deps.QuestRegistry,
deps.ProgressStore,
TimeProvider.System);
// Assert
Assert.False(result.Success);
Assert.Equal(QuestStateReasonCodes.UnknownQuest, result.ReasonCode);
}
[Fact]
public async Task TryAdvanceStep_ShouldDenyNotActive_WhenQuestNotAccepted()
{
@ -334,17 +375,15 @@ public sealed class QuestStateOperationsTests
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
using var scope = factory.Services.CreateScope();
var questRegistry = scope.ServiceProvider.GetRequiredService<IQuestDefinitionRegistry>();
var progressStore = scope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
var deps = ResolveDependencies(factory);
// Act
var accept = QuestStateOperations.TryAccept(PlayerId, GatherQuestId, questRegistry, progressStore);
var accept = QuestStateOperations.TryAccept(PlayerId, GatherQuestId, deps.QuestRegistry, deps.ProgressStore);
var complete = QuestStateOperations.TryMarkComplete(
PlayerId,
GatherQuestId,
questRegistry,
progressStore,
deps.QuestRegistry,
deps.ProgressStore,
TimeProvider.System);
// Assert
@ -370,9 +409,9 @@ public sealed class QuestStateOperationsTests
private static (IQuestDefinitionRegistry QuestRegistry, IPlayerQuestStateStore ProgressStore) ResolveDependencies(
InMemoryWebApplicationFactory factory)
{
var scope = factory.Services.CreateScope();
var services = factory.Services;
return (
scope.ServiceProvider.GetRequiredService<IQuestDefinitionRegistry>(),
scope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>());
services.GetRequiredService<IQuestDefinitionRegistry>(),
services.GetRequiredService<IPlayerQuestStateStore>());
}
}

View File

@ -26,6 +26,12 @@ public static class PrototypeE7M1QuestCatalogRules
/// <summary>First frozen onboarding quest id (gather intro).</summary>
public const string GatherIntroQuestId = "prototype_quest_gather_intro";
/// <summary>Refine onboarding quest id (requires gather intro complete).</summary>
public const string RefineIntroQuestId = "prototype_quest_refine_intro";
/// <summary>Combat onboarding quest id (no prerequisites).</summary>
public const string CombatIntroQuestId = "prototype_quest_combat_intro";
/// <summary>First objective id on gather intro step (frozen catalog).</summary>
public const string GatherIntroFirstObjectiveId = "gather_intro_obj_scrap";

View File

@ -23,12 +23,12 @@ public static class QuestStateOperations
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing))
{
return existing.Status switch
if (existing.Status == QuestProgressStatus.Completed)
{
QuestProgressStatus.Completed => Deny(QuestStateReasonCodes.AlreadyCompleted, existing),
QuestProgressStatus.Active => Deny(QuestStateReasonCodes.AlreadyActive, existing),
_ => Deny(QuestStateReasonCodes.UnknownQuest),
};
return Deny(QuestStateReasonCodes.AlreadyCompleted, existing);
}
return Deny(QuestStateReasonCodes.AlreadyActive, existing);
}
foreach (var prerequisiteId in definition.PrerequisiteQuestIds)
@ -108,14 +108,17 @@ public static class QuestStateOperations
return Deny(QuestStateReasonCodes.UnknownQuest);
}
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing) &&
existing.Status == QuestProgressStatus.Completed)
if (!progressStore.TryGetProgress(playerId, normalizedQuestId, out var progress))
{
return Success(existing);
return Deny(QuestStateReasonCodes.NotActive);
}
if (!progressStore.TryGetProgress(playerId, normalizedQuestId, out var active) ||
active.Status != QuestProgressStatus.Active)
if (progress.Status == QuestProgressStatus.Completed)
{
return Success(progress);
}
if (progress.Status != QuestProgressStatus.Active)
{
return Deny(QuestStateReasonCodes.NotActive);
}