using FinalFactory.Rendezvous.Contracts; namespace FinalFactory.Rendezvous.Server.Provisioning; internal sealed class GamePolicyRegistry { private readonly Dictionary<(GameId Game, EnvironmentId Environment), GamePolicy> _policies; private GamePolicyRegistry( Dictionary<(GameId Game, EnvironmentId Environment), GamePolicy> policies) => _policies = policies; public bool HasEnabledPolicies => _policies.Values.Any(static policy => policy.Enabled); public IEnumerable EnabledPolicies => _policies.Values.Where(static policy => policy.Enabled); public static GamePolicyRegistry Create(IEnumerable options) { GamePolicyOptions[] configuredPolicies = options.ToArray(); if (configuredPolicies.Length > ProvisioningLimits.MaxGamePolicies) { throw new ProvisioningConfigurationException( $"At most {ProvisioningLimits.MaxGamePolicies} game policies may be configured."); } Dictionary<(GameId Game, EnvironmentId Environment), GamePolicy> policies = []; foreach (GamePolicyOptions policyOptions in configuredPolicies) { Validate(policyOptions); GamePolicy policy = new(policyOptions); if (!policies.TryAdd((policy.GameId, policy.EnvironmentId), policy)) { throw new ProvisioningConfigurationException( $"Duplicate game/environment policy: {policy.GameId}/{policy.EnvironmentId}."); } } return new GamePolicyRegistry(policies); } public bool TryGet( GameId gameId, EnvironmentId environmentId, out GamePolicy? policy) { if (_policies.TryGetValue((gameId, environmentId), out GamePolicy? candidate) && candidate.Enabled) { policy = candidate; return true; } policy = null; return false; } private static void Validate(GamePolicyOptions options) { if (!GameId.TryParse(options.GameId, out _) || !EnvironmentId.TryParse(options.EnvironmentId, out _)) { throw new ProvisioningConfigurationException( "Game policies require valid game and environment IDs."); } if (options.ProtocolVersions.Count is 0 or > ProvisioningLimits.MaxProtocolVersionsPerPolicy || options.ProtocolVersions.Contains(0) || options.ProtocolVersions.Count != options.ProtocolVersions.Distinct().Count()) { throw new ProvisioningConfigurationException( $"Policy {options.GameId}/{options.EnvironmentId} requires unique non-zero protocol versions."); } if (options.Regions.Count is 0 or > ProvisioningLimits.MaxRegionsPerPolicy || options.Regions.Any(static region => !RegionId.TryParse(region, out _)) || options.Regions.Count != options.Regions.Distinct(StringComparer.Ordinal).Count()) { throw new ProvisioningConfigurationException( $"Policy {options.GameId}/{options.EnvironmentId} requires unique valid regions."); } if (options.VisibilityModes.Count == 0 || options.VisibilityModes.Any(static mode => !Enum.IsDefined(mode)) || options.VisibilityModes.Count != options.VisibilityModes.Distinct().Count() || options.PublisherTrustModes.Count == 0 || options.PublisherTrustModes.Any(static mode => !Enum.IsDefined(mode)) || options.PublisherTrustModes.Count != options.PublisherTrustModes.Distinct().Count()) { throw new ProvisioningConfigurationException( $"Policy {options.GameId}/{options.EnvironmentId} requires valid visibility and trust modes."); } if (options.MetadataMaxBytes is < 2 or > ContractLimits.MetadataMaxBytes || options.MetadataMaxKeys is < 0 or > ContractLimits.MetadataMaxKeys || options.MetadataValueMaxBytes.Count > options.MetadataMaxKeys || options.MetadataValueMaxBytes.Any(static item => string.IsNullOrWhiteSpace(item.Key) || !ContractValidation.IsUtf8LengthWithin(item.Key, ContractLimits.MetadataKeyMaxBytes) || item.Value is < 0 or > ContractLimits.MetadataValueMaxBytes) || options.RequiredMetadataKeys.Any(key => !options.MetadataValueMaxBytes.ContainsKey(key))) { throw new ProvisioningConfigurationException( $"Policy {options.GameId}/{options.EnvironmentId} has an invalid metadata schema."); } if (options.MaxListingsPerPrincipal is < 1 or > ProvisioningLimits.MaxListingsPerPrincipal || options.MaxAnonymousListingsPerAddress < 0 || options.MaxAnonymousListingsPerAddress > options.MaxListingsPerPrincipal || options.MaxActiveJoinAttempts is < 1 or > ProvisioningLimits.MaxActiveJoinAttemptsPerPolicy || !Enum.IsDefined(options.FallbackPolicy)) { throw new ProvisioningConfigurationException( $"Policy {options.GameId}/{options.EnvironmentId} has invalid quotas or fallback policy."); } } }