NEO-67: add IRecipeDefinitionRegistry and DI adapter
Thin registry over RecipeDefinitionCatalog mirrors NEO-52/59: TryGetDefinition, GetDefinitionsInIdOrder, AAA unit/host tests, README and alignment doc updates.pull/101/head
parent
7188c5b558
commit
81b0e3fc4b
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
**Prep (NEO-42):** Server ships **`RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine`** + **`RefineSkillXpConstants`** under `server/NeonSprawl.Server/Game/Skills/` — delegates to **`SkillProgressionGrantOperations.TryApplyGrant`** with **`refine`** + **`activity`** (prototype **10** XP), same persistence and **`allowedXpSourceKinds`** rules as **`POST …/skill-progression`**. **No** `CraftRequest` / **`CraftResult`** route yet; when this module adds authoritative craft/refine success, call that helper once on success ([NEO-42 implementation plan](../../plans/NEO-42-implementation-plan.md); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42)).
|
**Prep (NEO-42):** Server ships **`RefineActivitySkillXpGrant.GrantOnSuccessfulCraftOrRefine`** + **`RefineSkillXpConstants`** under `server/NeonSprawl.Server/Game/Skills/` — delegates to **`SkillProgressionGrantOperations.TryApplyGrant`** with **`refine`** + **`activity`** (prototype **10** XP), same persistence and **`allowedXpSourceKinds`** rules as **`POST …/skill-progression`**. **No** `CraftRequest` / **`CraftResult`** route yet; when this module adds authoritative craft/refine success, call that helper once on success ([NEO-42 implementation plan](../../plans/NEO-42-implementation-plan.md); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42)).
|
||||||
|
|
||||||
**E3M2-01 (NEO-65):** Frozen eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65 plan](../../plans/NEO-65-implementation-plan.md)). **E3M2-02 (NEO-66):** fail-fast server load under `server/NeonSprawl.Server/Game/Crafting/` — `RecipeDefinitionCatalogLoader`, `RecipeDefinitionCatalog`, cross-check vs item + skill catalogs, Slice 3 gate; [NEO-66 plan](../../plans/NEO-66-implementation-plan.md); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66).
|
**E3M2-01 (NEO-65):** Frozen eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65 plan](../../plans/NEO-65-implementation-plan.md)). **E3M2-02 (NEO-66):** fail-fast server load under `server/NeonSprawl.Server/Game/Crafting/` — `RecipeDefinitionCatalogLoader`, `RecipeDefinitionCatalog`, cross-check vs item + skill catalogs, Slice 3 gate; [NEO-66 plan](../../plans/NEO-66-implementation-plan.md); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **E3M2-03 (NEO-67):** injectable **`IRecipeDefinitionRegistry`** + DI; [NEO-67 plan](../../plans/NEO-67-implementation-plan.md).
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
|
||||||
| 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 |
|
| 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 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. **Slice 5 client (planned):** gather feedback HUD [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `Game/Interaction/` |
|
| E3.M1 | Ready | **NEO-41 landed (prototype, superseded on interact by NEO-63):** inline XP on **`resource_node`** interact. **NEO-57–NEO-61** — content, registry, HTTP defs, depletion store (see prior alignment). **NEO-62 landed:** **`GatherOperations`** + **`GatherResult`**. **NEO-63 landed:** **`POST …/interact`** **`resource_node`** → gather engine; **four** **`PrototypeInteractableRegistry`** anchors; Bruno gather → inventory + depletion deny ([NEO-63](../../plans/NEO-63-implementation-plan.md), [`NEO-63` manual QA](../../manual-qa/NEO-63.md)); [server README — Gather via interact (NEO-63)](../../../server/README.md#gather-via-interact-neo-63). **NEO-64 landed:** comment-only **`resource_gathered`** / **`gather_node_depleted`** hook sites in **`GatherOperations.TryGather`** ([NEO-64](../../plans/NEO-64-implementation-plan.md), [`NEO-64` manual QA](../../manual-qa/NEO-64.md)); Epic 3 Slice 2 backlog **E3M1-01**–**E3M1-08** complete. **Slice 5 client (planned):** gather feedback HUD [NEO-73](https://linear.app/neon-sprawl/issue/NEO-73) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md) … [NEO-64](../../plans/NEO-64-implementation-plan.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Gathering/`, `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. **Slice 5 client (planned):** inventory HUD [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [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), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) |
|
| 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. **Slice 5 client (planned):** inventory HUD [NEO-72](https://linear.app/neon-sprawl/issue/NEO-72) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [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), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) |
|
||||||
| E3.M2 | In Progress | **NEO-65 landed:** frozen prototype eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65](../../plans/NEO-65-implementation-plan.md)). **NEO-66 landed:** fail-fast server load of `content/recipes/*_recipes.json` at startup — `server/NeonSprawl.Server/Game/Crafting/` ([NEO-66](../../plans/NEO-66-implementation-plan.md)); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** — craft/refine success should award **`refine`** XP via shared NEO-38 grant path; no craft route yet. **Slice 3 backlog in Linear:** [NEO-67](https://linear.app/neon-sprawl/issue/NEO-67) → [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71); [E3M2-prototype-backlog.md](../../plans/E3M2-prototype-backlog.md) — **E3M2-03**–**E3M2-07** open. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. **Slice 5 client (planned):** craft UI [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74); capstone playable loop [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-65](../../plans/NEO-65-implementation-plan.md), [NEO-66](../../plans/NEO-66-implementation-plan.md), [NEO-67](https://linear.app/neon-sprawl/issue/NEO-67) … [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75), [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M2](E3_M2_RefinementAndRecipeExecution.md) |
|
| E3.M2 | In Progress | **NEO-65 landed:** frozen prototype eight-recipe catalog in [`content/recipes/prototype_recipes.json`](../../../content/recipes/prototype_recipes.json); [`recipe-def.schema.json`](../../../content/schemas/recipe-def.schema.json) + [`recipe-io-row.schema.json`](../../../content/schemas/recipe-io-row.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) Slice 3 gates ([NEO-65](../../plans/NEO-65-implementation-plan.md)). **NEO-66 landed:** fail-fast server load of `content/recipes/*_recipes.json` at startup — `server/NeonSprawl.Server/Game/Crafting/` ([NEO-66](../../plans/NEO-66-implementation-plan.md)); [server README — Recipe catalog](../../../server/README.md#recipe-catalog-contentrecipes-neo-66). **NEO-67 landed:** injectable **`IRecipeDefinitionRegistry`** + lookup tests ([NEO-67](../../plans/NEO-67-implementation-plan.md)). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** — craft/refine success should award **`refine`** XP via shared NEO-38 grant path; no craft route yet. **Slice 3 backlog in Linear:** [NEO-68](https://linear.app/neon-sprawl/issue/NEO-68) → [NEO-71](https://linear.app/neon-sprawl/issue/NEO-71); [E3M2-prototype-backlog.md](../../plans/E3M2-prototype-backlog.md) — **E3M2-04**–**E3M2-07** open. Upstream: E3.M1 **Ready**, E3.M3 inventory landed. **Slice 5 client (planned):** craft UI [NEO-74](https://linear.app/neon-sprawl/issue/NEO-74); capstone playable loop [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75) — [E3S5 backlog](../../plans/E3S5-client-prototype-backlog.md). | [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-65](../../plans/NEO-65-implementation-plan.md), [NEO-66](../../plans/NEO-66-implementation-plan.md), [NEO-67](https://linear.app/neon-sprawl/issue/NEO-67) … [NEO-75](https://linear.app/neon-sprawl/issue/NEO-75), [E3M2-prototype-backlog](../../plans/E3M2-prototype-backlog.md), [E3S5-client-prototype-backlog](../../plans/E3S5-client-prototype-backlog.md), [E3_M2](E3_M2_RefinementAndRecipeExecution.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,9 @@ Working backlog for **Epic 3 — Slice 3** ([recipes and crafting pipeline](../d
|
||||||
|
|
||||||
**Acceptance criteria**
|
**Acceptance criteria**
|
||||||
|
|
||||||
- [ ] Host resolves registry from DI; unknown `recipeId` distinguishable from valid ids in tests.
|
- [x] Host resolves registry from DI; unknown `recipeId` distinguishable from valid ids in tests.
|
||||||
|
|
||||||
|
**Landed ([NEO-67](https://linear.app/neon-sprawl/issue/NEO-67)):** `IRecipeDefinitionRegistry` + `RecipeDefinitionRegistry` in `server/NeonSprawl.Server/Game/Crafting/`; plan [NEO-67-implementation-plan.md](NEO-67-implementation-plan.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,10 @@
|
||||||
|
|
||||||
## Acceptance criteria checklist
|
## Acceptance criteria checklist
|
||||||
|
|
||||||
- [ ] DI resolves `IRecipeDefinitionRegistry` at host startup (test via `InMemoryWebApplicationFactory`).
|
- [x] DI resolves `IRecipeDefinitionRegistry` at host startup (test via `InMemoryWebApplicationFactory`).
|
||||||
- [ ] Unit tests (AAA): lookup for known prototype `recipeId` (e.g. `refine_scrap_standard`, `make_field_stim_mk0`) with expected `recipeKind`, `requiredSkillId`, and input/output rows.
|
- [x] Unit tests (AAA): lookup for known prototype `recipeId` (e.g. `refine_scrap_standard`, `make_field_stim_mk0`) with expected `recipeKind`, `requiredSkillId`, and input/output rows.
|
||||||
- [ ] Unit tests (AAA): unknown `recipeId` returns false / absent without throwing.
|
- [x] Unit tests (AAA): unknown `recipeId` returns false / absent without throwing.
|
||||||
- [ ] `GetDefinitionsInIdOrder` returns all eight loaded rows ordered by `id` (ordinal).
|
- [x] `GetDefinitionsInIdOrder` returns all eight loaded rows ordered by `id` (ordinal).
|
||||||
|
|
||||||
## Technical approach
|
## Technical approach
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,254 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using NeonSprawl.Server.Game.Crafting;
|
||||||
|
using NeonSprawl.Server.Game.Items;
|
||||||
|
using NeonSprawl.Server.Tests;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server.Tests.Game.Crafting;
|
||||||
|
|
||||||
|
public class RecipeDefinitionRegistryTests
|
||||||
|
{
|
||||||
|
private static readonly FrozenSet<string> KnownItemIds = PrototypeSlice1ItemCatalogRules.ExpectedItemIds;
|
||||||
|
|
||||||
|
private static readonly FrozenSet<string> KnownSkillIds = FrozenSet.ToFrozenSet(
|
||||||
|
["salvage", "refine", "intrusion"],
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private static RecipeDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary<string, RecipeDefRow> byId)
|
||||||
|
{
|
||||||
|
var catalog = new RecipeDefinitionCatalog("/tmp/recipes", byId, catalogJsonFileCount: 1);
|
||||||
|
return new RecipeDefinitionRegistry(catalog);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenRefineRecipeExists()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["refine_scrap_standard"] = new RecipeDefRow(
|
||||||
|
"refine_scrap_standard",
|
||||||
|
"Refine Scrap (Standard)",
|
||||||
|
"process",
|
||||||
|
"refine",
|
||||||
|
[new RecipeIoRow("scrap_metal_bulk", 5)],
|
||||||
|
[new RecipeIoRow("refined_plate_stock", 1)],
|
||||||
|
RequiredSkillLevel: null,
|
||||||
|
StationTag: null),
|
||||||
|
};
|
||||||
|
var registry = CreateRegistryFromRows(rows);
|
||||||
|
// Act
|
||||||
|
var found = registry.TryGetDefinition("refine_scrap_standard", out var def);
|
||||||
|
// Assert
|
||||||
|
Assert.True(found);
|
||||||
|
Assert.NotNull(def);
|
||||||
|
Assert.Equal("process", def.RecipeKind);
|
||||||
|
Assert.Equal("refine", def.RequiredSkillId);
|
||||||
|
Assert.Equal("Refine Scrap (Standard)", def.DisplayName);
|
||||||
|
Assert.Single(def.Inputs);
|
||||||
|
Assert.Equal("scrap_metal_bulk", def.Inputs[0].ItemId);
|
||||||
|
Assert.Equal(5, def.Inputs[0].Quantity);
|
||||||
|
Assert.Single(def.Outputs);
|
||||||
|
Assert.Equal("refined_plate_stock", def.Outputs[0].ItemId);
|
||||||
|
Assert.Equal(1, def.Outputs[0].Quantity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenMakeRecipeExists()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["make_field_stim_mk0"] = new RecipeDefRow(
|
||||||
|
"make_field_stim_mk0",
|
||||||
|
"Make Field Stim Mk0",
|
||||||
|
"make",
|
||||||
|
"refine",
|
||||||
|
[
|
||||||
|
new RecipeIoRow("refined_plate_stock", 2),
|
||||||
|
new RecipeIoRow("scrap_metal_bulk", 1),
|
||||||
|
],
|
||||||
|
[new RecipeIoRow("field_stim_mk0", 1)],
|
||||||
|
RequiredSkillLevel: null,
|
||||||
|
StationTag: null),
|
||||||
|
};
|
||||||
|
var registry = CreateRegistryFromRows(rows);
|
||||||
|
// Act
|
||||||
|
var found = registry.TryGetDefinition("make_field_stim_mk0", out var def);
|
||||||
|
// Assert
|
||||||
|
Assert.True(found);
|
||||||
|
Assert.NotNull(def);
|
||||||
|
Assert.Equal("make", def.RecipeKind);
|
||||||
|
Assert.Equal(2, def.Inputs.Count);
|
||||||
|
Assert.Equal("refined_plate_stock", def.Inputs[0].ItemId);
|
||||||
|
Assert.Equal("scrap_metal_bulk", def.Inputs[1].ItemId);
|
||||||
|
Assert.Single(def.Outputs);
|
||||||
|
Assert.Equal("field_stim_mk0", def.Outputs[0].ItemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryGetDefinition_ShouldReturnFalse_WhenRecipeIdIsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["refine_scrap_standard"] = new RecipeDefRow(
|
||||||
|
"refine_scrap_standard",
|
||||||
|
"Refine Scrap (Standard)",
|
||||||
|
"process",
|
||||||
|
"refine",
|
||||||
|
[new RecipeIoRow("scrap_metal_bulk", 5)],
|
||||||
|
[new RecipeIoRow("refined_plate_stock", 1)],
|
||||||
|
RequiredSkillLevel: null,
|
||||||
|
StationTag: null),
|
||||||
|
};
|
||||||
|
var registry = CreateRegistryFromRows(rows);
|
||||||
|
// Act
|
||||||
|
var found = registry.TryGetDefinition(null, out var def);
|
||||||
|
// Assert
|
||||||
|
Assert.False(found);
|
||||||
|
Assert.Null(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryGetDefinition_ShouldReturnFalse_WhenIdUnknown()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["refine_scrap_standard"] = new RecipeDefRow(
|
||||||
|
"refine_scrap_standard",
|
||||||
|
"Refine Scrap (Standard)",
|
||||||
|
"process",
|
||||||
|
"refine",
|
||||||
|
[new RecipeIoRow("scrap_metal_bulk", 5)],
|
||||||
|
[new RecipeIoRow("refined_plate_stock", 1)],
|
||||||
|
RequiredSkillLevel: null,
|
||||||
|
StationTag: null),
|
||||||
|
};
|
||||||
|
var registry = CreateRegistryFromRows(rows);
|
||||||
|
// Act
|
||||||
|
var found = registry.TryGetDefinition("not_a_real_recipe", out var def);
|
||||||
|
// Assert
|
||||||
|
Assert.False(found);
|
||||||
|
Assert.Null(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDefinitionsInIdOrder_ShouldListAllRowsOrderedById_WhenMultipleRecipes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var rows = new Dictionary<string, RecipeDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["make_field_stim_mk0"] = new RecipeDefRow(
|
||||||
|
"make_field_stim_mk0",
|
||||||
|
"Make Field Stim Mk0",
|
||||||
|
"make",
|
||||||
|
"refine",
|
||||||
|
[new RecipeIoRow("refined_plate_stock", 2)],
|
||||||
|
[new RecipeIoRow("field_stim_mk0", 1)],
|
||||||
|
RequiredSkillLevel: null,
|
||||||
|
StationTag: null),
|
||||||
|
["refine_scrap_efficient"] = new RecipeDefRow(
|
||||||
|
"refine_scrap_efficient",
|
||||||
|
"Refine Scrap (Efficient)",
|
||||||
|
"process",
|
||||||
|
"refine",
|
||||||
|
[new RecipeIoRow("scrap_metal_bulk", 10)],
|
||||||
|
[new RecipeIoRow("refined_plate_stock", 2)],
|
||||||
|
RequiredSkillLevel: null,
|
||||||
|
StationTag: null),
|
||||||
|
["refine_scrap_standard"] = new RecipeDefRow(
|
||||||
|
"refine_scrap_standard",
|
||||||
|
"Refine Scrap (Standard)",
|
||||||
|
"process",
|
||||||
|
"refine",
|
||||||
|
[new RecipeIoRow("scrap_metal_bulk", 5)],
|
||||||
|
[new RecipeIoRow("refined_plate_stock", 1)],
|
||||||
|
RequiredSkillLevel: null,
|
||||||
|
StationTag: null),
|
||||||
|
};
|
||||||
|
var registry = CreateRegistryFromRows(rows);
|
||||||
|
// Act
|
||||||
|
var list = registry.GetDefinitionsInIdOrder();
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, list.Count);
|
||||||
|
Assert.Equal("make_field_stim_mk0", list[0].Id);
|
||||||
|
Assert.Equal("refine_scrap_efficient", list[1].Id);
|
||||||
|
Assert.Equal("refine_scrap_standard", list[2].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var root = Directory.CreateTempSubdirectory("neon-sprawl-recipe-registry-loader-");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var recipesDir = Path.Combine(root.FullName, "content", "recipes");
|
||||||
|
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
|
||||||
|
Directory.CreateDirectory(recipesDir);
|
||||||
|
Directory.CreateDirectory(schemaDir);
|
||||||
|
var recipeDefSchemaPath = Path.Combine(schemaDir, "recipe-def.schema.json");
|
||||||
|
var recipeIoRowSchemaPath = Path.Combine(schemaDir, "recipe-io-row.schema.json");
|
||||||
|
File.Copy(RecipeCatalogTestPaths.DiscoverRepoRecipeDefSchemaPath(), recipeDefSchemaPath, overwrite: true);
|
||||||
|
File.Copy(RecipeCatalogTestPaths.DiscoverRepoRecipeIoRowSchemaPath(), recipeIoRowSchemaPath, overwrite: true);
|
||||||
|
File.Copy(
|
||||||
|
Path.Combine(RecipeCatalogTestPaths.DiscoverRepoRecipesDirectory(), "prototype_recipes.json"),
|
||||||
|
Path.Combine(recipesDir, "prototype_recipes.json"),
|
||||||
|
overwrite: true);
|
||||||
|
var loaded = RecipeDefinitionCatalogLoader.Load(
|
||||||
|
recipesDir,
|
||||||
|
recipeDefSchemaPath,
|
||||||
|
recipeIoRowSchemaPath,
|
||||||
|
KnownItemIds,
|
||||||
|
KnownSkillIds,
|
||||||
|
NullLogger.Instance);
|
||||||
|
var registry = new RecipeDefinitionRegistry(loaded);
|
||||||
|
// Act
|
||||||
|
var ok = registry.TryGetDefinition("refine_scrap_standard", out var refine);
|
||||||
|
var list = registry.GetDefinitionsInIdOrder();
|
||||||
|
// Assert
|
||||||
|
Assert.True(ok);
|
||||||
|
Assert.NotNull(refine);
|
||||||
|
Assert.Equal("process", refine.RecipeKind);
|
||||||
|
Assert.Equal("refine", refine.RequiredSkillId);
|
||||||
|
Assert.Equal(8, list.Count);
|
||||||
|
Assert.Equal("make_armor_quick", list[0].Id);
|
||||||
|
Assert.Equal("refine_scrap_standard", list[^1].Id);
|
||||||
|
}
|
||||||
|
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<IRecipeDefinitionRegistry>();
|
||||||
|
var found = registry.TryGetDefinition("refine_scrap_standard", out var refine);
|
||||||
|
var list = registry.GetDefinitionsInIdOrder();
|
||||||
|
// Assert
|
||||||
|
Assert.True(found);
|
||||||
|
Assert.NotNull(refine);
|
||||||
|
Assert.Equal("process", refine.RecipeKind);
|
||||||
|
Assert.Equal(8, list.Count);
|
||||||
|
Assert.Contains(list, r => r.Id == "make_field_stim_mk0" && r.RecipeKind == "make");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server.Game.Crafting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only access to validated <see cref="RecipeDefRow"/> entries loaded at startup (<see cref="RecipeDefinitionCatalog"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><b>E3.M2 (craft / refine):</b> callers validating <c>CraftRequest</c> and resolving I/O rows should depend on this interface
|
||||||
|
/// rather than <see cref="RecipeDefinitionCatalog"/> so recipe metadata stays centralized.</para>
|
||||||
|
/// <para><b>NEO-68:</b> HTTP/read-model projections should depend on this interface rather than reaching into the catalog.</para>
|
||||||
|
/// </remarks>
|
||||||
|
public interface IRecipeDefinitionRegistry
|
||||||
|
{
|
||||||
|
/// <summary>Attempts to resolve a recipe by stable <c>id</c> (see <c>recipe-def.schema.json</c>). Unknown ids and <c>null</c> return <c>false</c> without throwing.</summary>
|
||||||
|
bool TryGetDefinition(string? recipeId, [NotNullWhen(true)] out RecipeDefRow? definition);
|
||||||
|
|
||||||
|
/// <summary>Every loaded definition, ordered by <see cref="RecipeDefRow.Id"/> (ordinal).</summary>
|
||||||
|
IReadOnlyList<RecipeDefRow> GetDefinitionsInIdOrder();
|
||||||
|
}
|
||||||
|
|
@ -6,10 +6,10 @@ using NeonSprawl.Server.Game.Skills;
|
||||||
|
|
||||||
namespace NeonSprawl.Server.Game.Crafting;
|
namespace NeonSprawl.Server.Game.Crafting;
|
||||||
|
|
||||||
/// <summary>DI registration for the fail-fast recipe catalog (NEO-66).</summary>
|
/// <summary>DI registration for the fail-fast recipe catalog (NEO-66) and recipe definition registry (NEO-67).</summary>
|
||||||
public static class RecipeCatalogServiceCollectionExtensions
|
public static class RecipeCatalogServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>Binds <see cref="ContentPathsOptions"/> and registers <see cref="RecipeDefinitionCatalog"/> as a singleton.</summary>
|
/// <summary>Binds <see cref="ContentPathsOptions"/> and registers <see cref="RecipeDefinitionCatalog"/> and <see cref="IRecipeDefinitionRegistry"/> as singletons.</summary>
|
||||||
public static IServiceCollection AddRecipeDefinitionCatalog(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddRecipeDefinitionCatalog(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddOptions<ContentPathsOptions>()
|
services.AddOptions<ContentPathsOptions>()
|
||||||
|
|
@ -48,6 +48,9 @@ public static class RecipeCatalogServiceCollectionExtensions
|
||||||
logger);
|
logger);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddSingleton<IRecipeDefinitionRegistry>(sp =>
|
||||||
|
new RecipeDefinitionRegistry(sp.GetRequiredService<RecipeDefinitionCatalog>()));
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace NeonSprawl.Server.Game.Crafting;
|
namespace NeonSprawl.Server.Game.Crafting;
|
||||||
|
|
||||||
/// <summary>In-memory recipe catalog loaded at startup (NEO-66). NEO-67 adds injectable recipe definition registry for game callers.</summary>
|
/// <summary>In-memory recipe catalog loaded at startup (NEO-66). Game callers should use <see cref="IRecipeDefinitionRegistry"/> (NEO-67).</summary>
|
||||||
public sealed class RecipeDefinitionCatalog(
|
public sealed class RecipeDefinitionCatalog(
|
||||||
string recipesDirectory,
|
string recipesDirectory,
|
||||||
IReadOnlyDictionary<string, RecipeDefRow> byId,
|
IReadOnlyDictionary<string, RecipeDefRow> byId,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server.Game.Crafting;
|
||||||
|
|
||||||
|
/// <summary>Adapter over <see cref="RecipeDefinitionCatalog"/> (NEO-67).</summary>
|
||||||
|
public sealed class RecipeDefinitionRegistry(RecipeDefinitionCatalog catalog) : IRecipeDefinitionRegistry
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetDefinition(string? recipeId, [NotNullWhen(true)] out RecipeDefRow? definition)
|
||||||
|
{
|
||||||
|
if (recipeId is null)
|
||||||
|
{
|
||||||
|
definition = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catalog.ById.TryGetValue(recipeId, out var row))
|
||||||
|
{
|
||||||
|
definition = row;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
definition = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<RecipeDefRow> GetDefinitionsInIdOrder()
|
||||||
|
{
|
||||||
|
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
|
||||||
|
var list = new List<RecipeDefRow>(ids.Length);
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
list.Add(catalog.ById[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -78,7 +78,7 @@ On startup the host loads every **`*_recipes.json`** under the recipes directory
|
||||||
|
|
||||||
**Docker / CI:** include **`content/recipes`**, **`content/items`**, **`content/skills`**, and the two recipe schemas in the mounted **`content/`** tree; set **`Content__RecipesDirectory`** when layout differs.
|
**Docker / CI:** include **`content/recipes`**, **`content/items`**, **`content/skills`**, and the two recipe schemas in the mounted **`content/`** tree; set **`Content__RecipesDirectory`** when layout differs.
|
||||||
|
|
||||||
On success, **Information** logs include the resolved recipes directory path, distinct recipe count, and catalog file count. NEO-67 adds **`IRecipeDefinitionRegistry`** for game callers; until then, **`RecipeDefinitionCatalog`** is the startup-loaded snapshot.
|
On success, **Information** logs include the resolved recipes directory path, distinct recipe count, and catalog file count. Game code should use **`IRecipeDefinitionRegistry`** for lookups (NEO-67). The catalog singleton remains for fail-fast startup only; do not inject **`RecipeDefinitionCatalog`** in new game code.
|
||||||
|
|
||||||
## Resource node definitions (NEO-60)
|
## Resource node definitions (NEO-60)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue