using NeonSprawl.Server.Game.Factions; namespace NeonSprawl.Server.Game.Contracts; /// /// Server-authoritative contract issuance from template catalog + seed inputs (NEO-147). /// HTTP: E7M4-08 (NEO-151). 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, 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!); } 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)) { 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.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); }