From 1f47ea387b77e8a619d2c156e5cc0ae8ad252bdb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 21 Jun 2026 11:41:32 +0100 Subject: [PATCH] Move target cycling into daemon operations --- Aetheria.State.Verify/Program.cs | 40 +++++++++ Assets/Scripts/Gameplay/ActionGameManager.cs | 67 +++++++++++---- .../Gameplay/AetheriaDaemonOperations.cs | 15 ++++ .../Tests/DaemonRuntimeDocumentTests.cs | 73 ++++++++++++++++ .../Runtime/AetheriaRuntimeDaemonDocuments.cs | 3 + .../AetheriaRuntimeDaemonOperationClient.cs | 15 ++++ .../AetheriaRuntimeDaemonOperations.cs | 86 +++++++++++++++++++ 7 files changed, 282 insertions(+), 17 deletions(-) diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 8e010ab64..773071dcf 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -7686,6 +7686,10 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) var requiredTargetAuthoritySymbols = new[] { "ApplySetTarget(run, command)", + "ApplyTargetCycle(run, command, TargetCycleMode.Nearest)", + "ApplyTargetCycle(run, command, TargetCycleMode.Next)", + "ApplyTargetCycle(run, command, TargetCycleMode.Previous)", + "VisibleHostileTargets(zone, actor, actorIndex)", "actor.Contacts ?? Array.Empty()", "contact.TargetEntityIndex == targetIndex && contact.Visible" }; @@ -7699,6 +7703,42 @@ static void RequireUnityObserverDoesNotTickLocalSimulation(string root) string.Join(", ", missingTargetAuthoritySymbols)); } + var requiredUnityTargetRequestSymbols = new[] + { + "RequestTargetNearest()", + "RequestTargetNext()", + "RequestTargetPrevious()", + "observer.Operations.TargetNearest()", + "observer.Operations.TargetNext()", + "observer.Operations.TargetPrevious()" + }; + var unityTargetRequestSources = actionGameManager + "\n" + daemonOperations; + var missingUnityTargetRequestSymbols = requiredUnityTargetRequestSymbols + .Where(symbol => !unityTargetRequestSources.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + if (missingUnityTargetRequestSymbols.Length > 0) + { + throw new InvalidOperationException( + "Unity target cycling no longer lowers through typed daemon requests: " + + string.Join(", ", missingUnityTargetRequestSymbols)); + } + + var forbiddenUnityTargetCycleSymbols = new[] + { + ".MaxBy(x => CultMath.math.length(x.CultPosition - CurrentEntity.CultPosition))", + ".OrderBy(x => CultMath.math.length(x.CultPosition - CurrentEntity.CultPosition))", + "Array.IndexOf(targets" + }; + var unityTargetCycleHits = forbiddenUnityTargetCycleSymbols + .Where(symbol => actionGameManager.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + if (unityTargetCycleHits.Length > 0) + { + throw new InvalidOperationException( + "Unity target cycling still orders or selects targets from renderer-local entity state instead of daemon contacts: " + + string.Join(", ", unityTargetCycleHits)); + } + var requiredIntentAuthoritySymbols = new[] { "!HasWeaponGroup(entity, command.WeaponGroup)", diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index ce37b6bf7..bb492d71f 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -2067,31 +2067,17 @@ public class ActionGameManager : MonoBehaviour Input.Player.TargetNearest.performed += context => { - if(CurrentEntity.VisibleEnemies.Any()) - { - RequestTargetSelection(CurrentEntity.VisibleEnemies.Where(x => x != CurrentEntity) - .MaxBy(x => CultMath.math.length(x.CultPosition - CurrentEntity.CultPosition))); - } + RequestTargetNearest(); }; Input.Player.TargetNext.performed += context => { - if (!CurrentEntity.VisibleEnemies.Any()) return; - var targets = CurrentEntity.VisibleEnemies.Where(x => x != CurrentEntity) - .OrderBy(x => CultMath.math.length(x.CultPosition - CurrentEntity.CultPosition)) - .ToArray(); - var currentTargetIndex = Array.IndexOf(targets, CurrentEntity.Target.Value); - RequestTargetSelection(targets[(currentTargetIndex + 1) % targets.Length]); + RequestTargetNext(); }; Input.Player.TargetPrevious.performed += context => { - if (!CurrentEntity.VisibleEnemies.Any()) return; - var targets = CurrentEntity.VisibleEnemies.Where(x => x != CurrentEntity) - .OrderBy(x => CultMath.math.length(x.CultPosition - CurrentEntity.CultPosition)) - .ToArray(); - var currentTargetIndex = Array.IndexOf(targets, CurrentEntity.Target.Value); - RequestTargetSelection(targets[(currentTargetIndex + targets.Length - 1) % targets.Length]); + RequestTargetPrevious(); }; #endregion @@ -3743,6 +3729,21 @@ public class ActionGameManager : MonoBehaviour TryRequestDaemonTargetSelection(target); } + private void RequestTargetNearest() + { + TryRequestDaemonTargetCycle(AetheriaRuntimeDaemonCommandKinds.TargetNearest); + } + + private void RequestTargetNext() + { + TryRequestDaemonTargetCycle(AetheriaRuntimeDaemonCommandKinds.TargetNext); + } + + private void RequestTargetPrevious() + { + TryRequestDaemonTargetCycle(AetheriaRuntimeDaemonCommandKinds.TargetPrevious); + } + private bool TryRequestDaemonTargetSelection(Entity target) { var observer = ResolveDaemonObserver(); @@ -3775,6 +3776,38 @@ public class ActionGameManager : MonoBehaviour } } + private bool TryRequestDaemonTargetCycle(AetheriaRuntimeDaemonCommandKinds command) + { + var observer = ResolveDaemonObserver(); + if (observer == null || !observer.HasAuthoritativeState) + { + return false; + } + + try + { + switch (command) + { + case AetheriaRuntimeDaemonCommandKinds.TargetNearest: + observer.Operations.TargetNearest(); + return true; + case AetheriaRuntimeDaemonCommandKinds.TargetNext: + observer.Operations.TargetNext(); + return true; + case AetheriaRuntimeDaemonCommandKinds.TargetPrevious: + observer.Operations.TargetPrevious(); + return true; + default: + return false; + } + } + catch (Exception ex) + { + Debug.LogWarning($"Failed to send Aetheria daemon target cycle operation; operation not submitted: {ex.Message}"); + return false; + } + } + private void RequestOverrideShutdown(bool enabled) { TryRequestDaemonOverrideShutdown(enabled); diff --git a/Assets/Scripts/Gameplay/AetheriaDaemonOperations.cs b/Assets/Scripts/Gameplay/AetheriaDaemonOperations.cs index 5c4dc85c0..23fb13527 100644 --- a/Assets/Scripts/Gameplay/AetheriaDaemonOperations.cs +++ b/Assets/Scripts/Gameplay/AetheriaDaemonOperations.cs @@ -20,6 +20,21 @@ public sealed class AetheriaDaemonOperations return Send((client, observed) => client.ClearTarget(observed)); } + public AetheriaRuntimeDaemonCommandEnvelope TargetNearest() + { + return Send((client, observed) => client.TargetNearest(observed)); + } + + public AetheriaRuntimeDaemonCommandEnvelope TargetNext() + { + return Send((client, observed) => client.TargetNext(observed)); + } + + public AetheriaRuntimeDaemonCommandEnvelope TargetPrevious() + { + return Send((client, observed) => client.TargetPrevious(observed)); + } + public AetheriaRuntimeDaemonCommandEnvelope SetMoveVector( double directionX, double directionY, diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index d32cb40d0..a6a9823f9 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -1071,6 +1071,79 @@ public class DaemonRuntimeDocumentTests Assert.AreEqual(-1, run.Zones[0].Entities[0].TargetEntityIndex); } + [Test] + public void DaemonOperationsCyclesTargetsFromAuthoritativeContacts() + { + var run = RunWithTwoEntities(); + var zone = run.Zones[0]; + zone.Entities[0].PositionX = 0; + zone.Entities[0].PositionY = 0; + zone.Entities[1].PositionX = 5; + zone.Entities[1].PositionY = 0; + zone.Entities = zone.Entities + .Concat(new[] + { + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 2, + Name = "Near Hostile", + PositionX = 2, + PositionY = 0 + } + }) + .ToArray(); + zone.Entities[0].Contacts = new[] + { + new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = 1, + Visible = true, + Hostile = true + }, + new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = 2, + Visible = true, + Hostile = true + } + }; + + var nearest = AetheriaRuntimeDaemonCommandDocument.Create( + AetheriaRuntimeDaemonCommandKinds.TargetNearest, + "codex", + "session-target", + 15, + "zone.0.entity.0"); + var next = AetheriaRuntimeDaemonCommandDocument.Create( + AetheriaRuntimeDaemonCommandKinds.TargetNext, + "codex", + "session-target", + 16, + "zone.0.entity.0"); + var previous = AetheriaRuntimeDaemonCommandDocument.Create( + AetheriaRuntimeDaemonCommandKinds.TargetPrevious, + "codex", + "session-target", + 17, + "zone.0.entity.0"); + + var previousFromNoneResult = AetheriaRuntimeDaemonOperations.Execute(run, new[] { previous }); + Assert.AreEqual(1, previousFromNoneResult.AppliedCommandIds.Count); + Assert.AreEqual(2, zone.Entities[0].TargetEntityIndex); + + var nearestResult = AetheriaRuntimeDaemonOperations.Execute(run, new[] { nearest }); + Assert.AreEqual(1, nearestResult.AppliedCommandIds.Count); + Assert.AreEqual(2, zone.Entities[0].TargetEntityIndex); + + var nextResult = AetheriaRuntimeDaemonOperations.Execute(run, new[] { next }); + Assert.AreEqual(1, nextResult.AppliedCommandIds.Count); + Assert.AreEqual(1, zone.Entities[0].TargetEntityIndex); + + var previousResult = AetheriaRuntimeDaemonOperations.Execute(run, new[] { previous }); + Assert.AreEqual(1, previousResult.AppliedCommandIds.Count); + Assert.AreEqual(2, zone.Entities[0].TargetEntityIndex); + } + [Test] public void DaemonOperationsMutatesHotEntityControlsInDaemonState() { diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs index 2edeb5641..807ef858b 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs @@ -61,6 +61,9 @@ namespace GameCult.Aetheria.State.Verse Ping, SetTarget, ClearTarget, + TargetNearest, + TargetNext, + TargetPrevious, SetMoveVector, SetLookDirection, SetTractorPower, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationClient.cs index 8b03d3032..b2ed138cc 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationClient.cs @@ -117,6 +117,21 @@ namespace GameCult.Aetheria.State.Verse return Send(AetheriaRuntimeDaemonCommandKinds.ClearTarget, observed); } + public AetheriaRuntimeDaemonCommandEnvelope TargetNearest(AetheriaRuntimeObservedDaemonState? observed) + { + return Send(AetheriaRuntimeDaemonCommandKinds.TargetNearest, observed); + } + + public AetheriaRuntimeDaemonCommandEnvelope TargetNext(AetheriaRuntimeObservedDaemonState? observed) + { + return Send(AetheriaRuntimeDaemonCommandKinds.TargetNext, observed); + } + + public AetheriaRuntimeDaemonCommandEnvelope TargetPrevious(AetheriaRuntimeObservedDaemonState? observed) + { + return Send(AetheriaRuntimeDaemonCommandKinds.TargetPrevious, observed); + } + public AetheriaRuntimeDaemonCommandEnvelope SetMoveVector( AetheriaRuntimeObservedDaemonState? observed, double directionX, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs index 822ec8ede..4391d81c0 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs @@ -79,6 +79,12 @@ namespace GameCult.Aetheria.State.Verse return ApplySetTarget(run, command); case AetheriaRuntimeDaemonCommandKinds.ClearTarget: return ApplyClearTarget(run, command); + case AetheriaRuntimeDaemonCommandKinds.TargetNearest: + return ApplyTargetCycle(run, command, TargetCycleMode.Nearest); + case AetheriaRuntimeDaemonCommandKinds.TargetNext: + return ApplyTargetCycle(run, command, TargetCycleMode.Next); + case AetheriaRuntimeDaemonCommandKinds.TargetPrevious: + return ApplyTargetCycle(run, command, TargetCycleMode.Previous); case AetheriaRuntimeDaemonCommandKinds.SetLookDirection: return ApplySetLookDirection(run, command); case AetheriaRuntimeDaemonCommandKinds.SetTractorPower: @@ -189,6 +195,86 @@ namespace GameCult.Aetheria.State.Verse return true; } + private enum TargetCycleMode + { + Nearest, + Next, + Previous + } + + private static bool ApplyTargetCycle( + AetheriaRuntimeRunCheckpointCommit run, + AetheriaRuntimeDaemonCommandDocument command, + TargetCycleMode mode) + { + var actorKey = string.IsNullOrWhiteSpace(command.ActorEntityKey) + ? run.CurrentEntityKey + : command.ActorEntityKey; + if (!TryResolveEntity(run, actorKey, out var actorZone, out var actorIndex, out var actor)) + return false; + + var zone = (run.Zones ?? Array.Empty()) + .FirstOrDefault(candidate => candidate != null && candidate.ZoneIndex == actorZone); + if (zone == null) + return false; + + var orderedTargets = VisibleHostileTargets(zone, actor, actorIndex) + .OrderBy(target => target.DistanceSq) + .ThenBy(target => target.EntityIndex) + .ToArray(); + if (orderedTargets.Length == 0) + return false; + + if (mode == TargetCycleMode.Nearest) + { + actor.TargetEntityIndex = orderedTargets[0].EntityIndex; + return true; + } + + var currentIndex = Array.FindIndex( + orderedTargets, + target => target.EntityIndex == actor.TargetEntityIndex); + if (currentIndex < 0) + { + actor.TargetEntityIndex = orderedTargets[0].EntityIndex; + return true; + } + + var nextIndex = mode == TargetCycleMode.Next + ? (currentIndex + 1 + orderedTargets.Length) % orderedTargets.Length + : (currentIndex - 1 + orderedTargets.Length) % orderedTargets.Length; + actor.TargetEntityIndex = orderedTargets[nextIndex].EntityIndex; + return true; + } + + private static IEnumerable<(int EntityIndex, double DistanceSq)> VisibleHostileTargets( + AetheriaRuntimeZoneSnapshotCommit zone, + AetheriaRuntimeEntitySnapshotCommit actor, + int actorIndex) + { + var entities = zone.Entities ?? Array.Empty(); + foreach (var contact in actor.Contacts ?? Array.Empty()) + { + if (contact == null || + !contact.Visible || + !contact.Hostile || + contact.TargetEntityIndex == actorIndex || + contact.TargetEntityIndex < 0 || + contact.TargetEntityIndex >= entities.Count) + { + continue; + } + + var target = entities[contact.TargetEntityIndex]; + if (target == null) + continue; + + var deltaX = target.PositionX - actor.PositionX; + var deltaY = target.PositionY - actor.PositionY; + yield return (contact.TargetEntityIndex, (deltaX * deltaX) + (deltaY * deltaY)); + } + } + private static bool ApplyClearTarget( AetheriaRuntimeRunCheckpointCommit run, AetheriaRuntimeDaemonCommandDocument command)