using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using GameCult.Caching; using GameCult.Networking; using R3; namespace GameCult.Mesh { /// /// Options for creating a gameplay-facing CultMesh session facade. /// public sealed class CultMeshGameSessionOptions { /// /// Gets or sets the Verse catalog used by the session. /// public CultMeshVerseCatalog? VerseCatalog { get; set; } /// /// Gets or sets the peer catalog used by the session. /// public CultMeshPeerCatalog? PeerCatalog { get; set; } /// /// Gets or sets the authority lease catalog used by the session. /// public CultMeshAuthorityLeaseCatalog? AuthorityLeases { get; set; } /// /// Gets or sets the sole authority resolver used by the session. When omitted, the /// session installs a deny-by-default resolver over . /// public CultMeshAuthorityResolver? AuthorityResolver { get; set; } /// /// Gets or sets simulation consensus options. /// public CultNetSimulationConsensusOptions? ConsensusOptions { get; set; } /// /// Gets or sets whether incoming observation messages should update the session hub. /// public bool ServeSimulationObservations { get; set; } = true; /// /// Gets or sets whether the node should answer Verse catalog requests from the session catalog. /// public bool ServeVerseDiscovery { get; set; } = true; /// /// Gets or sets whether the node should answer peer exchange requests from the session peer catalog. /// public bool ServePeerExchange { get; set; } = true; } /// /// Gameplay-facing facade over common CultMesh runtime pieces. /// public sealed class CultMeshGameSession : IDisposable { private readonly List _owned = new(); private readonly HashSet _committedFactIds = new(StringComparer.Ordinal); private bool _disposed; /// /// Creates a game session facade. /// public CultMeshGameSession(CultMeshNode node, CultMeshGameSessionOptions? options = null) { Node = node ?? throw new ArgumentNullException(nameof(node)); options ??= new CultMeshGameSessionOptions(); VerseCatalog = options.VerseCatalog ?? new CultMeshVerseCatalog(); PeerCatalog = options.PeerCatalog ?? new CultMeshPeerCatalog(); AuthorityLeases = options.AuthorityLeases ?? new CultMeshAuthorityLeaseCatalog(); AuthorityResolver = options.AuthorityResolver ?? CultMeshAuthorityResolver.CreateDenyByDefault(AuthorityLeases); ObservationHub = new CultNetSimulationObservationHub(options.ConsensusOptions); FactCommitter = new CultMeshSimulationFactCommitter(Node.Database); if (options.VerseCatalog == null) { _owned.Add(VerseCatalog); } if (options.PeerCatalog == null) { _owned.Add(PeerCatalog); } if (options.ServeSimulationObservations) { _owned.Add(new CultNetSimulationObservationServer(Node.Server, ObservationHub)); } if (options.ServeVerseDiscovery) { _owned.Add(new CultMeshVerseDiscoveryServer(Node.Server, VerseCatalog)); } if (options.ServePeerExchange) { _owned.Add(new CultMeshPeerExchangeServer(Node.Server, PeerCatalog)); } } /// /// Gets the underlying node. /// public CultMeshNode Node { get; } /// /// Gets the session Verse catalog. /// public CultMeshVerseCatalog VerseCatalog { get; } /// /// Gets the session peer catalog. /// public CultMeshPeerCatalog PeerCatalog { get; } /// /// Gets the session authority lease catalog. /// public CultMeshAuthorityLeaseCatalog AuthorityLeases { get; } /// Gets the session authority decision owner. public CultMeshAuthorityResolver AuthorityResolver { get; } /// /// Gets the session simulation observation hub. /// public CultNetSimulationObservationHub ObservationHub { get; } /// /// Gets the session simulation fact committer. /// public CultMeshSimulationFactCommitter FactCommitter { get; } /// /// Watches consensus candidate updates. /// public Observable WatchCandidates() { ThrowIfDisposed(); return ObservationHub.WatchCandidates(); } /// /// Watches committed simulation facts. /// public Observable> WatchSimulationFacts() { ThrowIfDisposed(); return Node.Database.Watch(); } /// /// Applies a local client prediction through the underlying database. /// public Task> PredictAsync(CultRecordKey key, T document) where T : class { ThrowIfDisposed(); return Node.Database.PutPredictedAsync(key, document); } /// /// Submits one witness observation and returns the current candidates for that subject. /// public IReadOnlyList SubmitObservation( CultNetSimulationObservation observation) { ThrowIfDisposed(); return ObservationHub.Submit(observation); } /// /// Submits one witness observation and commits any new quorum facts it creates. /// public async Task>> SubmitAndCommitAsync( CultNetSimulationObservation observation) { ThrowIfDisposed(); var candidates = ObservationHub.Submit(observation); return await CommitQuorumCandidatesAsync(candidates).ConfigureAwait(false); } /// /// Commits quorum candidates that have not already been committed by this session. /// public async Task>> CommitQuorumCandidatesAsync( IEnumerable candidates) { ThrowIfDisposed(); if (candidates == null) throw new ArgumentNullException(nameof(candidates)); var handles = new List>(); foreach (var candidate in candidates.Where(candidate => candidate.HasQuorum)) { var factId = CultMeshSimulationFact.ComputeFactId(candidate); if (!_committedFactIds.Add(factId)) { continue; } var key = CultMeshSimulationFact.CreateRecordKey(candidate); if (await Node.Database.GetAsync(key).ConfigureAwait(false) != null) { continue; } handles.Add(await FactCommitter.CommitAsync(candidate).ConfigureAwait(false)); } return handles; } /// public void Dispose() { if (_disposed) { return; } _disposed = true; foreach (var owned in _owned) { owned.Dispose(); } ObservationHub.Dispose(); _owned.Clear(); } private void ThrowIfDisposed() { if (_disposed) { throw new ObjectDisposedException(nameof(CultMeshGameSession)); } } } }