diff --git a/Assets/Scripts/IADS/IADS.cs b/Assets/Scripts/IADS/IADS.cs index 7939cdf15..2a73e03e0 100644 --- a/Assets/Scripts/IADS/IADS.cs +++ b/Assets/Scripts/IADS/IADS.cs @@ -126,6 +126,9 @@ public void RegisterNewAsset(IInterceptor asset) { return; } + if (asset is InterceptorBase interceptorBase) { + interceptorBase.CommsParent = _commsAgent; + } _assets.Add(asset.HierarchicalAgent); } diff --git a/Assets/Scripts/Interceptors/CarrierBase.cs b/Assets/Scripts/Interceptors/CarrierBase.cs index f35363519..f8a2c2f3c 100644 --- a/Assets/Scripts/Interceptors/CarrierBase.cs +++ b/Assets/Scripts/Interceptors/CarrierBase.cs @@ -48,6 +48,9 @@ private IEnumerator ReleaseManager(float period) { if (agent is IInterceptor subInterceptor) { subInterceptor.OnAssignSubInterceptor += AssignSubInterceptor; subInterceptor.OnReassignTarget += ReassignTarget; + if (subInterceptor is InterceptorBase interceptorBase) { + interceptorBase.CommsParent = this; + } if (subInterceptor.Movement is MissileMovement movement) { movement.FlightPhase = Simulation.FlightPhase.Boost; } diff --git a/Assets/Scripts/Interceptors/InterceptorBase.cs b/Assets/Scripts/Interceptors/InterceptorBase.cs index 0609caeed..c85a8ee9c 100644 --- a/Assets/Scripts/Interceptors/InterceptorBase.cs +++ b/Assets/Scripts/Interceptors/InterceptorBase.cs @@ -5,6 +5,10 @@ // Base implementation of an interceptor. public abstract class InterceptorBase : AgentBase, IInterceptor { + // Optional mailbox receiver used to route requests to the logical parent interceptor or IADS + // proxy. + public IAgent CommsParent { get; set; } + public event InterceptorEventHandler OnHit; public event InterceptorEventHandler OnMiss; public event InterceptorEventHandler OnDestroyed; @@ -101,17 +105,17 @@ public bool EvaluateReassignedTarget(IHierarchical target) { } public void AssignSubInterceptor(IInterceptor subInterceptor) { - if (subInterceptor.CapacityRemaining <= 0) { + if (subInterceptor == null || subInterceptor.CapacityRemaining <= 0) { return; } // Find a new target for the sub-interceptor within the parent interceptor's assigned targets. IHierarchical target = HierarchicalAgent.FindNewTarget(subInterceptor.HierarchicalAgent, subInterceptor.CapacityRemaining); - // Evaluate the new target and decide whether to continue searching for other targets. - if (!subInterceptor.EvaluateReassignedTarget(target)) { - // Propagate the sub-interceptor target assignment to the parent interceptor above. - OnAssignSubInterceptor?.Invoke(subInterceptor); + if (target != null) { + SendAssignTargetToSub(subInterceptor, target); + } else { + SendAssignRequestToParent(subInterceptor); } } @@ -123,7 +127,7 @@ public void ReassignTarget(IHierarchical target) { // another sub-interceptor(s) to pursue the target(s). // 3. Propagate the target re-assignment to the parent interceptor above. if (CapacityPlannedRemaining <= 0) { - OnReassignTarget?.Invoke(target); + SendReassignRequestToParent(target); return; } @@ -155,7 +159,7 @@ protected override void FixedUpdate() { List escapingTargets = targetHierarchicals.Where(EscapeDetector.IsEscaping).ToList(); foreach (var target in escapingTargets) { - OnReassignTarget?.Invoke(target); + SendReassignRequestToParent(target); } if (escapingTargets.Count == targetHierarchicals.Count) { RequestReassignment(this); @@ -279,7 +283,7 @@ private void RegisterMiss(IInterceptor interceptor) { RequestTargetReassignment(interceptor); // Request a new target from the parent interceptor. - OnAssignSubInterceptor?.Invoke(interceptor); + SendAssignRequestToParent(interceptor); } private void RegisterDestroyed(IInterceptor interceptor) { @@ -296,7 +300,7 @@ private void RequestTargetReassignment(IInterceptor interceptor) { List targetHierarchicals = target.LeafHierarchicals(activeOnly: true, withTargetOnly: false); foreach (var targetHierarchical in targetHierarchicals) { - OnReassignTarget?.Invoke(targetHierarchical); + SendReassignRequestToParent(targetHierarchical); } RequestReassignment(interceptor); @@ -305,7 +309,7 @@ private void RequestTargetReassignment(IInterceptor interceptor) { private void RequestReassignment(IInterceptor interceptor) { if (interceptor.IsReassignable) { // Request a new target from the parent interceptor. - OnAssignSubInterceptor?.Invoke(interceptor); + SendAssignRequestToParent(interceptor); } } @@ -335,7 +339,7 @@ private IEnumerator UnassignedTargetsManager(float period) { filteredTargets.OrderBy(target => Vector3.Distance(Position, target.Position)); var excessTargets = orderedTargets.Skip(CapacityPlannedRemaining); foreach (var target in excessTargets) { - OnReassignTarget?.Invoke(target); + SendReassignRequestToParent(target); } unassignedTargets = orderedTargets.Take(CapacityPlannedRemaining); } else { @@ -362,4 +366,71 @@ private IEnumerator UnassignedTargetsManager(float period) { newSubHierarchical.RecursiveCluster(maxClusterSize: CapacityPerSubInterceptor); } } + + // Send an AssignTargetRequest message to the parent mailbox receiver. + private void SendAssignRequestToParent(IInterceptor subInterceptor) { + IAgent parent = CommsParent; + if (subInterceptor == null) { + return; + } + if (parent == null) { + OnAssignSubInterceptor?.Invoke(subInterceptor); + return; + } + SendMailboxMessage(new AssignTargetRequestMessage(this, parent, subInterceptor)); + } + + // Send a ReassignTargetRequest message to the parent mailbox receiver. + private void SendReassignRequestToParent(IHierarchical target) { + IAgent parent = CommsParent; + if (target == null) { + return; + } + if (parent == null) { + OnReassignTarget?.Invoke(target); + return; + } + SendMailboxMessage(new ReassignTargetRequestMessage(this, parent, target)); + } + + // Send an AssignTargetResponse message to the child mailbox receiver. + private void SendAssignTargetToSub(IInterceptor subInterceptor, IHierarchical target) { + if (subInterceptor == null || target == null) { + return; + } + SendMailboxMessage(new AssignTargetResponseMessage(this, subInterceptor, target)); + } + + // Send a message through the mailbox. + private void SendMailboxMessage(Message message) { + if (message == null) { + return; + } + Mailbox mailbox = Mailbox.GetOrCreateInstance(); + if (mailbox == null) { + return; + } + mailbox.Send(message); + } + + // Execution happens here after receiving and reading message. PayloadData is read and handled. + protected override void OnMessage(Message message) { + if (message == null) { + return; + } + switch (message) { + case AssignTargetRequestMessage assignRequest: + AssignSubInterceptor(assignRequest.PayloadData.SubInterceptor); + break; + case ReassignTargetRequestMessage reassignRequest: + ReassignTarget(reassignRequest.PayloadData.Target); + break; + case AssignTargetResponseMessage assignTarget: + IHierarchical assignedTarget = assignTarget.PayloadData.Target; + if (assignedTarget != null) { + EvaluateReassignedTarget(assignedTarget); + } + break; + } + } } diff --git a/Assets/Tests/EditMode/IADSMailboxTests.cs b/Assets/Tests/EditMode/IADSMailboxTests.cs index 59f6dfd43..b7c658be5 100644 --- a/Assets/Tests/EditMode/IADSMailboxTests.cs +++ b/Assets/Tests/EditMode/IADSMailboxTests.cs @@ -174,6 +174,24 @@ public void MailboxDelivery_ForDifferentReceiver_DoesNotTriggerIadsResponse() { Assert.IsNull(deliveredResponse); } + // Verifies that top-level launchers route mailbox requests to the IADS proxy. + [Test] + public void RegisterNewLauncher_SetsCommsParentToIadsProxy() { + var launcherObject = new GameObject("Launcher"); + try { + var launcher = launcherObject.AddComponent(); + launcherObject.AddComponent(); + launcher.HierarchicalAgent = new HierarchicalAgent(launcher); + launcher.InvokeAwakeForTest(); + + _iads.RegisterNewLauncher(launcher); + + Assert.AreSame(_commsAgent, launcher.CommsParent); + } finally { + Object.DestroyImmediate(launcherObject); + } + } + private static SimManager CreateSimManagerStub() { return (SimManager)FormatterServices.GetUninitializedObject(typeof(SimManager)); } @@ -333,4 +351,18 @@ public void Terminate() { public Transformation GetRelativeTransformation(IHierarchical target) => default; public Transformation GetRelativeTransformation(in Vector3 waypoint) => default; } + + private sealed class TestLauncherInterceptor : InterceptorBase, IAgent { + public override bool IsPursuer => false; + + public void InvokeAwakeForTest() { + base.Awake(); + } + + void IAgent.CreateTargetModel(IHierarchical target) {} + + void IAgent.DestroyTargetModel() {} + + void IAgent.UpdateTargetModel() {} + } } diff --git a/Assets/Tests/EditMode/IADSMailboxTests.cs.meta b/Assets/Tests/EditMode/IADSMailboxTests.cs.meta new file mode 100644 index 000000000..47e831d52 --- /dev/null +++ b/Assets/Tests/EditMode/IADSMailboxTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 21e3b19c559d14154a7b6884582418ab \ No newline at end of file diff --git a/Assets/Tests/EditMode/InterceptorBaseMailboxTests.cs b/Assets/Tests/EditMode/InterceptorBaseMailboxTests.cs new file mode 100644 index 000000000..99cce955e --- /dev/null +++ b/Assets/Tests/EditMode/InterceptorBaseMailboxTests.cs @@ -0,0 +1,339 @@ +using NUnit.Framework; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using UnityEngine; + +public class InterceptorBaseMailboxTests : TestBase { + private Mailbox _mailbox; + private SimManager _simManager; + private TestInterceptor _interceptor; + + [SetUp] + public void SetUp() { + SetMailboxInstance(null); + SetSimManagerInstance(null); + + _simManager = CreateSimManagerStub(); + SetPrivateField(_simManager, "_dummyAgents", new List()); + SetSimManagerInstance(_simManager); + SetElapsedTime(0f); + + _mailbox = new GameObject("Mailbox").AddComponent(); + + _interceptor = new GameObject("Interceptor").AddComponent(); + _interceptor.gameObject.AddComponent(); + _interceptor.HierarchicalAgent = new HierarchicalAgent(_interceptor); + _interceptor.InvokeAwakeForTest(); + _interceptor.StaticConfig = CreateStaticConfig(Configs.AgentType.CarrierInterceptor); + } + + [TearDown] + public void TearDown() { + if (_interceptor?.TargetModel != null) { + _interceptor.DestroyTargetModel(); + } + if (_interceptor != null) { + Object.DestroyImmediate(_interceptor.gameObject); + } + if (_mailbox != null) { + Object.DestroyImmediate(_mailbox.gameObject); + } + SetMailboxInstance(null); + SetSimManagerInstance(null); + } + + // Verifies that a mailbox-delivered AssignTargetResponse message updates the interceptor target. + [Test] + public void MailboxDelivery_AssignTargetResponseMessage_AssignsHierarchicalTarget() { + var target = new FixedHierarchical(position: new Vector3(10f, 0f, 0f)); + var message = new AssignTargetResponseMessage(new StubAgent(Configs.AgentType.Vessel), + _interceptor, target); + + _mailbox.Configure(null); + _mailbox.Send(message); + + InvokePrivateMethod(_mailbox, "Update"); + + Assert.AreSame(target, _interceptor.HierarchicalAgent.Target); + } + + // Verifies that a mailbox-delivered reassignment request is queued for later clustering. + [Test] + public void MailboxDelivery_ReassignTargetRequest_QueuesUnassignedTarget() { + SetPrivateField(_interceptor, "_capacityPerSubInterceptor", 1); + SetPrivateField(_interceptor, "_numSubInterceptorsPlannedRemaining", 1); + + var target = new FixedHierarchical(position: new Vector3(5f, 0f, 0f)); + var message = new ReassignTargetRequestMessage(new StubAgent(Configs.AgentType.Vessel), + _interceptor, target); + + _mailbox.Configure(null); + _mailbox.Send(message); + + InvokePrivateMethod(_mailbox, "Update"); + + var queuedTargets = GetPrivateField>(_interceptor, "_unassignedTargets"); + Assert.True(queuedTargets.Contains(target)); + } + + // Verifies that a mailbox-delivered AssignTargetRequest message to a parent interceptor produces + // an AssignTargetResponse message for the requesting sub-interceptor. + [Test] + public void MailboxDelivery_AssignTargetRequest_SendsAssignTargetResponseToSubInterceptor() { + SetPrivateField(_interceptor, "_capacityPerSubInterceptor", 1); + var expectedTarget = new FixedHierarchical(position: new Vector3(12f, 0f, 0f)); + _interceptor.HierarchicalAgent.Target = CreateCluster(expectedTarget); + + var subInterceptor = new StubInterceptor(Configs.AgentType.MissileInterceptor, capacity: 1); + subInterceptor.HierarchicalAgent = new HierarchicalAgent(subInterceptor); + + AssignTargetResponseMessage deliveredMessage = null; + _mailbox.OnMessageDelivered += (_, message) => { + if (message is AssignTargetResponseMessage assignTargetResponse && + ReferenceEquals(assignTargetResponse.Receiver, subInterceptor)) { + deliveredMessage = assignTargetResponse; + } + }; + + _mailbox.Configure(null); + _mailbox.Send(new AssignTargetRequestMessage(new StubAgent(Configs.AgentType.Vessel), + _interceptor, subInterceptor)); + + InvokePrivateMethod(_mailbox, "Update"); + Assert.IsNull(deliveredMessage); + + InvokePrivateMethod(_mailbox, "Update"); + Assert.NotNull(deliveredMessage); + Assert.AreSame(_interceptor, deliveredMessage.Sender); + Assert.AreSame(subInterceptor, deliveredMessage.Receiver); + + List assignedTargets = deliveredMessage.PayloadData.Target.LeafHierarchicals( + activeOnly: true, withTargetOnly: false); + Assert.AreEqual(1, assignedTargets.Count); + Assert.AreSame(expectedTarget, assignedTargets[0]); + } + + // Verifies that a sub-interceptor assignment request is sent through the mailbox to the parent + // receiver when no target is currently available. + [Test] + public void AssignSubInterceptor_WithoutTarget_SendsAssignTargetRequestToParentMailboxReceiver() { + var parent = new StubAgent(Configs.AgentType.Vessel); + _interceptor.CommsParent = parent; + + var subInterceptor = new StubInterceptor(Configs.AgentType.MissileInterceptor, capacity: 1); + subInterceptor.HierarchicalAgent = new HierarchicalAgent(subInterceptor); + + AssignTargetRequestMessage deliveredMessage = null; + _mailbox.OnMessageDelivered += (_, message) => { + if (message is AssignTargetRequestMessage assignRequest && + ReferenceEquals(assignRequest.Receiver, parent)) { + deliveredMessage = assignRequest; + } + }; + + _mailbox.Configure(null); + _interceptor.AssignSubInterceptor(subInterceptor); + + InvokePrivateMethod(_mailbox, "Update"); + + Assert.NotNull(deliveredMessage); + Assert.AreSame(_interceptor, deliveredMessage.Sender); + Assert.AreSame(parent, deliveredMessage.Receiver); + Assert.AreSame(subInterceptor, deliveredMessage.PayloadData.SubInterceptor); + } + + // Verifies that reassignment propagation uses the mailbox when the interceptor has no remaining + // planned capacity. + [Test] + public void ReassignTarget_WithoutPlannedCapacity_SendsRequestToParentMailboxReceiver() { + SetPrivateField(_interceptor, "_capacityPerSubInterceptor", 1); + SetPrivateField(_interceptor, "_numSubInterceptorsPlannedRemaining", 0); + _interceptor.CommsParent = new StubAgent(Configs.AgentType.ShoreBattery); + + var target = new FixedHierarchical(position: new Vector3(3f, 0f, 0f)); + ReassignTargetRequestMessage deliveredMessage = null; + _mailbox.OnMessageDelivered += (_, message) => { + if (message is ReassignTargetRequestMessage reassignRequest && + ReferenceEquals(reassignRequest.Receiver, _interceptor.CommsParent)) { + deliveredMessage = reassignRequest; + } + }; + + _mailbox.Configure(null); + _interceptor.ReassignTarget(target); + + InvokePrivateMethod(_mailbox, "Update"); + + Assert.NotNull(deliveredMessage); + Assert.AreSame(_interceptor, deliveredMessage.Sender); + Assert.AreSame(_interceptor.CommsParent, deliveredMessage.Receiver); + Assert.AreSame(target, deliveredMessage.PayloadData.Target); + } + + private static SimManager CreateSimManagerStub() { + return (SimManager)FormatterServices.GetUninitializedObject(typeof(SimManager)); + } + + private static Configs.StaticConfig CreateStaticConfig(Configs.AgentType agentType) { + return new Configs.StaticConfig { + AgentType = agentType, + BodyConfig = new Configs.BodyConfig { Mass = 1f, CrossSectionalArea = 1f }, + LiftDragConfig = new Configs.LiftDragConfig { DragCoefficient = 1f, LiftDragRatio = 1f }, + }; + } + + private static HierarchicalBase CreateCluster(params IHierarchical[] targets) { + var cluster = new HierarchicalBase(); + foreach (IHierarchical target in targets) { + cluster.AddSubHierarchical(target); + } + return cluster; + } + + private static void SetMailboxInstance(Mailbox mailbox) { + FieldInfo instanceField = typeof(Mailbox).GetField( + "k__BackingField", BindingFlags.NonPublic | BindingFlags.Static); + instanceField.SetValue(null, mailbox); + } + + private static void SetSimManagerInstance(SimManager simManager) { + FieldInfo instanceField = + typeof(SimManager) + .GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Static); + instanceField.SetValue(null, simManager); + } + + private void SetElapsedTime(float elapsedTime) { + FieldInfo elapsedTimeField = typeof(SimManager) + .GetField("k__BackingField", + BindingFlags.NonPublic | BindingFlags.Instance); + elapsedTimeField.SetValue(_simManager, elapsedTime); + } + + private sealed class TestInterceptor : InterceptorBase, IAgent { + public void InvokeAwakeForTest() { + base.Awake(); + } + + void IAgent.CreateTargetModel(IHierarchical target) {} + + void IAgent.DestroyTargetModel() {} + + void IAgent.UpdateTargetModel() {} + } + + private sealed class StubInterceptor : IInterceptor { + private readonly int _capacity; + + public event InterceptorEventHandler OnHit; + public event InterceptorEventHandler OnMiss; + public event InterceptorEventHandler OnDestroyed; + public event InterceptorEventHandler OnAssignSubInterceptor; + public event TargetReassignEventHandler OnReassignTarget; + public event AgentTerminatedEventHandler OnTerminated; + + public HierarchicalAgent HierarchicalAgent { get; set; } + public Configs.StaticConfig StaticConfig { get; set; } + public Configs.AgentConfig AgentConfig { get; set; } + public IMovement Movement { get; set; } + public IController Controller { get; set; } + public ISensor Sensor { get; set; } + public IAgent TargetModel { get; set; } + public Vector3 Position { get; set; } + public Vector3 Velocity { get; set; } = Vector3.forward; + public float Speed => Velocity.magnitude; + public Vector3 Acceleration { get; set; } + public Vector3 AccelerationInput { get; set; } + public bool IsPursuer => true; + public float ElapsedTime => 0f; + public bool IsTerminated { get; private set; } + public GameObject gameObject => null; + public Transform Transform => null; + public Vector3 Up => Vector3.up; + public Vector3 Forward => Vector3.forward; + public Vector3 Right => Vector3.right; + public Quaternion InverseRotation => Quaternion.identity; + public IEscapeDetector EscapeDetector { get; set; } + public int Capacity => _capacity; + public int CapacityPerSubInterceptor => _capacity; + public int CapacityPlannedRemaining => _capacity; + public int CapacityRemaining => _capacity; + public int NumSubInterceptors => 0; + public int NumSubInterceptorsPlannedRemaining => 0; + public int NumSubInterceptorsRemaining => 0; + public bool IsReassignable => true; + + public StubInterceptor(Configs.AgentType agentType, int capacity) { + _capacity = capacity; + StaticConfig = CreateStaticConfig(agentType); + } + + public float MaxForwardAcceleration() => 1f; + public float MaxNormalAcceleration() => 1f; + public void CreateTargetModel(IHierarchical target) {} + public void DestroyTargetModel() {} + public void UpdateTargetModel() {} + public bool EvaluateReassignedTarget(IHierarchical target) => false; + public void AssignSubInterceptor(IInterceptor subInterceptor) { + OnAssignSubInterceptor?.Invoke(subInterceptor); + } + public void ReassignTarget(IHierarchical target) { + OnReassignTarget?.Invoke(target); + } + + public void Terminate() { + IsTerminated = true; + OnTerminated?.Invoke(this); + } + + public Transformation GetRelativeTransformation(IAgent target) => default; + public Transformation GetRelativeTransformation(IHierarchical target) => default; + public Transformation GetRelativeTransformation(in Vector3 waypoint) => default; + } + + private sealed class StubAgent : IAgent { + public event AgentTerminatedEventHandler OnTerminated; + + public HierarchicalAgent HierarchicalAgent { get; set; } + public Configs.StaticConfig StaticConfig { get; set; } + public Configs.AgentConfig AgentConfig { get; set; } + public IMovement Movement { get; set; } + public IController Controller { get; set; } + public ISensor Sensor { get; set; } + public IAgent TargetModel { get; set; } + public Vector3 Position { get; set; } + public Vector3 Velocity { get; set; } + public float Speed => Velocity.magnitude; + public Vector3 Acceleration { get; set; } + public Vector3 AccelerationInput { get; set; } + public bool IsPursuer => false; + public float ElapsedTime => 0f; + public bool IsTerminated { get; private set; } + public GameObject gameObject => null; + public Transform Transform => null; + public Vector3 Up => Vector3.up; + public Vector3 Forward => Vector3.forward; + public Vector3 Right => Vector3.right; + public Quaternion InverseRotation => Quaternion.identity; + + public StubAgent(Configs.AgentType agentType) { + StaticConfig = CreateStaticConfig(agentType); + } + + public float MaxForwardAcceleration() => 0f; + public float MaxNormalAcceleration() => 0f; + public void CreateTargetModel(IHierarchical target) {} + public void DestroyTargetModel() {} + public void UpdateTargetModel() {} + + public void Terminate() { + IsTerminated = true; + OnTerminated?.Invoke(this); + } + + public Transformation GetRelativeTransformation(IAgent target) => default; + public Transformation GetRelativeTransformation(IHierarchical target) => default; + public Transformation GetRelativeTransformation(in Vector3 waypoint) => default; + } +} diff --git a/Assets/Tests/EditMode/InterceptorBaseMailboxTests.cs.meta b/Assets/Tests/EditMode/InterceptorBaseMailboxTests.cs.meta new file mode 100644 index 000000000..9bbbe3433 --- /dev/null +++ b/Assets/Tests/EditMode/InterceptorBaseMailboxTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 645d0f38b2a5d4766933397deb52320c \ No newline at end of file