diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 692a78ab3..16e4fbad5 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -24,6 +24,38 @@ internal sealed class AetheriaDaemonYmirSmokeChecks AgentMinesAsteroidThroughEquippedBehavior(); CargoCapacityComesFromHullAndCatalogVolumes(); AgentSurveysBodyIntoCorporationKnowledge(); + AgentTowsStationIntoPersistentOrbit(); + } + + private static void AgentTowsStationIntoPersistentOrbit() + { + var tug = Entity(0, 0, "workers"); + var station = Entity(1, 0, "workers"); + station.Kind = "station"; + tug.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Tow]; + var zone = new AetheriaRuntimeZoneSnapshotCommit + { + ZoneIndex = 0, Entities = [tug, station], + Orbits = [new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "parent-orbit", FixedPositionX = 20 }], + Bodies = [new AetheriaRuntimeBodySnapshotCommit { BodyKey = "parent-body", Kind = "planet", OrbitKey = "parent-orbit" }] + }; + var run = new AetheriaRuntimeRunCheckpointCommit + { + CurrentZoneIndex = 0, CurrentEntityKey = "zone.0.entity.0", Zones = [zone], + AgentTasks = [new AetheriaRuntimeAgentTaskCommit { TaskId = "tow-1", CorporationKey = "workers", TaskType = AetheriaRuntimeAgentTaskTypes.Tow, ZoneIndex = 0, Status = AetheriaRuntimeAgentTaskStatuses.Queued, TargetEntityIndex = 1, OrbitParentKey = "parent-body", OrbitDistance = 20, CompletionRadius = 5 }] + }; + AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-tow-attach-smoke.cc"), run, + new AetheriaRuntimeDaemonTickOptions { FrameId = 1, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 1, BuildPublications = false }); + Require(run.Zones[0].Entities[0].ChildEntityIndices.Contains(1), "tow pickup must attach station to tug parentage"); + RequireEqual("", run.Zones[0].Entities[1].OrbitKey, "attached station must no longer own an orbit"); + + AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-tow-detach-smoke.cc"), run, + new AetheriaRuntimeDaemonTickOptions { FrameId = 2, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 2, BuildPublications = false }); + Require(!run.Zones[0].Entities[0].ChildEntityIndices.Contains(1), "tow delivery must detach station parentage"); + Require(!string.IsNullOrWhiteSpace(run.Zones[0].Entities[1].OrbitKey), "delivered station must own a persistent orbit"); + Require(run.Zones[0].Orbits.Any(orbit => orbit.OrbitKey == run.Zones[0].Entities[1].OrbitKey && orbit.ParentOrbitKey == "parent-orbit" && Math.Abs(orbit.Distance - 20) < 0.001), + "delivered orbit must preserve requested parent and radius"); + RequireEqual(AetheriaRuntimeAgentTaskStatuses.Completed, run.AgentTasks[0].Status, "tow task must complete only after detach applies"); } private static void AgentSurveysBodyIntoCorporationKnowledge() diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs index e8a5112cc..b86b784ab 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs @@ -55,6 +55,8 @@ namespace GameCult.Aetheria.State.Verse [Key(19)] public string Phase { get; set; } = ""; [Key(20)] public IReadOnlyList TargetBodyKeys { get; set; } = Array.Empty(); [Key(21)] public int CircuitIndex { get; set; } + [Key(22)] public string OrbitParentKey { get; set; } = ""; + [Key(23)] public double OrbitDistance { get; set; } } [MessagePackObject] @@ -74,6 +76,8 @@ namespace GameCult.Aetheria.State.Verse [Key(11)] public string ItemKey { get; set; } = ""; [Key(12)] public int Quantity { get; set; } [Key(13)] public IReadOnlyList TargetBodyKeys { get; set; } = Array.Empty(); + [Key(14)] public string OrbitParentKey { get; set; } = ""; + [Key(15)] public double OrbitDistance { get; set; } } public static class AetheriaRuntimeAgentScheduler @@ -137,6 +141,15 @@ namespace GameCult.Aetheria.State.Verse if (agent != null) Complete(task, agent, frameId); } + foreach (var task in (run?.AgentTasks ?? Array.Empty()) + .Where(task => task != null && string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Tow, StringComparison.Ordinal))) + { + var detach = CommandId(task, frameId, "detach"); + var agent = FindEntity(run, task.ZoneIndex, task.AssignedEntityIndex); + if (agent == null) continue; + if (applied.Contains(detach)) Complete(task, agent, frameId); + else if (rejected.Contains(detach)) Fail(task, agent); + } } private static void AssignQueuedTasks(AetheriaRuntimeRunCheckpointCommit run, long frameId) @@ -210,6 +223,11 @@ namespace GameCult.Aetheria.State.Verse commands.AddRange(PlanPatrol(zone!, task, agent, frameId)); continue; } + if (string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Tow, StringComparison.Ordinal)) + { + commands.AddRange(PlanTow(run, zone!, task, agent, frameId)); + continue; + } var target = task.TargetEntityIndex < 0 ? null @@ -455,6 +473,41 @@ namespace GameCult.Aetheria.State.Verse }; } + private static IReadOnlyList PlanTow( + AetheriaRuntimeRunCheckpointCommit run, + AetheriaRuntimeZoneSnapshotCommit zone, + AetheriaRuntimeAgentTaskCommit task, + AetheriaRuntimeEntitySnapshotCommit agent, + long frameId) + { + var station = zone.Entities.FirstOrDefault(value => value != null && value.EntityIndex == task.TargetEntityIndex); + var parentBody = (zone.Bodies ?? Array.Empty()) + .FirstOrDefault(value => value != null && string.Equals(value.BodyKey, task.OrbitParentKey, StringComparison.Ordinal)); + var parent = AetheriaRuntimeDaemonRenderQueries.QueryBodyPoses(zone) + .FirstOrDefault(value => string.Equals(value.BodyKey, task.OrbitParentKey, StringComparison.Ordinal)); + if (station == null || parentBody == null || string.IsNullOrWhiteSpace(parent.BodyKey) || string.IsNullOrWhiteSpace(parentBody.OrbitKey)) { Fail(task, agent); return Array.Empty(); } + var attached = (agent.ChildEntityIndices ?? Array.Empty()).Contains(station.EntityIndex); + if (!attached) + { + var dx = station.PositionX - agent.PositionX; var dz = station.PositionZ - agent.PositionZ; + if (Math.Sqrt(dx * dx + dz * dz) > Math.Max(0.01, task.CompletionRadius)) + return new[] { Movement(task, agent, frameId, dx, dz, 1) }; + task.Phase = "pickup"; + return new[] { Movement(task, agent, frameId, 0, 0, 0), Command(task, agent, frameId, AetheriaRuntimeDaemonCommandKinds.TowToStation, "attach", command => { command.TargetEntityKey = EntityKey(run, task.ZoneIndex, station.EntityIndex); command.TextValue = "attach"; }) }; + } + task.Phase = "delivery"; + var radialX = agent.PositionX - parent.CenterX; var radialZ = agent.PositionZ - parent.CenterZ; + var length = Math.Sqrt(radialX * radialX + radialZ * radialZ); + if (length < 0.001) { radialX = 1; radialZ = 0; length = 1; } + var destinationX = parent.CenterX + radialX / length * task.OrbitDistance; + var destinationZ = parent.CenterZ + radialZ / length * task.OrbitDistance; + var dxDelivery = destinationX - agent.PositionX; var dzDelivery = destinationZ - agent.PositionZ; + if (Math.Sqrt(dxDelivery * dxDelivery + dzDelivery * dzDelivery) > Math.Max(0.01, task.CompletionRadius)) + return new[] { Movement(task, agent, frameId, dxDelivery, dzDelivery, 1) }; + task.Phase = "detach"; + return new[] { Movement(task, agent, frameId, 0, 0, 0), Command(task, agent, frameId, AetheriaRuntimeDaemonCommandKinds.TowToStation, "detach", command => { command.TargetEntityKey = EntityKey(run, task.ZoneIndex, station.EntityIndex); command.TextValue = "detach"; command.SubjectKey = parentBody.OrbitKey; command.ScalarValue = task.OrbitDistance; command.PositionX = destinationX; command.PositionZ = destinationZ; }) }; + } + private static IReadOnlyList PlanPatrol( AetheriaRuntimeZoneSnapshotCommit zone, AetheriaRuntimeAgentTaskCommit task, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs index d9c3428e5..f5ead8a29 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonGameSurfaceBuilder.cs @@ -190,6 +190,8 @@ namespace GameCult.Aetheria.State.Verse ("phase", task.Phase), ("targetBodyKeys", string.Join(",", task.TargetBodyKeys ?? Array.Empty())), ("circuitIndex", task.CircuitIndex.ToString(CultureInfo.InvariantCulture)), + ("orbitParentKey", task.OrbitParentKey), + ("orbitDistance", task.OrbitDistance.ToString("0.###", CultureInfo.InvariantCulture)), ("targetPosition", string.Join(",", new[] { task.TargetPositionX.ToString("0.###", CultureInfo.InvariantCulture), diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs index 39885db52..ba8bfdc17 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs @@ -19,8 +19,6 @@ namespace GameCult.Aetheria.State.Verse new List(); public List Wormholes { get; } = new List(); - public List Towing { get; } = - new List(); public bool SensorPingRequested { get; set; } public bool HasAny => @@ -30,8 +28,7 @@ namespace GameCult.Aetheria.State.Verse Behaviors.Count > 0 || Consumables.Count > 0 || Docking.Count > 0 || - Wormholes.Count > 0 || - Towing.Count > 0; + Wormholes.Count > 0; } public sealed class AetheriaRuntimeDaemonMovementIntent @@ -82,12 +79,4 @@ namespace GameCult.Aetheria.State.Verse public double PositionY { get; set; } } - public sealed class AetheriaRuntimeDaemonTowIntent - { - public string ActorEntityKey { get; set; } = ""; - public string StationEntityKey { get; set; } = ""; - public int TargetZoneIndex { get; set; } = -1; - public double PositionX { get; set; } - public double PositionY { get; set; } - } } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs index a313f04f5..76dc919f1 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs @@ -176,7 +176,7 @@ namespace GameCult.Aetheria.State.Verse case AetheriaRuntimeDaemonCommandKinds.EnterWormhole: return ApplyEnterWormholeIntent(run, command, context.Intents); case AetheriaRuntimeDaemonCommandKinds.TowToStation: - return ApplyTowToStationIntent(run, command, context.Intents); + return ApplyTowToStation(run, command, context.DockingDistance); case AetheriaRuntimeDaemonCommandKinds.IssueAgentTask: return ApplyIssueAgentTask(run, command); case AetheriaRuntimeDaemonCommandKinds.CancelAgentTask: @@ -215,6 +215,9 @@ namespace GameCult.Aetheria.State.Verse { return false; } + if (string.Equals(taskType, AetheriaRuntimeAgentTaskTypes.Tow, StringComparison.Ordinal) && + (request.TargetEntityIndex < 0 || string.IsNullOrWhiteSpace(request.OrbitParentKey) || request.OrbitDistance <= 0)) + return false; run.AgentTasks = (run.AgentTasks ?? Array.Empty()) .Concat(new[] @@ -238,6 +241,8 @@ namespace GameCult.Aetheria.State.Verse ? "pickup" : "", TargetBodyKeys = request.TargetBodyKeys ?? Array.Empty(), + OrbitParentKey = request.OrbitParentKey ?? "", + OrbitDistance = request.OrbitDistance, Status = AetheriaRuntimeAgentTaskStatuses.Queued } }) @@ -1498,38 +1503,29 @@ namespace GameCult.Aetheria.State.Verse return true; } - private static bool ApplyTowToStationIntent( + private static bool ApplyTowToStation( AetheriaRuntimeRunCheckpointCommit run, AetheriaRuntimeDaemonCommandDocument command, - AetheriaRuntimeDaemonIntentState intents) + double attachmentDistance) { var actor = ResolveActorEntityKey(run, command); - if (string.IsNullOrWhiteSpace(actor) || - string.IsNullOrWhiteSpace(command.TargetEntityKey) || - !TryResolveEntity( - run, - command.TargetEntityKey, - out var stationZoneIndex, - out _, - out var station)) - { + if (!TryResolveEntity(run, actor, out var actorZone, out _, out var towing) || + !TryResolveEntity(run, command.TargetEntityKey, out var stationZone, out var stationIndex, out var station) || actorZone != stationZone) return false; + var children = (towing.ChildEntityIndices ?? Array.Empty()).ToList(); + if (string.Equals(command.TextValue, "attach", StringComparison.Ordinal)) + { + if (children.Contains(stationIndex) || Math.Pow(station.PositionX - towing.PositionX, 2) + Math.Pow(station.PositionZ - towing.PositionZ, 2) > attachmentDistance * attachmentDistance) + return false; + children.Add(stationIndex); towing.ChildEntityIndices = children; station.OrbitKey = ""; station.PositionX = towing.PositionX; station.PositionZ = towing.PositionZ; return true; } - - var destinationX = station.PositionX; - var destinationY = station.PositionZ; - if (!MoveEntityToZone(run, actor, stationZoneIndex, destinationX, destinationY, out var movedEntityKey)) + if (!string.Equals(command.TextValue, "detach", StringComparison.Ordinal) || !children.Remove(stationIndex) || string.IsNullOrWhiteSpace(command.SubjectKey) || command.ScalarValue <= 0) return false; - - intents.Towing.Add(new AetheriaRuntimeDaemonTowIntent - { - ActorEntityKey = movedEntityKey, - StationEntityKey = command.TargetEntityKey ?? "", - TargetZoneIndex = stationZoneIndex, - PositionX = destinationX, - PositionY = destinationY - }); - return true; + towing.ChildEntityIndices = children; station.PositionX = command.PositionX; station.PositionZ = command.PositionZ; + var zone = run.Zones.First(value => value.ZoneIndex == actorZone); + var orbitKey = $"tow:{command.CommandId}"; + zone.Orbits = (zone.Orbits ?? Array.Empty()).Concat(new[] { new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = orbitKey, ParentOrbitKey = command.SubjectKey, Distance = command.ScalarValue, FixedPositionX = command.PositionX, FixedPositionY = command.PositionZ } }).ToArray(); + station.OrbitKey = orbitKey; return true; } private static bool MoveEntityToZone( diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationsClient.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationsClient.cs index d8994f95b..6ae98880e 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationsClient.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperationsClient.cs @@ -515,7 +515,9 @@ public sealed class AetheriaRuntimeDaemonOperationsClient .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(key => key.Trim()) .Where(key => !string.IsNullOrWhiteSpace(key)) - .ToArray() + .ToArray(), + OrbitParentKey = ReadPayloadString(request, "orbitParentKey", ""), + OrbitDistance = ReadPayloadDouble(request, "orbitDistance", 0) }), AetheriaRuntimeDaemonCommandKinds.CancelAgentTask => client.CancelAgentTask( frame, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs index 270f82bd4..146f9ae02 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs @@ -178,6 +178,14 @@ namespace GameCult.Aetheria.State.Verse if (Math.Abs(entity.VelocityX) + Math.Abs(entity.VelocityY) > 0.01) Face(entity, entity.VelocityX, entity.VelocityY); } + foreach (var parent in entities) + foreach (var childIndex in parent.ChildEntityIndices ?? Array.Empty()) + { + var child = entities.FirstOrDefault(value => value.EntityIndex == childIndex); + if (child == null) continue; + child.PositionX = parent.PositionX; child.PositionZ = parent.PositionZ; + child.VelocityX = parent.VelocityX; child.VelocityY = parent.VelocityY; + } } private static void StepCombat( diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs index 35dee02e3..0424b17d3 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs @@ -922,6 +922,9 @@ namespace GameCult.Aetheria.State.Verse [Key(39)] public string AssignedAgentTaskId { get; set; } = ""; + + [Key(40)] + public string OrbitKey { get; set; } = ""; } [MessagePackObject]