NEO-52: add IItemDefinitionRegistry with DI and lookup tests
Thin adapter over ItemDefinitionCatalog mirroring NEO-35 skill registry; registers singleton in AddItemDefinitionCatalog with TryGetDefinition, GetDefinitionsInIdOrder, and AAA unit + host DI tests.pull/87/head
parent
1e9f1e9a3a
commit
792b38d8a9
|
|
@ -54,7 +54,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
|
|||
| 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. **Still planned:** `GatherResult`, yields, inventory per [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Interaction/`, `Game/Skills/` |
|
||||
| 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). **Still planned:** `IItemDefinitionRegistry` (NEO-52+), inventory store, HTTP. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.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)). **Still planned:** inventory store, HTTP. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -39,10 +39,10 @@
|
|||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [ ] DI resolves `IItemDefinitionRegistry` at host startup (test via `InMemoryWebApplicationFactory`).
|
||||
- [ ] Unit tests (AAA): lookup for known prototype `itemId` (e.g. `scrap_metal_bulk`, `prototype_armor_shell`) with expected metadata.
|
||||
- [ ] Unit tests (AAA): unknown `itemId` returns false / absent without throwing.
|
||||
- [ ] `GetDefinitionsInIdOrder` returns all loaded rows ordered by `id` (ordinal).
|
||||
- [x] DI resolves `IItemDefinitionRegistry` at host startup (test via `InMemoryWebApplicationFactory`).
|
||||
- [x] Unit tests (AAA): lookup for known prototype `itemId` (e.g. `scrap_metal_bulk`, `prototype_armor_shell`) with expected metadata.
|
||||
- [x] Unit tests (AAA): unknown `itemId` returns false / absent without throwing.
|
||||
- [x] `GetDefinitionsInIdOrder` returns all loaded rows ordered by `id` (ordinal).
|
||||
|
||||
## Technical approach
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
using System.IO;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Tests;
|
||||
using Xunit;
|
||||
|
||||
namespace NeonSprawl.Server.Tests.Game.Items;
|
||||
|
||||
public class ItemDefinitionRegistryTests
|
||||
{
|
||||
private static ItemDefinitionRegistry CreateRegistryFromRows(IReadOnlyDictionary<string, ItemDefRow> byId)
|
||||
{
|
||||
var catalog = new ItemDefinitionCatalog("/tmp/catalog", byId, catalogJsonFileCount: 1);
|
||||
return new ItemDefinitionRegistry(catalog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetDefinition_ShouldReturnTrueAndExpectedMetadata_WhenIdExists()
|
||||
{
|
||||
// Arrange
|
||||
var rows = new Dictionary<string, ItemDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
["scrap_metal_bulk"] = new ItemDefRow(
|
||||
"scrap_metal_bulk",
|
||||
"Scrap Metal (Bulk)",
|
||||
"material",
|
||||
StackMax: 999,
|
||||
InventorySlotKind: "bag",
|
||||
Rarity: null,
|
||||
BindPolicy: null,
|
||||
DurabilityMax: null),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(rows);
|
||||
// Act
|
||||
var found = registry.TryGetDefinition("scrap_metal_bulk", out var def);
|
||||
// Assert
|
||||
Assert.True(found);
|
||||
Assert.NotNull(def);
|
||||
Assert.Equal("material", def.PrototypeRole);
|
||||
Assert.Equal("Scrap Metal (Bulk)", def.DisplayName);
|
||||
Assert.Equal(999, def.StackMax);
|
||||
Assert.Equal("bag", def.InventorySlotKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetDefinition_ShouldReturnFalse_WhenItemIdIsNull()
|
||||
{
|
||||
// Arrange
|
||||
var rows = new Dictionary<string, ItemDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
["scrap_metal_bulk"] = new ItemDefRow(
|
||||
"scrap_metal_bulk",
|
||||
"Scrap Metal (Bulk)",
|
||||
"material",
|
||||
StackMax: 999,
|
||||
InventorySlotKind: "bag",
|
||||
Rarity: null,
|
||||
BindPolicy: null,
|
||||
DurabilityMax: 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, ItemDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
["scrap_metal_bulk"] = new ItemDefRow(
|
||||
"scrap_metal_bulk",
|
||||
"Scrap Metal (Bulk)",
|
||||
"material",
|
||||
StackMax: 999,
|
||||
InventorySlotKind: "bag",
|
||||
Rarity: null,
|
||||
BindPolicy: null,
|
||||
DurabilityMax: null),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(rows);
|
||||
// Act
|
||||
var found = registry.TryGetDefinition("not_a_real_item", out var def);
|
||||
// Assert
|
||||
Assert.False(found);
|
||||
Assert.Null(def);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetDefinitionsInIdOrder_ShouldListAllRowsOrderedById_WhenMultipleItems()
|
||||
{
|
||||
// Arrange
|
||||
var rows = new Dictionary<string, ItemDefRow>(StringComparer.Ordinal)
|
||||
{
|
||||
["survey_drone_kit"] = new ItemDefRow(
|
||||
"survey_drone_kit",
|
||||
"Survey Drone Kit",
|
||||
"utility",
|
||||
StackMax: 1,
|
||||
InventorySlotKind: "bag",
|
||||
Rarity: null,
|
||||
BindPolicy: null,
|
||||
DurabilityMax: null),
|
||||
["scrap_metal_bulk"] = new ItemDefRow(
|
||||
"scrap_metal_bulk",
|
||||
"Scrap Metal (Bulk)",
|
||||
"material",
|
||||
StackMax: 999,
|
||||
InventorySlotKind: "bag",
|
||||
Rarity: null,
|
||||
BindPolicy: null,
|
||||
DurabilityMax: null),
|
||||
["prototype_armor_shell"] = new ItemDefRow(
|
||||
"prototype_armor_shell",
|
||||
"Prototype Armor Shell",
|
||||
"equip_stub",
|
||||
StackMax: 1,
|
||||
InventorySlotKind: "equipment",
|
||||
Rarity: null,
|
||||
BindPolicy: null,
|
||||
DurabilityMax: null),
|
||||
};
|
||||
var registry = CreateRegistryFromRows(rows);
|
||||
// Act
|
||||
var list = registry.GetDefinitionsInIdOrder();
|
||||
// Assert
|
||||
Assert.Equal(3, list.Count);
|
||||
Assert.Equal("prototype_armor_shell", list[0].Id);
|
||||
Assert.Equal("scrap_metal_bulk", list[1].Id);
|
||||
Assert.Equal("survey_drone_kit", list[2].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetDefinition_ShouldMatchLoaderCatalog_WhenUsingPrototypeFixture()
|
||||
{
|
||||
// Arrange
|
||||
var root = Directory.CreateTempSubdirectory("neon-sprawl-item-registry-loader-");
|
||||
try
|
||||
{
|
||||
var itemsDir = Path.Combine(root.FullName, "content", "items");
|
||||
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
|
||||
Directory.CreateDirectory(itemsDir);
|
||||
Directory.CreateDirectory(schemaDir);
|
||||
var schemaPath = Path.Combine(schemaDir, "item-def.schema.json");
|
||||
File.Copy(ItemCatalogTestPaths.DiscoverRepoItemDefSchemaPath(), schemaPath, overwrite: true);
|
||||
File.Copy(
|
||||
Path.Combine(ItemCatalogTestPaths.DiscoverRepoItemsDirectory(), "prototype_items.json"),
|
||||
Path.Combine(itemsDir, "prototype_items.json"),
|
||||
overwrite: true);
|
||||
var loaded = ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, NullLogger.Instance);
|
||||
var registry = new ItemDefinitionRegistry(loaded);
|
||||
// Act
|
||||
var ok = registry.TryGetDefinition("prototype_armor_shell", out var equipStub);
|
||||
// Assert
|
||||
Assert.True(ok);
|
||||
Assert.NotNull(equipStub);
|
||||
Assert.Equal("equip_stub", equipStub.PrototypeRole);
|
||||
Assert.Equal("equipment", equipStub.InventorySlotKind);
|
||||
Assert.Equal(1, equipStub.StackMax);
|
||||
}
|
||||
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<IItemDefinitionRegistry>();
|
||||
var found = registry.TryGetDefinition("scrap_metal_bulk", out var scrap);
|
||||
// Assert
|
||||
Assert.True(found);
|
||||
Assert.NotNull(scrap);
|
||||
Assert.Equal("material", scrap.PrototypeRole);
|
||||
Assert.Equal(999, scrap.StackMax);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only access to validated <see cref="ItemDefRow"/> entries loaded at startup (<see cref="ItemDefinitionCatalog"/>).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>E3.M3 (inventory / craft / gather):</b> callers granting or validating items should depend on this interface
|
||||
/// rather than <see cref="ItemDefinitionCatalog"/> so stack limits and slot kinds stay centralized.</para>
|
||||
/// <para><b>NEO-53:</b> HTTP/read-model projections should depend on this interface rather than reaching into the catalog.</para>
|
||||
/// </remarks>
|
||||
public interface IItemDefinitionRegistry
|
||||
{
|
||||
/// <summary>Attempts to resolve an item by stable <c>id</c> (see <c>item-def.schema.json</c>). Unknown ids and <c>null</c> return <c>false</c> without throwing.</summary>
|
||||
bool TryGetDefinition(string? itemId, [NotNullWhen(true)] out ItemDefRow? definition);
|
||||
|
||||
/// <summary>Every loaded definition, ordered by <see cref="ItemDefRow.Id"/> (ordinal).</summary>
|
||||
IReadOnlyList<ItemDefRow> GetDefinitionsInIdOrder();
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ namespace NeonSprawl.Server.Game.Items;
|
|||
/// <summary>DI registration for the fail-fast item catalog (NEO-51).</summary>
|
||||
public static class ItemCatalogServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>Binds <see cref="ContentPathsOptions"/> and registers <see cref="ItemDefinitionCatalog"/> as a singleton.</summary>
|
||||
/// <summary>Binds <see cref="ContentPathsOptions"/> and registers <see cref="ItemDefinitionCatalog"/> and <see cref="IItemDefinitionRegistry"/> as singletons.</summary>
|
||||
public static IServiceCollection AddItemDefinitionCatalog(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<ContentPathsOptions>()
|
||||
|
|
@ -30,6 +30,9 @@ public static class ItemCatalogServiceCollectionExtensions
|
|||
return ItemDefinitionCatalogLoader.Load(itemsDir, schemaPath, logger);
|
||||
});
|
||||
|
||||
services.AddSingleton<IItemDefinitionRegistry>(sp =>
|
||||
new ItemDefinitionRegistry(sp.GetRequiredService<ItemDefinitionCatalog>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using System.Collections.ObjectModel;
|
|||
|
||||
namespace NeonSprawl.Server.Game.Items;
|
||||
|
||||
/// <summary>In-memory item catalog loaded at startup (NEO-51). Game code should prefer injectable item registry for lookups (NEO-52).</summary>
|
||||
/// <summary>In-memory item catalog loaded at startup (NEO-51). Game code should prefer <see cref="IItemDefinitionRegistry"/> for lookups (NEO-52).</summary>
|
||||
public sealed class ItemDefinitionCatalog(
|
||||
string itemsDirectory,
|
||||
IReadOnlyDictionary<string, ItemDefRow> byId,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Items;
|
||||
|
||||
/// <summary>Adapter over <see cref="ItemDefinitionCatalog"/> (NEO-52).</summary>
|
||||
public sealed class ItemDefinitionRegistry(ItemDefinitionCatalog catalog) : IItemDefinitionRegistry
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool TryGetDefinition(string? itemId, [NotNullWhen(true)] out ItemDefRow? definition)
|
||||
{
|
||||
if (itemId is null)
|
||||
{
|
||||
definition = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (catalog.ById.TryGetValue(itemId, out var row))
|
||||
{
|
||||
definition = row;
|
||||
return true;
|
||||
}
|
||||
|
||||
definition = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ItemDefRow> GetDefinitionsInIdOrder()
|
||||
{
|
||||
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
|
||||
var list = new List<ItemDefRow>(ids.Length);
|
||||
foreach (var id in ids)
|
||||
{
|
||||
list.Add(catalog.ById[id]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ On startup the host loads every **`*_items.json`** under the items directory, va
|
|||
|
||||
**Docker / CI:** include **`content/items`** and **`content/schemas/item-def.schema.json`** in the mounted **`content/`** tree; set **`Content__ItemsDirectory`** when layout differs.
|
||||
|
||||
On success, **Information** logs include the resolved items directory path, distinct item count, and catalog file count. Game code should use **`IItemDefinitionRegistry`** for lookups after **NEO-52**; until then use **`ItemDefinitionCatalog.TryGetItem`**.
|
||||
On success, **Information** logs include the resolved items directory path, distinct item count, and catalog file count. Game code should use **`IItemDefinitionRegistry`** for lookups (NEO-52).
|
||||
|
||||
## Mastery catalog (`content/mastery`, NEO-46)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue