Persist behavior runtime state in checkpoints

This commit is contained in:
Meta Vens 2026-06-14 14:29:04 +01:00
commit 16a92b0173
8 changed files with 311 additions and 6 deletions

Binary file not shown.

View file

@ -406,6 +406,52 @@ try
AmmoIntervalProgress = 0.6
}
},
BehaviorStates = new[]
{
new AetheriaRuntimeBehaviorStateCommit
{
OwnerKind = "equipment",
OwnerIndex = 0,
BehaviorIndex = 2,
BehaviorKind = "Sensor",
Pinging = true,
PingCooldown = 0.25,
PingLerp = 0.5,
PingRadius = 1200.0,
PingedEntityCount = 3
},
new AetheriaRuntimeBehaviorStateCommit
{
OwnerKind = "equipment",
OwnerIndex = 1,
BehaviorIndex = 0,
BehaviorKind = "Radiator",
RadiatorTemperature = 450.0,
Emissivity = 0.8,
PumpedHeat = 12.0,
WasteHeat = 1.5,
EnergyUsage = 2.25
},
new AetheriaRuntimeBehaviorStateCommit
{
OwnerKind = "equipment",
OwnerIndex = 2,
BehaviorIndex = 0,
BehaviorKind = "Reactor",
ReactorDraw = 4.5,
ReactorLoadRatio = 1.25
},
new AetheriaRuntimeBehaviorStateCommit
{
OwnerKind = "equipment",
OwnerIndex = 3,
BehaviorIndex = 0,
BehaviorKind = "Capacitor",
CapacitorCharge = 7.5,
CapacitorCapacity = 10.0,
CapacitorEfficiency = 0.95
}
},
StatGrids = new[]
{
new AetheriaRuntimeEntityStatGridCommit
@ -513,6 +559,18 @@ try
entity.WeaponStates[2].BehaviorKind == "ConstantWeapon" &&
entity.WeaponStates[2].Reloading &&
entity.WeaponStates[2].AmmoIntervalProgress == 0.6 &&
entity.BehaviorStates.Length == 4 &&
entity.BehaviorStates[0].BehaviorKind == "Sensor" &&
entity.BehaviorStates[0].Pinging &&
entity.BehaviorStates[0].PingRadius == 1200.0 &&
entity.BehaviorStates[1].BehaviorKind == "Radiator" &&
entity.BehaviorStates[1].RadiatorTemperature == 450.0 &&
entity.BehaviorStates[1].PumpedHeat == 12.0 &&
entity.BehaviorStates[2].BehaviorKind == "Reactor" &&
entity.BehaviorStates[2].ReactorLoadRatio == 1.25 &&
entity.BehaviorStates[3].BehaviorKind == "Capacitor" &&
entity.BehaviorStates[3].CapacitorCharge == 7.5 &&
entity.BehaviorStates[3].CapacitorEfficiency == 0.95 &&
entity.WeaponGroups.Length == 1 &&
entity.StatGrids.Length == 2 &&
entity.StatGrids.Any(grid => grid.Name == "temperature" && grid.Values.Length == 2 && grid.Values[1] == 281.0) &&

View file

@ -442,10 +442,40 @@ public static class AetheriaRuntimeCommitLogApplier
StatGrids = ToEntityStatGrids(entity.StatGrids),
ActiveConsumables = ToActiveConsumables(entity.ActiveConsumables),
BehaviorProgress = ToBehaviorProgress(entity.BehaviorProgress),
WeaponStates = ToWeaponStates(entity.WeaponStates)
WeaponStates = ToWeaponStates(entity.WeaponStates),
BehaviorStates = ToBehaviorStates(entity.BehaviorStates)
};
}
private static AetheriaBehaviorStateSnapshot[] ToBehaviorStates(
IReadOnlyList<AetheriaRuntimeBehaviorStateCommit>? states)
{
return (states ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
.Select(state => new AetheriaBehaviorStateSnapshot
{
OwnerKind = state.OwnerKind ?? "",
OwnerIndex = state.OwnerIndex,
BehaviorIndex = state.BehaviorIndex,
BehaviorKind = state.BehaviorKind ?? "",
Pinging = state.Pinging,
PingCooldown = state.PingCooldown,
PingLerp = state.PingLerp,
PingRadius = state.PingRadius,
PingedEntityCount = state.PingedEntityCount,
RadiatorTemperature = state.RadiatorTemperature,
Emissivity = state.Emissivity,
PumpedHeat = state.PumpedHeat,
WasteHeat = state.WasteHeat,
EnergyUsage = state.EnergyUsage,
ReactorDraw = state.ReactorDraw,
ReactorLoadRatio = state.ReactorLoadRatio,
CapacitorCharge = state.CapacitorCharge,
CapacitorCapacity = state.CapacitorCapacity,
CapacitorEfficiency = state.CapacitorEfficiency
})
.ToArray();
}
private static AetheriaWeaponStateSnapshot[] ToWeaponStates(
IReadOnlyList<AetheriaRuntimeWeaponStateCommit>? states)
{

View file

@ -488,6 +488,9 @@ public sealed class AetheriaEntitySnapshot
[Key(22)]
public AetheriaWeaponStateSnapshot[] WeaponStates { get; set; } = [];
[Key(23)]
public AetheriaBehaviorStateSnapshot[] BehaviorStates { get; set; } = [];
}
[MessagePackObject]
@ -614,6 +617,67 @@ public sealed class AetheriaWeaponStateSnapshot
public double AmmoIntervalProgress { get; set; }
}
[MessagePackObject]
public sealed class AetheriaBehaviorStateSnapshot
{
[Key(0)]
public string OwnerKind { get; set; } = "";
[Key(1)]
public int OwnerIndex { get; set; } = -1;
[Key(2)]
public int BehaviorIndex { get; set; } = -1;
[Key(3)]
public string BehaviorKind { get; set; } = "";
[Key(4)]
public bool Pinging { get; set; }
[Key(5)]
public double PingCooldown { get; set; }
[Key(6)]
public double PingLerp { get; set; }
[Key(7)]
public double PingRadius { get; set; }
[Key(8)]
public int PingedEntityCount { get; set; }
[Key(9)]
public double RadiatorTemperature { get; set; }
[Key(10)]
public double Emissivity { get; set; }
[Key(11)]
public double PumpedHeat { get; set; }
[Key(12)]
public double WasteHeat { get; set; }
[Key(13)]
public double EnergyUsage { get; set; }
[Key(14)]
public double ReactorDraw { get; set; }
[Key(15)]
public double ReactorLoadRatio { get; set; }
[Key(16)]
public double CapacitorCharge { get; set; }
[Key(17)]
public double CapacitorCapacity { get; set; }
[Key(18)]
public double CapacitorEfficiency { get; set; }
}
[MessagePackObject]
public sealed class AetheriaActionBarBinding
{

View file

@ -789,10 +789,89 @@ public class ActionGameManager : MonoBehaviour
StatGrids = ProjectEntityStatGrids(entity),
ActiveConsumables = ProjectActiveConsumables(entity),
BehaviorProgress = ProjectBehaviorProgress(entity),
WeaponStates = ProjectWeaponStates(entity)
WeaponStates = ProjectWeaponStates(entity),
BehaviorStates = ProjectBehaviorStates(entity)
};
}
private static AetheriaRuntimeBehaviorStateCommit[] ProjectBehaviorStates(Entity entity)
{
var states = new List<AetheriaRuntimeBehaviorStateCommit>();
for (var ownerIndex = 0; ownerIndex < entity.Equipment.Count; ownerIndex++)
states.AddRange(ProjectBehaviorStates("equipment", ownerIndex, entity.Equipment[ownerIndex].Behaviors));
var activeConsumables = entity.ActiveConsumables;
for (var ownerIndex = 0; ownerIndex < activeConsumables.Count; ownerIndex++)
states.AddRange(ProjectBehaviorStates("active_consumable", ownerIndex, activeConsumables[ownerIndex].Behaviors));
return states.ToArray();
}
private static IEnumerable<AetheriaRuntimeBehaviorStateCommit> ProjectBehaviorStates(
string ownerKind,
int ownerIndex,
IReadOnlyList<Behavior> behaviors)
{
if (behaviors == null)
yield break;
for (var behaviorIndex = 0; behaviorIndex < behaviors.Count; behaviorIndex++)
{
var behavior = behaviors[behaviorIndex];
AetheriaRuntimeBehaviorStateCommit state = null;
if (behavior is Sensor sensor)
{
state = new AetheriaRuntimeBehaviorStateCommit
{
Pinging = sensor.Pinging,
PingCooldown = sensor.Cooldown,
PingLerp = sensor.PingLerp,
PingRadius = sensor.PingRadius,
PingedEntityCount = sensor.PingedEntityCount
};
}
else if (behavior is Radiator radiator)
{
state = new AetheriaRuntimeBehaviorStateCommit
{
RadiatorTemperature = radiator.RadiatorTemperature,
Emissivity = radiator.Emissivity,
PumpedHeat = radiator.PumpedHeat,
WasteHeat = radiator.WasteHeat,
EnergyUsage = radiator.EnergyUsage
};
}
else if (behavior is Reactor reactor)
{
state = new AetheriaRuntimeBehaviorStateCommit
{
ReactorDraw = reactor.Draw,
ReactorLoadRatio = reactor.CurrentLoadRatio
};
}
else if (behavior is Capacitor capacitor)
{
state = new AetheriaRuntimeBehaviorStateCommit
{
CapacitorCharge = capacitor.Charge,
CapacitorCapacity = capacitor.Capacity,
CapacitorEfficiency = capacitor.Efficiency
};
}
if (state == null)
continue;
state.OwnerKind = ownerKind;
state.OwnerIndex = ownerIndex;
state.BehaviorIndex = behaviorIndex;
state.BehaviorKind = behavior.Data?.Kind ?? "";
yield return state;
}
}
private static AetheriaRuntimeWeaponStateCommit[] ProjectWeaponStates(Entity entity)
{
var states = new List<AetheriaRuntimeWeaponStateCommit>();

View file

@ -66,6 +66,9 @@ public class Sensor : Behavior, IEventBehavior
{
get => _pingRadius;
}
public float PingLerp => _pingLerp;
public bool Pinging => _pinging;
public int PingedEntityCount => _pingedEntities.Count;
public float PingBrightness => pow(1 - _pingLerp, _data.PingRadiusExponent);
@ -153,4 +156,4 @@ public class Sensor : Behavior, IEventBehavior
}
return true;
}
}
}

