10 KiB
NEO-102 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-102 |
| Title | E5M3-03: Injectable encounter/reward registries + DI |
| Linear | https://linear.app/neon-sprawl/issue/NEO-102/e5m3-03-injectable-encounterreward-registries-di |
| Module | E5.M3 — EncounterAndRewardTables · Epic 5 Slice 3 · backlog E5M3-03 |
| Branch | NEO-102-injectable-encounter-reward-registries-di |
| Precursor | NEO-101 — fail-fast encounter + reward-table catalog load (landed on main) |
| Pattern | NEO-52 / NEO-79 — thin registry adapter over startup catalog + DI; strict split after NEO-101 loader |
| Blocks | NEO-103 — GET /game/world/encounter-definitions; E5M3-05+ progress/completion engines |
| Client counterpart | None — server-only registry surface; player-visible work starts at NEO-110 / NEO-111 |
Kickoff clarifications
No clarifications needed — E5M3-03 backlog, NEO-101 plan strict split, and NEO-52/79 registry precedent settle API surface, test scope, and DI wiring. User confirmed Proceed — no clarifications needed on kickoff.
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| Registry API surface | Include TryNormalizeKnown + GetDefinitionsInIdOrder on both registries? |
Yes — E5M3-03 backlog + IAbilityDefinitionRegistry (NEO-79) pattern. |
Adopted |
| Lookup naming | TryGetDefinition vs domain-specific names? |
TryGetDefinition — item/skill/ability/recipe precedent; catalogs keep TryGetEncounter / TryGetRewardTable. |
Adopted |
| Test scope | Registry unit tests only, or also host DI resolve? | Both — mirror NEO-52/79; NEO-101 already covers catalog boot. | Adopted |
| Program.cs eager resolve | Eager-resolve registries at boot? | Omit — catalog singletons already eager-resolved (NEO-101); registry is thin adapter. | Adopted |
| Call site migration | Migrate existing consumers off catalog types? | None this story — no game code injects encounter/reward catalogs yet; HTTP is NEO-103. | Adopted |
Goal, scope, and out-of-scope
Goal: Provide IEncounterDefinitionRegistry and IRewardTableDefinitionRegistry backed by the startup-loaded EncounterDefinitionCatalog and RewardTableDefinitionCatalog: resolve by stable id, enumerate definitions in id order, and expose TryNormalizeKnown for fail-closed id validation. Register both in DI so NEO-103+ (HTTP, progress store, completion engine) depend on interfaces instead of catalog types.
In scope (from Linear + E5M3-03):
EncounterDefinitionRegistry+RewardTableDefinitionRegistrythin adapters over NEO-101 catalogs.- DI registration in
AddEncounterAndRewardCatalogs. - Unit tests (AAA): known prototype id lookup + metadata; unknown id / null miss without throwing;
TryNormalizeKnowntrim/case/whitespace behavior; enumeration order; host resolves both registries from DI.
Out of scope (from Linear + backlog):
- HTTP routes (NEO-103).
- Progress store, completion grants, combat hooks (E5M3-05+).
- Changing loader, E5M3 gate, or catalog load semantics (NEO-101).
- Godot / client changes.
Acceptance criteria checklist
- Registries resolve frozen encounter + reward table ids (
prototype_combat_pocket,prototype_combat_pocket_clear). - Unknown id normalization fails closed (
TryNormalizeKnownreturns false; no throw).
Implementation reconciliation (shipped)
- Interfaces:
IEncounterDefinitionRegistry,IRewardTableDefinitionRegistrywithTryGetDefinition,TryNormalizeKnown,GetDefinitionsInIdOrder. - Adapters:
EncounterDefinitionRegistry,RewardTableDefinitionRegistryover NEO-101 catalogs (cached id-order lists). - DI:
AddEncounterAndRewardCatalogsregisters both registries after catalog singletons; noProgram.cschange. - Tests: 18 AAA tests in
EncounterDefinitionRegistryTests+RewardTableDefinitionRegistryTests(unit + host DI). - Docs:
server/README.mdregistry sections; alignment register E5.M3 row updated.
Technical approach
-
IEncounterDefinitionRegistry— mirrorIAbilityDefinitionRegistry:TryGetDefinition(string? encounterId, [NotNullWhen(true)] out EncounterDefRow? definition)— null and unknown ids return false without throwing.TryNormalizeKnown(string? rawEncounterId, [NotNullWhen(true)] out string normalized)— trim + lowercase + catalog lookup; null/empty/whitespace return false.GetDefinitionsInIdOrder()— all rows ordered byEncounterDefRow.Id(ordinal).- XML remarks: encounter progress/completion callers (NEO-104+) and HTTP read model (NEO-103) should use this interface, not
EncounterDefinitionCatalog.
-
IRewardTableDefinitionRegistry— same surface overRewardTableDefRow/RewardTableDefinitionCatalog. -
EncounterDefinitionRegistry— primary-constructor adapter delegating toEncounterDefinitionCatalog.TryGetEncounter; cacheGetDefinitionsInIdOrderat construction (NEO-79 pattern). -
RewardTableDefinitionRegistry— same overRewardTableDefinitionCatalog.TryGetRewardTable. -
DI — extend
AddEncounterAndRewardCatalogs:services.AddSingleton<IEncounterDefinitionRegistry>(sp => new EncounterDefinitionRegistry(sp.GetRequiredService<EncounterDefinitionCatalog>()));services.AddSingleton<IRewardTableDefinitionRegistry>(sp => new RewardTableDefinitionRegistry(sp.GetRequiredService<RewardTableDefinitionCatalog>()));- No change to
Program.cseager-resolve (catalogs only).
-
Comments / docs — tighten catalog summaries (already reference NEO-102); update server README encounter + reward-table sections to document registries as preferred lookup surface (replace “land in NEO-102” placeholders).
-
Tests — new
EncounterDefinitionRegistryTests.csandRewardTableDefinitionRegistryTests.csmirroringAbilityDefinitionRegistryTests:- In-memory catalog helper building registry from row dictionaries.
- Prototype fixture via repo catalogs or inline rows.
Host_ShouldResolveRegistriesFromDi_WhenStartupSucceedsin one file (resolve both interfaces viaInMemoryWebApplicationFactory, assert frozen ids + grant metadata).
-
Alignment doc — update E5.M3 row in
documentation_and_implementation_alignment.mdwhen complete.
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Encounters/IEncounterDefinitionRegistry.cs |
Public contract: lookup, normalize, enumerate. |
server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionRegistry.cs |
Singleton adapter over EncounterDefinitionCatalog. |
server/NeonSprawl.Server/Game/Encounters/IRewardTableDefinitionRegistry.cs |
Public contract: lookup, normalize, enumerate. |
server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionRegistry.cs |
Singleton adapter over RewardTableDefinitionCatalog. |
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionRegistryTests.cs |
AAA unit + host DI tests for encounter registry. |
server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionRegistryTests.cs |
AAA unit + host DI tests for reward-table registry. |
docs/plans/NEO-102-implementation-plan.md |
This plan. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Encounters/EncounterCatalogServiceCollectionExtensions.cs |
Register IEncounterDefinitionRegistry + IRewardTableDefinitionRegistry after catalog singletons. |
server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionCatalog.cs |
Confirm summary points at IEncounterDefinitionRegistry (replace NEO-102 placeholder wording if needed). |
server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalog.cs |
Confirm summary points at IRewardTableDefinitionRegistry. |
server/README.md |
Encounter + reward-table catalog sections: document registries as preferred lookup surface. |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E5.M3 row — note NEO-102 registries when complete. |
Tests
| Test file | What it covers |
|---|---|
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionRegistryTests.cs |
Unit: TryGetDefinition returns true + expected metadata for prototype_combat_pocket (displayName, completionCriteriaKind, requiredNpcInstanceIds, rewardTableId); false for null/unknown. Unit: TryNormalizeKnown trim/case success; false for null/whitespace/unknown. Unit: GetDefinitionsInIdOrder ordinal order. Host: InMemoryWebApplicationFactory resolves IEncounterDefinitionRegistry with frozen encounter row. |
server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionRegistryTests.cs |
Unit: TryGetDefinition returns true + fixed grants for prototype_combat_pocket_clear (scrap_metal_bulk ×10, contract_handoff_token ×1); false for null/unknown. Unit: TryNormalizeKnown trim/case success; false for null/whitespace/unknown. Unit: GetDefinitionsInIdOrder ordinal order. Host: InMemoryWebApplicationFactory resolves IRewardTableDefinitionRegistry with frozen grant quantities. |
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
| Constants drift vs catalog load | No runtime re-validation — registry delegates to NEO-101 fail-fast catalogs only. | adopted |
| Duplicate host boot tests | Keep NEO-101 catalog boot tests; add registry-specific DI resolve assertions (do not remove catalog coverage). | adopted |
| NEO-103 HTTP projection | NEO-103 should inject registries, not catalogs — document in README now. | adopted |