Merge pull request #94 from ViPro-Technologies/NEO-59-e3m1-resource-node-definition-registry-di
NEO-59: E3.M1 resource-node definition registry + DIpull/95/head
commit
878454e3ed
|
|
@ -53,7 +53,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
|
|||
| E2.M1 | In Progress | **NEO-33 landed:** frozen prototype trio **`salvage`** / **`refine`** / **`intrusion`** in [`content/skills/prototype_skills.json`](../../../content/skills/prototype_skills.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) enforce schema, duplicate `id`, exact trio ids, and category coverage; designer note on **`allowedXpSourceKinds`** in [E2_M1](E2_M1_SkillDefinitionRegistry.md) + [`content/README.md`](../../../content/README.md). **NEO-34 landed:** fail-fast server load of `content/skills/*.json` at startup — `server/NeonSprawl.Server/Game/Skills/` ([NEO-34](../../plans/NEO-34-implementation-plan.md)); config + discovery in [server README — Skill catalog](../../../server/README.md#skill-catalog-contentskills-neo-34). **NEO-35 landed:** `ISkillDefinitionRegistry` / `SkillDefinitionRegistry` + DI ([NEO-35](../../plans/NEO-35-implementation-plan.md)). **NEO-36 landed:** versioned **`GET /game/world/skill-definitions`** ([NEO-36](../../plans/NEO-36-implementation-plan.md)) — `SkillDefinitionsWorldApi` + `SkillDefinitionsListDtos` in `server/NeonSprawl.Server/Game/Skills/`; Bruno `bruno/neon-sprawl-server/skill-definitions/`; manual QA [`NEO-36`](../../manual-qa/NEO-36.md). **E2.M2 consumer (NEO-38 landed):** grant path validates `skillId` + `sourceKind` vs catalog **`allowedXpSourceKinds`** on **`POST …/skill-progression`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); see [server README — Skill progression grant](../../../server/README.md#skill-progression-grant-neo-38) and [E2_M2](E2_M2_XpAwardAndLevelEngine.md). | [NEO-33](../../plans/NEO-33-implementation-plan.md), [NEO-34](../../plans/NEO-34-implementation-plan.md), [NEO-35](../../plans/NEO-35-implementation-plan.md), [NEO-36](../../plans/NEO-36-implementation-plan.md); [E2_M1](E2_M1_SkillDefinitionRegistry.md); [module_dependency_register.md](module_dependency_register.md) **E2.M1 note**; `server/NeonSprawl.Server/Game/Skills/`; [`docs/manual-qa/NEO-36.md`](../../manual-qa/NEO-36.md); [server README — Skill definitions (NEO-36)](../../../server/README.md#skill-definitions-neo-36) |
|
||||
| E2.M3 | In Progress | **NEO-45 landed:** prototype **`salvage`** mastery catalog + CI gates (see [NEO-45 plan](../../plans/NEO-45-implementation-plan.md)). **NEO-46 landed:** fail-fast server load under `server/NeonSprawl.Server/Game/Mastery/` — `MasteryCatalogLoader`, `IMasteryCatalogRegistry`, cross-check vs `ISkillDefinitionRegistry`, Slice 4 + **`tierIndex`** 1..N gate; see [NEO-46 plan](../../plans/NEO-46-implementation-plan.md). **NEO-47 landed:** `PerkUnlockEngine`, `IPlayerPerkStateStore` + **`V004`**, level-up hook in skill XP grants; see [NEO-47 plan](../../plans/NEO-47-implementation-plan.md). **NEO-49 landed:** comment-only **`perk_unlock`** telemetry hook site in [`PerkUnlockEngine.TryUnlockPerks`](../../../server/NeonSprawl.Server/Game/Mastery/PerkUnlockEngine.cs) ([NEO-49 plan](../../plans/NEO-49-implementation-plan.md), [`NEO-49` manual QA](../../manual-qa/NEO-49.md)); [server README — Perk unlock telemetry (NEO-49)](../../../server/README.md#perk-unlock-engine-and-telemetry-hooks-neo-47-neo-49). **NEO-48 landed:** **`GET`/`POST /game/players/{id}/perk-state`** — `PerkStateApi` + DTOs in `Game/Mastery/` ([NEO-48](../../plans/NEO-48-implementation-plan.md), [`NEO-48` manual QA](../../manual-qa/NEO-48.md)); [server README — Perk state (NEO-48)](../../../server/README.md#perk-state-neo-48); Bruno `bruno/neon-sprawl-server/perk-state/`. | [NEO-45](../../plans/NEO-45-implementation-plan.md), [NEO-46](../../plans/NEO-46-implementation-plan.md), [NEO-47](../../plans/NEO-47-implementation-plan.md), [NEO-48](../../plans/NEO-48-implementation-plan.md), [NEO-49](../../plans/NEO-49-implementation-plan.md), [E2M3-pre-production-backlog](../../plans/E2M3-pre-production-backlog.md), [E2_M3](E2_M3_MasteryAndPerkUnlocks.md) |
|
||||
| E2.M2 | In Progress | **NEO-37 landed:** versioned **`GET /game/players/{id}/skill-progression`** ([NEO-37](../../plans/NEO-37-implementation-plan.md)) — read model for every registered skill; known-player gate via `IPositionStateStore`; [server README — Skill progression snapshot (NEO-37)](../../../server/README.md#skill-progression-snapshot-neo-37); manual QA [`NEO-37`](../../manual-qa/NEO-37.md). **NEO-38 landed:** **`POST`** same path — grant apply, persistence (`IPlayerSkillProgressionStore`, `V003` migration), structured denies + **`levelUps`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); manual QA [`NEO-38`](../../manual-qa/NEO-38.md); Bruno `bruno/neon-sprawl-server/skill-progression/`; [server README — Skill progression grant (NEO-38)](../../../server/README.md#skill-progression-grant-neo-38). **NEO-39 landed:** data-driven `LevelCurve` content + schema + CI validation (`*_level_curve.json`) with fail-fast startup schema checks; progression GET/POST level resolution now uses `ISkillLevelCurve` content-backed thresholds ([NEO-39](../../plans/NEO-39-implementation-plan.md)); manual QA [`NEO-39`](../../manual-qa/NEO-39.md). **NEO-40 landed:** comment-only telemetry hook sites in [`SkillProgressionSnapshotApi.cs`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs) on **`POST …/skill-progression`** for future **`xp_grant`** / **`level_up`** ([NEO-40](../../plans/NEO-40-implementation-plan.md), [`NEO-40` manual QA](../../manual-qa/NEO-40.md)). **NEO-41 landed:** gather prototype — **`POST …/interact`** with **`kind: resource_node`** grants **`salvage`** + **`activity`** (10 XP) via [`SkillProgressionGrantOperations`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperations.cs) ([NEO-41](../../plans/NEO-41-implementation-plan.md), [`NEO-41` manual QA](../../manual-qa/NEO-41.md)); [server README — Interaction](../../../server/README.md#interaction-neo-9). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack; **E3.M2** must invoke on craft/refine success ([NEO-42](../../plans/NEO-42-implementation-plan.md), [`NEO-42` manual QA](../../manual-qa/NEO-42.md)); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42). **NEO-43 landed (prep):** **`MissionRewardSkillXpGrant`** / **`MissionRewardSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack with fixed **`sourceKind: mission_reward`**; **E7.M2** must invoke from quest hand-in ([NEO-43](../../plans/NEO-43-implementation-plan.md), [`NEO-43` manual QA](../../manual-qa/NEO-43.md)); [server README — Mission / quest reward (NEO-43)](../../../server/README.md#mission--quest-reward--skill-xp-neo-43). **Slice 3 still open:** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [epic_02 — E2.M2 + Slice 3](../epics/epic_02_skills_and_progression.md). | [NEO-37](../../plans/NEO-37-implementation-plan.md), [NEO-38](../../plans/NEO-38-implementation-plan.md), [NEO-39](../../plans/NEO-39-implementation-plan.md), [NEO-40](../../plans/NEO-40-implementation-plan.md), [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-43](../../plans/NEO-43-implementation-plan.md), [E2_M2](E2_M2_XpAwardAndLevelEngine.md); label **`E2.M2`** on NEO-37–NEO-41, NEO-42–NEO-44 |
|
||||
| E3.M1 | In Progress | **NEO-41 landed (prototype):** `POST …/interact` success on **`resource_node`** (`prototype_resource_node_alpha`) applies **`salvage`** skill XP (**`sourceKind: activity`**, 10 XP) via shared NEO-38 grant operations. **NEO-57 landed:** frozen four-node catalog in [`content/resource-nodes/`](../../../content/resource-nodes/); [`resource-node-def.schema.json`](../../../content/schemas/resource-node-def.schema.json), [`resource-yield-row.schema.json`](../../../content/schemas/resource-yield-row.schema.json); CI gates in [`validate_content.py`](../../../scripts/validate_content.py). **NEO-58 landed:** fail-fast server load of `content/resource-nodes/*` at startup — `server/NeonSprawl.Server/Game/Gathering/` ([NEO-58](../../plans/NEO-58-implementation-plan.md)); [server README — Resource-node catalog](../../../server/README.md#resource-node-catalog-contentresource-nodes-neo-58). **Still planned:** `IResourceNodeDefinitionRegistry`, `GatherResult`, depletion, inventory grants per [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-57](../../plans/NEO-57-implementation-plan.md), [NEO-58](../../plans/NEO-58-implementation-plan.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `content/resource-nodes/`, `server/NeonSprawl.Server/Game/Gathering/`, `server/NeonSprawl.Server/Game/Interaction/` |
|
||||
| E3.M1 | In Progress | **NEO-41 landed (prototype):** `POST …/interact` success on **`resource_node`** (`prototype_resource_node_alpha`) applies **`salvage`** skill XP (**`sourceKind: activity`**, 10 XP) via shared NEO-38 grant operations. **NEO-57 landed:** frozen four-node catalog in [`content/resource-nodes/`](../../../content/resource-nodes/); [`resource-node-def.schema.json`](../../../content/schemas/resource-node-def.schema.json), [`resource-yield-row.schema.json`](../../../content/schemas/resource-yield-row.schema.json); CI gates in [`validate_content.py`](../../../scripts/validate_content.py). **NEO-58 landed:** fail-fast server load of `content/resource-nodes/*` at startup — `server/NeonSprawl.Server/Game/Gathering/` ([NEO-58](../../plans/NEO-58-implementation-plan.md)); [server README — Resource-node catalog](../../../server/README.md#resource-node-catalog-contentresource-nodes-neo-58). **NEO-59 landed:** injectable **`IResourceNodeDefinitionRegistry`** + lookup tests ([NEO-59](../../plans/NEO-59-implementation-plan.md)). **Still planned:** `GatherResult`, depletion, inventory grants per [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-57](../../plans/NEO-57-implementation-plan.md), [NEO-58](../../plans/NEO-58-implementation-plan.md), [NEO-59](../../plans/NEO-59-implementation-plan.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `content/resource-nodes/`, `server/NeonSprawl.Server/Game/Gathering/`, `server/NeonSprawl.Server/Game/Interaction/` |
|
||||
| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **NEO-54 landed:** **`IPlayerInventoryStore`** + **`PlayerInventoryOperations`** — 24 bag + 1 equipment slots, stack rules, `V005` migration ([NEO-54](../../plans/NEO-54-implementation-plan.md)). **NEO-55 landed:** **`GET`/`POST /game/players/{id}/inventory`** — `PlayerInventoryApi` + DTOs in `Game/Items/` ([NEO-55](../../plans/NEO-55-implementation-plan.md)); [server README — Player inventory (NEO-54 store, NEO-55 HTTP)](../../../server/README.md#player-inventory-neo-54-store-neo-55-http); Bruno `bruno/neon-sprawl-server/inventory/`. **NEO-56 landed:** comment-only **`item_created`** / **`inventory_transfer_denied`** telemetry hook sites in [`PlayerInventoryOperations`](../../../server/NeonSprawl.Server/Game/Items/PlayerInventoryOperations.cs) ([NEO-56](../../plans/NEO-56-implementation-plan.md), [`NEO-56` manual QA](../../manual-qa/NEO-56.md)); no E9.M1 ingest. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [NEO-54](../../plans/NEO-54-implementation-plan.md), [NEO-55](../../plans/NEO-55-implementation-plan.md), [NEO-56](../../plans/NEO-56-implementation-plan.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ Working backlog for **Epic 3 — Slice 2** ([gather nodes and outputs](../decomp
|
|||
- [x] Server refuses boot when resource-node catalog invalid (mirror E3.M3 / E2.M1 catalog behavior).
|
||||
- [x] Catalog resolves `ResourceNodeDef` and yield row by `nodeDefId`.
|
||||
|
||||
**Landed:** [NEO-58](https://linear.app/neon-sprawl/issue/NEO-58) — server loader; registry DI is E3M1-03.
|
||||
**Landed:** [NEO-59](https://linear.app/neon-sprawl/issue/NEO-59) — injectable registry + DI; server loader was [NEO-58](https://linear.app/neon-sprawl/issue/NEO-58).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ Working backlog for **Epic 3 — Slice 2** ([gather nodes and outputs](../decomp
|
|||
|
||||
**Acceptance criteria**
|
||||
|
||||
- [ ] Host resolves registry from DI; unknown `nodeDefId` distinguishable from valid ids in tests.
|
||||
- [x] Host resolves registry from DI; unknown `nodeDefId` distinguishable from valid ids in tests.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
# NEO-59 — Implementation plan
|
||||
|
||||
## Story reference
|
||||
|
||||
| Field | Value |
|
||||
|--------|--------|
|
||||
| **Key** | NEO-59 |
|
||||
| **Title** | E3.M1: Resource node definition registry + DI |
|
||||
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-59/e3m1-resource-node-definition-registry-di |
|
||||
| **Module** | [E3.M1 — ResourceNodeAndGatherLoop](../decomposition/modules/E3_M1_ResourceNodeAndGatherLoop.md) · Epic 3 Slice 2 · backlog **E3M1-03** |
|
||||
| **Branch** | `NEO-59-e3m1-resource-node-definition-registry-di` |
|
||||
| **Precursor** | [NEO-58](https://linear.app/neon-sprawl/issue/NEO-58) — fail-fast `ResourceNodeCatalog` load (**merged** to `main` via PR #93) |
|
||||
| **Pattern** | [NEO-52](https://linear.app/neon-sprawl/issue/NEO-52) — thin registry adapter over startup catalog + DI; strict split after NEO-58 loader |
|
||||
|
||||
## Kickoff clarifications
|
||||
|
||||
| Topic | Question | Agent recommendation | Answer |
|
||||
|--------|----------|----------------------|--------|
|
||||
| **Enumeration API** | Include `GetDefinitionsInIdOrder()` now? | **Yes** — mirror `IItemDefinitionRegistry` / `ISkillDefinitionRegistry`; [NEO-60](https://linear.app/neon-sprawl/issue/NEO-60) `GET /game/world/resource-node-definitions` will need ordered defs without reaching into `ResourceNodeCatalog`. | **User:** include enumeration. |
|
||||
| **Yield access** | How should yield rows be exposed? | **Separate `TryGetYield(nodeDefId)`** alongside `TryGetDefinition` — mirrors `ResourceNodeCatalog.TryGetNode` / `TryGetYield`; backlog E3M1-03 tests call for yield metadata + capacity (`maxGathers`) fields. | **User:** separate `TryGetYield`. |
|
||||
| **Lookup naming** | `TryGetDefinition` vs `TryGetNode`? | **`TryGetDefinition(string? nodeDefId, …)`** — same naming as item/skill registries; catalog keeps `TryGetNode` for direct catalog access. | **Adopted** — NEO-52 / NEO-58 precedent. |
|
||||
| **Program.cs eager resolve** | Eager-resolve `IResourceNodeDefinitionRegistry` at boot? | **Omit** — `ResourceNodeCatalog` is already eager-resolved in `Program.cs` (NEO-58); registry is a thin adapter (NEO-52 default). | **Adopted** |
|
||||
| **Runtime validation** | Re-validate nodes/yields in registry? | **No** — catalog load is fail-fast (NEO-58); registry delegates to loaded rows only. | **Adopted** |
|
||||
|
||||
## Goal, scope, and out-of-scope
|
||||
|
||||
**Goal:** Provide **`IResourceNodeDefinitionRegistry`** backed by the startup-loaded **`ResourceNodeCatalog`**: resolve node defs and yield rows by stable **`nodeDefId`**, enumerate node defs in id order, expose prototype metadata (`displayName`, `gatherLens`, `maxGathers`, yield `itemId` / `quantity`). Register in DI so **NEO-60+** (HTTP), **NEO-61** (depletion), **NEO-62** (gather engine), and interact wiring depend on the interface instead of the catalog type.
|
||||
|
||||
**In scope (from Linear + [E3M1-03](E3M1-prototype-backlog.md#e3m1-03--resource-node-definition-registry--di)):**
|
||||
|
||||
- `ResourceNodeDefinitionRegistry` thin adapter over `ResourceNodeCatalog`.
|
||||
- DI registration alongside existing catalog singleton.
|
||||
- Unit tests (AAA): known prototype id lookup + node metadata + yield metadata; unknown `nodeDefId` returns false without throwing; enumeration order; host resolves registry from DI.
|
||||
|
||||
**Out of scope (from Linear + backlog):**
|
||||
|
||||
- HTTP ([NEO-60](https://linear.app/neon-sprawl/issue/NEO-60)).
|
||||
- Persistence of depletion state ([NEO-61](https://linear.app/neon-sprawl/issue/NEO-61)).
|
||||
- Gather engine / inventory mutation ([NEO-62](https://linear.app/neon-sprawl/issue/NEO-62)).
|
||||
- Changing loader, Slice 2 gate, or catalog load semantics (NEO-58).
|
||||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [x] DI resolves `IResourceNodeDefinitionRegistry` at host startup (test via `InMemoryWebApplicationFactory`).
|
||||
- [x] Unit tests (AAA): lookup for known prototype `nodeDefId` (e.g. `prototype_resource_node_alpha`) with expected node metadata (`gatherLens`, `maxGathers`) and yield metadata (`itemId`, `quantity`).
|
||||
- [x] Unit tests (AAA): unknown `nodeDefId` returns false / absent without throwing (both node and yield lookups).
|
||||
- [x] `GetDefinitionsInIdOrder` returns all loaded node defs ordered by `nodeDefId` (ordinal).
|
||||
|
||||
## Technical approach
|
||||
|
||||
1. **`IResourceNodeDefinitionRegistry`** — mirror [`IItemDefinitionRegistry`](../../server/NeonSprawl.Server/Game/Items/IItemDefinitionRegistry.cs):
|
||||
- `TryGetDefinition(string? nodeDefId, [NotNullWhen(true)] out ResourceNodeDefRow? definition)` — null and unknown ids return false without throwing.
|
||||
- `TryGetYield(string? nodeDefId, [NotNullWhen(true)] out ResourceYieldRow? yield)` — same contract for yield rows.
|
||||
- `GetDefinitionsInIdOrder()` — all node defs ordered by `ResourceNodeDefRow.NodeDefId` (ordinal).
|
||||
- XML remarks: gather engine / depletion / interact callers (NEO-61+) should use this interface; HTTP read model (NEO-60) should not reach into `ResourceNodeCatalog`.
|
||||
|
||||
2. **`ResourceNodeDefinitionRegistry`** — primary-constructor adapter over `ResourceNodeCatalog` (same pattern as [`ItemDefinitionRegistry`](../../server/NeonSprawl.Server/Game/Items/ItemDefinitionRegistry.cs)).
|
||||
|
||||
3. **DI** — extend [`AddResourceNodeCatalog`](../../server/NeonSprawl.Server/Game/Gathering/ResourceNodeCatalogServiceCollectionExtensions.cs) to register `IResourceNodeDefinitionRegistry` → `ResourceNodeDefinitionRegistry` after catalog registration. No change to `Program.cs` eager-resolve (catalog only).
|
||||
|
||||
4. **Comments** — update `ResourceNodeCatalog` summary to point at `IResourceNodeDefinitionRegistry`; update [server README](../../server/README.md) resource-node catalog section (currently says “lands in NEO-59”).
|
||||
|
||||
5. **Tests** — new `ResourceNodeDefinitionRegistryTests.cs` mirroring [`ItemDefinitionRegistryTests`](../../server/NeonSprawl.Server.Tests/Game/Items/ItemDefinitionRegistryTests.cs): in-memory catalog helper, loader-backed prototype fixture via `ResourceNodeCatalogTestPaths`, host DI test.
|
||||
|
||||
## Files to add
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `server/NeonSprawl.Server/Game/Gathering/IResourceNodeDefinitionRegistry.cs` | Public contract: node lookup, yield lookup, enumerate, remarks for NEO-60/61/62 callers. |
|
||||
| `server/NeonSprawl.Server/Game/Gathering/ResourceNodeDefinitionRegistry.cs` | Singleton adapter over `ResourceNodeCatalog`. |
|
||||
| `server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionRegistryTests.cs` | AAA unit + host DI tests (mirror `ItemDefinitionRegistryTests`). |
|
||||
|
||||
## Files to modify
|
||||
|
||||
| Path | Rationale |
|
||||
|------|-----------|
|
||||
| `server/NeonSprawl.Server/Game/Gathering/ResourceNodeCatalogServiceCollectionExtensions.cs` | Register `IResourceNodeDefinitionRegistry` → `ResourceNodeDefinitionRegistry` after catalog singleton. |
|
||||
| `server/NeonSprawl.Server/Game/Gathering/ResourceNodeCatalog.cs` | Tighten summary comment to reference `IResourceNodeDefinitionRegistry` (replace “Prefer registry in NEO-59” placeholder). |
|
||||
| `server/README.md` | Resource-node catalog section: document `IResourceNodeDefinitionRegistry` as preferred lookup surface. |
|
||||
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E3.M1 row — note NEO-59 registry when landed. |
|
||||
|
||||
## Tests
|
||||
|
||||
| Test file | What it covers |
|
||||
|-----------|----------------|
|
||||
| `server/NeonSprawl.Server.Tests/Game/Gathering/ResourceNodeDefinitionRegistryTests.cs` | **Unit:** `TryGetDefinition` for `prototype_resource_node_alpha` returns true with `GatherLens`, `MaxGathers`, `DisplayName`; `TryGetYield` for same id returns true with `ItemId` (`scrap_metal_bulk`) and `Quantity`; null and unknown id return false without throw for both methods; `GetDefinitionsInIdOrder` count (4) and ordinal order for multi-node fixture; loader-backed prototype fixture matches catalog. **Host:** `InMemoryWebApplicationFactory` resolves `IResourceNodeDefinitionRegistry` and finds `prototype_resource_node_alpha` with expected yield. |
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
| Question / risk | Agent recommendation | Status |
|
||||
|-----------------|----------------------|--------|
|
||||
| **Catalog vs registry for game code** | New callers inject `IResourceNodeDefinitionRegistry`; leave `ResourceNodeCatalog` eager-resolve in `Program.cs` for fail-fast only. | **adopted** |
|
||||
| **Duplicate API on catalog** | Keep `ResourceNodeCatalog.TryGetNode` / `TryGetYield` for loader/tests; registry is the game-facing surface (NEO-52 precedent). | **adopted** |
|
||||
| **NEO-60 HTTP projection** | API layer joins node + yield via registry’s two TryGet methods when building DTOs; no combined read model in NEO-59. | **adopted** |
|
||||
|
||||
## Decisions (kickoff)
|
||||
|
||||
- **`GetDefinitionsInIdOrder()` included** — enumeration for NEO-60 HTTP without catalog coupling.
|
||||
- **Separate `TryGetYield`** — yield metadata available to gather engine / tests without a combined DTO.
|
||||
- **`TryGetDefinition` naming** — consistent with item/skill registries; parameter is `nodeDefId`.
|
||||
- **No `Program.cs` registry eager-resolve** — catalog fail-fast only (NEO-52 pattern).
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Code review — NEO-59 resource-node definition registry + DI
|
||||
|
||||
**Date:** 2026-05-24
|
||||
**Scope:** Branch `NEO-59-e3m1-resource-node-definition-registry-di` · commits `4921f2c`–`01dec2b` vs `main`
|
||||
**Base:** `main`
|
||||
|
||||
## Verdict
|
||||
|
||||
**Approve**
|
||||
|
||||
## Summary
|
||||
|
||||
NEO-59 adds `IResourceNodeDefinitionRegistry` backed by the startup-loaded `ResourceNodeCatalog` (NEO-58), following the NEO-52 `IItemDefinitionRegistry` pattern exactly. The registry exposes `TryGetDefinition`, `TryGetYield`, and `GetDefinitionsInIdOrder`; DI is wired inside the existing `AddResourceNodeCatalog` extension so no `Program.cs` change is needed. Nine focused AAA tests (null id, unknown id, known id with metadata, ordinal enumeration, loader-backed fixture, host DI resolution) all pass. Docs (implementation plan, alignment table, server README, E3M1 backlog) match the landed behavior.
|
||||
|
||||
## Documentation checked
|
||||
|
||||
| Document | Result |
|
||||
|----------|--------|
|
||||
| [`docs/plans/NEO-59-implementation-plan.md`](../plans/NEO-59-implementation-plan.md) | **Matches** — scope, acceptance checklist, technical approach, and file list align with the diff; all four checklist items marked complete. |
|
||||
| [`docs/plans/E3M1-prototype-backlog.md`](../plans/E3M1-prototype-backlog.md) | **Matches** — E3M1-03 acceptance items flipped with NEO-59 landed note. |
|
||||
| [`docs/decomposition/modules/documentation_and_implementation_alignment.md`](../decomposition/modules/documentation_and_implementation_alignment.md) | **Matches** — E3.M1 row notes NEO-59 registry + plan link; "Still planned" updated to drop registry. |
|
||||
| [`server/README.md`](../../server/README.md) | **Matches** — resource-node catalog section updated: registry guidance and "do not inject `ResourceNodeCatalog` in new game code" advisory added. |
|
||||
|
||||
Register/tracking: alignment table updated; no further register change required.
|
||||
|
||||
## Blocking issues
|
||||
|
||||
None.
|
||||
|
||||
## Suggestions
|
||||
|
||||
None.
|
||||
|
||||
## Nits
|
||||
|
||||
- ~~`GetDefinitionsInIdOrder()` iterates `.Keys` to build a sorted array then does a second dictionary lookup per key. Iterating key-value pairs directly is slightly more idiomatic and avoids the double lookup:~~
|
||||
|
||||
~~```csharp~~
|
||||
~~return catalog.NodesById~~
|
||||
~~ .OrderBy(kv => kv.Key, StringComparer.Ordinal)~~
|
||||
~~ .Select(kv => kv.Value)~~
|
||||
~~ .ToList();~~
|
||||
~~```~~
|
||||
|
||||
~~The current form matches `ItemDefinitionRegistry` exactly, so consistency may outweigh the change. No action required.~~ Done.
|
||||
|
||||
- The null-id and unknown-id tests each build a fully populated `nodes`/`yields` dictionary even though the exercised code paths exit before those entries are touched. Consistent with `ItemDefinitionRegistryTests` — no action required.
|
||||
|
||||
- No `docs/manual-qa/NEO-59.md` — consistent with plan (no user-facing HTTP endpoint in this ticket; NEO-60 covers that).
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
dotnet test server/NeonSprawl.Server.Tests --filter "FullyQualifiedName~ResourceNodeDefinitionRegistry"
|
||||
# Passed! Failed: 0, Passed: 9, Skipped: 0
|
||||
```
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
using System.IO;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NeonSprawl.Server.Game.Gathering;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
namespace NeonSprawl.Server.Tests.Game.Gathering;
|
||||
|
||||
public class ResourceNodeDefinitionRegistryTests
|
||||
{
|
||||
private static ResourceNodeDefinitionRegistry CreateRegistryFromRows(
|
||||
IReadOnlyDictionary<string, ResourceNodeDefRow> nodesById,
|
||||
IReadOnlyDictionary<string, ResourceYieldRow> yieldsByNodeDefId)
|
||||
{
|
||||
var catalog = new ResourceNodeCatalog(
|
||||
"/tmp/resource-nodes",
|
||||
nodesById,
|
||||
yieldsByNodeDefId,
|
||||
nodeCatalogJsonFileCount: 1,
|
||||
yieldCatalogJsonFileCount: 1);
|
||||
return new ResourceNodeDefinitionRegistry(catalog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenNodeDefIdExists()
|
||||
{
|
||||
// Arrange
|
||||
const string nodeDefId = "prototype_resource_node_alpha";
|
||||
var nodes = new Dictionary<string, ResourceNodeDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceNodeDefRow(
|
||||
nodeDefId,
|
||||
"Prototype Salvage Heap",
|
||||
"consumer_salvage",
|
||||
MaxGathers: 10),
|
||||
};
|
||||
var yields = new Dictionary<string, ResourceYieldRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceYieldRow(nodeDefId, "scrap_metal_bulk", Quantity: 1),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(nodes, yields);
|
||||
// Act
|
||||
var found = registry.TryGetDefinition(nodeDefId, out var def);
|
||||
// Assert
|
||||
Assert.True(found);
|
||||
Assert.NotNull(def);
|
||||
Assert.Equal("Prototype Salvage Heap", def.DisplayName);
|
||||
Assert.Equal("consumer_salvage", def.GatherLens);
|
||||
Assert.Equal(10, def.MaxGathers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetYield_ShouldReturnTrueAndExpectedMetadata_WhenNodeDefIdExists()
|
||||
{
|
||||
// Arrange
|
||||
const string nodeDefId = "prototype_resource_node_alpha";
|
||||
var nodes = new Dictionary<string, ResourceNodeDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceNodeDefRow(
|
||||
nodeDefId,
|
||||
"Prototype Salvage Heap",
|
||||
"consumer_salvage",
|
||||
MaxGathers: 10),
|
||||
};
|
||||
var yields = new Dictionary<string, ResourceYieldRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceYieldRow(nodeDefId, "scrap_metal_bulk", Quantity: 1),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(nodes, yields);
|
||||
// Act
|
||||
var found = registry.TryGetYield(nodeDefId, out var yield);
|
||||
// Assert
|
||||
Assert.True(found);
|
||||
Assert.NotNull(yield);
|
||||
Assert.Equal("scrap_metal_bulk", yield.ItemId);
|
||||
Assert.Equal(1, yield.Quantity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetDefinition_ShouldReturnFalse_WhenNodeDefIdIsNull()
|
||||
{
|
||||
// Arrange
|
||||
const string nodeDefId = "prototype_resource_node_alpha";
|
||||
var nodes = new Dictionary<string, ResourceNodeDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceNodeDefRow(
|
||||
nodeDefId,
|
||||
"Prototype Salvage Heap",
|
||||
"consumer_salvage",
|
||||
MaxGathers: 10),
|
||||
};
|
||||
var yields = new Dictionary<string, ResourceYieldRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceYieldRow(nodeDefId, "scrap_metal_bulk", Quantity: 1),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(nodes, yields);
|
||||
// Act
|
||||
var found = registry.TryGetDefinition(null, out var def);
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
Assert.Null(def);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetYield_ShouldReturnFalse_WhenNodeDefIdIsNull()
|
||||
{
|
||||
// Arrange
|
||||
const string nodeDefId = "prototype_resource_node_alpha";
|
||||
var nodes = new Dictionary<string, ResourceNodeDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceNodeDefRow(
|
||||
nodeDefId,
|
||||
"Prototype Salvage Heap",
|
||||
"consumer_salvage",
|
||||
MaxGathers: 10),
|
||||
};
|
||||
var yields = new Dictionary<string, ResourceYieldRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceYieldRow(nodeDefId, "scrap_metal_bulk", Quantity: 1),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(nodes, yields);
|
||||
// Act
|
||||
var found = registry.TryGetYield(null, out var yield);
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
Assert.Null(yield);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetDefinition_ShouldReturnFalse_WhenNodeDefIdUnknown()
|
||||
{
|
||||
// Arrange
|
||||
const string nodeDefId = "prototype_resource_node_alpha";
|
||||
var nodes = new Dictionary<string, ResourceNodeDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceNodeDefRow(
|
||||
nodeDefId,
|
||||
"Prototype Salvage Heap",
|
||||
"consumer_salvage",
|
||||
MaxGathers: 10),
|
||||
};
|
||||
var yields = new Dictionary<string, ResourceYieldRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceYieldRow(nodeDefId, "scrap_metal_bulk", Quantity: 1),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(nodes, yields);
|
||||
// Act
|
||||
var found = registry.TryGetDefinition("not_a_real_node", out var def);
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
Assert.Null(def);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetYield_ShouldReturnFalse_WhenNodeDefIdUnknown()
|
||||
{
|
||||
// Arrange
|
||||
const string nodeDefId = "prototype_resource_node_alpha";
|
||||
var nodes = new Dictionary<string, ResourceNodeDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceNodeDefRow(
|
||||
nodeDefId,
|
||||
"Prototype Salvage Heap",
|
||||
"consumer_salvage",
|
||||
MaxGathers: 10),
|
||||
};
|
||||
var yields = new Dictionary<string, ResourceYieldRow>(StringComparer.Ordinal)
|
||||
{
|
||||
[nodeDefId] = new ResourceYieldRow(nodeDefId, "scrap_metal_bulk", Quantity: 1),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(nodes, yields);
|
||||
// Act
|
||||
var found = registry.TryGetYield("not_a_real_node", out var yield);
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
Assert.Null(yield);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDefinitionsInIdOrder_ShouldListAllRowsOrderedByNodeDefId_WhenMultipleNodes()
|
||||
{
|
||||
// Arrange
|
||||
var nodes = new Dictionary<string, ResourceNodeDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
["prototype_urban_bulk_delta"] = new ResourceNodeDefRow(
|
||||
"prototype_urban_bulk_delta",
|
||||
"Prototype Urban Bulk",
|
||||
"urban_bulk",
|
||||
MaxGathers: 10),
|
||||
["prototype_bio_mat_gamma"] = new ResourceNodeDefRow(
|
||||
"prototype_bio_mat_gamma",
|
||||
"Prototype Bio Mat",
|
||||
"bio",
|
||||
MaxGathers: 10),
|
||||
["prototype_resource_node_alpha"] = new ResourceNodeDefRow(
|
||||
"prototype_resource_node_alpha",
|
||||
"Prototype Salvage Heap",
|
||||
"consumer_salvage",
|
||||
MaxGathers: 10),
|
||||
["prototype_subsurface_vein_beta"] = new ResourceNodeDefRow(
|
||||
"prototype_subsurface_vein_beta",
|
||||
"Prototype Subsurface Vein",
|
||||
"subsurface",
|
||||
MaxGathers: 10),
|
||||
};
|
||||
var yields = new Dictionary<string, ResourceYieldRow>(StringComparer.Ordinal)
|
||||
{
|
||||
["prototype_urban_bulk_delta"] = new ResourceYieldRow(
|
||||
"prototype_urban_bulk_delta",
|
||||
"scrap_metal_bulk",
|
||||
Quantity: 5),
|
||||
["prototype_bio_mat_gamma"] = new ResourceYieldRow(
|
||||
"prototype_bio_mat_gamma",
|
||||
"scrap_metal_bulk",
|
||||
Quantity: 3),
|
||||
["prototype_resource_node_alpha"] = new ResourceYieldRow(
|
||||
"prototype_resource_node_alpha",
|
||||
"scrap_metal_bulk",
|
||||
Quantity: 1),
|
||||
["prototype_subsurface_vein_beta"] = new ResourceYieldRow(
|
||||
"prototype_subsurface_vein_beta",
|
||||
"scrap_metal_bulk",
|
||||
Quantity: 2),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(nodes, yields);
|
||||
// Act
|
||||
var list = registry.GetDefinitionsInIdOrder();
|
||||
// Assert
|
||||
Assert.Equal(4, list.Count);
|
||||
Assert.Equal("prototype_bio_mat_gamma", list[0].NodeDefId);
|
||||
Assert.Equal("prototype_resource_node_alpha", list[1].NodeDefId);
|
||||
Assert.Equal("prototype_subsurface_vein_beta", list[2].NodeDefId);
|
||||
Assert.Equal("prototype_urban_bulk_delta", list[3].NodeDefId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture()
|
||||
{
|
||||
// Arrange
|
||||
var root = Directory.CreateTempSubdirectory("neon-sprawl-resource-node-registry-loader-");
|
||||
try
|
||||
{
|
||||
var resourceNodesDir = Path.Combine(root.FullName, "content", "resource-nodes");
|
||||
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
|
||||
Directory.CreateDirectory(resourceNodesDir);
|
||||
Directory.CreateDirectory(schemaDir);
|
||||
var nodeSchemaPath = Path.Combine(schemaDir, "resource-node-def.schema.json");
|
||||
var yieldSchemaPath = Path.Combine(schemaDir, "resource-yield-row.schema.json");
|
||||
File.Copy(ResourceNodeCatalogTestPaths.DiscoverRepoResourceNodeDefSchemaPath(), nodeSchemaPath, overwrite: true);
|
||||
File.Copy(ResourceNodeCatalogTestPaths.DiscoverRepoResourceYieldRowSchemaPath(), yieldSchemaPath, overwrite: true);
|
||||
File.Copy(
|
||||
Path.Combine(ResourceNodeCatalogTestPaths.DiscoverRepoResourceNodesDirectory(), "prototype_resource_nodes.json"),
|
||||
Path.Combine(resourceNodesDir, "prototype_resource_nodes.json"),
|
||||
overwrite: true);
|
||||
File.Copy(
|
||||
Path.Combine(ResourceNodeCatalogTestPaths.DiscoverRepoResourceNodesDirectory(), "prototype_resource_yields.json"),
|
||||
Path.Combine(resourceNodesDir, "prototype_resource_yields.json"),
|
||||
overwrite: true);
|
||||
var itemIds = new HashSet<string>(StringComparer.Ordinal) { "scrap_metal_bulk" };
|
||||
var loaded = ResourceNodeCatalogLoader.Load(
|
||||
resourceNodesDir,
|
||||
nodeSchemaPath,
|
||||
yieldSchemaPath,
|
||||
itemIds,
|
||||
NullLogger.Instance);
|
||||
var registry = new ResourceNodeDefinitionRegistry(loaded);
|
||||
// Act
|
||||
var ok = registry.TryGetDefinition("prototype_resource_node_alpha", out var node);
|
||||
var yieldOk = registry.TryGetYield("prototype_resource_node_alpha", out var yield);
|
||||
// Assert
|
||||
Assert.True(ok);
|
||||
Assert.NotNull(node);
|
||||
Assert.Equal("consumer_salvage", node.GatherLens);
|
||||
Assert.Equal(10, node.MaxGathers);
|
||||
Assert.True(yieldOk);
|
||||
Assert.NotNull(yield);
|
||||
Assert.Equal("scrap_metal_bulk", yield.ItemId);
|
||||
Assert.Equal(1, yield.Quantity);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.Delete(root.FullName, recursive: true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Best-effort: transient lock or race on some hosts; temp dir is unique per run.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Host_ShouldResolveRegistryFromDi_WhenStartupSucceeds()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
using var client = factory.CreateClient();
|
||||
_ = await client.GetAsync("/health");
|
||||
// Act
|
||||
var registry = factory.Services.GetRequiredService<IResourceNodeDefinitionRegistry>();
|
||||
var found = registry.TryGetDefinition("prototype_resource_node_alpha", out var node);
|
||||
var yieldFound = registry.TryGetYield("prototype_resource_node_alpha", out var yield);
|
||||
// Assert
|
||||
Assert.True(found);
|
||||
Assert.NotNull(node);
|
||||
Assert.Equal("consumer_salvage", node.GatherLens);
|
||||
Assert.Equal(10, node.MaxGathers);
|
||||
Assert.True(yieldFound);
|
||||
Assert.NotNull(yield);
|
||||
Assert.Equal("scrap_metal_bulk", yield.ItemId);
|
||||
Assert.Equal(1, yield.Quantity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Gathering;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only access to validated resource-node definitions and yield rows loaded at startup
|
||||
/// (<see cref="ResourceNodeCatalog"/>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>E3.M1 (gather / depletion / interact):</b> callers resolving node defs or yields should depend on this
|
||||
/// interface rather than <see cref="ResourceNodeCatalog"/> so gather lens, capacity, and yield metadata stay
|
||||
/// centralized.</para>
|
||||
/// <para><b>NEO-60:</b> HTTP/read-model projections should depend on this interface rather than reaching into the
|
||||
/// catalog.</para>
|
||||
/// </remarks>
|
||||
public interface IResourceNodeDefinitionRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to resolve a node def by stable <c>nodeDefId</c> (see <c>resource-node-def.schema.json</c>).
|
||||
/// Unknown ids and <c>null</c> return <c>false</c> without throwing.
|
||||
/// </summary>
|
||||
bool TryGetDefinition(string? nodeDefId, [NotNullWhen(true)] out ResourceNodeDefRow? definition);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve the fixed yield row for <paramref name="nodeDefId"/> (see
|
||||
/// <c>resource-yield-row.schema.json</c>). Unknown ids and <c>null</c> return <c>false</c> without throwing.
|
||||
/// </summary>
|
||||
bool TryGetYield(string? nodeDefId, [NotNullWhen(true)] out ResourceYieldRow? yield);
|
||||
|
||||
/// <summary>Every loaded node definition, ordered by <see cref="ResourceNodeDefRow.NodeDefId"/> (ordinal).</summary>
|
||||
IReadOnlyList<ResourceNodeDefRow> GetDefinitionsInIdOrder();
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ using System.Collections.ObjectModel;
|
|||
|
||||
namespace NeonSprawl.Server.Game.Gathering;
|
||||
|
||||
/// <summary>In-memory resource-node catalog loaded at startup (NEO-58). Prefer registry in NEO-59 for game lookups.</summary>
|
||||
/// <summary>In-memory resource-node catalog loaded at startup (NEO-58). Game callers should use <see cref="IResourceNodeDefinitionRegistry"/>.</summary>
|
||||
public sealed class ResourceNodeCatalog(
|
||||
string resourceNodesDirectory,
|
||||
IReadOnlyDictionary<string, ResourceNodeDefRow> nodesById,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ using NeonSprawl.Server.Game.Skills;
|
|||
|
||||
namespace NeonSprawl.Server.Game.Gathering;
|
||||
|
||||
/// <summary>DI registration for the fail-fast resource-node catalog (NEO-58).</summary>
|
||||
/// <summary>DI registration for the fail-fast resource-node catalog and registry (NEO-58, NEO-59).</summary>
|
||||
public static class ResourceNodeCatalogServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>Binds <see cref="ContentPathsOptions"/> and registers <see cref="ResourceNodeCatalog"/> as a singleton.</summary>
|
||||
/// <summary>
|
||||
/// Binds <see cref="ContentPathsOptions"/> and registers <see cref="ResourceNodeCatalog"/> and
|
||||
/// <see cref="IResourceNodeDefinitionRegistry"/> as singletons.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddResourceNodeCatalog(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<ContentPathsOptions>()
|
||||
|
|
@ -45,6 +48,9 @@ public static class ResourceNodeCatalogServiceCollectionExtensions
|
|||
logger);
|
||||
});
|
||||
|
||||
services.AddSingleton<IResourceNodeDefinitionRegistry>(sp =>
|
||||
new ResourceNodeDefinitionRegistry(sp.GetRequiredService<ResourceNodeCatalog>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Gathering;
|
||||
|
||||
/// <summary>Adapter over <see cref="ResourceNodeCatalog"/> (NEO-59).</summary>
|
||||
public sealed class ResourceNodeDefinitionRegistry(ResourceNodeCatalog catalog) : IResourceNodeDefinitionRegistry
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDefinition(string? nodeDefId, [NotNullWhen(true)] out ResourceNodeDefRow? definition)
|
||||
{
|
||||
if (nodeDefId is null)
|
||||
{
|
||||
definition = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (catalog.NodesById.TryGetValue(nodeDefId, out var row))
|
||||
{
|
||||
definition = row;
|
||||
return true;
|
||||
}
|
||||
|
||||
definition = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetYield(string? nodeDefId, [NotNullWhen(true)] out ResourceYieldRow? yield)
|
||||
{
|
||||
if (nodeDefId is null)
|
||||
{
|
||||
yield = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (catalog.YieldsByNodeDefId.TryGetValue(nodeDefId, out var row))
|
||||
{
|
||||
yield = row;
|
||||
return true;
|
||||
}
|
||||
|
||||
yield = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ResourceNodeDefRow> GetDefinitionsInIdOrder() =>
|
||||
catalog.NodesById
|
||||
.OrderBy(kv => kv.Key, StringComparer.Ordinal)
|
||||
.Select(kv => kv.Value)
|
||||
.ToList();
|
||||
}
|
||||
|
|
@ -62,7 +62,9 @@ On startup the host loads every **`*_resource_nodes.json`** and **`*_resource_yi
|
|||
|
||||
**Docker / CI:** include **`content/resource-nodes`**, **`content/items`**, and the two resource-node schemas in the mounted **`content/`** tree; set **`Content__ResourceNodesDirectory`** when layout differs.
|
||||
|
||||
On success, **Information** logs include the resolved resource-nodes directory path, distinct node count, yield row count, and catalog file counts. Injectable **`IResourceNodeDefinitionRegistry`** lands in NEO-59.
|
||||
On success, **Information** logs include the resolved resource-nodes directory path, distinct node count, yield row count, and catalog file counts.
|
||||
|
||||
**`IResourceNodeDefinitionRegistry`** (NEO-59) is the preferred lookup surface for gather engine, depletion, interact, and HTTP callers — resolve node defs and yield rows by **`nodeDefId`**, enumerate defs in id order. The catalog singleton remains for fail-fast startup only; do not inject **`ResourceNodeCatalog`** in new game code.
|
||||
|
||||
## Item definitions (NEO-53)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue