using NeonSprawl.Server.Game.Encounters; using NeonSprawl.Server.Game.Factions; using NeonSprawl.Server.Game.Items; using NeonSprawl.Server.Game.Skills; namespace NeonSprawl.Server.Game.Contracts; /// /// Issue-time economy and reward-bundle validation for contract templates (NEO-150). /// Mirrors startup catalog gates for defense in depth at . /// public static class ContractEconomyValidation { /// /// Validates band caps and cross-refs for one template against live registries. /// public static bool TryValidateTemplate( ContractTemplateRow template, IEncounterDefinitionRegistry encounterRegistry, IItemDefinitionRegistry itemRegistry, ISkillDefinitionRegistry skillRegistry, IFactionDefinitionRegistry factionRegistry, out string? reasonCode) { ArgumentNullException.ThrowIfNull(template); ArgumentNullException.ThrowIfNull(encounterRegistry); ArgumentNullException.ThrowIfNull(itemRegistry); ArgumentNullException.ThrowIfNull(skillRegistry); ArgumentNullException.ThrowIfNull(factionRegistry); reasonCode = null; var rowsById = new Dictionary(StringComparer.Ordinal) { [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) { reasonCode = ContractGeneratorReasonCodes.EconomyCapExceeded; return false; } var knownEncounterIds = encounterRegistry.GetDefinitionsInIdOrder() .Select(static d => d.Id) .ToHashSet(StringComparer.Ordinal); var knownFactionIds = factionRegistry.GetDefinitionsInIdOrder() .Select(static d => d.Id) .ToHashSet(StringComparer.Ordinal); var skillDefsById = skillRegistry.GetDefinitionsInIdOrder() .ToDictionary(static d => d.Id, StringComparer.Ordinal); if (PrototypeE7M4ContractCatalogRules.TryGetCrossRefError( rowsById, knownEncounterIds, knownFactionIds, skillDefsById) is not null) { reasonCode = ContractGeneratorReasonCodes.InvalidRewardBundle; return false; } return true; } }