NEO-59: add IResourceNodeDefinitionRegistry and DI adapter
Thin registry over ResourceNodeCatalog with node/yield lookup, enumeration, and AAA unit + host DI tests mirroring NEO-52.pull/94/head
parent
4921f2c25b
commit
bd43951f80
|
|
@ -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,58 @@
|
|||
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()
|
||||
{
|
||||
var ids = catalog.NodesById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
|
||||
var list = new List<ResourceNodeDefRow>(ids.Length);
|
||||
foreach (var id in ids)
|
||||
{
|
||||
list.Add(catalog.NodesById[id]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue