277 lines
11 KiB
C#
277 lines
11 KiB
C#
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NeonSprawl.Server.Game.Encounters;
|
|
using Xunit;
|
|
|
|
namespace NeonSprawl.Server.Tests.Game.Encounters;
|
|
|
|
public class RewardTableDefinitionCatalogLoaderTests
|
|
{
|
|
private const string ValidPrototypeCatalogJson =
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"rewardTables": [
|
|
{
|
|
"id": "prototype_combat_pocket_clear",
|
|
"displayName": "Prototype Combat Pocket Clear",
|
|
"fixedGrants": [
|
|
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
|
|
{ "itemId": "contract_handoff_token", "quantity": 1 }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
|
|
private static (string Root, string RewardTablesDir, string TableSchemaPath, string GrantRowSchemaPath) CreateTempContentLayout()
|
|
{
|
|
var root = Directory.CreateTempSubdirectory("neon-sprawl-rewardtablecat-");
|
|
var rewardTablesDir = Path.Combine(root.FullName, "content", "reward-tables");
|
|
var schemaDir = Path.Combine(root.FullName, "content", "schemas");
|
|
Directory.CreateDirectory(rewardTablesDir);
|
|
Directory.CreateDirectory(schemaDir);
|
|
var tableSchemaPath = Path.Combine(schemaDir, "reward-table.schema.json");
|
|
var grantRowSchemaPath = Path.Combine(schemaDir, "reward-grant-row.schema.json");
|
|
File.Copy(EncounterCatalogTestPaths.DiscoverRepoRewardTableDefSchemaPath(), tableSchemaPath, overwrite: true);
|
|
File.Copy(EncounterCatalogTestPaths.DiscoverRepoRewardGrantRowSchemaPath(), grantRowSchemaPath, overwrite: true);
|
|
return (root.FullName, rewardTablesDir, tableSchemaPath, grantRowSchemaPath);
|
|
}
|
|
|
|
private static void WriteCatalog(string rewardTablesDir, string catalogJson) =>
|
|
File.WriteAllText(Path.Combine(rewardTablesDir, "prototype_reward_tables.json"), catalogJson, Encoding.UTF8);
|
|
|
|
private static RewardTableDefinitionCatalog LoadCatalog(
|
|
string rewardTablesDir,
|
|
string tableSchemaPath,
|
|
string grantRowSchemaPath,
|
|
IReadOnlySet<string>? knownItemIds = null) =>
|
|
RewardTableDefinitionCatalogLoader.Load(
|
|
rewardTablesDir,
|
|
tableSchemaPath,
|
|
grantRowSchemaPath,
|
|
knownItemIds ?? EncounterCatalogTestPaths.Slice1KnownItemIds,
|
|
NullLogger.Instance);
|
|
|
|
[Fact]
|
|
public void Load_ShouldSucceed_WhenCatalogMatchesPrototypeContract()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
|
|
WriteCatalog(rewardTablesDir, ValidPrototypeCatalogJson);
|
|
// Act
|
|
var catalog = LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath);
|
|
// Assert
|
|
Assert.Equal(1, catalog.DistinctRewardTableCount);
|
|
Assert.Equal(1, catalog.CatalogJsonFileCount);
|
|
Assert.True(catalog.TryGetRewardTable("prototype_combat_pocket_clear", out var table));
|
|
Assert.NotNull(table);
|
|
Assert.Equal("Prototype Combat Pocket Clear", table!.DisplayName);
|
|
Assert.Equal(2, table.FixedGrants.Count);
|
|
Assert.Equal("scrap_metal_bulk", table.FixedGrants[0].ItemId);
|
|
Assert.Equal(10, table.FixedGrants[0].Quantity);
|
|
Assert.Equal("contract_handoff_token", table.FixedGrants[1].ItemId);
|
|
Assert.Equal(1, table.FixedGrants[1].Quantity);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenRewardTablesIsNotArray()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
|
|
File.WriteAllText(
|
|
Path.Combine(rewardTablesDir, "bad_reward_tables.json"),
|
|
"""{"schemaVersion": 1, "rewardTables": "nope"}""",
|
|
Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("bad_reward_tables.json", ioe.Message, StringComparison.Ordinal);
|
|
Assert.Contains("expected top-level 'rewardTables' array", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenSchemaVersionIsNotOne()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
|
|
File.WriteAllText(
|
|
Path.Combine(rewardTablesDir, "bad_reward_tables.json"),
|
|
"""{"schemaVersion": 2, "rewardTables": []}""",
|
|
Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("expected schemaVersion 1", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenDuplicateRewardTableIdAcrossFiles()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
|
|
WriteCatalog(rewardTablesDir, ValidPrototypeCatalogJson);
|
|
File.WriteAllText(
|
|
Path.Combine(rewardTablesDir, "extra_reward_tables.json"),
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"rewardTables": [
|
|
{
|
|
"id": "prototype_combat_pocket_clear",
|
|
"displayName": "Duplicate Table",
|
|
"fixedGrants": [
|
|
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
|
|
{ "itemId": "contract_handoff_token", "quantity": 1 }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""",
|
|
Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("duplicate reward table id 'prototype_combat_pocket_clear'", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenE5M3AllowlistMismatch()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
|
|
File.WriteAllText(
|
|
Path.Combine(rewardTablesDir, "prototype_reward_tables.json"),
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"rewardTables": [
|
|
{
|
|
"id": "wrong_reward_table_id",
|
|
"displayName": "Wrong Table",
|
|
"fixedGrants": [
|
|
{ "itemId": "scrap_metal_bulk", "quantity": 10 },
|
|
{ "itemId": "contract_handoff_token", "quantity": 1 }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""",
|
|
Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("prototype E5M3 expects exactly reward table ids", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenUnknownItemIdInFixedGrants()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
|
|
File.WriteAllText(
|
|
Path.Combine(rewardTablesDir, "prototype_reward_tables.json"),
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"rewardTables": [
|
|
{
|
|
"id": "prototype_combat_pocket_clear",
|
|
"displayName": "Prototype Combat Pocket Clear",
|
|
"fixedGrants": [
|
|
{ "itemId": "unknown_item_id", "quantity": 1 }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""",
|
|
Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("itemId 'unknown_item_id' is not in item catalogs", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenDuplicateItemIdInFixedGrants()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
|
|
File.WriteAllText(
|
|
Path.Combine(rewardTablesDir, "prototype_reward_tables.json"),
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"rewardTables": [
|
|
{
|
|
"id": "prototype_combat_pocket_clear",
|
|
"displayName": "Prototype Combat Pocket Clear",
|
|
"fixedGrants": [
|
|
{ "itemId": "scrap_metal_bulk", "quantity": 5 },
|
|
{ "itemId": "scrap_metal_bulk", "quantity": 5 }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""",
|
|
Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("duplicate itemId 'scrap_metal_bulk'", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenFrozenGrantQuantitiesMismatch()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, tableSchemaPath, grantRowSchemaPath) = CreateTempContentLayout();
|
|
File.WriteAllText(
|
|
Path.Combine(rewardTablesDir, "prototype_reward_tables.json"),
|
|
"""
|
|
{
|
|
"schemaVersion": 1,
|
|
"rewardTables": [
|
|
{
|
|
"id": "prototype_combat_pocket_clear",
|
|
"displayName": "Prototype Combat Pocket Clear",
|
|
"fixedGrants": [
|
|
{ "itemId": "scrap_metal_bulk", "quantity": 9 },
|
|
{ "itemId": "contract_handoff_token", "quantity": 1 }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""",
|
|
Encoding.UTF8);
|
|
// Act
|
|
var ex = Record.Exception(() => LoadCatalog(rewardTablesDir, tableSchemaPath, grantRowSchemaPath));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("prototype_combat_pocket_clear fixedGrants must match", ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Load_ShouldThrow_WhenSchemaFileMissing()
|
|
{
|
|
// Arrange
|
|
var (_, rewardTablesDir, _, grantRowSchemaPath) = CreateTempContentLayout();
|
|
WriteCatalog(rewardTablesDir, ValidPrototypeCatalogJson);
|
|
var missingSchema = Path.Combine(rewardTablesDir, "missing-reward-table.schema.json");
|
|
// Act
|
|
var ex = Record.Exception(() =>
|
|
LoadCatalog(rewardTablesDir, missingSchema, grantRowSchemaPath));
|
|
// Assert
|
|
var ioe = Assert.IsType<InvalidOperationException>(ex);
|
|
Assert.Contains("missing schema file", ioe.Message, StringComparison.Ordinal);
|
|
Assert.Contains(missingSchema, ioe.Message, StringComparison.Ordinal);
|
|
}
|
|
}
|