NEO-150: Address code review findings
Extract ContractPrototypeTestFixtures, add skill XP over-cap test, null-guard template, validation-order comment, and NEO-151 handoff note.pull/192/head
parent
e3d8458a66
commit
70e3e9a17d
|
|
@ -54,7 +54,7 @@ Slice 4 AC — issued contracts validate against **prototype economy caps** and
|
||||||
- **`ContractEconomyValidation.TryValidateTemplate`** — delegates to `PrototypeE7M4ContractCatalogRules.TryGetBandCapGateError` + `TryGetCrossRefError` for one template row against live registries.
|
- **`ContractEconomyValidation.TryValidateTemplate`** — delegates to `PrototypeE7M4ContractCatalogRules.TryGetBandCapGateError` + `TryGetCrossRefError` for one template row against live registries.
|
||||||
- **`ContractGeneratorOperations.TryIssue`** — extended signature (encounter/item/skill/faction registries); economy validation after template selection, before persist.
|
- **`ContractGeneratorOperations.TryIssue`** — extended signature (encounter/item/skill/faction registries); economy validation after template selection, before persist.
|
||||||
- **`ContractGeneratorReasonCodes`:** `economy_cap_exceeded`, `invalid_reward_bundle`.
|
- **`ContractGeneratorReasonCodes`:** `economy_cap_exceeded`, `invalid_reward_bundle`.
|
||||||
- **Tests:** `ContractEconomyValidationTests` (3 unit), `ContractGeneratorOperationsTests` (+2 deny paths); all `TryIssue` call sites updated; **911** tests green.
|
- **Tests:** `ContractEconomyValidationTests` (4 unit), `ContractGeneratorOperationsTests` (+2 deny paths), shared `ContractPrototypeTestFixtures`; all `TryIssue` call sites updated; **912** tests green.
|
||||||
- **Docs:** `server/README.md`, `E7_M4_ContractMissionGenerator.md`, alignment register, module dependency register.
|
- **Docs:** `server/README.md`, `E7_M4_ContractMissionGenerator.md`, alignment register, module dependency register.
|
||||||
|
|
||||||
## Technical approach
|
## Technical approach
|
||||||
|
|
@ -165,6 +165,7 @@ Manual Godot QA: **none** (server policy; capstone NEO-154).
|
||||||
| `TryIssue` signature churn | Update all call sites in this story; NEO-151 HTTP wrapper inherits extended signature | `adopted` |
|
| `TryIssue` signature churn | Update all call sites in this story; NEO-151 HTTP wrapper inherits extended signature | `adopted` |
|
||||||
| Client deny copy for `invalid_reward_bundle` | Defer readable copy to NEO-153; server ships stable code now | `deferred` |
|
| Client deny copy for `invalid_reward_bundle` | Defer readable copy to NEO-153; server ships stable code now | `deferred` |
|
||||||
| Telemetry `reward_anomaly` at economy deny | Defer comment-only hooks to NEO-152 | `deferred` |
|
| Telemetry `reward_anomaly` at economy deny | Defer comment-only hooks to NEO-152 | `deferred` |
|
||||||
|
| **NEO-151 HTTP `TryIssue` wiring** | Production DI path must pass the same four registries already used in `ContractGeneratorOperationsIntegrationTests` (`IEncounterDefinitionRegistry`, `IItemDefinitionRegistry`, `ISkillDefinitionRegistry`, `IFactionDefinitionRegistry`) — verify at NEO-151 kickoff | `pending` |
|
||||||
|
|
||||||
## Client counterpart
|
## Client counterpart
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,16 +34,16 @@ None.
|
||||||
|
|
||||||
## Suggestions
|
## Suggestions
|
||||||
|
|
||||||
1. **Deduplicate prototype registry test fixtures** — `ContractEconomyValidationTests.CreatePrototypeRegistries()` and `ContractGeneratorOperationsTests.CreatePrototypeCatalogRegistries()` are nearly identical (~50 lines). Consider a shared internal test helper (e.g. `ContractPrototypeTestFixtures`) to avoid drift when prototype catalog ids change.
|
1. ~~**Deduplicate prototype registry test fixtures** — `ContractEconomyValidationTests.CreatePrototypeRegistries()` and `ContractGeneratorOperationsTests.CreatePrototypeCatalogRegistries()` are nearly identical (~50 lines). Consider a shared internal test helper (e.g. `ContractPrototypeTestFixtures`) to avoid drift when prototype catalog ids change.~~ **Done.** — `ContractPrototypeTestFixtures.CreateCatalogRegistries()`.
|
||||||
|
|
||||||
2. **Broaden deny-path test matrix (optional)** — Current tests exercise item qty over cap and unknown item id only. Band-cap and cross-ref helpers also cover skill XP, reputation grants, unknown encounter, and disallowed `mission_reward` skill sources (covered at loader level in `ContractTemplateCatalogLoaderTests`). One additional `[Theory]` or a skill-Xp-over-cap case in `ContractEconomyValidationTests` would document parity without much cost.
|
2. ~~**Broaden deny-path test matrix (optional)** — Current tests exercise item qty over cap and unknown item id only. Band-cap and cross-ref helpers also cover skill XP, reputation grants, unknown encounter, and disallowed `mission_reward` skill sources (covered at loader level in `ContractTemplateCatalogLoaderTests`). One additional `[Theory]` or a skill-Xp-over-cap case in `ContractEconomyValidationTests` would document parity without much cost.~~ **Done.** — `TryValidateTemplate_ShouldDenyEconomyCapExceeded_WhenSkillXpOverBandCap`.
|
||||||
|
|
||||||
3. **NEO-151 call-site checklist** — When HTTP lands, confirm the production DI path passes the same four registries already wired in integration tests. No code change needed here; worth a one-line note in NEO-151 plan.
|
3. ~~**NEO-151 call-site checklist** — When HTTP lands, confirm the production DI path passes the same four registries already wired in integration tests. No code change needed here; worth a one-line note in NEO-151 plan.~~ **Done.** — handoff row in [NEO-150 plan](../plans/NEO-150-implementation-plan.md) open questions (`pending` for NEO-151 kickoff).
|
||||||
|
|
||||||
## Nits
|
## Nits
|
||||||
|
|
||||||
- Nit: `ContractEconomyValidation.TryValidateTemplate` does not null-check `template`; safe today because `TrySelectTemplate` only returns success with a non-null template, but `ArgumentNullException.ThrowIfNull(template)` would mirror the registry guards.
|
- ~~Nit: `ContractEconomyValidation.TryValidateTemplate` does not null-check `template`; safe today because `TrySelectTemplate` only returns success with a non-null template, but `ArgumentNullException.ThrowIfNull(template)` would mirror the registry guards.~~ **Done.**
|
||||||
- Nit: Issue-time validation order is band-cap → cross-ref; startup loader runs cross-ref → band-cap. Intentional per plan (cap failures get `economy_cap_exceeded`), but worth keeping in mind if both gates ever fail on the same row during debugging.
|
- ~~Nit: Issue-time validation order is band-cap → cross-ref; startup loader runs cross-ref → band-cap. Intentional per plan (cap failures get `economy_cap_exceeded`), but worth keeping in mind if both gates ever fail on the same row during debugging.~~ **Done.** — inline comment in `ContractEconomyValidation`.
|
||||||
- Nit: Per-issue allocation of encounter/faction hash sets and skill dictionary is fine for prototype; revisit only if issue volume becomes hot-path.
|
- Nit: Per-issue allocation of encounter/faction hash sets and skill dictionary is fine for prototype; revisit only if issue volume becomes hot-path.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,17 @@
|
||||||
using NeonSprawl.Server.Game.Contracts;
|
using NeonSprawl.Server.Game.Contracts;
|
||||||
using NeonSprawl.Server.Game.Encounters;
|
using NeonSprawl.Server.Game.Encounters;
|
||||||
using NeonSprawl.Server.Game.Factions;
|
|
||||||
using NeonSprawl.Server.Game.Items;
|
|
||||||
using NeonSprawl.Server.Game.Quests;
|
using NeonSprawl.Server.Game.Quests;
|
||||||
using NeonSprawl.Server.Game.Skills;
|
|
||||||
|
|
||||||
namespace NeonSprawl.Server.Tests.Game.Contracts;
|
namespace NeonSprawl.Server.Tests.Game.Contracts;
|
||||||
|
|
||||||
public sealed class ContractEconomyValidationTests
|
public sealed class ContractEconomyValidationTests
|
||||||
{
|
{
|
||||||
private const string TemplateId = "prototype_contract_clear_combat_pocket";
|
|
||||||
private const string EncounterId = "prototype_combat_pocket";
|
|
||||||
private const string GridOperatorsFactionId = "prototype_faction_grid_operators";
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TryValidateTemplate_ShouldReturnTrue_WhenPrototypeFreezeRowAtCaps()
|
public void TryValidateTemplate_ShouldReturnTrue_WhenPrototypeFreezeRowAtCaps()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var template = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[TemplateId];
|
var template = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[ContractPrototypeTestFixtures.TemplateId];
|
||||||
var registries = CreatePrototypeRegistries();
|
var registries = ContractPrototypeTestFixtures.CreateCatalogRegistries();
|
||||||
// Act
|
// Act
|
||||||
var ok = ContractEconomyValidation.TryValidateTemplate(
|
var ok = ContractEconomyValidation.TryValidateTemplate(
|
||||||
template,
|
template,
|
||||||
|
|
@ -36,7 +29,7 @@ public sealed class ContractEconomyValidationTests
|
||||||
public void TryValidateTemplate_ShouldDenyEconomyCapExceeded_WhenItemQuantityOverBandCap()
|
public void TryValidateTemplate_ShouldDenyEconomyCapExceeded_WhenItemQuantityOverBandCap()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var prototype = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[TemplateId];
|
var prototype = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[ContractPrototypeTestFixtures.TemplateId];
|
||||||
var overCapTemplate = prototype with
|
var overCapTemplate = prototype with
|
||||||
{
|
{
|
||||||
CompletionRewardBundle = new QuestRewardBundleRow(
|
CompletionRewardBundle = new QuestRewardBundleRow(
|
||||||
|
|
@ -44,7 +37,33 @@ public sealed class ContractEconomyValidationTests
|
||||||
prototype.CompletionRewardBundle.SkillXpGrants,
|
prototype.CompletionRewardBundle.SkillXpGrants,
|
||||||
prototype.CompletionRewardBundle.ReputationGrants),
|
prototype.CompletionRewardBundle.ReputationGrants),
|
||||||
};
|
};
|
||||||
var registries = CreatePrototypeRegistries();
|
var registries = ContractPrototypeTestFixtures.CreateCatalogRegistries();
|
||||||
|
// Act
|
||||||
|
var ok = ContractEconomyValidation.TryValidateTemplate(
|
||||||
|
overCapTemplate,
|
||||||
|
registries.EncounterRegistry,
|
||||||
|
registries.ItemRegistry,
|
||||||
|
registries.SkillRegistry,
|
||||||
|
registries.FactionRegistry,
|
||||||
|
out var reasonCode);
|
||||||
|
// Assert
|
||||||
|
Assert.False(ok);
|
||||||
|
Assert.Equal(ContractGeneratorReasonCodes.EconomyCapExceeded, reasonCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryValidateTemplate_ShouldDenyEconomyCapExceeded_WhenSkillXpOverBandCap()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var prototype = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[ContractPrototypeTestFixtures.TemplateId];
|
||||||
|
var overCapTemplate = prototype with
|
||||||
|
{
|
||||||
|
CompletionRewardBundle = new QuestRewardBundleRow(
|
||||||
|
prototype.CompletionRewardBundle.ItemGrants,
|
||||||
|
[new QuestSkillXpGrantRow("salvage", 26)],
|
||||||
|
prototype.CompletionRewardBundle.ReputationGrants),
|
||||||
|
};
|
||||||
|
var registries = ContractPrototypeTestFixtures.CreateCatalogRegistries();
|
||||||
// Act
|
// Act
|
||||||
var ok = ContractEconomyValidation.TryValidateTemplate(
|
var ok = ContractEconomyValidation.TryValidateTemplate(
|
||||||
overCapTemplate,
|
overCapTemplate,
|
||||||
|
|
@ -62,7 +81,7 @@ public sealed class ContractEconomyValidationTests
|
||||||
public void TryValidateTemplate_ShouldDenyInvalidRewardBundle_WhenItemIdUnknown()
|
public void TryValidateTemplate_ShouldDenyInvalidRewardBundle_WhenItemIdUnknown()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var prototype = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[TemplateId];
|
var prototype = PrototypeE7M4ContractCatalogRules.ExpectedTemplateFreeze[ContractPrototypeTestFixtures.TemplateId];
|
||||||
var unknownItemTemplate = prototype with
|
var unknownItemTemplate = prototype with
|
||||||
{
|
{
|
||||||
CompletionRewardBundle = new QuestRewardBundleRow(
|
CompletionRewardBundle = new QuestRewardBundleRow(
|
||||||
|
|
@ -70,7 +89,7 @@ public sealed class ContractEconomyValidationTests
|
||||||
prototype.CompletionRewardBundle.SkillXpGrants,
|
prototype.CompletionRewardBundle.SkillXpGrants,
|
||||||
prototype.CompletionRewardBundle.ReputationGrants),
|
prototype.CompletionRewardBundle.ReputationGrants),
|
||||||
};
|
};
|
||||||
var registries = CreatePrototypeRegistries();
|
var registries = ContractPrototypeTestFixtures.CreateCatalogRegistries();
|
||||||
// Act
|
// Act
|
||||||
var ok = ContractEconomyValidation.TryValidateTemplate(
|
var ok = ContractEconomyValidation.TryValidateTemplate(
|
||||||
unknownItemTemplate,
|
unknownItemTemplate,
|
||||||
|
|
@ -83,58 +102,4 @@ public sealed class ContractEconomyValidationTests
|
||||||
Assert.False(ok);
|
Assert.False(ok);
|
||||||
Assert.Equal(ContractGeneratorReasonCodes.InvalidRewardBundle, reasonCode);
|
Assert.Equal(ContractGeneratorReasonCodes.InvalidRewardBundle, reasonCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CatalogRegistries CreatePrototypeRegistries()
|
|
||||||
{
|
|
||||||
var encounterRows = new Dictionary<string, EncounterDefRow>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
[EncounterId] = new(
|
|
||||||
EncounterId,
|
|
||||||
"Prototype Combat Pocket",
|
|
||||||
"defeat_all_targets",
|
|
||||||
["prototype_npc_melee", "prototype_npc_ranged", "prototype_npc_elite"],
|
|
||||||
"prototype_combat_pocket_clear"),
|
|
||||||
};
|
|
||||||
var encounterRegistry = new EncounterDefinitionRegistry(
|
|
||||||
new EncounterDefinitionCatalog("/tmp/encounters", encounterRows, catalogJsonFileCount: 1));
|
|
||||||
|
|
||||||
var itemRows = new Dictionary<string, ItemDefRow>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
["scrap_metal_bulk"] = new(
|
|
||||||
"scrap_metal_bulk",
|
|
||||||
"Scrap Metal (Bulk)",
|
|
||||||
"material",
|
|
||||||
StackMax: 999,
|
|
||||||
InventorySlotKind: "bag",
|
|
||||||
Rarity: null,
|
|
||||||
BindPolicy: null,
|
|
||||||
DurabilityMax: null),
|
|
||||||
};
|
|
||||||
var itemRegistry = new ItemDefinitionRegistry(
|
|
||||||
new ItemDefinitionCatalog("/tmp/items", itemRows, catalogJsonFileCount: 1));
|
|
||||||
|
|
||||||
var skillRows = new Dictionary<string, SkillDefRow>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
["salvage"] = new("salvage", "gather", "Salvage", ["activity", "mission_reward"]),
|
|
||||||
["refine"] = new("refine", "process", "Refine", ["activity", "mission_reward", "trainer"]),
|
|
||||||
["intrusion"] = new("intrusion", "tech", "Intrusion", ["activity", "mission_reward", "book_or_item"]),
|
|
||||||
};
|
|
||||||
var skillRegistry = new SkillDefinitionRegistry(
|
|
||||||
new SkillDefinitionCatalog("/tmp/skills", skillRows, catalogJsonFileCount: 1));
|
|
||||||
|
|
||||||
var factionRows = new Dictionary<string, FactionDefRow>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
[GridOperatorsFactionId] = PrototypeE7M3FactionCatalogRules.ExpectedFactionFreeze[GridOperatorsFactionId],
|
|
||||||
};
|
|
||||||
var factionRegistry = new FactionDefinitionRegistry(
|
|
||||||
new FactionDefinitionCatalog("/tmp/factions", factionRows, catalogJsonFileCount: 1));
|
|
||||||
|
|
||||||
return new CatalogRegistries(encounterRegistry, itemRegistry, skillRegistry, factionRegistry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record CatalogRegistries(
|
|
||||||
IEncounterDefinitionRegistry EncounterRegistry,
|
|
||||||
IItemDefinitionRegistry ItemRegistry,
|
|
||||||
ISkillDefinitionRegistry SkillRegistry,
|
|
||||||
IFactionDefinitionRegistry FactionRegistry);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@ public sealed class ContractGeneratorOperationsTests
|
||||||
var positionOptions = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId });
|
var positionOptions = Options.Create(new GamePositionOptions { DevPlayerId = PlayerId });
|
||||||
var instanceStore = new InMemoryContractInstanceStore(positionOptions);
|
var instanceStore = new InMemoryContractInstanceStore(positionOptions);
|
||||||
var standingStore = CreateStandingStore();
|
var standingStore = CreateStandingStore();
|
||||||
var catalogRegistries = CreatePrototypeCatalogRegistries();
|
var catalogRegistries = ContractPrototypeTestFixtures.CreateCatalogRegistries();
|
||||||
return new GeneratorTestDependencies(
|
return new GeneratorTestDependencies(
|
||||||
templateRegistry,
|
templateRegistry,
|
||||||
catalogRegistries.EncounterRegistry,
|
catalogRegistries.EncounterRegistry,
|
||||||
|
|
@ -348,54 +348,6 @@ public sealed class ContractGeneratorOperationsTests
|
||||||
new FakeTimeProvider(IssuedAt));
|
new FakeTimeProvider(IssuedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CatalogRegistries CreatePrototypeCatalogRegistries()
|
|
||||||
{
|
|
||||||
var encounterRows = new Dictionary<string, EncounterDefRow>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
[EncounterId] = new(
|
|
||||||
EncounterId,
|
|
||||||
"Prototype Combat Pocket",
|
|
||||||
"defeat_all_targets",
|
|
||||||
["prototype_npc_melee", "prototype_npc_ranged", "prototype_npc_elite"],
|
|
||||||
"prototype_combat_pocket_clear"),
|
|
||||||
};
|
|
||||||
var encounterRegistry = new EncounterDefinitionRegistry(
|
|
||||||
new EncounterDefinitionCatalog("/tmp/encounters", encounterRows, catalogJsonFileCount: 1));
|
|
||||||
|
|
||||||
var itemRows = new Dictionary<string, ItemDefRow>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
["scrap_metal_bulk"] = new(
|
|
||||||
"scrap_metal_bulk",
|
|
||||||
"Scrap Metal (Bulk)",
|
|
||||||
"material",
|
|
||||||
StackMax: 999,
|
|
||||||
InventorySlotKind: "bag",
|
|
||||||
Rarity: null,
|
|
||||||
BindPolicy: null,
|
|
||||||
DurabilityMax: null),
|
|
||||||
};
|
|
||||||
var itemRegistry = new ItemDefinitionRegistry(
|
|
||||||
new ItemDefinitionCatalog("/tmp/items", itemRows, catalogJsonFileCount: 1));
|
|
||||||
|
|
||||||
var skillRows = new Dictionary<string, SkillDefRow>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
["salvage"] = new("salvage", "gather", "Salvage", ["activity", "mission_reward"]),
|
|
||||||
["refine"] = new("refine", "process", "Refine", ["activity", "mission_reward", "trainer"]),
|
|
||||||
["intrusion"] = new("intrusion", "tech", "Intrusion", ["activity", "mission_reward", "book_or_item"]),
|
|
||||||
};
|
|
||||||
var skillRegistry = new SkillDefinitionRegistry(
|
|
||||||
new SkillDefinitionCatalog("/tmp/skills", skillRows, catalogJsonFileCount: 1));
|
|
||||||
|
|
||||||
var factionRows = new Dictionary<string, FactionDefRow>(StringComparer.Ordinal)
|
|
||||||
{
|
|
||||||
[GridOperatorsFactionId] = PrototypeE7M3FactionCatalogRules.ExpectedFactionFreeze[GridOperatorsFactionId],
|
|
||||||
};
|
|
||||||
var factionRegistry = new FactionDefinitionRegistry(
|
|
||||||
new FactionDefinitionCatalog("/tmp/factions", factionRows, catalogJsonFileCount: 1));
|
|
||||||
|
|
||||||
return new CatalogRegistries(encounterRegistry, itemRegistry, skillRegistry, factionRegistry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InMemoryFactionStandingStore CreateStandingStore()
|
private static InMemoryFactionStandingStore CreateStandingStore()
|
||||||
{
|
{
|
||||||
var byId = new Dictionary<string, FactionDefRow>(StringComparer.Ordinal)
|
var byId = new Dictionary<string, FactionDefRow>(StringComparer.Ordinal)
|
||||||
|
|
@ -423,12 +375,6 @@ public sealed class ContractGeneratorOperationsTests
|
||||||
IFactionStandingStore StandingStore,
|
IFactionStandingStore StandingStore,
|
||||||
TimeProvider TimeProvider);
|
TimeProvider TimeProvider);
|
||||||
|
|
||||||
private sealed record CatalogRegistries(
|
|
||||||
IEncounterDefinitionRegistry EncounterRegistry,
|
|
||||||
IItemDefinitionRegistry ItemRegistry,
|
|
||||||
ISkillDefinitionRegistry SkillRegistry,
|
|
||||||
IFactionDefinitionRegistry FactionRegistry);
|
|
||||||
|
|
||||||
/// <summary>Simulates store create failure without populating the out snapshot (Bugbot regression).</summary>
|
/// <summary>Simulates store create failure without populating the out snapshot (Bugbot regression).</summary>
|
||||||
private sealed class CreateActiveNullSnapshotStore(IContractInstanceStore inner) : IContractInstanceStore
|
private sealed class CreateActiveNullSnapshotStore(IContractInstanceStore inner) : IContractInstanceStore
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
using NeonSprawl.Server.Game.Encounters;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
|
using NeonSprawl.Server.Game.Items;
|
||||||
|
using NeonSprawl.Server.Game.Skills;
|
||||||
|
|
||||||
|
namespace NeonSprawl.Server.Tests.Game.Contracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared in-memory prototype definition registries for contract generator and economy tests (NEO-150).
|
||||||
|
/// Keep ids aligned with <see cref="Game.Contracts.PrototypeE7M4ContractCatalogRules"/> freeze table.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ContractPrototypeTestFixtures
|
||||||
|
{
|
||||||
|
public const string TemplateId = "prototype_contract_clear_combat_pocket";
|
||||||
|
public const string EncounterId = "prototype_combat_pocket";
|
||||||
|
public const string GridOperatorsFactionId = "prototype_faction_grid_operators";
|
||||||
|
|
||||||
|
public static ContractPrototypeCatalogRegistries CreateCatalogRegistries()
|
||||||
|
{
|
||||||
|
var encounterRows = new Dictionary<string, EncounterDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
[EncounterId] = new(
|
||||||
|
EncounterId,
|
||||||
|
"Prototype Combat Pocket",
|
||||||
|
"defeat_all_targets",
|
||||||
|
["prototype_npc_melee", "prototype_npc_ranged", "prototype_npc_elite"],
|
||||||
|
"prototype_combat_pocket_clear"),
|
||||||
|
};
|
||||||
|
var encounterRegistry = new EncounterDefinitionRegistry(
|
||||||
|
new EncounterDefinitionCatalog("/tmp/encounters", encounterRows, catalogJsonFileCount: 1));
|
||||||
|
|
||||||
|
var itemRows = new Dictionary<string, ItemDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["scrap_metal_bulk"] = new(
|
||||||
|
"scrap_metal_bulk",
|
||||||
|
"Scrap Metal (Bulk)",
|
||||||
|
"material",
|
||||||
|
StackMax: 999,
|
||||||
|
InventorySlotKind: "bag",
|
||||||
|
Rarity: null,
|
||||||
|
BindPolicy: null,
|
||||||
|
DurabilityMax: null),
|
||||||
|
};
|
||||||
|
var itemRegistry = new ItemDefinitionRegistry(
|
||||||
|
new ItemDefinitionCatalog("/tmp/items", itemRows, catalogJsonFileCount: 1));
|
||||||
|
|
||||||
|
var skillRows = new Dictionary<string, SkillDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["salvage"] = new("salvage", "gather", "Salvage", ["activity", "mission_reward"]),
|
||||||
|
["refine"] = new("refine", "process", "Refine", ["activity", "mission_reward", "trainer"]),
|
||||||
|
["intrusion"] = new("intrusion", "tech", "Intrusion", ["activity", "mission_reward", "book_or_item"]),
|
||||||
|
};
|
||||||
|
var skillRegistry = new SkillDefinitionRegistry(
|
||||||
|
new SkillDefinitionCatalog("/tmp/skills", skillRows, catalogJsonFileCount: 1));
|
||||||
|
|
||||||
|
var factionRows = new Dictionary<string, FactionDefRow>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
[GridOperatorsFactionId] = PrototypeE7M3FactionCatalogRules.ExpectedFactionFreeze[GridOperatorsFactionId],
|
||||||
|
};
|
||||||
|
var factionRegistry = new FactionDefinitionRegistry(
|
||||||
|
new FactionDefinitionCatalog("/tmp/factions", factionRows, catalogJsonFileCount: 1));
|
||||||
|
|
||||||
|
return new ContractPrototypeCatalogRegistries(
|
||||||
|
encounterRegistry,
|
||||||
|
itemRegistry,
|
||||||
|
skillRegistry,
|
||||||
|
factionRegistry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record ContractPrototypeCatalogRegistries(
|
||||||
|
IEncounterDefinitionRegistry EncounterRegistry,
|
||||||
|
IItemDefinitionRegistry ItemRegistry,
|
||||||
|
ISkillDefinitionRegistry SkillRegistry,
|
||||||
|
IFactionDefinitionRegistry FactionRegistry);
|
||||||
|
|
@ -22,6 +22,7 @@ public static class ContractEconomyValidation
|
||||||
IFactionDefinitionRegistry factionRegistry,
|
IFactionDefinitionRegistry factionRegistry,
|
||||||
out string? reasonCode)
|
out string? reasonCode)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(template);
|
||||||
ArgumentNullException.ThrowIfNull(encounterRegistry);
|
ArgumentNullException.ThrowIfNull(encounterRegistry);
|
||||||
ArgumentNullException.ThrowIfNull(itemRegistry);
|
ArgumentNullException.ThrowIfNull(itemRegistry);
|
||||||
ArgumentNullException.ThrowIfNull(skillRegistry);
|
ArgumentNullException.ThrowIfNull(skillRegistry);
|
||||||
|
|
@ -33,6 +34,7 @@ public static class ContractEconomyValidation
|
||||||
[template.Id] = template,
|
[template.Id] = template,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Band-cap before cross-ref so deny maps to economy_cap_exceeded (startup loader runs cross-ref first).
|
||||||
if (PrototypeE7M4ContractCatalogRules.TryGetBandCapGateError(rowsById) is not null)
|
if (PrototypeE7M4ContractCatalogRules.TryGetBandCapGateError(rowsById) is not null)
|
||||||
{
|
{
|
||||||
reasonCode = ContractGeneratorReasonCodes.EconomyCapExceeded;
|
reasonCode = ContractGeneratorReasonCodes.EconomyCapExceeded;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue