Release agent controls with terminal tasks
This commit is contained in:
parent
14fa364336
commit
d6562f69cb
5 changed files with 120 additions and 6 deletions
|
|
@ -211,6 +211,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
SchedulerAssignsHighestPriorityCompatibleTask,
|
||||
SchedulerRequeuesTaskFromDeadAgent,
|
||||
SchedulerCollapsesDuplicateAssignmentMarkers,
|
||||
CancellingAgentTaskReleasesSchedulerControls,
|
||||
SchedulerAssignsShortestGalaxyRoute,
|
||||
WormholeTraversalUsesPersistedEnterTransferExitPhases,
|
||||
AgentTraversesGalaxyRouteBeforeExecutingTask,
|
||||
|
|
@ -4999,6 +5000,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
first.AssignedAgentTaskId = "single-owner";
|
||||
var duplicate = Entity(1, 0, "workers");
|
||||
duplicate.AssignedAgentTaskId = "single-owner";
|
||||
duplicate.HelmForward = 1;
|
||||
duplicate.TargetEntityIndex = 7;
|
||||
var task = AgentTask("single-owner", 1);
|
||||
task.Status = AetheriaRuntimeAgentTaskStatuses.Assigned;
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
|
|
@ -5015,6 +5018,84 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
"one task must have exactly one active carrier after reconciliation");
|
||||
RequireEqual(first.EntityIndex, task.AssignedEntityIndex,
|
||||
"duplicate assignment reconciliation must be deterministic");
|
||||
RequireNear(0, duplicate.HelmForward, 0.000001,
|
||||
"duplicate assignment reconciliation must release the losing scheduler controls");
|
||||
RequireEqual(-1, duplicate.TargetEntityIndex,
|
||||
"duplicate assignment reconciliation must release the losing scheduler target");
|
||||
}
|
||||
|
||||
private static void CancellingAgentTaskReleasesSchedulerControls()
|
||||
{
|
||||
var agent = Entity(0, 0, "workers");
|
||||
agent.AssignedAgentTaskId = "tow-station";
|
||||
agent.HelmStrafe = 0.75;
|
||||
agent.HelmForward = 1;
|
||||
agent.TargetEntityIndex = 2;
|
||||
agent.ChildEntityIndices = [2];
|
||||
var station = Entity(2, 0, "workers");
|
||||
station.Kind = "station";
|
||||
var task = new AetheriaRuntimeAgentTaskCommit
|
||||
{
|
||||
TaskId = "tow-station",
|
||||
CorporationKey = "workers",
|
||||
TaskType = AetheriaRuntimeAgentTaskTypes.Tow,
|
||||
Status = AetheriaRuntimeAgentTaskStatuses.Assigned,
|
||||
AssignedEntityIndex = 0,
|
||||
TargetEntityIndex = 2,
|
||||
PendingQuantity = 4,
|
||||
Phase = "delivery"
|
||||
};
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "agent-cancel-smoke",
|
||||
CurrentZoneIndex = 0,
|
||||
CurrentEntityKey = "",
|
||||
AgentTasks = [task],
|
||||
Zones =
|
||||
[
|
||||
new AetheriaRuntimeZoneSnapshotCommit
|
||||
{
|
||||
ZoneIndex = 0,
|
||||
Entities = [agent, Entity(1, 0, "workers"), station]
|
||||
}
|
||||
]
|
||||
};
|
||||
var cancel = AetheriaRuntimeDaemonCommandDocument.Create(
|
||||
AetheriaRuntimeDaemonCommandKinds.CancelAgentTask,
|
||||
"commander-smoke",
|
||||
"starbridge-smoke",
|
||||
12,
|
||||
"");
|
||||
cancel.CommandId = "cancel-tow-station";
|
||||
cancel.AgentTask = new AetheriaRuntimeAgentTaskCommand { TaskId = task.TaskId };
|
||||
|
||||
var result = AetheriaRuntimeDaemonOperations.Execute(
|
||||
run,
|
||||
[cancel],
|
||||
new AetheriaRuntimeDaemonOperationContext());
|
||||
|
||||
Require(result.AppliedCommandIds.Contains(cancel.CommandId, StringComparer.Ordinal),
|
||||
"task cancellation must commit through the daemon command boundary");
|
||||
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Cancelled, task.Status,
|
||||
"cancelled task must become terminal immediately");
|
||||
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Cancelled, task.Phase,
|
||||
"cancelled task must not retain a counterfeit in-progress phase");
|
||||
RequireEqual(-1, task.AssignedEntityIndex,
|
||||
"cancelled task must release its assigned agent identity");
|
||||
RequireEqual(0, task.PendingQuantity,
|
||||
"cancelled transfer work must not retain pending quantity");
|
||||
RequireEqual("", agent.AssignedAgentTaskId,
|
||||
"cancelled agent must release the durable assignment marker");
|
||||
RequireNear(0, agent.HelmStrafe, 0.000001,
|
||||
"cancelled agent must release scheduler-owned strafe input");
|
||||
RequireNear(0, agent.HelmForward, 0.000001,
|
||||
"cancelled agent must release scheduler-owned forward input");
|
||||
RequireEqual(-1, agent.TargetEntityIndex,
|
||||
"cancelled agent must release scheduler-owned targeting");
|
||||
Require(!agent.ChildEntityIndices.Contains(station.EntityIndex),
|
||||
"cancelling an active tow must release the station at its authoritative world position");
|
||||
RequireEqual(0, AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 13).Count,
|
||||
"cancelled work must not produce another scheduler command on the next frame");
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAgentTaskCommit AgentTask(string id, int priority) => new()
|
||||
|
|
|
|||
|
|
@ -160,6 +160,27 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
}
|
||||
|
||||
internal static void ReleaseControls(
|
||||
AetheriaRuntimeEntitySnapshotCommit agent,
|
||||
AetheriaRuntimeAgentTaskCommit? task)
|
||||
{
|
||||
if (agent == null)
|
||||
return;
|
||||
|
||||
agent.AssignedAgentTaskId = "";
|
||||
agent.HelmStrafe = 0;
|
||||
agent.HelmForward = 0;
|
||||
agent.TargetEntityIndex = -1;
|
||||
if (task != null &&
|
||||
string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Tow, StringComparison.Ordinal) &&
|
||||
task.TargetEntityIndex >= 0)
|
||||
{
|
||||
agent.ChildEntityIndices = (agent.ChildEntityIndices ?? Array.Empty<int>())
|
||||
.Where(index => index != task.TargetEntityIndex)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssignQueuedTasks(AetheriaRuntimeRunCheckpointCommit run, long frameId)
|
||||
{
|
||||
var tasks = (run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>()).Where(task => task != null).ToArray();
|
||||
|
|
@ -1171,14 +1192,14 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
task.Status = AetheriaRuntimeAgentTaskStatuses.Completed;
|
||||
task.CompletedFrameId = frameId;
|
||||
agent.AssignedAgentTaskId = "";
|
||||
ReleaseControls(agent, task);
|
||||
}
|
||||
|
||||
private static void Fail(AetheriaRuntimeAgentTaskCommit task, AetheriaRuntimeEntitySnapshotCommit agent)
|
||||
{
|
||||
task.Status = AetheriaRuntimeAgentTaskStatuses.Failed;
|
||||
task.PendingQuantity = 0;
|
||||
agent.AssignedAgentTaskId = "";
|
||||
ReleaseControls(agent, task);
|
||||
}
|
||||
|
||||
private static string CommandId(AetheriaRuntimeAgentTaskCommit task, long frameId, string phase) =>
|
||||
|
|
@ -1210,7 +1231,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
if (!string.IsNullOrWhiteSpace(entity.AssignedAgentTaskId) &&
|
||||
(!entity.IsActive || !activeTaskIds.Contains(entity.AssignedAgentTaskId)))
|
||||
entity.AssignedAgentTaskId = "";
|
||||
ReleaseControls(entity, tasks.FirstOrDefault(task =>
|
||||
string.Equals(task.TaskId, entity.AssignedAgentTaskId, StringComparison.Ordinal)));
|
||||
}
|
||||
foreach (var task in tasks.Where(task => string.Equals(task.Status, AetheriaRuntimeAgentTaskStatuses.Assigned, StringComparison.Ordinal)))
|
||||
{
|
||||
|
|
@ -1227,7 +1249,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
task.AssignedEntityIndex = carriers[0].EntityIndex;
|
||||
foreach (var duplicate in carriers.Skip(1))
|
||||
duplicate.AssignedAgentTaskId = "";
|
||||
ReleaseControls(duplicate, task);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -416,10 +416,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.SelectMany(zone => zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()))
|
||||
{
|
||||
if (string.Equals(entity.AssignedAgentTaskId, task.TaskId, StringComparison.Ordinal))
|
||||
entity.AssignedAgentTaskId = "";
|
||||
AetheriaRuntimeAgentScheduler.ReleaseControls(entity, task);
|
||||
}
|
||||
task.Status = AetheriaRuntimeAgentTaskStatuses.Cancelled;
|
||||
task.AssignedEntityIndex = -1;
|
||||
task.PendingQuantity = 0;
|
||||
task.Phase = AetheriaRuntimeAgentTaskStatuses.Cancelled;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1133,6 +1133,15 @@ The task's weapon-group field is now an observed last selection, not an attack
|
|||
policy owner. Daemon smoke proves close/open strafe, DPS group selection,
|
||||
projectile lead, hardpoint gating, progressive lock, and target destruction.
|
||||
|
||||
Task assignment is the only owner of autonomous helm and target controls for an
|
||||
uncontrolled agent. Cancel, complete, failure, stale-assignment repair, and
|
||||
duplicate-carrier repair all use `AetheriaRuntimeAgentScheduler.ReleaseControls`.
|
||||
That transition clears the durable assignment marker, both helm axes, and the
|
||||
scheduler target; a tow task also releases its target from the agent's child
|
||||
list at the already-authoritative world position. Task status and phase are
|
||||
terminal facts after cancellation. They are not a repair signal that permits
|
||||
the previous controller state to survive for another tick.
|
||||
|
||||
### Authoritative Flight Actuator Contract
|
||||
|
||||
- Owner: Aetheria daemon owns persistent local helm axes, desired look
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Starbridge | original/shared architecture intent | one daemon session | pilot + commander surfaces | Unity + Electron | commander task board and autonomous explore execution wired; Electron Eve lowering and split-target proof missing | commander orders plus four pilots and autonomous workers |
|
||||
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; `Switch`, one-shot `Trigger`, normalized `Cooldown`, adjustable high/low-pass `Thermotoggle`, `EnergyDraw`, equipment `Heat`, atomic `ItemUsage`, and thermal `Wear` execute in authored group order and stop downstream effects on refusal; each row records whether the chain reached it. `Thruster`, rare `AetherDrive`, and `Radiator` now pause their groups at the specialized owner, report the exact fossil success result, and resume common tails at most once, so idle or unfunded actuators cannot consume wear; radiator emission remains its independent always-update path. Cooldown projects generic progress and thermostat target is daemon-commanded state seeded from authored data. Equipped and timed-consumable `StatModifier` sources persist daemon-owned lifecycle state and every runtime stat reader applies the fossil `base × multiplier product + constant sum` rule through one query; legacy `Data` kind references and required-behavior filters are honored | remaining specialized return-value composition and canonical consumable-content proof |
|
||||
| Sensors/visibility | installed sensor sensitivity and angular curve, `Visibility`, `Reflector`, decaying signature sources, stellar light, ping state, and observer information thresholds | daemon | accumulated contacts, named visibility sources, and ping events | indicators/maps and provider environment lowering | every operational installed Sensor executes the fossil visibility/sensitivity/angle/distance information equation and decay; visibility is thresholded by the provider setting. Actor-scoped ping owns energy, cooldown, expansion radius, reconnect-stable crossed-target state, one-time information boost, and transient signature. Operational `Visibility` equipment publishes evaluated constant signature; committed weapons publish authored signature including charged-shot multiplication; transient weapon and ping sources decay by the fossil exponential rule. Reflectors multiply evaluated cross-section by the exponent-8 stellar `PowerPulse` after thermal settlement. Gameplay light and environment projection share resolved authored orbit/body centers and provider-owned sun radius inputs. Contact refresh no longer invents perfect information or overwrites emitted visibility; docked craft consume one daemon-derived union of craft and dock-parent contacts through both Eve rows and SoA bodies, with sensor-source provenance | active consumable visibility/reflector effects, complete source cardinality, classification timelines, and canonical catalog station-array data |
|
||||
| AI/tasks | corporation task scheduler plus agent state machines and installed orbital `TurretController` | daemon | commander task board, corporation survey ledger, controller aim/state, and shared semantic controls | Eve commander/pilot clients | point exploration, loadout-driven attack, haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home are wired through shared commands. Ship combat samples the fossil DPS/range policy, selects live weapon groups, leads projectiles, strafes relative to target attitude, and gates fire on catalog placement rotation. Chain-admitted orbital controllers independently acquire the first visible hostile ship, publish predictive look direction, and trigger only exact aligned in-range weapon behaviors through the shared combat lifecycle; the canonical preceding `EnergyDraw` now blocks acquisition, aim, and fire when power is unavailable, and non-player identity grants no implicit group-zero fire. Cross-zone task and home routes survive the full daemon-owned wormhole timeline; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing | cancellation, reconnect, multi-client command transfer, and model-local barrel-direction metadata |
|
||||
| AI/tasks | corporation task scheduler plus agent state machines and installed orbital `TurretController` | daemon | commander task board, corporation survey ledger, controller aim/state, and shared semantic controls | Eve commander/pilot clients | point exploration, loadout-driven attack, haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home are wired through shared commands. Ship combat samples the fossil DPS/range policy, selects live weapon groups, leads projectiles, strafes relative to target attitude, and gates fire on catalog placement rotation. Chain-admitted orbital controllers independently acquire the first visible hostile ship, publish predictive look direction, and trigger only exact aligned in-range weapon behaviors through the shared combat lifecycle; the canonical preceding `EnergyDraw` now blocks acquisition, aim, and fire when power is unavailable, and non-player identity grants no implicit group-zero fire. Cross-zone task and home routes survive the full daemon-owned wormhole timeline; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing. Cancel, complete, fail, stale-assignment repair, and duplicate-carrier repair share one scheduler-owned release primitive that zeros persisted helm input, clears the scheduler target, releases towing parentage, and prevents a terminal task from emitting another command | reconnect, multi-client command transfer, and model-local barrel-direction metadata |
|
||||
| Consumables | cargo-backed timed behavior containers | daemon | dynamic actions, active effects, and lifecycle facts | action bar/schematic | daemon atomically resolves typed metadata and concrete cargo, preserves consumed-instance quality, enforces authored stacking, evaluates effectiveness/quality, and executes ordered `EnergyDraw`, fossil-compatible no-op `Heat`, `ItemUsage`, and stateful `Cooldown` payloads after energy tick initialization; every effect and payload state has stable authored identity; cooldowns update before the ordered chain, require state strictly below zero, reset to one on success, and continue aging behind earlier failures; false and unsupported payloads stop later execution with a typed fact; timers retain the fossil's exact-zero final tick; Unity reconciles presentation by effect identity and cannot restore active behavior state by list index | catalog revision pinning, stat modifiers, remaining behavior families, and reconnect presentation proof |
|
||||
|
||||
## Ownership Invariants
|
||||
|
|
|
|||
Loading…
Reference in a new issue