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; /// /// Server-authoritative contract issuance from template catalog + seed inputs (NEO-147). /// HTTP: E7M4-08 (NEO-151). Economy validation: E7M4-07 (NEO-150). Telemetry hook sites: E7M4-09 (NEO-152). /// public static class ContractGeneratorOperations { /// /// Issues an active contract instance when template selection and store gates pass. /// public static ContractIssueOperationResult TryIssue( string playerId, string? templateId, string seedBucket, int? zoneDifficultyBand, IContractTemplateRegistry templateRegistry, IEncounterDefinitionRegistry encounterRegistry, IItemDefinitionRegistry itemRegistry, ISkillDefinitionRegistry skillRegistry, IFactionDefinitionRegistry factionRegistry, IContractInstanceStore instanceStore, IFactionStandingStore standingStore, TimeProvider timeProvider) { var normalizedPlayerId = ContractInstanceIds.NormalizePlayerId(playerId); var normalizedSeedBucket = ContractInstanceIds.NormalizeSeedBucket(seedBucket); var band = zoneDifficultyBand ?? 1; if (normalizedPlayerId.Length == 0 || normalizedSeedBucket.Length == 0) { return Deny(ContractGeneratorReasonCodes.InvalidIds); } if (!instanceStore.CanWritePlayer(normalizedPlayerId)) { return Deny(ContractGeneratorReasonCodes.PlayerNotWritable); } if (instanceStore.TryGetActiveForPlayer(normalizedPlayerId, out var activeSnapshot)) { return Deny(ContractGeneratorReasonCodes.ActiveContractExists, activeSnapshot); } if (!TrySelectTemplate( templateId, band, normalizedPlayerId, templateRegistry, standingStore, out var template, out var denyReason)) { return Deny(denyReason!); } if (!ContractEconomyValidation.TryValidateTemplate( template!, encounterRegistry, itemRegistry, skillRegistry, factionRegistry, out var economyReason)) { // --- Telemetry hook site (NEO-152): future E9.M1 catalog event `reward_anomaly` --- // TODO(E9.M1): catalog emit — economy validation deny at issue (defense-in-depth; startup load should prevent most). // Planned payload fields: playerId, templateId, reasonCode (economy_cap_exceeded | invalid_reward_bundle). // No ingest or ILogger here (comments-only). return Deny(economyReason!); } var instanceId = ContractInstanceIds.MakeDeterministicInstanceId( normalizedPlayerId, template!.Id, normalizedSeedBucket); if (instanceId.Length == 0) { return Deny(ContractGeneratorReasonCodes.InvalidIds); } if (instanceStore.TryCreateActive( normalizedPlayerId, instanceId, template.Id, normalizedSeedBucket, timeProvider.GetUtcNow(), out var snapshot)) { // --- Telemetry hook site (NEO-152): future E9.M1 catalog event `contract_issued` --- // TODO(E9.M1): catalog emit — first-time active instance persist only (deny paths excluded). // Planned payload fields: playerId, contractInstanceId, templateId, seedBucket, encounterTemplateId, issuedAt. // No ingest or ILogger here (comments-only). return Success(snapshot, template.EncounterTemplateId); } return DenyCreateFailure(instanceStore, normalizedPlayerId, snapshot); } private static bool TrySelectTemplate( string? templateId, int band, string normalizedPlayerId, IContractTemplateRegistry templateRegistry, IFactionStandingStore standingStore, out ContractTemplateRow? template, out string? denyReason) { template = null; denyReason = null; var normalizedTemplateId = ContractInstanceIds.NormalizeTemplateId(templateId); if (normalizedTemplateId.Length > 0) { if (!templateRegistry.TryGetDefinition(normalizedTemplateId, out template)) { denyReason = ContractGeneratorReasonCodes.UnknownTemplate; return false; } if (!IsEligible(template, band, normalizedPlayerId, standingStore)) { denyReason = ContractGeneratorReasonCodes.NoEligibleTemplate; template = null; return false; } return true; } foreach (var candidate in templateRegistry.GetDefinitionsInIdOrder()) { if (!IsEligible(candidate, band, normalizedPlayerId, standingStore)) { continue; } template = candidate; return true; } denyReason = ContractGeneratorReasonCodes.NoEligibleTemplate; return false; } private static bool IsEligible( ContractTemplateRow template, int band, string normalizedPlayerId, IFactionStandingStore standingStore) { if (template.ZoneDifficultyBand != band) { return false; } var gate = FactionGateOperations.TryEvaluate( normalizedPlayerId, [template.MinFactionStanding], standingStore); return gate.Success; } private static ContractIssueOperationResult DenyCreateFailure( IContractInstanceStore instanceStore, string normalizedPlayerId, ContractInstanceState? attemptSnapshot) { if (instanceStore.TryGetActiveForPlayer(normalizedPlayerId, out var active)) { return Deny(ContractGeneratorReasonCodes.ActiveContractExists, active); } if (attemptSnapshot is { Status: ContractInstanceStatus.Active }) { return Deny(ContractGeneratorReasonCodes.ActiveContractExists, attemptSnapshot); } return Deny(ContractGeneratorReasonCodes.InvalidIds, attemptSnapshot); } private static ContractIssueOperationResult Success( ContractInstanceState snapshot, string encounterTemplateId) => new(true, null, snapshot, encounterTemplateId); private static ContractIssueOperationResult Deny( string reasonCode, ContractInstanceState? snapshot = null) => new(false, reasonCode, snapshot, null); }