diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index f032cc9f3..ed49dd5bb 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -1838,6 +1838,8 @@ static void RequireDaemonRenderQueryAuthority(string root) "public static int QueryCompassMarkers(", "public static int[] QueryVisibleEntityIndices(", "public static int QueryVisibleEntityIndices(", + "AetheriaRuntimeDaemonEntityContact", + "public static bool TryQueryEntityContact(", "AetheriaRuntimeDaemonWormholeExit", "public static AetheriaRuntimeDaemonWormholeExit[] QueryWormholeExits(", "public static int QueryWormholeExits(", @@ -7712,12 +7714,22 @@ static void RequireMainMenuContinueRunState(string root) "lockWeapon.RestoreRuntimeState(", "drive.RestoreRuntimeState(", "resourceScanner.RestoreRuntimeState(", + "AetheriaRuntimeDaemonRenderQueries.TryQueryEntityContact(", + "GetObservedInfoGathered(CurrentEntity, target)", + "IsObservedHostileContact(CurrentEntity, CurrentEntity.Target.Value)", + "_observedEntityFacadesByRecordKey", "entity.RestoreStatGrids(entitySnapshot.StatGrids)", "RestoreThermalExposure((float)entitySnapshot.Heatstroke, (float)entitySnapshot.Hypothermia)", "entity.HeatsinksEnabled = entitySnapshot.HeatsinksEnabled", "RestoreDroppedPickupsFromDaemonZoneState" }; + if (actionGameManager.Contains("_authoritativeDaemonEntities", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "Unity entity wrappers are observed daemon facades, not authoritative state; keep the daemon as authority."); + } + var missingGameplaySymbols = requiredGameplaySymbols .Where(symbol => !actionGameManager.Contains(symbol, StringComparison.Ordinal)) .ToArray(); @@ -7732,7 +7744,10 @@ static void RequireMainMenuContinueRunState(string root) var forbiddenGameplaySymbols = new[] { "if (run == null ||\r\n run.CurrentZoneIndex < 0 ||\r\n run.CurrentZoneEntityIndex < 0)", - "var currentEntityKey = $\"{zoneEntityKeyPrefix}{run.CurrentZoneEntityIndex}.v1\"" + "var currentEntityKey = $\"{zoneEntityKeyPrefix}{run.CurrentZoneEntityIndex}.v1\"", + "TargetVisibilityFill.fillAmount = Mathf.Lerp(.25f, .75f, (CurrentEntity.EntityInfoGathered[target] - threshold) / (1 - threshold));", + "VisibilityToTargetFill.fillAmount = Mathf.Lerp(.25f, .75f, target.EntityInfoGathered[CurrentEntity] / threshold);", + "CurrentEntity.Target.Value.IsHostileTo(CurrentEntity)" }; var forbiddenGameplayHits = forbiddenGameplaySymbols diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 9d32d8aa8..29b1fbf52 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -66,7 +66,7 @@ public class ActionGameManager : MonoBehaviour private string _lastAppliedAuthoritativeDaemonFramePath = ""; private string _lastAppliedAuthoritativeDaemonRunId = ""; private int _lastAppliedAuthoritativeDaemonZoneIndex = -1; - private readonly Dictionary _authoritativeDaemonEntities = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _observedEntityFacadesByRecordKey = new Dictionary(StringComparer.Ordinal); private static DirectoryInfo _gameDataDirectory; public static DirectoryInfo GameDataDirectory { @@ -2665,14 +2665,14 @@ public class ActionGameManager : MonoBehaviour { if (!string.Equals(_lastAppliedAuthoritativeDaemonRunId, runId, StringComparison.Ordinal) || _lastAppliedAuthoritativeDaemonZoneIndex != zoneIndex || - _authoritativeDaemonEntities.Count != entitySnapshots.Count) + _observedEntityFacadesByRecordKey.Count != entitySnapshots.Count) { return false; } foreach (var snapshot in entitySnapshots) { - if (!_authoritativeDaemonEntities.ContainsKey(snapshot.RecordKey)) + if (!_observedEntityFacadesByRecordKey.ContainsKey(snapshot.RecordKey)) { return false; } @@ -2688,7 +2688,7 @@ public class ActionGameManager : MonoBehaviour { foreach (var entitySnapshot in entitySnapshots) { - if (!_authoritativeDaemonEntities.TryGetValue(entitySnapshot.RecordKey, out var entity)) + if (!_observedEntityFacadesByRecordKey.TryGetValue(entitySnapshot.RecordKey, out var entity)) continue; entity.DaemonEntityIndex = EntityIndexFromRecordKey(entitySnapshot.RecordKey); @@ -2702,10 +2702,10 @@ public class ActionGameManager : MonoBehaviour if (entity.Settings != null) entity.Settings.ShutdownPerformance = (float)entitySnapshot.ShutdownPerformance; entity.RestoreThermalExposure((float)entitySnapshot.Heatstroke, (float)entitySnapshot.Hypothermia); - RestoreRuntimeBehaviorStateFromTypedSnapshot(entity, entitySnapshot, _authoritativeDaemonEntities); + RestoreRuntimeBehaviorStateFromTypedSnapshot(entity, entitySnapshot, _observedEntityFacadesByRecordKey); } - foreach (var entity in _authoritativeDaemonEntities.Values) + foreach (var entity in _observedEntityFacadesByRecordKey.Values) { entity.EntityInfoGathered.Clear(); entity.EntityHostility.Clear(); @@ -2717,15 +2717,15 @@ public class ActionGameManager : MonoBehaviour foreach (var entitySnapshot in entitySnapshots) { - if (!_authoritativeDaemonEntities.TryGetValue(entitySnapshot.RecordKey, out var entity)) + if (!_observedEntityFacadesByRecordKey.TryGetValue(entitySnapshot.RecordKey, out var entity)) continue; - RestoreEntityContactsFromTypedSnapshot(entity, entitySnapshot, _authoritativeDaemonEntities); - if (_authoritativeDaemonEntities.TryGetValue(entitySnapshot.TargetEntityKey, out var target)) + RestoreEntityContactsFromTypedSnapshot(entity, entitySnapshot, _observedEntityFacadesByRecordKey); + if (_observedEntityFacadesByRecordKey.TryGetValue(entitySnapshot.TargetEntityKey, out var target)) entity.Target.Value = target; } - if (_authoritativeDaemonEntities.TryGetValue(currentEntityKey, out var currentEntity) && + if (_observedEntityFacadesByRecordKey.TryGetValue(currentEntityKey, out var currentEntity) && CurrentEntity != currentEntity) { RestoreCurrentEntityBinding(currentEntity, Array.Empty()); @@ -2788,9 +2788,9 @@ public class ActionGameManager : MonoBehaviour entity.Target.Value = target; } - _authoritativeDaemonEntities.Clear(); + _observedEntityFacadesByRecordKey.Clear(); foreach (var restoredEntity in restoredEntities) - _authoritativeDaemonEntities[restoredEntity.Key] = restoredEntity.Value; + _observedEntityFacadesByRecordKey[restoredEntity.Key] = restoredEntity.Value; if (restoredEntities.TryGetValue(currentEntityKey, out var currentEntity)) RestoreCurrentEntityBinding(currentEntity, actionBarBindings); @@ -3135,6 +3135,42 @@ public class ActionGameManager : MonoBehaviour return FindDaemonZoneSnapshot(CurrentDaemonGalaxyZone); } + private bool TryQueryDaemonEntityContact( + Entity observer, + Entity target, + out AetheriaRuntimeDaemonEntityContact contact) + { + contact = default; + if (observer == null || target == null) + return false; + + return AetheriaRuntimeDaemonRenderQueries.TryQueryEntityContact( + FindCurrentDaemonZoneSnapshot(), + observer.DaemonEntityIndex, + target.DaemonEntityIndex, + out contact); + } + + private float GetObservedInfoGathered(Entity observer, Entity target) + { + if (TryQueryDaemonEntityContact(observer, target, out var contact)) + return (float)contact.InfoGathered; + + return observer != null && + target != null && + observer.EntityInfoGathered.TryGetValue(target, out var infoGathered) + ? infoGathered + : 0f; + } + + private bool IsObservedHostileContact(Entity observer, Entity target) + { + if (TryQueryDaemonEntityContact(observer, target, out var contact)) + return contact.Hostile; + + return target != null && target.IsHostileTo(observer); + } + public void RequestTowToStation() { TryRequestDaemonTowToStation(); @@ -3442,8 +3478,10 @@ public class ActionGameManager : MonoBehaviour if (target != null) { var threshold = Settings.GameplaySettings.TargetDetectionInfoThreshold; - TargetVisibilityFill.fillAmount = Mathf.Lerp(.25f, .75f, (CurrentEntity.EntityInfoGathered[target] - threshold) / (1 - threshold)); - VisibilityToTargetFill.fillAmount = Mathf.Lerp(.25f, .75f, target.EntityInfoGathered[CurrentEntity] / threshold); + var targetInfoGathered = GetObservedInfoGathered(CurrentEntity, target); + var visibilityToTarget = GetObservedInfoGathered(target, CurrentEntity); + TargetVisibilityFill.fillAmount = Mathf.Lerp(.25f, .75f, (targetInfoGathered - threshold) / (1 - threshold)); + VisibilityToTargetFill.fillAmount = Mathf.Lerp(.25f, .75f, visibilityToTarget / threshold); var targetHull = ItemManager.GetRuntimeItem(target.Hull); var targetMaxDurability = targetHull?.Durability > 0 ? (float)targetHull.Durability @@ -3887,7 +3925,7 @@ public class ActionGameManager : MonoBehaviour return ""; } - foreach (var pair in _authoritativeDaemonEntities) + foreach (var pair in _observedEntityFacadesByRecordKey) { if (ReferenceEquals(pair.Value, entity)) return pair.Key; @@ -4009,7 +4047,9 @@ public class ActionGameManager : MonoBehaviour foreach (var (targetLock, indicator, spin) in _lockingIndicators) { - var showLockingIndicator = targetLock.Lock > .01f && CurrentEntity.Target.Value != null && CurrentEntity.Target.Value.IsHostileTo(CurrentEntity); + var showLockingIndicator = targetLock.Lock > .01f && + CurrentEntity.Target.Value != null && + IsObservedHostileContact(CurrentEntity, CurrentEntity.Target.Value); indicator.gameObject.SetActive(showLockingIndicator); if(showLockingIndicator) { diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs index eacf49f58..361f1fdc0 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonRenderQueries.cs @@ -188,6 +188,44 @@ namespace GameCult.Aetheria.State.Verse public bool Hostile { get; } } + public readonly struct AetheriaRuntimeDaemonEntityContact + { + public AetheriaRuntimeDaemonEntityContact( + int observerEntityIndex, + int targetEntityIndex, + double targetPositionX, + double targetPositionZ, + double deltaX, + double deltaZ, + double distance, + double infoGathered, + bool hostile, + bool visible) + { + ObserverEntityIndex = observerEntityIndex; + TargetEntityIndex = targetEntityIndex; + TargetPositionX = targetPositionX; + TargetPositionZ = targetPositionZ; + DeltaX = deltaX; + DeltaZ = deltaZ; + Distance = Math.Max(0, distance); + InfoGathered = infoGathered; + Hostile = hostile; + Visible = visible; + } + + public int ObserverEntityIndex { get; } + public int TargetEntityIndex { get; } + public double TargetPositionX { get; } + public double TargetPositionZ { get; } + public double DeltaX { get; } + public double DeltaZ { get; } + public double Distance { get; } + public double InfoGathered { get; } + public bool Hostile { get; } + public bool Visible { get; } + } + public readonly struct AetheriaRuntimeDaemonWormholeExit { public AetheriaRuntimeDaemonWormholeExit( @@ -521,6 +559,48 @@ namespace GameCult.Aetheria.State.Verse return entityIndices.Count; } + public static bool TryQueryEntityContact( + AetheriaRuntimeZoneSnapshotCommit? zone, + int observerEntityIndex, + int targetEntityIndex, + out AetheriaRuntimeDaemonEntityContact entityContact) + { + entityContact = default; + if (zone == null || observerEntityIndex < 0 || targetEntityIndex < 0) + return false; + + var entities = BuildEntityMap(zone); + if (!entities.TryGetValue(observerEntityIndex, out var observer) || + !entities.TryGetValue(targetEntityIndex, out var target)) + { + return false; + } + + foreach (var contact in observer.Contacts ?? Array.Empty()) + { + if (contact == null || contact.TargetEntityIndex != targetEntityIndex) + continue; + + var deltaX = target.PositionX - observer.PositionX; + var deltaZ = target.PositionZ - observer.PositionZ; + var distance = Math.Sqrt(deltaX * deltaX + deltaZ * deltaZ); + entityContact = new AetheriaRuntimeDaemonEntityContact( + observer.EntityIndex, + target.EntityIndex, + target.PositionX, + target.PositionZ, + deltaX, + deltaZ, + distance, + contact.InfoGathered, + contact.Hostile, + contact.Visible); + return true; + } + + return false; + } + public static AetheriaRuntimeDaemonWormholeExit[] QueryWormholeExits( AetheriaRuntimeRunCheckpointCommit? run, AetheriaRuntimeZoneSnapshotCommit? zone,