neon-sprawl/docs/plans/NEO-114-implementation-plan.md

9.4 KiB

NEO-114 — Implementation plan

Story reference

Field Value
Key NEO-114
Title E7M1-03: Injectable quest definition registry + DI
Linear https://linear.app/neon-sprawl/issue/NEO-114/e7m1-03-injectable-quest-definition-registry-di
Module E7.M1 — QuestStateMachine · Epic 7 Slice 1 · backlog E7M1-03
Branch NEO-114-e7m1-injectable-quest-definition-registry-di
Precursor NEO-113 — fail-fast quest catalog load (landed on main)
Pattern NEO-102 / NEO-52 — thin registry adapter over startup catalog + DI; strict split after NEO-113 loader
Blocks NEO-115 — GET /game/world/quest-definitions; E7M1-05+ quest runtime
Client counterpart None — server-only registry surface; player-visible work starts at NEO-122 / NEO-123

Kickoff clarifications

Topic Question Agent recommendation Answer
Registry API surface Include TryNormalizeKnown + GetDefinitionsInIdOrder on IQuestDefinitionRegistry? Yes — mirror NEO-102 / IEncounterDefinitionRegistry; NEO-115 (E7M1-04 HTTP) needs enumeration; accept flow will need fail-closed id normalization. Adopted — user chose full surface.
Lookup naming TryGetDefinition vs TryGetQuest? TryGetDefinition — item/skill/ability/recipe/encounter precedent; catalog keeps TryGetQuest. Adopted
DI wiring Separate AddQuestDefinitionRegistry extension vs extend catalog registration? Extend AddQuestDefinitionCatalog — register IQuestDefinitionRegistry after QuestDefinitionCatalog singleton (same file as NEO-113). Adopted
Test scope Registry unit tests only, or also host DI resolve? Both — mirror NEO-102; NEO-113 already covers catalog boot. Adopted
Program.cs eager resolve Eager-resolve registry at boot? Omit — catalog singleton already eager-resolved (NEO-113); registry is thin adapter. Adopted
Call site migration Migrate existing consumers off QuestDefinitionCatalog? None this story — no game code injects quest catalog yet; HTTP is NEO-115. Adopted

Goal, scope, and out-of-scope

Goal: Provide IQuestDefinitionRegistry backed by the startup-loaded QuestDefinitionCatalog: resolve by stable quest id, enumerate definitions in id order, and expose TryNormalizeKnown for fail-closed id validation. Register in DI so NEO-115+ (HTTP, player quest state, accept/advance engines) depend on the interface instead of the catalog type.

In scope (from Linear + E7M1-03):

  • IQuestDefinitionRegistry + QuestDefinitionRegistry thin adapter over NEO-113 catalog.
  • DI registration in AddQuestDefinitionCatalog.
  • Unit tests (AAA): all four frozen quest ids resolve with expected metadata; unknown id / null miss without throwing; TryNormalizeKnown trim/case/whitespace behavior; enumeration order; host resolves registry from DI.

Out of scope (from Linear + backlog):

  • HTTP routes (NEO-115 / E7M1-04).
  • Player quest state, accept/advance hooks (E7M1-05+).
  • Changing loader, E7M1 gates, or catalog load semantics (NEO-113).
  • Godot / client changes.

Acceptance criteria checklist

  • Registry resolves all four frozen quest ids at runtime (PrototypeE7M1QuestCatalogRules.ExpectedQuestIds).
  • Unknown quest id fails closed (TryGetDefinition / TryNormalizeKnown return false; no throw).

Implementation reconciliation (shipped)

  • Interface: IQuestDefinitionRegistry with TryGetDefinition, TryNormalizeKnown, GetDefinitionsInIdOrder.
  • Adapter: QuestDefinitionRegistry over NEO-113 QuestDefinitionCatalog (cached id-order list).
  • DI: AddQuestDefinitionCatalog registers IQuestDefinitionRegistry after catalog singleton; no Program.cs change.
  • Tests: AAA tests in QuestDefinitionRegistryTests (unit + host DI; four frozen ids + unknown/null/normalize/order).
  • Docs: server/README.md quest registry section; alignment register E7.M1 row updated.

