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));
}
}
}
}