Move target cycling into daemon operations

This commit is contained in:
Meta Vens 2026-06-21 11:41:32 +01:00
commit 1f47ea387b
7 changed files with 282 additions and 17 deletions

View file

@ -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<AetheriaRuntimeEntityContactCommit>()",
"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)",

View file

@ -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);

View file

@ -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,

View file

@ -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()
{

View file

@ -61,6 +61,9 @@ namespace GameCult.Aetheria.State.Verse
Ping,
SetTarget,
ClearTarget,
TargetNearest,
TargetNext,
TargetPrevious,
SetMoveVector,
SetLookDirection,
SetTractorPower,

View file

@ -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,

View file

@ -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<AetheriaRuntimeZoneSnapshotCommit>())
.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<AetheriaRuntimeEntitySnapshotCommit>();
foreach (var contact in actor.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
{
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)