Restore Continue current entity from typed state

This commit is contained in:
Meta Vens 2026-06-16 06:41:35 +01:00
commit 8b8a86f6ae
7 changed files with 164 additions and 8 deletions

Binary file not shown.

View file

@ -907,6 +907,7 @@ try
packageZones[0].DroppedPickups[0].Item.ItemKey != "aetheria.item_definition:legacy:smoke:ore" ||
packageZones[0].DroppedPickups[0].Item.Quantity != 11 ||
packageEntities.Count != 1 ||
packageEntities[0].RecordKey != "global:aetheria.run_state.smoke-run.zone.0.entity.0.v1" ||
packageEntities[0].Name != "Unity Smoke Ship" ||
packageEntities[0].FactionKey != "aetheria.corporation:legacy:smoke:faction" ||
packageEntities[0].Equipment[0].Quality != 0.9 ||

View file

@ -791,6 +791,24 @@ static void RequireMainMenuContinueRunState(string root)
var actionGameManager = File.Exists(actionGameManagerPath)
? File.ReadAllText(actionGameManagerPath)
: throw new InvalidOperationException("Cannot verify Continue run authority; ActionGameManager.cs is missing.");
var packageSnapshotPath = Path.Combine(
root,
"Packages",
"org.gamecult.aetheria.state",
"Runtime",
"AetheriaRuntimeCatalogSnapshot.cs");
var packageSnapshot = File.Exists(packageSnapshotPath)
? File.ReadAllText(packageSnapshotPath)
: throw new InvalidOperationException("Cannot verify Continue entity identity; package runtime snapshots are missing.");
var packageStorePath = Path.Combine(
root,
"Packages",
"org.gamecult.aetheria.state",
"Runtime",
"AetheriaRuntimeCatalogStore.cs");
var packageStore = File.Exists(packageStorePath)
? File.ReadAllText(packageStorePath)
: throw new InvalidOperationException("Cannot verify Continue entity readback; package runtime store is missing.");
var requiredMenuSymbols = new[]
{
@ -823,6 +841,11 @@ static void RequireMainMenuContinueRunState(string root)
"AetheriaRuntimeRunStateSnapshot ContinueRunState",
"ResolveStartZone(continuingRun)",
"if (continuingRun != null)",
"RestoreCurrentEntityFromTypedRun(continuingRun)",
"ReadEntitySnapshots(RuntimeStateFilePath)",
"entity.RecordKey",
"CreateEntityConstructionBlueprint(entitySnapshot, true)",
"BindToEntity(entity)",
"ContinueRunState = null",
"RestoreDroppedPickupsFromTypedZoneState"
};
@ -837,6 +860,25 @@ static void RequireMainMenuContinueRunState(string root)
"ActionGameManager no longer has the typed Continue boot path: " +
string.Join(", ", missingGameplaySymbols));
}
var requiredPackageSymbols = new[]
{
"public string RecordKey",
"ReadEntitySnapshotPayload(record.Key, record.Payload)"
};
var missingPackageSymbols = requiredPackageSymbols
.Where(symbol =>
!packageSnapshot.Contains(symbol, StringComparison.Ordinal) &&
!packageStore.Contains(symbol, StringComparison.Ordinal))
.ToArray();
if (missingPackageSymbols.Length > 0)
{
throw new InvalidOperationException(
"Unity package entity readback no longer preserves record identity for Continue: " +
string.Join(", ", missingPackageSymbols));
}
}
static void RequirePropertiesPanelReadOnlyInspector(string root)

View file

