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

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.md section 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, EncounterDefinitionCatalogLoader under Game/Encounters/ with CI-parity gates.
  • Catalogs: RewardTableDefinitionCatalog, EncounterDefinitionCatalog singletons; eager resolve in Program.cs.
  • DI: AddEncounterAndRewardCatalogs — reward tables load after item catalog; encounters load after reward tables.
  • Config: ContentPathsOptions encounter + reward-table path keys; InMemoryWebApplicationFactory pins repo paths.
  • Tests: 22 AAA tests in RewardTableDefinitionCatalogLoaderTests + EncounterDefinitionCatalogLoaderTests (loader + host boot, incl. row schema violation).
  • Docs: server/README.md catalog sections; alignment register E5.M3 row updated.

Technical approach

  1. Game/Encounters/ reward-table loaderRewardTableDefinitionCatalogLoader.Load(rewardTablesDirectory, rewardTableSchemaPath, rewardGrantRowSchemaPath, knownItemIds, logger):

    • Enumerate *_reward_tables.json (top-level only), ordered by path.
    • Per file: parse JSON; require schemaVersion == 1 and top-level rewardTables array.
    • Register reward-grant-row.schema.json in SchemaRegistry.Global before evaluating reward-table.schema.json (recipe I/O row pattern).
    • Per row: JsonSchema.Net validation; track id; reject duplicate id across files.
    • Cross-ref: each fixedGrants[].itemIdknownItemIds; reject duplicate itemId within 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_GRANTS in validate_content.py).
    • Throw InvalidOperationException with aggregated sorted messages prefixed Reward table catalog validation failed:.
  2. RewardTableDefinitionCatalog — immutable snapshot: directory path, ById, DistinctRewardTableCount, CatalogJsonFileCount; TryGetRewardTable(id, out RewardTableDefRow?).

  3. RewardTableDefRow / RewardGrantRow — load-time DTOs (Id, DisplayName, FixedGrants with ItemId + Quantity).

  4. Game/Encounters/ encounter loaderEncounterDefinitionCatalogLoader.Load(encountersDirectory, encounterDefSchemaPath, knownRewardTableIds, logger):

    • Enumerate *_encounters.json (top-level only), ordered by path.
    • Per file: schemaVersion == 1, top-level encounters array; row schema via encounter-def.schema.json.
    • Duplicate id rejection across files.
    • Run PrototypeE5M3EncounterCatalogRules: ids == { prototype_combat_pocket }; requiredNpcInstanceIds set equals PrototypeNpcRegistry instance id set; each rewardTableIdknownRewardTableIds.
    • Throw with prefix Encounter catalog validation failed:.
  5. EncounterDefinitionCatalogById, counts, TryGetEncounter(id, out EncounterDefRow?).

  6. EncounterDefRowId, DisplayName, CompletionCriteriaKind, RequiredNpcInstanceIds, RewardTableId.

  7. Path resolutionEncounterCatalogPathResolution / RewardTableCatalogPathResolution: TryDiscover*Directory, Resolve*Directory, Resolve*SchemaPath (ancestor walk from AppContext.BaseDirectory for content/encounters and content/reward-tables; schema defaults under sibling content/schemas/).

  8. ContentPathsOptions — add EncountersDirectory, EncounterDefSchemaPath, RewardTablesDirectory, RewardTableDefSchemaPath, RewardGrantRowSchemaPath.

  9. EncounterCatalogServiceCollectionExtensions.AddEncounterAndRewardCatalogs (single extension, two singletons):

    • Factory for RewardTableDefinitionCatalog depends on ItemDefinitionCatalog (known item ids).
    • Factory for EncounterDefinitionCatalog depends on RewardTableDefinitionCatalog (known reward table ids).
    • No IEncounterDefinitionRegistry / IRewardTableDefinitionRegistry yet (NEO-102).
  10. Program.cs — register extension; after Build(), eager-resolve both catalogs before Run() (same as items/recipes/NPC behaviors).

  11. InMemoryWebApplicationFactory — pin Content:EncountersDirectory and Content:RewardTablesDirectory to repo paths so existing tests keep booting.

  12. 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, duplicate fixedGrants[].itemId, wrong grant quantities; host resolve via DI + /health; host fail with empty encounters dir.

  13. Docsserver/README.md sections (config keys, discovery, fail-fast, load order); documentation_and_implementation_alignment.md E5.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