14 KiB
NEO-101 — Implementation plan
Story reference
| Field | Value |
|---|---|
| Key | NEO-101 |
| Title | E5M3-02: Server encounter + reward catalog load (fail-fast) |
| Linear | https://linear.app/neon-sprawl/issue/NEO-101/e5m3-02-server-encounter-reward-catalog-load-fail-fast |
| Module | E5.M3 — EncounterAndRewardTables · Epic 5 Slice 3 · backlog E5M3-02 |
| Branch | NEO-101-encounter-reward-catalog-load-fail-fast |
| Precursor | NEO-100 — encounter + reward-table schemas, catalogs, CI gates (landed on main) |
| Pattern | NEO-51 / NEO-66 / NEO-88 — row JSON Schema + catalog envelope + in-process C# gates mirroring scripts/validate_content.py |
| Blocks | NEO-102 — injectable encounter/reward registries + DI |
| Client counterpart | None — server-only catalog load; player-visible work starts at NEO-110 / NEO-111 |
Kickoff clarifications
| Topic | Question | Agent recommendation | Answer |
|---|---|---|---|
| Test scope | Loader unit tests only, or also WebApplicationFactory host boot failure / DI resolve tests? |
Both — mirror NEO-51 and NEO-88 (Host_ShouldResolveCatalogFromDi, Host_ShouldFailStartup_WhenCatalogDirectoryInvalid); Linear AC “host fails on invalid JSON” is stronger with an integration boot test. |
User: loader + host boot tests. |
| NEO-101 vs NEO-102 split | Ship IEncounterDefinitionRegistry / IRewardTableDefinitionRegistry on this story? |
Strict split — E5M3-02: loader + catalog singletons + eager fail-fast boot; NEO-102 adds injectable registries (E5M3-03). Same pattern as NEO-51/52. | Adopted — backlog out-of-scope; no separate question needed. |
| Runtime validation | In-process C# vs subprocess validate_content.py? |
In-process C# mirroring scripts/validate_content.py E5M3 gates (NEO-51/66 precedent; no Python on server images). |
Adopted |
| Cross-ref at load | Validate fixedGrants[].itemId against item catalog? NPC ids against PrototypeNpcRegistry? |
Yes — reward-table loader takes IReadOnlySet<string> knownItemIds from ItemDefinitionCatalog (recipe pattern); encounter loader cross-checks requiredNpcInstanceIds set against PrototypeNpcRegistry.GetInstanceIdsInOrder() and rewardTableId against loaded reward-table ids. Load reward tables before encounters (same order as CI main()). |
Adopted |
Goal, scope, and out-of-scope
Goal: Disk → host: startup load of content/encounters/*.json and content/reward-tables/*.json with CI-parity validation; process refuses to listen when catalogs are missing, malformed, or fail prototype E5M3 gates.
In scope (from Linear + E5M3-02):
- Loader + catalog types under
server/NeonSprawl.Server/Game/Encounters/. - Fail-fast on malformed files, duplicate ids, allowlist mismatch with CI, broken cross-refs (item ids, NPC instance ids,
rewardTableId). - Unit tests (AAA) for loader happy path and failure modes; host boot tests (AAA).
server/README.mdsection for encounter + reward-table catalogs.
Out of scope (from Linear + backlog):
- Injectable registry interfaces (NEO-102 / E5M3-03).
- HTTP projection, progress store, runtime completion (E5M3-04+).
- Godot client.
Acceptance criteria checklist
- Host fails startup on invalid encounter or reward-table JSON (mirror CI rules).
- Tests cover at least one happy path and one malformed catalog rejection (loader + host boot per kickoff).
Implementation reconciliation (shipped)
- Loaders:
RewardTableDefinitionCatalogLoader,EncounterDefinitionCatalogLoaderunderGame/Encounters/with CI-parity gates. - Catalogs:
RewardTableDefinitionCatalog,EncounterDefinitionCatalogsingletons; eager resolve inProgram.cs. - DI:
AddEncounterAndRewardCatalogs— reward tables load after item catalog; encounters load after reward tables. - Config:
ContentPathsOptionsencounter + reward-table path keys;InMemoryWebApplicationFactorypins repo paths. - Tests: 19 AAA tests in
RewardTableDefinitionCatalogLoaderTests+EncounterDefinitionCatalogLoaderTests(loader + host boot). - Docs:
server/README.mdcatalog sections; alignment register E5.M3 row updated.
Technical approach
-
Game/Encounters/reward-table loader —RewardTableDefinitionCatalogLoader.Load(rewardTablesDirectory, rewardTableSchemaPath, rewardGrantRowSchemaPath, knownItemIds, logger):- Enumerate
*_reward_tables.json(top-level only), ordered by path. - Per file: parse JSON; require
schemaVersion == 1and top-levelrewardTablesarray. - Register
reward-grant-row.schema.jsoninSchemaRegistry.Globalbefore evaluatingreward-table.schema.json(recipe I/O row pattern). - Per row: JsonSchema.Net validation; track
id; reject duplicateidacross files. - Cross-ref: each
fixedGrants[].itemId∈knownItemIds; reject duplicateitemIdwithin a table (mirror CI_validate_reward_table_catalogs). - Run
PrototypeE5M3RewardTableCatalogRules: ids =={ prototype_combat_pocket_clear }; frozen grant quantities{ scrap_metal_bulk: 10, contract_handoff_token: 1 }(sync comment →PROTOTYPE_E5M3_REWARD_TABLE_IDS/PROTOTYPE_E5M3_REWARD_GRANTSinvalidate_content.py). - Throw
InvalidOperationExceptionwith aggregated sorted messages prefixedReward table catalog validation failed:.
- Enumerate
-
RewardTableDefinitionCatalog— immutable snapshot: directory path,ById,DistinctRewardTableCount,CatalogJsonFileCount;TryGetRewardTable(id, out RewardTableDefRow?). -
RewardTableDefRow/RewardGrantRow— load-time DTOs (Id,DisplayName,FixedGrantswithItemId+Quantity). -
Game/Encounters/encounter loader —EncounterDefinitionCatalogLoader.Load(encountersDirectory, encounterDefSchemaPath, knownRewardTableIds, logger):- Enumerate
*_encounters.json(top-level only), ordered by path. - Per file:
schemaVersion == 1, top-levelencountersarray; row schema viaencounter-def.schema.json. - Duplicate
idrejection across files. - Run
PrototypeE5M3EncounterCatalogRules: ids =={ prototype_combat_pocket };requiredNpcInstanceIdsset equalsPrototypeNpcRegistryinstance id set; eachrewardTableId∈knownRewardTableIds. - Throw with prefix
Encounter catalog validation failed:.
- Enumerate
-
EncounterDefinitionCatalog—ById, counts,TryGetEncounter(id, out EncounterDefRow?). -
EncounterDefRow—Id,DisplayName,CompletionCriteriaKind,RequiredNpcInstanceIds,RewardTableId. -
Path resolution —
EncounterCatalogPathResolution/RewardTableCatalogPathResolution:TryDiscover*Directory,Resolve*Directory,Resolve*SchemaPath(ancestor walk fromAppContext.BaseDirectoryforcontent/encountersandcontent/reward-tables; schema defaults under siblingcontent/schemas/). -
ContentPathsOptions— addEncountersDirectory,EncounterDefSchemaPath,RewardTablesDirectory,RewardTableDefSchemaPath,RewardGrantRowSchemaPath. -
EncounterCatalogServiceCollectionExtensions.AddEncounterAndRewardCatalogs(single extension, two singletons):- Factory for
RewardTableDefinitionCatalogdepends onItemDefinitionCatalog(known item ids). - Factory for
EncounterDefinitionCatalogdepends onRewardTableDefinitionCatalog(known reward table ids). - No
IEncounterDefinitionRegistry/IRewardTableDefinitionRegistryyet (NEO-102).
- Factory for
-
Program.cs— register extension; afterBuild(), eager-resolve both catalogs beforeRun()(same as items/recipes/NPC behaviors). -
InMemoryWebApplicationFactory— pinContent:EncountersDirectoryandContent:RewardTablesDirectoryto repo paths so existing tests keep booting. -
Tests — temp-dir fixtures copying repo schemas; valid JSON matching prototype_reward_tables.json and prototype_encounters.json; negative cases for schema violation, duplicate id, wrong allowlist id, unknown item id, wrong NPC set, broken
rewardTableId, duplicatefixedGrants[].itemId, wrong grant quantities; host resolve via DI +/health; host fail with empty encounters dir. -
Docs —
server/README.mdsections (config keys, discovery, fail-fast, load order);documentation_and_implementation_alignment.mdE5.M3 row note server load landed when complete.
Files to add
| Path | Purpose |
|---|---|
server/NeonSprawl.Server/Game/Encounters/RewardGrantRow.cs |
Load-time DTO for a single fixed grant row. |
server/NeonSprawl.Server/Game/Encounters/RewardTableDefRow.cs |
Load-time reward table row DTO. |
server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalog.cs |
In-memory reward-table catalog + TryGetRewardTable. |
server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalogLoader.cs |
Disk I/O, schema validation, item cross-ref, E5M3 gates, logging. |
server/NeonSprawl.Server/Game/Encounters/RewardTableCatalogPathResolution.cs |
Directory/schema discovery for reward tables. |
server/NeonSprawl.Server/Game/Encounters/PrototypeE5M3RewardTableCatalogRules.cs |
Frozen reward-table ids + grant quantity gate (sync → Python). |
server/NeonSprawl.Server/Game/Encounters/EncounterDefRow.cs |
Load-time encounter row DTO. |
server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionCatalog.cs |
In-memory encounter catalog + TryGetEncounter. |
server/NeonSprawl.Server/Game/Encounters/EncounterDefinitionCatalogLoader.cs |
Disk I/O, schema validation, NPC + rewardTableId cross-ref, E5M3 gates. |
server/NeonSprawl.Server/Game/Encounters/EncounterCatalogPathResolution.cs |
Directory/schema discovery for encounters. |
server/NeonSprawl.Server/Game/Encounters/PrototypeE5M3EncounterCatalogRules.cs |
Frozen encounter ids + NPC instance set + rewardTableId cross-ref rules. |
server/NeonSprawl.Server/Game/Encounters/EncounterCatalogServiceCollectionExtensions.cs |
AddEncounterAndRewardCatalogs DI registration. |
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterCatalogTestPaths.cs |
Repo content/encounters + content/reward-tables + schema discovery for tests. |
server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionCatalogLoaderTests.cs |
AAA loader tests for reward-table catalog. |
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionCatalogLoaderTests.cs |
AAA loader + host boot tests for both catalogs. |
docs/plans/NEO-101-implementation-plan.md |
This plan. |
Files to modify
| Path | Rationale |
|---|---|
server/NeonSprawl.Server/Game/Skills/ContentPathsOptions.cs |
Add encounter + reward-table directory and schema path options under Content section. |
server/NeonSprawl.Server/Program.cs |
Register encounter/reward catalogs; eager-resolve after build. |
server/NeonSprawl.Server/appsettings.json |
Optional empty Content:EncountersDirectory / RewardTablesDirectory / schema override keys for documentation. |
server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs |
Pin encounter + reward-table content paths so test host boots with repo catalogs. |
server/README.md |
New encounter + reward-table catalog sections (config, discovery, fail-fast, load order). |
docs/decomposition/modules/documentation_and_implementation_alignment.md |
E5.M3 row — note NEO-101 server load when complete. |
Tests
| Test file | What it covers |
|---|---|
server/NeonSprawl.Server.Tests/Game/Encounters/RewardTableDefinitionCatalogLoaderTests.cs |
Unit: valid prototype reward-table catalog loads; rewardTables not array; schema violation; duplicate id; wrong/missing E5M3 table id; unknown itemId in fixedGrants; duplicate itemId in fixedGrants; wrong frozen grant quantities; schemaVersion ≠ 1; missing schema file. |
server/NeonSprawl.Server.Tests/Game/Encounters/EncounterDefinitionCatalogLoaderTests.cs |
Unit: valid prototype encounter catalog loads (with pre-built reward-table id set); encounters not array; schema violation; duplicate id; wrong/missing E5M3 encounter id; wrong requiredNpcInstanceIds set; unknown rewardTableId; schemaVersion ≠ 1. Host: InMemoryWebApplicationFactory + /health + DI resolve both catalogs (encounter prototype_combat_pocket, reward table prototype_combat_pocket_clear); WebApplicationFactory with empty encounters dir fails startup with actionable message. |
Open questions / risks
| Question / risk | Agent recommendation | Status |
|---|---|---|
| Constants drift vs Python | Comment on PrototypeE5M3* rules pointing at scripts/validate_content.py PROTOTYPE_E5M3_* constants and gate functions. |
adopted |
| NPC instance ids are code constants, not content | Encounter cross-ref uses PrototypeNpcRegistry.GetInstanceIdsInOrder() set equality (same as CI PROTOTYPE_E5M2_NPC_INSTANCE_IDS). |
adopted |
| Test cwd / CI | InMemoryWebApplicationFactory must set Content:EncountersDirectory and Content:RewardTablesDirectory explicitly. |
adopted |
| Load order in DI | Register reward-table catalog factory before encounter catalog; encounter factory reads reward-table ids from resolved singleton. | adopted |