diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 955bd9ee6..a67348424 100644 Binary files a/.voidbot/state/aetheria.cc and b/.voidbot/state/aetheria.cc differ diff --git a/Aetheria.State.Smoke/Program.cs b/Aetheria.State.Smoke/Program.cs index b1bc1c373..7f61561b5 100644 --- a/Aetheria.State.Smoke/Program.cs +++ b/Aetheria.State.Smoke/Program.cs @@ -233,6 +233,16 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s DockingBayAssignments = [-1], Visibility = 12.75, VisibilitySourceCount = 3, + Contacts = + [ + new AetheriaEntityContactSnapshot + { + TargetEntityKey = entityKey.ToString(), + InfoGathered = 0.85, + Hostile = true, + Visible = true + } + ], WeaponGroups = [ new AetheriaWeaponGroupSnapshot @@ -627,6 +637,11 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether entitySnapshot.DockingBayAssignments[0] != -1 || entitySnapshot.Visibility != 12.75 || entitySnapshot.VisibilitySourceCount != 3 || + entitySnapshot.Contacts.Length != 1 || + entitySnapshot.Contacts[0].TargetEntityKey != entityKey.ToString() || + entitySnapshot.Contacts[0].InfoGathered != 0.85 || + !entitySnapshot.Contacts[0].Hostile || + !entitySnapshot.Contacts[0].Visible || entitySnapshot.WeaponGroups.Length != 1 || entitySnapshot.WeaponStates.Length != 1 || entitySnapshot.WeaponStates[0].BehaviorKind != "LockWeapon" || diff --git a/Aetheria.State.Unity.Smoke/Program.cs b/Aetheria.State.Unity.Smoke/Program.cs index fd9e642c4..78a679bb8 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -359,6 +359,16 @@ try DockingBayAssignments = new[] { -1 }, Visibility = 12.75, VisibilitySourceCount = 3, + Contacts = new[] + { + new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = 0, + InfoGathered = 0.85, + Hostile = true, + Visible = true + } + }, WeaponGroups = new[] { new[] { 0 } }, ActiveConsumables = new[] { @@ -693,6 +703,11 @@ try entity.DockingBayAssignments[0] == -1 && entity.Visibility == 12.75 && entity.VisibilitySourceCount == 3 && + entity.Contacts.Length == 1 && + entity.Contacts[0].TargetEntityKey == "global:aetheria.run_state.smoke-run.zone.0.entity.0.v1" && + entity.Contacts[0].InfoGathered == 0.85 && + entity.Contacts[0].Hostile && + entity.Contacts[0].Visible && entity.ActiveConsumables[0].ItemKey == "aetheria.item_definition:legacy:smoke:consumable" && entity.ActiveConsumables[0].RemainingDuration == 3.0 && entity.BehaviorProgress.Length == 2 && @@ -818,6 +833,11 @@ try packageEntities[0].DockingBayAssignments[0] != -1 || packageEntities[0].Visibility != 12.75 || packageEntities[0].VisibilitySourceCount != 3 || + packageEntities[0].Contacts.Count != 1 || + packageEntities[0].Contacts[0].TargetEntityKey != "global:aetheria.run_state.smoke-run.zone.0.entity.0.v1" || + packageEntities[0].Contacts[0].InfoGathered != 0.85 || + !packageEntities[0].Contacts[0].Hostile || + !packageEntities[0].Contacts[0].Visible || packageEntities[0].ActiveConsumables[0].ItemKey != "aetheria.item_definition:legacy:smoke:consumable" || packageEntities[0].BehaviorProgress.Count != 2 || packageEntities[0].WeaponStates.Count != 4 || diff --git a/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs b/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs index 6622331f1..1d54b123f 100644 --- a/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs +++ b/Aetheria.State/AetheriaRuntimeCommitLogApplier.cs @@ -460,10 +460,28 @@ public static class AetheriaRuntimeCommitLogApplier DockingBayContents = ToCargoBays(entity.DockingBayContents), DockingBayAssignments = ToIntArray(entity.DockingBayAssignments), Visibility = entity.Visibility, - VisibilitySourceCount = entity.VisibilitySourceCount + VisibilitySourceCount = entity.VisibilitySourceCount, + Contacts = ToEntityContacts(entity.Contacts, runId, zoneIndex) }; } + private static AetheriaEntityContactSnapshot[] ToEntityContacts( + IReadOnlyList? contacts, + string runId, + int zoneIndex) + { + return (contacts ?? Array.Empty()) + .Where(contact => contact.TargetEntityIndex >= 0) + .Select(contact => new AetheriaEntityContactSnapshot + { + TargetEntityKey = EntityKey(runId, zoneIndex, contact.TargetEntityIndex).ToString(), + InfoGathered = contact.InfoGathered, + Hostile = contact.Hostile, + Visible = contact.Visible + }) + .ToArray(); + } + private static AetheriaBehaviorStateSnapshot[] ToBehaviorStates( IReadOnlyList? states) { diff --git a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs index bbd1df5d4..2c074ddac 100644 --- a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs +++ b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs @@ -509,6 +509,26 @@ public sealed class AetheriaEntitySnapshot [Key(28)] public int VisibilitySourceCount { get; set; } + + [Key(29)] + public AetheriaEntityContactSnapshot[] Contacts { get; set; } = []; +} + +[MessagePackObject] +public sealed class AetheriaEntityContactSnapshot +{ + [Key(0)] + [CultReference(typeof(AetheriaEntitySnapshot))] + public string TargetEntityKey { get; set; } = ""; + + [Key(1)] + public double InfoGathered { get; set; } + + [Key(2)] + public bool Hostile { get; set; } + + [Key(3)] + public bool Visible { get; set; } } [MessagePackObject] diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 4f7061103..08a761b0c 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -787,6 +787,7 @@ public class ActionGameManager : MonoBehaviour .ToArray() ?? Array.Empty(), Visibility = entity.Visibility, VisibilitySourceCount = entity.VisibilitySources.Count, + Contacts = ProjectEntityContacts(zone, entity), ChildEntityIndices = entity.Children? .Select(child => zone.Entities.IndexOf(child)) .Where(index => index >= 0) @@ -802,6 +803,25 @@ public class ActionGameManager : MonoBehaviour }; } + private static AetheriaRuntimeEntityContactCommit[] ProjectEntityContacts(Zone zone, Entity entity) + { + return entity.EntityInfoGathered + .Select(contact => + { + var targetIndex = zone.Entities.IndexOf(contact.Key); + entity.EntityHostility.TryGetValue(contact.Key, out var hostile); + return new AetheriaRuntimeEntityContactCommit + { + TargetEntityIndex = targetIndex, + InfoGathered = contact.Value, + Hostile = hostile, + Visible = entity.VisibleEntities.Contains(contact.Key) + }; + }) + .Where(contact => contact.TargetEntityIndex >= 0) + .ToArray(); + } + private static AetheriaRuntimeCargoBayLoadoutCommit[] ProjectRuntimeCargoBays(IEnumerable bays) where TBay : EquippedCargoBay { diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index 2a2bc4d48..3baffd7eb 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -788,7 +788,8 @@ namespace GameCult.Aetheria.State.Unity IReadOnlyList dockingBayContents, IReadOnlyList dockingBayAssignments, double visibility, - int visibilitySourceCount) + int visibilitySourceCount, + IReadOnlyList contacts) { Name = name; Kind = kind; @@ -823,6 +824,7 @@ namespace GameCult.Aetheria.State.Unity DockingBayAssignments = dockingBayAssignments; Visibility = visibility; VisibilitySourceCount = visibilitySourceCount; + Contacts = contacts; } public string Name { get; } @@ -858,6 +860,23 @@ namespace GameCult.Aetheria.State.Unity public IReadOnlyList DockingBayAssignments { get; } public double Visibility { get; } public int VisibilitySourceCount { get; } + public IReadOnlyList Contacts { get; } + } + + public sealed class AetheriaRuntimeEntityContactSnapshot + { + public AetheriaRuntimeEntityContactSnapshot(string targetEntityKey, double infoGathered, bool hostile, bool visible) + { + TargetEntityKey = targetEntityKey; + InfoGathered = infoGathered; + Hostile = hostile; + Visible = visible; + } + + public string TargetEntityKey { get; } + public double InfoGathered { get; } + public bool Hostile { get; } + public bool Visible { get; } } public sealed class AetheriaRuntimeActionBarBindingSnapshot diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index 9a0f75fd7..643c3778d 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -537,7 +537,8 @@ namespace GameCult.Aetheria.State.Unity var dockingBayAssignments = ReadFieldInt32Array(ref reader, fields, 26); var visibility = ReadFieldDouble(ref reader, fields, 27); var visibilitySourceCount = ReadFieldInt32(ref reader, fields, 28); - SkipRemaining(ref reader, fields, 29); + var contacts = ReadFieldEntityContacts(ref reader, fields, 29); + SkipRemaining(ref reader, fields, 30); return new AetheriaRuntimeEntitySnapshot( name, kind, @@ -571,7 +572,27 @@ namespace GameCult.Aetheria.State.Unity dockingBayContents, dockingBayAssignments, visibility, - visibilitySourceCount); + visibilitySourceCount, + contacts); + } + + private static IReadOnlyList ReadFieldEntityContacts(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var contacts = new AetheriaRuntimeEntityContactSnapshot[count]; + for (var contact = 0; contact < count; contact++) + { + var contactFields = reader.ReadArrayHeader(); + var targetEntityKey = ReadFieldString(ref reader, contactFields, 0); + var infoGathered = ReadFieldDouble(ref reader, contactFields, 1); + var hostile = ReadFieldBool(ref reader, contactFields, 2); + var visible = ReadFieldBool(ref reader, contactFields, 3); + SkipRemaining(ref reader, contactFields, 4); + contacts[contact] = new AetheriaRuntimeEntityContactSnapshot(targetEntityKey, infoGathered, hostile, visible); + } + + return contacts; } private static Vector2Value ReadFieldVector2(ref MessagePackReader reader, int fields, int index) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs index 02d93c112..795d83d31 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStateCommitDocument.cs @@ -585,6 +585,25 @@ namespace GameCult.Aetheria.State.Unity [Key(33)] public int VisibilitySourceCount { get; set; } + + [Key(34)] + public IReadOnlyList Contacts { get; set; } = Array.Empty(); + } + + [MessagePackObject] + public sealed class AetheriaRuntimeEntityContactCommit + { + [Key(0)] + public int TargetEntityIndex { get; set; } = -1; + + [Key(1)] + public double InfoGathered { get; set; } + + [Key(2)] + public bool Hostile { get; set; } + + [Key(3)] + public bool Visible { get; set; } } [MessagePackObject] diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 05abfcc92..eebfb8175 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -153,6 +153,7 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. They also carry public runtime session state: velocity, target entity reference, active flag, heatsink toggle, shutdown override, tractor power, heatstroke/hypothermia accumulators, aggregate visibility/source-count state, + contact rows for gathered information, hostility, and visible classification, and active consumable item/timer rows. Entity equipment, cargo-bay, and docking-bay slot rows preserve item-instance facts needed for restore: item key, quality, durability, and quantity. Those @@ -850,6 +851,9 @@ First Aetheria surfaces to publish: - Done: preserve aggregate entity visibility and visibility-source count in typed entity snapshots, so `Entity.VisibilitySources` no longer hides untyped session pressure. + - Done: preserve entity contact rows for gathered info, hostility, and + visible classification, so detection/contact state is typed snapshot data + instead of only reactive runtime collections. - Remaining: add typed documents/mappers for runtime object graphs, typed behavior factory construction, remaining behavior-private state not covered by progress, weapon, sensor, radiator, reactor, or capacitor rows,