using System; using System.Collections.Generic; using System.IO.MemoryMappedFiles; using System.Linq; using System.Security.Cryptography; using System.Text; #nullable enable namespace GameCult.Aetheria.State.Verse { public static class AetheriaRuntimeDaemonSoaFramePublisher { private const string BufferId = "current-zone-entities-hot"; private const int EntityRenderGroupId = 1; private const int FloatStride = 4; private const int Float3Stride = 12; private const int IntStride = 4; private const int ByteStride = 1; private const int RetainedBufferCount = 4; private const string EntityProxyMeshAssetKey = "daemon.entity_proxy.mesh"; private const string EntityProxyMaterialAssetKey = "daemon.entity_proxy.material"; private const string EntityProxyMeshUri = "cultmesh://aetheria/assets/daemon/entity_proxy/mesh"; private const string EntityProxyMaterialUri = "cultmesh://aetheria/assets/daemon/entity_proxy/material"; private static readonly object Sync = new object(); private static readonly Dictionary RetainedBuffers = new Dictionary(StringComparer.Ordinal); private static long RetainedBufferUseSequence; public static AetheriaRuntimeDaemonSoaViewDocument BuildCurrentZoneEntities( string stateFilePath, AetheriaRuntimeDaemonFrameDocument frame) { if (string.IsNullOrWhiteSpace(stateFilePath)) throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath)); if (frame == null) throw new ArgumentNullException(nameof(frame)); var run = frame.Run ?? new AetheriaRuntimeRunCheckpointCommit(); var zone = (run.Zones ?? Array.Empty()) .FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == run.CurrentZoneIndex); var entities = (zone?.Entities ?? Array.Empty()) .Where(entity => entity != null) .OrderBy(entity => entity.EntityIndex) .ToArray(); var count = entities.Length; var generation = Math.Max(frame.FrameId, 0); var layout = EntityHotSlabLayout.Create(count); var location = CreateLocation(stateFilePath, frame.SessionId, generation); var buffer = RetainBuffer(location, layout.TotalByteLength); WriteEntities(buffer.Accessor, layout, entities); var view = AetheriaRuntimeDaemonSoaViewDocument.Create( string.IsNullOrWhiteSpace(frame.DaemonId) ? "aetheria-daemon" : frame.DaemonId, string.IsNullOrWhiteSpace(frame.SessionId) ? "local" : frame.SessionId, frame.FrameId, generation, new[] { new AetheriaRuntimeDaemonSoaBufferDocument { BufferId = BufferId, DisplayName = "Current zone daemon entity hot slab", Backend = AetheriaRuntimeDaemonSoaBackends.MemoryMappedFile, Location = location, ByteOffset = 0, ByteLength = layout.TotalByteLength, Generation = generation, DaemonWritable = true, ObserverWritable = false } }, layout.CreateColumns(count), layout.CreateDirtyRanges(count, generation), backend: AetheriaRuntimeDaemonSoaBackends.MemoryMappedFile, synchronizationMode: AetheriaRuntimeDaemonSoaSynchronizationModes.ImmutableFrame, renderGroups: CreateRenderGroups(entities)); return view; } private static IReadOnlyList CreateRenderGroups( IReadOnlyList entities) { if (entities.Count == 0) return Array.Empty(); var minX = entities.Min(entity => (float)entity.PositionX); var minY = entities.Min(entity => (float)entity.PositionY); var minZ = entities.Min(entity => (float)entity.PositionZ); var maxX = entities.Max(entity => (float)entity.PositionX); var maxY = entities.Max(entity => (float)entity.PositionY); var maxZ = entities.Max(entity => (float)entity.PositionZ); const float padding = 16.0f; return new[] { new AetheriaRuntimeDaemonRenderGroupDocument { GroupId = EntityRenderGroupId, MeshKey = EntityProxyMeshUri, MaterialKey = EntityProxyMaterialUri, MeshAsset = AetheriaRuntimeAssetRef.FromKey( EntityProxyMeshAssetKey, AetheriaRuntimeAssetKinds.Mesh, EntityProxyMeshUri, AetheriaRuntimeAssetTransports.CultMesh), MaterialAsset = AetheriaRuntimeAssetRef.FromKey( EntityProxyMaterialAssetKey, AetheriaRuntimeAssetKinds.Material, EntityProxyMaterialUri, AetheriaRuntimeAssetTransports.CultMesh), SubMeshIndex = 0, Layer = 0, ShaderKey = "aetheria.daemon.entity-proxy", DisplayName = "Daemon current-zone entities", InstanceCount = entities.Count, BoundsCenterX = (minX + maxX) * 0.5f, BoundsCenterY = (minY + maxY) * 0.5f, BoundsCenterZ = (minZ + maxZ) * 0.5f, BoundsSizeX = Math.Max(maxX - minX + padding, padding), BoundsSizeY = Math.Max(maxY - minY + padding, padding), BoundsSizeZ = Math.Max(maxZ - minZ + padding, padding), ShadowMode = AetheriaRuntimeDaemonRenderShadowModes.On, ReceiveShadows = true, DefaultScale = 1.0f, Lod = -1 } }; } private static void WriteEntities( MemoryMappedViewAccessor accessor, EntityHotSlabLayout layout, IReadOnlyList entities) { for (var index = 0; index < entities.Count; index++) { var entity = entities[index]; accessor.Write(layout.EntityIndex + index * IntStride, entity.EntityIndex); WriteFloat3(accessor, layout.Position, index, entity.PositionX, entity.PositionY, entity.PositionZ); WriteFloat(accessor, layout.RotationRadians, index, Math.Atan2(entity.DirectionX, entity.DirectionY)); WriteFloat3(accessor, layout.Velocity, index, entity.VelocityX, 0.0, entity.VelocityY); WriteFloat(accessor, layout.PhysicsBodyRadius, index, 1.0); WriteFloat(accessor, layout.PhysicsBodyMass, index, 1.0); WriteFloat(accessor, layout.PhysicsBodyInverseMass, index, 1.0); WriteFloat(accessor, layout.RenderScale, index, 1.0); accessor.Write(layout.RenderVisibility + index * ByteStride, (byte)(entity.IsActive ? 1 : 0)); accessor.Write(layout.RenderLod + index * IntStride, 0); accessor.Write(layout.RenderGroupId + index * IntStride, (uint)EntityRenderGroupId); } } private static void WriteFloat(MemoryMappedViewAccessor accessor, long byteOffset, int index, double value) { accessor.Write(byteOffset + index * FloatStride, IsFinite(value) ? (float)value : 0.0f); } private static void WriteFloat3( MemoryMappedViewAccessor accessor, long byteOffset, int index, double x, double y, double z) { var elementOffset = byteOffset + index * Float3Stride; accessor.Write(elementOffset, IsFinite(x) ? (float)x : 0.0f); accessor.Write(elementOffset + FloatStride, IsFinite(y) ? (float)y : 0.0f); accessor.Write(elementOffset + FloatStride * 2, IsFinite(z) ? (float)z : 0.0f); } private static bool IsFinite(double value) { return !double.IsNaN(value) && !double.IsInfinity(value); } private static RetainedMappedBuffer RetainBuffer(string location, long byteLength) { lock (Sync) { if (RetainedBuffers.TryGetValue(location, out var existing)) { existing.MarkUsed(++RetainedBufferUseSequence); return existing; } var memory = MemoryMappedFile.CreateOrOpen(location, byteLength, MemoryMappedFileAccess.ReadWrite); var accessor = memory.CreateViewAccessor(0, byteLength, MemoryMappedFileAccess.ReadWrite); var buffer = new RetainedMappedBuffer(memory, accessor, ++RetainedBufferUseSequence); RetainedBuffers[location] = buffer; TrimRetainedBuffers(location); return buffer; } } private static void TrimRetainedBuffers(string retainedLocation) { if (RetainedBuffers.Count <= RetainedBufferCount) return; foreach (var key in RetainedBuffers .Where(pair => !string.Equals(pair.Key, retainedLocation, StringComparison.Ordinal)) .OrderBy(pair => pair.Value.LastUsedSequence) .Select(pair => pair.Key) .Take(RetainedBuffers.Count - RetainedBufferCount) .ToArray()) { RetainedBuffers[key].Dispose(); RetainedBuffers.Remove(key); } } private static string CreateLocation(string stateFilePath, string sessionId, long generation) { using var sha = SHA256.Create(); var identity = $"{stateFilePath}|{sessionId}|{generation}"; var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(identity)); return "Aetheria.Daemon.Soa." + BitConverter.ToString(hash, 0, 12).Replace("-", ""); } private sealed class RetainedMappedBuffer : IDisposable { private readonly MemoryMappedFile _memory; public RetainedMappedBuffer(MemoryMappedFile memory, MemoryMappedViewAccessor accessor, long lastUsedSequence) { _memory = memory; Accessor = accessor; LastUsedSequence = lastUsedSequence; } public MemoryMappedViewAccessor Accessor { get; } public long LastUsedSequence { get; private set; } public void MarkUsed(long sequence) { LastUsedSequence = sequence; } public void Dispose() { Accessor.Dispose(); _memory.Dispose(); } } private readonly struct EntityHotSlabLayout { private EntityHotSlabLayout( long entityIndex, long position, long rotationRadians, long velocity, long physicsBodyRadius, long physicsBodyMass, long physicsBodyInverseMass, long renderScale, long renderVisibility, long renderLod, long renderGroupId, long totalByteLength) { EntityIndex = entityIndex; Position = position; RotationRadians = rotationRadians; Velocity = velocity; PhysicsBodyRadius = physicsBodyRadius; PhysicsBodyMass = physicsBodyMass; PhysicsBodyInverseMass = physicsBodyInverseMass; RenderScale = renderScale; RenderVisibility = renderVisibility; RenderLod = renderLod; RenderGroupId = renderGroupId; TotalByteLength = totalByteLength; } public long EntityIndex { get; } public long Position { get; } public long RotationRadians { get; } public long Velocity { get; } public long PhysicsBodyRadius { get; } public long PhysicsBodyMass { get; } public long PhysicsBodyInverseMass { get; } public long RenderScale { get; } public long RenderVisibility { get; } public long RenderLod { get; } public long RenderGroupId { get; } public long TotalByteLength { get; } public static EntityHotSlabLayout Create(int count) { count = Math.Max(0, count); var offset = 0L; var entityIndex = Take(ref offset, count, IntStride); var position = Take(ref offset, count, Float3Stride); var rotationRadians = Take(ref offset, count, FloatStride); var velocity = Take(ref offset, count, Float3Stride); var physicsBodyRadius = Take(ref offset, count, FloatStride); var physicsBodyMass = Take(ref offset, count, FloatStride); var physicsBodyInverseMass = Take(ref offset, count, FloatStride); var renderScale = Take(ref offset, count, FloatStride); var renderVisibility = Take(ref offset, count, ByteStride); offset = Align4(offset); var renderLod = Take(ref offset, count, IntStride); var renderGroupId = Take(ref offset, count, IntStride); return new EntityHotSlabLayout( entityIndex, position, rotationRadians, velocity, physicsBodyRadius, physicsBodyMass, physicsBodyInverseMass, renderScale, renderVisibility, renderLod, renderGroupId, Math.Max(1, offset)); } public AetheriaRuntimeDaemonSoaColumnDocument[] CreateColumns(int count) { return new[] { Column("entity-index", AetheriaRuntimeDaemonSoaColumnKinds.EntityIndex, "int32", EntityIndex, IntStride, count, "index", "world"), Column("position", AetheriaRuntimeDaemonSoaColumnKinds.Position, "float3", Position, Float3Stride, count, "world_units", "world"), Column("rotation-radians", AetheriaRuntimeDaemonSoaColumnKinds.RotationRadians, "float32", RotationRadians, FloatStride, count, "radians", "world"), Column("velocity", AetheriaRuntimeDaemonSoaColumnKinds.Velocity, "float3", Velocity, Float3Stride, count, "world_units_per_second", "world"), Column("physics-body-radius", AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyRadius, "float32", PhysicsBodyRadius, FloatStride, count, "world_units", "world"), Column("physics-body-mass", AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyMass, "float32", PhysicsBodyMass, FloatStride, count, "mass_units", "world"), Column("physics-body-inverse-mass", AetheriaRuntimeDaemonSoaColumnKinds.PhysicsBodyInverseMass, "float32", PhysicsBodyInverseMass, FloatStride, count, "inverse_mass_units", "world"), Column("render-scale", AetheriaRuntimeDaemonSoaColumnKinds.RenderScale, "float32", RenderScale, FloatStride, count, "scale", "world"), Column("render-visibility", AetheriaRuntimeDaemonSoaColumnKinds.RenderVisibility, "uint8", RenderVisibility, ByteStride, count, "bool", "world"), Column("render-lod", AetheriaRuntimeDaemonSoaColumnKinds.RenderLod, "int32", RenderLod, IntStride, count, "lod", "world"), Column("render-group-id", AetheriaRuntimeDaemonSoaColumnKinds.RenderGroupId, "uint32", RenderGroupId, IntStride, count, "id", "world") }; } public AetheriaRuntimeDaemonSoaDirtyRangeDocument[] CreateDirtyRanges(int count, long generation) { return CreateColumns(count) .Select(column => new AetheriaRuntimeDaemonSoaDirtyRangeDocument { ColumnId = column.ColumnId, StartIndex = 0, Count = count, Generation = generation }) .ToArray(); } private static AetheriaRuntimeDaemonSoaColumnDocument Column( string columnId, string kind, string scalarType, long byteOffset, int stride, int count, string unit, string coordinateSpace) { return new AetheriaRuntimeDaemonSoaColumnDocument { ColumnId = columnId, Kind = kind, BufferId = BufferId, ScalarType = scalarType, ByteOffset = byteOffset, ElementStride = stride, ElementCount = count, Unit = unit, CoordinateSpace = coordinateSpace }; } private static long Take(ref long offset, int count, int stride) { var current = offset; offset += (long)count * stride; return current; } private static long Align4(long value) { var remainder = value % 4; return remainder == 0 ? value : value + 4 - remainder; } } } }