Rebuild station towing without teleportation

This commit is contained in:
Meta Vens 2026-07-11 18:53:20 +01:00
commit e3c81ff2ec
8 changed files with 124 additions and 39 deletions

View file

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

View file

@ -55,6 +55,8 @@ namespace GameCult.Aetheria.State.Verse
[Key(19)] public string Phase { get; set; } = "";
[Key(20)] public IReadOnlyList<string> TargetBodyKeys { get; set; } = Array.Empty<string>();
[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<string> TargetBodyKeys { get; set; } = Array.Empty<string>();
[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<AetheriaRuntimeAgentTaskCommit>())
.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<AetheriaRuntimeDaemonCommandDocument> 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<AetheriaRuntimeBodySnapshotCommit>())
.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<AetheriaRuntimeDaemonCommandDocument>(); }
var attached = (agent.ChildEntityIndices ?? Array.Empty<int>()).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<AetheriaRuntimeDaemonCommandDocument> PlanPatrol(
AetheriaRuntimeZoneSnapshotCommit zone,
AetheriaRuntimeAgentTaskCommit task,

View file

@ -190,6 +190,8 @@ namespace GameCult.Aetheria.State.Verse
("phase", task.Phase),
("targetBodyKeys", string.Join(",", task.TargetBodyKeys ?? Array.Empty<string>())),
("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),

View file

@ -19,8 +19,6 @@ namespace GameCult.Aetheria.State.Verse
new List<AetheriaRuntimeDaemonDockingIntent>();
public List<AetheriaRuntimeDaemonWormholeIntent> Wormholes { get; } =
new List<AetheriaRuntimeDaemonWormholeIntent>();
public List<AetheriaRuntimeDaemonTowIntent> Towing { get; } =
new List<AetheriaRuntimeDaemonTowIntent>();
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; }
}
}

View file

@ -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<AetheriaRuntimeAgentTaskCommit>())
.Concat(new[]
@ -238,6 +241,8 @@ namespace GameCult.Aetheria.State.Verse
? "pickup"
: "",
TargetBodyKeys = request.TargetBodyKeys ?? Array.Empty<string>(),
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<int>()).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<AetheriaRuntimeOrbitSnapshotCommit>()).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(

View file

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

View file

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

View file

@ -922,6 +922,9 @@ namespace GameCult.Aetheria.State.Verse
[Key(39)]
public string AssignedAgentTaskId { get; set; } = "";
[Key(40)]
public string OrbitKey { get; set; } = "";
}
[MessagePackObject]