using System; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using GameCult.Caching; using GameCult.Networking; using MessagePack; namespace GameCult.Mesh { /// /// Committed simulation fact derived from witness consensus. /// [CultDocument("gamecult.mesh.simulation_fact", "gamecult.mesh.simulation_fact.v1")] public sealed class CultMeshSimulationFact { /// /// Stable fact identifier. /// [Key(0)] [CultIndex] public string FactId { get; set; } = string.Empty; /// /// Shard containing the simulation. /// [Key(1)] [CultIndex] public string ShardId { get; set; } = string.Empty; /// /// Shard authority epoch observed by witnesses. /// [Key(2)] public long ShardEpoch { get; set; } /// /// Simulation frame or tick. /// [Key(3)] [CultIndex] public long Frame { get; set; } /// /// Subject the fact is about. /// [Key(4)] [CultIndex] public string SubjectId { get; set; } = string.Empty; /// /// Fact kind, such as hit, collision, or ownership. /// [Key(5)] [CultIndex] public string ClaimKind { get; set; } = string.Empty; /// /// Hash of the accepted claim. /// [Key(6)] public string ClaimHash { get; set; } = string.Empty; /// /// Optional readable or app-specific claim detail. /// [Key(7)] public string? ClaimSummary { get; set; } /// /// Number of distinct witnesses supporting the fact. /// [Key(8)] public int WitnessCount { get; set; } /// /// Supporting witness weight. /// [Key(9)] public double SupportWeight { get; set; } /// /// Total observed weight for the same subject/frame/kind. /// [Key(10)] public double TotalWeight { get; set; } /// /// Support divided by total observed weight. /// [Key(11)] public double Confidence { get; set; } /// /// Commit timestamp. /// [Key(12)] public string CommittedAt { get; set; } = string.Empty; /// /// Builds a deterministic record key for a simulation fact. /// public static CultRecordKey CreateRecordKey(CultNetSimulationConsensusCandidate candidate) { if (candidate == null) throw new ArgumentNullException(nameof(candidate)); return new CultRecordKey("simulation:" + ComputeFactId(candidate)); } /// /// Builds a stable fact id for a consensus candidate. /// public static string ComputeFactId(CultNetSimulationConsensusCandidate candidate) { if (candidate == null) throw new ArgumentNullException(nameof(candidate)); var canonical = string.Join("\u001F", candidate.ShardId, candidate.ShardEpoch, candidate.Frame, candidate.SubjectId, candidate.ClaimKind); using var sha = SHA256.Create(); var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(canonical)); return string.Concat(bytes.Select(value => value.ToString("x2"))); } /// /// Creates a committed fact document from a quorum candidate. /// public static CultMeshSimulationFact FromCandidate( CultNetSimulationConsensusCandidate candidate, string? committedAt = null) { if (candidate == null) throw new ArgumentNullException(nameof(candidate)); return new CultMeshSimulationFact { FactId = ComputeFactId(candidate), ShardId = candidate.ShardId, ShardEpoch = candidate.ShardEpoch, Frame = candidate.Frame, SubjectId = candidate.SubjectId, ClaimKind = candidate.ClaimKind, ClaimHash = candidate.ClaimHash, ClaimSummary = candidate.ClaimSummary, WitnessCount = candidate.WitnessCount, SupportWeight = candidate.SupportWeight, TotalWeight = candidate.TotalWeight, Confidence = candidate.Confidence, CommittedAt = string.IsNullOrWhiteSpace(committedAt) ? DateTimeOffset.UtcNow.ToString("O") : committedAt! }; } } /// /// Commits quorum simulation candidates into the CultMesh database. /// public sealed class CultMeshSimulationFactCommitter { private readonly CultNetDatabase _database; /// /// Creates a simulation fact committer. /// public CultMeshSimulationFactCommitter(CultNetDatabase database) { _database = database ?? throw new ArgumentNullException(nameof(database)); } /// /// Commits a quorum candidate as a simulation fact document. /// public async Task> CommitAsync( CultNetSimulationConsensusCandidate candidate) { if (candidate == null) throw new ArgumentNullException(nameof(candidate)); if (!candidate.HasQuorum) { throw new InvalidOperationException("Simulation candidate cannot be committed before quorum."); } var fact = CultMeshSimulationFact.FromCandidate(candidate); return await _database.PutAsync(CultMeshSimulationFact.CreateRecordKey(candidate), fact) .ConfigureAwait(false); } } }