Technical approach

  1. IQuestDefinitionRegistry — mirror IEncounterDefinitionRegistry:

    • TryGetDefinition(string? questId, [NotNullWhen(true)] out QuestDefRow? definition) — null and unknown ids return false without throwing.
    • TryNormalizeKnown(string? rawQuestId, [NotNullWhen(true)] out string normalized) — trim + lowercase + catalog lookup; null/empty/whitespace return false.
    • GetDefinitionsInIdOrder() — all rows ordered by QuestDefRow.Id (ordinal).
    • XML remarks: quest runtime / accept callers (NEO-116+) and HTTP read model (NEO-115) should use this interface, not QuestDefinitionCatalog.
  2. QuestDefinitionRegistry — primary-constructor adapter delegating to QuestDefinitionCatalog.TryGetQuest; cache GetDefinitionsInIdOrder at construction (NEO-102 / NEO-79 pattern).

  3. DI — extend QuestCatalogServiceCollectionExtensions.AddQuestDefinitionCatalog:

    • services.AddSingleton<IQuestDefinitionRegistry>(sp => new QuestDefinitionRegistry(sp.GetRequiredService<QuestDefinitionCatalog>()));
    • Update extension XML summary to document both catalog + registry registration.
    • No change to Program.cs eager-resolve (catalog only).
  4. Comments / docsQuestDefinitionCatalog summary already references NEO-114; update server README quest catalog section to document registry as preferred lookup surface (replace “will be” placeholder with concrete method list, mirror encounter section).

  5. Tests — new QuestDefinitionRegistryTests.cs mirroring EncounterDefinitionRegistryTests:

    • Unit (in-memory catalog fixture): each of four ExpectedQuestIds resolves via TryGetDefinition with non-empty DisplayName and step count smoke; null/unknown/empty TryGetDefinition false; TryNormalizeKnown success for trimmed mixed-case known id; false for null/whitespace/unknown; GetDefinitionsInIdOrder length 4 and ordinal id order.
    • Host: InMemoryWebApplicationFactory + /health + DI resolve IQuestDefinitionRegistry; TryGetDefinition("prototype_quest_gather_intro") true; unknown id false; TryNormalizeKnown on a frozen id.
  6. Alignment register — when complete, note NEO-114 registry in documentation_and_implementation_alignment.md E7.M1 row (registry landed; runtime still Planned until E7M1-05+).

Files to add

Path Purpose
server/NeonSprawl.Server/Game/Quests/IQuestDefinitionRegistry.cs Public contract: lookup, normalize, enumerate.
server/NeonSprawl.Server/Game/Quests/QuestDefinitionRegistry.cs Thin adapter over QuestDefinitionCatalog.
server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionRegistryTests.cs AAA unit + host DI tests for registry.
docs/plans/NEO-114-implementation-plan.md This plan.

Files to modify

Path Rationale
server/NeonSprawl.Server/Game/Quests/QuestCatalogServiceCollectionExtensions.cs Register IQuestDefinitionRegistry after catalog singleton; update doc comment.
server/README.md Quest catalog section — registry is preferred lookup surface (methods + normalization note).
docs/decomposition/modules/documentation_and_implementation_alignment.md E7.M1 row — note NEO-114 registry when complete.

Tests

Test file What it covers
server/NeonSprawl.Server.Tests/Game/Quests/QuestDefinitionRegistryTests.cs Unit: TryGetDefinition true + metadata for each PrototypeE7M1QuestCatalogRules.ExpectedQuestIds member (at least displayName + Steps.Count smoke on gather intro and operator chain); false for null/unknown. Unit: TryNormalizeKnown trim/case success on known id; false for null/whitespace/unknown. Unit: GetDefinitionsInIdOrder returns 4 rows in ordinal id order. Host: InMemoryWebApplicationFactory resolves IQuestDefinitionRegistry; frozen id lookup; unknown + normalize failures.

Open questions / risks

Question / risk Agent recommendation Status
Case sensitivity on raw lookup TryGetDefinition is case-sensitive on catalog keys; wire validation uses TryNormalizeKnown (document in README like encounters). adopted
Constants for tests Reuse PrototypeE7M1QuestCatalogRules.ExpectedQuestIds / named ids in tests; avoid duplicating id strings. adopted
Registry vs catalog injection in new code No new QuestDefinitionCatalog injections this repo pass; enforcement via XML + README only until analyzers exist. adopted