View file

@ -564,6 +564,9 @@ namespace GameCult.Aetheria.State.Unity
[Key(27)]
public IReadOnlyList<AetheriaRuntimeWeaponStateCommit> WeaponStates { get; set; } = Array.Empty<AetheriaRuntimeWeaponStateCommit>();
[Key(28)]
public IReadOnlyList<AetheriaRuntimeBehaviorStateCommit> BehaviorStates { get; set; } = Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
}
[MessagePackObject]
@ -671,4 +674,65 @@ namespace GameCult.Aetheria.State.Unity
[Key(16)]
public double AmmoIntervalProgress { get; set; }
}
[MessagePackObject]
public sealed class AetheriaRuntimeBehaviorStateCommit
{
[Key(0)]
public string OwnerKind { get; set; } = "";
[Key(1)]
public int OwnerIndex { get; set; } = -1;
[Key(2)]
public int BehaviorIndex { get; set; } = -1;
[Key(3)]
public string BehaviorKind { get; set; } = "";
[Key(4)]
public bool Pinging { get; set; }
[Key(5)]
public double PingCooldown { get; set; }
[Key(6)]
public double PingLerp { get; set; }
[Key(7)]
public double PingRadius { get; set; }
[Key(8)]
public int PingedEntityCount { get; set; }
[Key(9)]
public double RadiatorTemperature { get; set; }
[Key(10)]
public double Emissivity { get; set; }
[Key(11)]
public double PumpedHeat { get; set; }
[Key(12)]
public double WasteHeat { get; set; }
[Key(13)]
public double EnergyUsage { get; set; }
[Key(14)]
public double ReactorDraw { get; set; }
[Key(15)]
public double ReactorLoadRatio { get; set; }
[Key(16)]
public double CapacitorCharge { get; set; }
[Key(17)]
public double CapacitorCapacity { get; set; }
[Key(18)]
public double CapacitorEfficiency { get; set; }
}
}

