From 74b3b3d72aa37a0b6bca2a92bb619718cb3e175c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 22 Jun 2026 15:04:12 +0100 Subject: [PATCH] Query daemon presentation entities by viewport --- Aetheria.State.Verify/Program.cs | 22 ++++- Assets/Scripts/Zone Display/ZoneRenderer.cs | 51 +++++++--- .../AetheriaRuntimeDaemonRenderQueries.cs | 93 +++++++++++++++++++ 3 files changed, 153 insertions(+), 13 deletions(-) diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 5bd579c6e..570215ca7 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -1910,6 +1910,10 @@ static void RequireDaemonRenderQueryAuthority(string root) "public static int QueryCompassMarkers(", "public static int[] QueryVisibleEntityIndices(", "public static int QueryVisibleEntityIndices(", + "public static int[] QueryPresentationEntityIndices(", + "public static int QueryPresentationEntityIndices(", + "TryParseEntityIndex(run?.CurrentEntityKey)", + "ContainsPoint(viewport, entity.PositionX, entity.PositionZ)", "AetheriaRuntimeDaemonEntityContact", "public static bool TryQueryEntityContact(", "public static AetheriaRuntimeDaemonEntityContact[] QueryEntityContacts(", @@ -2141,6 +2145,9 @@ static void RequireDaemonRenderQueryAuthority(string root) "private readonly List _daemonAsteroidBeltPoses", "private readonly List _daemonCompassMarkers", "private readonly Dictionary _daemonCompassMarkersByEntityIndex", + "private IReadOnlyDictionary _observedEntityFacadesByDaemonIndex", + "private readonly List _daemonPresentationEntityIndices", + "private readonly HashSet _daemonPresentationEntityIndicesSet", "private readonly List _daemonVisibleEntityIndices", "private readonly HashSet _daemonVisibleEntityIndicesSet", "private readonly HashSet _visibleDaemonEntityIndices", @@ -2155,6 +2162,7 @@ static void RequireDaemonRenderQueryAuthority(string root) "AetheriaRuntimeRunCheckpointCommit daemonRun)", "_daemonZoneSnapshot = daemonZone;", "_daemonRunSnapshot = daemonRun;", + "_observedEntityFacadesByDaemonIndex = observedEntityFacadesByDaemonIndex;", "AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight(", "AetheriaRuntimeDaemonRenderQueries.ResolveZoneRenderRadius(", " 2000);", @@ -2174,8 +2182,11 @@ static void RequireDaemonRenderQueryAuthority(string root) "beltPosesByBodyKey.TryGetValue(bodyKey, out var beltPose)", "LoadAsteroidBelt(beltPose)", "void LoadAsteroidBelt(AetheriaRuntimeDaemonAsteroidBeltPose beltPose)", - "foreach (var entitySnapshot in daemonZone?.Entities ?? Array.Empty())", - "observedEntityFacadesByDaemonIndex.TryGetValue(entitySnapshot.EntityIndex, out var entity)", + "private void SyncDaemonEntityInstances()", + "AetheriaRuntimeDaemonRenderQueries.QueryPresentationEntityIndices(", + "_observedEntityFacadesByDaemonIndex.TryGetValue(entityIndex, out var entity)", + "Loading entity {entity.Name} from daemon presentation query", + "UnloadEntity(pair.Value.Entity)", "private readonly Dictionary _entityInstancesByDaemonIndex", "public IReadOnlyDictionary DaemonEntityInstances => _entityInstancesByDaemonIndex;", "public bool TryGetEntityInstance(int daemonEntityIndex, out EntityInstance instance)", @@ -2229,6 +2240,13 @@ static void RequireDaemonRenderQueryAuthority(string root) "ZoneRenderer still accepts the whole Unity GameSettings object instead of explicit renderer asset/tuning inputs."); } + if (zoneRenderer.Contains("foreach (var entitySnapshot in daemonZone?.Entities", StringComparison.Ordinal) || + zoneRenderer.Contains("Loading entity {entity.Name} from daemon entity snapshot", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "ZoneRenderer must query daemon presentation entities instead of loading every daemon entity snapshot into a mirrored Unity level."); + } + var entityInstancePath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "EntityInstance.cs"); var entityInstance = File.Exists(entityInstancePath) ? File.ReadAllText(entityInstancePath) diff --git a/Assets/Scripts/Zone Display/ZoneRenderer.cs b/Assets/Scripts/Zone Display/ZoneRenderer.cs index d56e93fb3..3d3d272f9 100644 --- a/Assets/Scripts/Zone Display/ZoneRenderer.cs +++ b/Assets/Scripts/Zone Display/ZoneRenderer.cs @@ -98,6 +98,9 @@ public class ZoneRenderer : MonoBehaviour new List(); private readonly Dictionary _daemonCompassMarkersByEntityIndex = new Dictionary(); + private IReadOnlyDictionary _observedEntityFacadesByDaemonIndex; + private readonly List _daemonPresentationEntityIndices = new List(); + private readonly HashSet _daemonPresentationEntityIndicesSet = new HashSet(); private readonly List _daemonVisibleEntityIndices = new List(); private readonly HashSet _daemonVisibleEntityIndicesSet = new HashSet(); private readonly HashSet _visibleDaemonEntityIndices = new HashSet(); @@ -221,21 +224,13 @@ public class ZoneRenderer : MonoBehaviour AetheriaRuntimeRunCheckpointCommit daemonRun = null) { ClearZone(); + _observedEntityFacadesByDaemonIndex = observedEntityFacadesByDaemonIndex; ApplyDaemonFrame(daemonZone, daemonRun); RefreshDaemonBodyPoses(); RefreshDaemonAsteroidBeltPoses(); SyncDaemonBodyViews(); - - foreach (var entitySnapshot in daemonZone?.Entities ?? Array.Empty()) - { - if (entitySnapshot != null && - observedEntityFacadesByDaemonIndex != null && - observedEntityFacadesByDaemonIndex.TryGetValue(entitySnapshot.EntityIndex, out var entity)) - { - Debug.Log($"Loading entity {entity.Name} from daemon entity snapshot {entitySnapshot.EntityIndex}"); - LoadEntity(entity); - } - } + RefreshDaemonVisibleEntityInstances(); + SyncDaemonEntityInstances(); foreach (var exit in _daemonWormholeExits) AddWormhole(exit); @@ -529,6 +524,7 @@ public class ZoneRenderer : MonoBehaviour RefreshDaemonAsteroidBeltPoses(); SyncDaemonBodyViews(); RefreshDaemonVisibleEntityInstances(); + SyncDaemonEntityInstances(); RefreshDaemonCompassMarkers(); Plane[] planes = GeometryUtility.CalculateFrustumPlanes(MainCamera); @@ -792,6 +788,39 @@ public class ZoneRenderer : MonoBehaviour _visibleDaemonEntityIndices.Add(entityIndex); } + private void SyncDaemonEntityInstances() + { + AetheriaRuntimeDaemonRenderQueries.QueryPresentationEntityIndices( + _daemonRunSnapshot, + _daemonZoneSnapshot, + PerspectiveEntity?.DaemonEntityIndex ?? -1, + RenderSettings.TargetDetectionInfoThreshold, + ResolveDaemonRenderViewport(), + _daemonPresentationEntityIndices); + _daemonPresentationEntityIndicesSet.Clear(); + foreach (var entityIndex in _daemonPresentationEntityIndices) + _daemonPresentationEntityIndicesSet.Add(entityIndex); + + foreach (var entityIndex in _daemonPresentationEntityIndices) + { + if (_entityInstancesByDaemonIndex.ContainsKey(entityIndex) || + _observedEntityFacadesByDaemonIndex == null || + !_observedEntityFacadesByDaemonIndex.TryGetValue(entityIndex, out var entity)) + { + continue; + } + + Debug.Log($"Loading entity {entity.Name} from daemon presentation query {entityIndex}"); + LoadEntity(entity); + } + + foreach (var pair in _entityInstancesByDaemonIndex.ToArray()) + { + if (!_daemonPresentationEntityIndicesSet.Contains(pair.Key)) + UnloadEntity(pair.Value.Entity); + } + } + private float GetTerrainHeight(float2 position) { return (float)AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight( diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs index 56ac5e6d8..2fbd5ffdf 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs @@ -978,6 +978,71 @@ namespace GameCult.Aetheria.State.Verse return entityIndices.Count; } + public static int[] QueryPresentationEntityIndices( + AetheriaRuntimeRunCheckpointCommit? run, + AetheriaRuntimeZoneSnapshotCommit? zone, + int observerEntityIndex, + double minimumInfoGathered, + AetheriaRuntimeXzRect viewport) + { + var entityIndices = new List(); + QueryPresentationEntityIndices(run, zone, observerEntityIndex, minimumInfoGathered, viewport, entityIndices); + return entityIndices.Count == 0 ? Array.Empty() : entityIndices.ToArray(); + } + + public static int QueryPresentationEntityIndices( + AetheriaRuntimeRunCheckpointCommit? run, + AetheriaRuntimeZoneSnapshotCommit? zone, + int observerEntityIndex, + double minimumInfoGathered, + AetheriaRuntimeXzRect viewport, + List entityIndices) + { + if (entityIndices == null) throw new ArgumentNullException(nameof(entityIndices)); + entityIndices.Clear(); + if (zone == null) + return 0; + + var selected = new HashSet(); + var currentEntityIndex = TryParseEntityIndex(run?.CurrentEntityKey); + if (currentEntityIndex >= 0) + selected.Add(currentEntityIndex); + + var entities = BuildEntityMap(zone); + if (observerEntityIndex >= 0) + { + if (entities.TryGetValue(observerEntityIndex, out var observer)) + { + selected.Add(observer.EntityIndex); + foreach (var contact in observer.Contacts ?? Array.Empty()) + { + if (contact != null && + contact.Visible && + contact.InfoGathered > minimumInfoGathered && + entities.ContainsKey(contact.TargetEntityIndex)) + { + selected.Add(contact.TargetEntityIndex); + } + } + } + } + + foreach (var entity in zone.Entities ?? Array.Empty()) + { + if (entity != null && + entity.EntityIndex >= 0 && + ContainsPoint(viewport, entity.PositionX, entity.PositionZ)) + { + selected.Add(entity.EntityIndex); + } + } + + foreach (var entityIndex in selected.OrderBy(index => index)) + entityIndices.Add(entityIndex); + + return entityIndices.Count; + } + public static bool TryQueryEntityContact( AetheriaRuntimeZoneSnapshotCommit? zone, int observerEntityIndex, @@ -1345,6 +1410,34 @@ namespace GameCult.Aetheria.State.Verse return dx * dx + dz * dz <= radius * radius; } + private static bool ContainsPoint(AetheriaRuntimeXzRect rect, double x, double z) + { + return x >= rect.MinX && + x <= rect.MaxX && + z >= rect.MinZ && + z <= rect.MaxZ; + } + + private static int TryParseEntityIndex(string? key) + { + if (string.IsNullOrWhiteSpace(key)) + return -1; + + var marker = ".entity."; + var markerIndex = key.LastIndexOf(marker, StringComparison.Ordinal); + if (markerIndex < 0) + return -1; + + var start = markerIndex + marker.Length; + var end = start; + while (end < key.Length && char.IsDigit(key[end])) + end++; + + return int.TryParse(key.Substring(start, end - start), out var value) + ? value + : -1; + } + private static bool IntersectsBounds( AetheriaRuntimeDaemonRenderGroupDocument group, double minX,