using System; using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; using GameCult.Caching; using MessagePack; using static GameCult.Geometry.CultGeometryDocuments; namespace GameCult.Geometry { /// /// Schema versions for CultCache-native geometry pipeline documents. /// public static class CultGeometrySchemaVersions { public const string Domain = "gamecult.geometry.domain.v1"; public const string BuildRequest = "gamecult.geometry.build_request.v1"; public const string SelectedCut = "gamecult.geometry.selected_cut.v1"; public const string ChunkArtifact = "gamecult.geometry.chunk_artifact.v1"; } /// /// CultCache document containing a hierarchical geometry domain tree. /// [MessagePackObject] [CultDocument("gamecult.geometry.domain", CultGeometrySchemaVersions.Domain)] public sealed class CultGeometryDomainDocument { [Key(0)] [CultName] public string DomainId { get; set; } = string.Empty; [Key(1)] [CultIndex] public string RootKey { get; set; } = string.Empty; [Key(2)] public string SourceRuntime { get; set; } = string.Empty; [Key(3)] public CultGeometryDomainNode Root { get; set; } = new CultGeometryDomainNode(); [Key(4)] public string CreatedAt { get; set; } = string.Empty; public static CultRecordKey CreateRecordKey(CultGeometryDomainDocument domain) { if (domain == null) throw new ArgumentNullException(nameof(domain)); return new CultRecordKey("geometry:domain:" + StableHash( domain.RootKey, domain.SourceRuntime, domain.Root.StableFingerprint())); } } /// /// One node in a geometry domain tree. /// [MessagePackObject] public sealed class CultGeometryDomainNode { [Key(0)] public string Name { get; set; } = string.Empty; [Key(1)] public string Kind { get; set; } = string.Empty; [Key(2)] public float[] Translation { get; set; } = Array.Empty(); [Key(3)] public float[] RotationXyzw { get; set; } = Array.Empty(); [Key(4)] public ulong Seed { get; set; } [Key(5)] public CultGeometryFeatureClaim[] Claims { get; set; } = Array.Empty(); [Key(6)] public CultGeometryDomainNode[] Children { get; set; } = Array.Empty(); public string StableFingerprint() { var parts = new[] { Name, Kind, StableVector(Translation), StableVector(RotationXyzw), Seed.ToString(CultureInfo.InvariantCulture) } .Concat(Claims.Select(claim => claim.StableFingerprint())) .Concat(Children.Select(child => child.StableFingerprint())) .ToArray(); return StableArray(parts); } } /// /// A domain feature claim before CSG lowering. /// [MessagePackObject] public sealed class CultGeometryFeatureClaim { [Key(0)] public string Name { get; set; } = string.Empty; [Key(1)] public float[] Translation { get; set; } = Array.Empty(); [Key(2)] public float[] RotationXyzw { get; set; } = Array.Empty(); [Key(3)] public float[] SupportCenter { get; set; } = Array.Empty(); [Key(4)] public float[] SupportSize { get; set; } = Array.Empty(); [Key(5)] public string Kind { get; set; } = string.Empty; [Key(6)] public uint Material { get; set; } [Key(7)] public string LoweringPolicy { get; set; } = string.Empty; public string StableFingerprint() => StableArray([ Name, StableVector(Translation), StableVector(RotationXyzw), StableVector(SupportCenter), StableVector(SupportSize), Kind, Material.ToString(CultureInfo.InvariantCulture), LoweringPolicy ]); } /// /// CultCache document requesting one geometry LOD build. /// [MessagePackObject] [CultDocument("gamecult.geometry.build_request", CultGeometrySchemaVersions.BuildRequest)] public sealed class CultGeometryBuildRequest { [Key(0)] [CultName] public string RequestId { get; set; } = string.Empty; [Key(1)] [MessagePackFormatter(typeof(CultRecordRefFormatter))] public CultRecordRef DomainKey { get; set; } [Key(2)] [CultIndex] public string WorkerGroup { get; set; } = string.Empty; [Key(3)] public float[] CameraPosition { get; set; } = Array.Empty(); [Key(4)] public float[] FrustumMin { get; set; } = Array.Empty(); [Key(5)] public float[] FrustumMax { get; set; } = Array.Empty(); [Key(6)] public float ViewportHeightPixels { get; set; } [Key(7)] public float VerticalFovRadians { get; set; } [Key(8)] public float TargetError { get; set; } [Key(9)] public int TriangleBudget { get; set; } [Key(10)] public int ColliderBudget { get; set; } [Key(11)] public string[] SemanticFilter { get; set; } = Array.Empty(); [Key(12)] public string[] RequestedChunkKeys { get; set; } = Array.Empty(); [Key(13)] public string[] DirtyDomainKeys { get; set; } = Array.Empty(); [Key(14)] public string CreatedAt { get; set; } = string.Empty; public static CultRecordKey CreateRecordKey(CultGeometryBuildRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); return new CultRecordKey("geometry:request:" + StableHash( request.DomainKey.Key.Value, request.WorkerGroup, StableVector(request.CameraPosition), StableVector(request.FrustumMin), StableVector(request.FrustumMax), request.ViewportHeightPixels.ToString("R", CultureInfo.InvariantCulture), request.VerticalFovRadians.ToString("R", CultureInfo.InvariantCulture), request.TargetError.ToString("R", CultureInfo.InvariantCulture), request.TriangleBudget.ToString(CultureInfo.InvariantCulture), request.ColliderBudget.ToString(CultureInfo.InvariantCulture), StableArray(request.SemanticFilter), StableArray(request.RequestedChunkKeys), StableArray(request.DirtyDomainKeys))); } } /// /// CultCache document describing the selected domain cut produced for one build request. /// [MessagePackObject] [CultDocument("gamecult.geometry.selected_cut", CultGeometrySchemaVersions.SelectedCut)] public sealed class CultGeometrySelectedCutManifest { [Key(0)] [CultName] public string CutId { get; set; } = string.Empty; [Key(1)] [MessagePackFormatter(typeof(CultRecordRefFormatter))] public CultRecordRef RequestKey { get; set; } [Key(2)] public string[] SelectedNodes { get; set; } = Array.Empty(); [Key(3)] public string[] DeferredChildRequests { get; set; } = Array.Empty(); [Key(4)] public string[] ParentFallbackNodes { get; set; } = Array.Empty(); [Key(5)] public CultGeometryContributionRow[] Diagnostics { get; set; } = Array.Empty(); public static CultRecordKey CreateRecordKey(CultGeometrySelectedCutManifest cut) { if (cut == null) throw new ArgumentNullException(nameof(cut)); return new CultRecordKey("geometry:cut:" + StableHash( cut.RequestKey.Key.Value, cut.CutId, StableArray(cut.SelectedNodes), StableArray(cut.DeferredChildRequests), StableArray(cut.ParentFallbackNodes))); } } /// /// One diagnostic row explaining selected-cut pressure. /// [MessagePackObject] public sealed class CultGeometryContributionRow { [Key(0)] public string DomainKey { get; set; } = string.Empty; [Key(1)] public string Kind { get; set; } = string.Empty; [Key(2)] public float Contribution { get; set; } [Key(3)] public float ProjectedScreenError { get; set; } [Key(4)] public float SemanticPriority { get; set; } [Key(5)] public int EstimatedTriangleCost { get; set; } [Key(6)] public int ChildCost { get; set; } [Key(7)] public int RemainingTriangleBudget { get; set; } [Key(8)] public bool Requested { get; set; } [Key(9)] public bool Dirty { get; set; } [Key(10)] public bool Selected { get; set; } [Key(11)] public bool UsedFallback { get; set; } [Key(12)] public bool DeferredByBudget { get; set; } } /// /// CultCache document carrying one emitted geometry chunk and its collider payload. /// [MessagePackObject] [CultDocument("gamecult.geometry.chunk_artifact", CultGeometrySchemaVersions.ChunkArtifact)] public sealed class CultGeometryChunkArtifact { [Key(0)] [CultName] public string ChunkId { get; set; } = string.Empty; [Key(1)] [MessagePackFormatter(typeof(CultRecordRefFormatter))] public CultRecordRef CutKey { get; set; } [Key(2)] [CultIndex] public string SelectedCutId { get; set; } = string.Empty; [Key(3)] public float[] BoundsMin { get; set; } = Array.Empty(); [Key(4)] public float[] BoundsMax { get; set; } = Array.Empty(); [Key(5)] public string[] SourceDomainKeys { get; set; } = Array.Empty(); [Key(6)] public string[] SourceClaimKeys { get; set; } = Array.Empty(); [Key(7)] public CultGeometryTriangleMesh RenderMesh { get; set; } = new CultGeometryTriangleMesh(); [Key(8)] public CultGeometryTriangleMesh? ColliderMesh { get; set; } [Key(9)] public int InputBrushes { get; set; } [Key(10)] public int CandidatePairs { get; set; } [Key(11)] public int RejectedPairs { get; set; } [Key(12)] public ulong StableClipSeed { get; set; } [Key(13)] public bool SupportsParentChildCoexistence { get; set; } public static CultRecordKey CreateRecordKey(CultGeometryChunkArtifact chunk) { if (chunk == null) throw new ArgumentNullException(nameof(chunk)); return new CultRecordKey("geometry:chunk:" + StableHash( chunk.CutKey.Key.Value, chunk.ChunkId, chunk.SelectedCutId, StableArray(chunk.SourceDomainKeys), StableArray(chunk.SourceClaimKeys), chunk.RenderMesh.StableFingerprint(), chunk.ColliderMesh?.StableFingerprint() ?? string.Empty)); } } /// /// Packed triangle mesh payload for CultCache persistence and CultNet replication. /// [MessagePackObject] public sealed class CultGeometryTriangleMesh { [Key(0)] public float[] Positions { get; set; } = Array.Empty(); [Key(1)] public float[] Normals { get; set; } = Array.Empty(); [Key(2)] public float[] Uvs { get; set; } = Array.Empty(); [Key(3)] public uint[] Indices { get; set; } = Array.Empty(); [Key(4)] public uint[] TriangleMaterials { get; set; } = Array.Empty(); [IgnoreMember] public int TriangleCount => Indices.Length / 3; public string StableFingerprint() => StableHash( StableVector(Positions), StableVector(Normals), StableVector(Uvs), string.Join(",", Indices.Select(value => value.ToString(CultureInfo.InvariantCulture))), string.Join(",", TriangleMaterials.Select(value => value.ToString(CultureInfo.InvariantCulture)))); } internal static class CultGeometryDocuments { public static string StableHash(params string[] parts) { using var sha = SHA256.Create(); var canonical = string.Join("\u001f", parts); var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(canonical)); return string.Concat(bytes.Select(value => value.ToString("x2"))); } public static string StableVector(float[] values) { return string.Join(",", values.Select(StableFloat)); } public static string StableFloat(float value) { return BitConverter.ToUInt32(BitConverter.GetBytes(value), 0) .ToString("x8", CultureInfo.InvariantCulture); } public static string StableArray(string[] values) { return string.Join("\u001e", values); } } }