@ -250,6 +250,45 @@ public class ActionGameManager : MonoBehaviour
return blueprint;
}
private EntityConstructionBlueprint CreateEntityConstructionBlueprint(AetheriaRuntimeEntitySnapshot entity, bool isCurrentEntity)
{
if (entity == null)
return null;
var hull = CreateEquippableLoadoutItem(new AetheriaRuntimeLoadoutItemSnapshot(
entity.HullItemKey,
1,
1,
1,
true,
false));
if (hull == null)
return null;
EntityConstructionBlueprint blueprint = string.Equals(entity.Kind, "orbital", StringComparison.OrdinalIgnoreCase)
? new OrbitalEntityConstructionBlueprint()
: new ShipConstructionBlueprint
{
Position = new float3((float)entity.PositionX, (float)entity.PositionY, (float)entity.PositionZ),
Direction = new float2((float)entity.DirectionX, (float)entity.DirectionY),
IsPlayerShip = isCurrentEntity
};
blueprint.Name = entity.Name ?? "";
blueprint.FactionKey = entity.FactionKey ?? "";
blueprint.Faction = ParseLegacyGuidFromReferenceKey(blueprint.FactionKey, "aetheria.corporation");
blueprint.Hull = hull;
blueprint.Equipment = CreateEquippableSlots(entity.Equipment);
blueprint.CargoBays = CreateEquippableSlots(entity.CargoBays);
blueprint.DockingBays = CreateEquippableSlots(entity.DockingBays);
blueprint.CargoContents = CreateCargoBayContents(entity.CargoContents);
blueprint.DockingBayContents = CreateCargoBayContents(entity.DockingBayContents);
blueprint.DockingBayAssignments = entity.DockingBayAssignments.ToArray();
blueprint.WeaponGroups = entity.WeaponGroups.Select(group => group.ToArray()).ToArray();
blueprint.Children = Array.Empty<EntityConstructionBlueprint>();
return blueprint;
}
private (int2 position, EquippableItem item)[] CreateEquippableSlots(
IReadOnlyList<AetheriaRuntimeLoadoutItemSlotSnapshot> slots)
{
@ -259,6 +298,21 @@ public class ActionGameManager : MonoBehaviour
.ToArray();
}
private (int2 position, EquippableItem item)[] CreateEquippableSlots(
IReadOnlyList<AetheriaRuntimeEntityItemSlotSnapshot> slots)
{
return slots
.Select(slot => (position: new int2(slot.X, slot.Y), item: CreateEquippableLoadoutItem(new AetheriaRuntimeLoadoutItemSnapshot(
slot.ItemKey,
slot.Quality,
slot.Durability,
slot.Quantity,
slot.Enabled,
slot.OverrideShutdown))))
.Where(slot => slot.item != null)
.ToArray();
}
private (int2 position, ItemInstance item)[][] CreateCargoBayContents(
IReadOnlyList<AetheriaRuntimeCargoBayLoadoutSnapshot> bays)
{
@ -2428,6 +2482,7 @@ public class ActionGameManager : MonoBehaviour
if (continuingRun != null)
{
RestoreCurrentEntityFromTypedRun(continuingRun);
ContinueRunState = null;
return;
}
@ -2447,6 +2502,50 @@ public class ActionGameManager : MonoBehaviour
}
}
private void RestoreCurrentEntityFromTypedRun(AetheriaRuntimeRunStateSnapshot run)
{
if (run == null ||
run.CurrentZoneIndex < 0 ||
run.CurrentZoneEntityIndex < 0)
{
return;
}
var entityKey = $"global:aetheria.run_state.{run.RunId}.zone.{run.CurrentZoneIndex}.entity.{run.CurrentZoneEntityIndex}.v1";
AetheriaRuntimeEntitySnapshot entitySnapshot;
try
{
entitySnapshot = AetheriaRuntimeCatalogStore.ReadEntitySnapshots(RuntimeStateFilePath)
.FirstOrDefault(entity => string.Equals(entity.RecordKey, entityKey, StringComparison.Ordinal));
}
catch (Exception ex)
{
Debug.LogError($"Failed to read typed current entity snapshot {entityKey}: {ex}");
return;
}
var blueprint = CreateEntityConstructionBlueprint(entitySnapshot, true);
if (blueprint == null)
{
Debug.LogWarning($"Typed current entity snapshot {entityKey} could not be lowered into a runtime entity.");
return;
}
var entity = EntityConstructionBlueprintProjector.InstantiateFromBlueprint(ItemManager, Zone, blueprint, true);
if (entity == null)
return;
entity.Position = new float3((float)entitySnapshot.PositionX, (float)entitySnapshot.PositionY, (float)entitySnapshot.PositionZ);
entity.Direction = new float2((float)entitySnapshot.DirectionX, (float)entitySnapshot.DirectionY);
entity.Velocity = new float2((float)entitySnapshot.VelocityX, (float)entitySnapshot.VelocityY);
entity.OverrideShutdown = entitySnapshot.OverrideShutdown;
entity.TractorPower = (float)entitySnapshot.TractorPower;
entity.Zone = Zone;
Zone.Entities.Add(entity);
entity.Activate();
BindToEntity(entity);
}
private static GalaxyZone ResolveStartZone(AetheriaRuntimeRunStateSnapshot run)
{
if (CurrentGalaxy?.Zones == null || CurrentGalaxy.Zones.Length == 0)

View file

@ -847,6 +847,7 @@ namespace GameCult.Aetheria.State.Unity
public sealed class AetheriaRuntimeEntitySnapshot
{
public AetheriaRuntimeEntitySnapshot(
string recordKey,
string name,
string kind,
double positionX,
@ -882,6 +883,7 @@ namespace GameCult.Aetheria.State.Unity
int visibilitySourceCount,
IReadOnlyList<AetheriaRuntimeEntityContactSnapshot> contacts)
{
RecordKey = recordKey;
Name = name;
Kind = kind;
PositionX = positionX;
@ -918,6 +920,7 @@ namespace GameCult.Aetheria.State.Unity
Contacts = contacts;
}
public string RecordKey { get; }
public string Name { get; }
public string Kind { get; }
public double PositionX { get; }

View file

@ -162,7 +162,7 @@ namespace GameCult.Aetheria.State.Unity
schemaName != EntitySnapshotSchema)
continue;
entities.Add(ReadEntitySnapshotPayload(record.Payload));
entities.Add(ReadEntitySnapshotPayload(record.Key, record.Payload));
}
return entities.OrderBy(entity => entity.Name, StringComparer.Ordinal).ToArray();
@ -534,7 +534,7 @@ namespace GameCult.Aetheria.State.Unity
return pickups;
}
private static AetheriaRuntimeEntitySnapshot ReadEntitySnapshotPayload(byte[] payload)
private static AetheriaRuntimeEntitySnapshot ReadEntitySnapshotPayload(string recordKey, byte[] payload)
{
var reader = new MessagePackReader(payload);
var fields = reader.ReadArrayHeader();
@ -570,6 +570,7 @@ namespace GameCult.Aetheria.State.Unity
var contacts = ReadFieldEntityContacts(ref reader, fields, 29);
SkipRemaining(ref reader, fields, 30);
return new AetheriaRuntimeEntitySnapshot(
recordKey,
name,
kind,
position.X,

View file

@ -967,13 +967,23 @@ First Aetheria surfaces to publish:
`AetheriaRuntimeCatalogStore`, selects the newest run, regenerates the
galaxy from the saved generation seed, and passes the selected run to
`ActionGameManager` so boot enters the saved current zone and lowers typed
pickup rows. Entity/player reconstruction from `AetheriaEntitySnapshot`
remains future Continue work, not a fake restored ship.
pickup rows.
- Done: restore the current player entity from typed state during Continue
boot. Package entity readback now preserves `AetheriaEntitySnapshot` record
identity, and `ActionGameManager` resolves the exact
`RunId + ZoneIndex + CurrentZoneEntityIndex` entity record, lowers it
through the existing construction blueprint path, restores hull,
equipment, cargo, docking contents, weapon groups, position, direction,
velocity, shutdown override, and tractor power, then binds the lowered
entity as the current player ship.
- Remaining: add typed documents/mappers for runtime object graphs,
complete Continue/run entity reconstruction from typed state, 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.
complete full Continue/run entity graph reconstruction from typed state
for non-current entities, child/docked entity activation, targets,
contacts, active consumables, behavior-private state not covered by
progress, weapon, sensor, radiator, reactor, or capacitor rows,
heatstroke/hypothermia runtime restore, typed behavior factory
construction, and any catalog fields not covered by the stable
scalar/fingerprint/payload pass.
4. Runtime cutover
- Done: add a Unity-facing typed catalog read facade and smoke proving it can