View file

@ -148,6 +148,9 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
Weapon behaviors now also publish typed runtime rows for firing state, ammo,
burst timing, cooldown progress, charged-weapon charging/charge state, and
constant-weapon reload/ammo-interval state.
Sensor, radiator, reactor, and capacitor behaviors publish typed state rows
for ping state, radiator temperature/throughput, reactor draw/load, and
capacitor charge/capacity/efficiency.
`RuntimeEntityBlueprint` still exists for construction/loadout projection,
but those live hull grids and session scalars are not blueprint fields and
cannot be restored by the old blueprint projector path.
@ -773,12 +776,15 @@ First Aetheria surfaces to publish:
- Done: extend run checkpoint commits and smoke coverage to carry typed
weapon behavior state rows for instant, charged, and constant weapon
runtime internals.
- Done: extend run checkpoint commits and smoke coverage to carry typed
behavior state rows for sensor ping state plus radiator, reactor, and
capacitor runtime internals.
- Done: cut live simulation grids out of `RuntimeEntityBlueprint`; loadout
and construction templates no longer capture or restore temperature,
armor, max-armor, or hull-conductivity state.
- Remaining: add typed documents/mappers for runtime object graphs,
typed behavior factory construction, behavior-private simulation state
such as sensor ping state, radiator/reactor/capacitor runtime internals,
typed behavior factory construction, remaining behavior-private state not
covered by progress, weapon, sensor, radiator, reactor, or capacitor rows,
and any catalog fields not covered by the stable scalar/fingerprint/
payload pass.
@ -1173,7 +1179,8 @@ First Aetheria surfaces to publish:
includes orbit/body rows for generated celestial state, and the entity smoke
includes typed simulation stat grids, public entity session scalars, and
active consumable timer rows, plus typed behavior progress rows for
`IProgressBehavior` surfaces and typed weapon runtime rows. The Unity runtime smoke also proves
`IProgressBehavior` surfaces, typed weapon runtime rows, and typed
sensor/radiator/reactor/capacitor behavior state rows. The Unity runtime smoke also proves
package-owned readback of typed loadout templates from `.cc` records. It also
proves `aetheria.runtime_session.v1` is advertised and survives reopen as the
typed daemon-session signal.