diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 495449747..2dae5a3b3 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -1561,7 +1561,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks availableHull.Hardpoints = [ new AetheriaRuntimeHardpoint("ControlModule", 0, 0, 1, 1, 1, one, "", "None", 0), - new AetheriaRuntimeHardpoint("Weapon", 1, 0, 1, 1, 1, one, "", "None", 0), + new AetheriaRuntimeHardpoint("Weapon", 1, 0, 1, 1, 1, one, "", "Clockwise", 0), new AetheriaRuntimeHardpoint("Sensors", 2, 0, 1, 1, 1, one, "", "None", 0) ]; var unavailableHull = Item("cheap-foreign-hull", AetheriaRuntimeItemCategories.Hull, "foreign", 1); @@ -1637,6 +1637,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks first.Equipment.Any(value => value.ItemKey == "ship-sensor") && first.Equipment.All(value => value.ItemKey != "station-sensor"), "generated ships must fit hardpoint equipment and reject station-sized sensor gear"); + RequireEqual("Clockwise", first.Equipment.Single(value => value.ItemKey == "cannon").Rotation, + "generated equipment must preserve the placement rotation used by hardpoint fitting"); Require(first.HullItemKey == second.HullItemKey && first.Equipment.Select(value => value.ItemKey).SequenceEqual(second.Equipment.Select(value => value.ItemKey)), "same seed, map, faction and catalog must produce the same loadout"); diff --git a/Aetheria.State.Daemon/AetheriaDaemonLoadoutGenerator.cs b/Aetheria.State.Daemon/AetheriaDaemonLoadoutGenerator.cs index 2a5926d75..2a93e2b31 100644 --- a/Aetheria.State.Daemon/AetheriaDaemonLoadoutGenerator.cs +++ b/Aetheria.State.Daemon/AetheriaDaemonLoadoutGenerator.cs @@ -66,8 +66,9 @@ public sealed class AetheriaDaemonLoadoutGenerator continue; } - slots.Add(Slot(hardpoint.PositionX, hardpoint.PositionY, item.ItemKey)); - Reserve(occupied, item, hardpoint.PositionX, hardpoint.PositionY, ParseRotation(hardpoint.Rotation)); + var rotation = ParseRotation(hardpoint.Rotation); + slots.Add(Slot(hardpoint.PositionX, hardpoint.PositionY, item.ItemKey, rotation)); + Reserve(occupied, item, hardpoint.PositionX, hardpoint.PositionY, rotation); previous.Add(item); } @@ -141,7 +142,7 @@ public sealed class AetheriaDaemonLoadoutGenerator if (selected.Item == null || !selected.Fit.HasValue) throw new InvalidOperationException("No available mandatory equipment fits the remaining hull space."); var fit = selected.Fit.Value; - slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey)); + slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey, fit.Rotation)); Reserve(occupied, selected.Item, fit.X, fit.Y, fit.Rotation); return selected.Item; } @@ -163,7 +164,7 @@ public sealed class AetheriaDaemonLoadoutGenerator if (selected.Item == null || !selected.Fit.HasValue) return null; var fit = selected.Fit.Value; - slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey)); + slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey, fit.Rotation)); Reserve(occupied, selected.Item, fit.X, fit.Y, fit.Rotation); return selected.Item; } @@ -201,7 +202,7 @@ public sealed class AetheriaDaemonLoadoutGenerator var fit = FindFit(interiorCells, width, height, item, occupied); if (!fit.HasValue) continue; Reserve(occupied, item, fit.Value.X, fit.Value.Y, fit.Value.Rotation); - packed.Add(CargoSlot(fit.Value.X, fit.Value.Y, item.ItemKey)); + packed.Add(CargoSlot(fit.Value.X, fit.Value.Y, item.ItemKey, fit.Value.Rotation)); } return packed.ToArray(); } @@ -362,6 +363,14 @@ public sealed class AetheriaDaemonLoadoutGenerator _ => 0 }; + private static string RotationName(int rotation) => (((rotation % 4) + 4) % 4) switch + { + 1 => "Clockwise", + 2 => "Half", + 3 => "CounterClockwise", + _ => "None" + }; + private static bool HasBehavior(AetheriaRuntimeCatalogItem item, string kind) => (item.BehaviorKinds ?? Array.Empty()).Contains(kind, StringComparer.Ordinal); @@ -375,19 +384,21 @@ public sealed class AetheriaDaemonLoadoutGenerator (item.BehaviorKinds ?? Array.Empty()).Any(kind => kind.Contains("Weapon", StringComparison.Ordinal))); - private static AetheriaEntityItemSlot Slot(int x, int y, string itemKey) => new() + private static AetheriaEntityItemSlot Slot(int x, int y, string itemKey, int rotation = 0) => new() { Position = new AetheriaGridCoord { X = x, Y = y }, ItemKey = itemKey, - Quality = 1, Durability = 1, Quantity = 1, Enabled = true + Quality = 1, Durability = 1, Quantity = 1, Enabled = true, + Rotation = RotationName(rotation) }; private static AetheriaLoadoutItemSlot CargoSlot(int index, string itemKey) => CargoSlot(index % 4, index / 4, itemKey); - private static AetheriaLoadoutItemSlot CargoSlot(int x, int y, string itemKey) => new() + private static AetheriaLoadoutItemSlot CargoSlot(int x, int y, string itemKey, int rotation = 0) => new() { Position = new AetheriaGridCoord { X = x, Y = y }, - Item = new AetheriaLoadoutItem { ItemKey = itemKey, Quality = 1, Durability = 1, Quantity = 1, Enabled = true } + Item = new AetheriaLoadoutItem { ItemKey = itemKey, Quality = 1, Durability = 1, Quantity = 1, Enabled = true }, + Rotation = RotationName(rotation) }; } diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index de38adf03..41f983eca 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -2916,6 +2916,7 @@ static AetheriaRuntimeLoadoutItemSlotCommit[] ToEntitySlotCommits( { X = slot.Position?.X ?? 0, Y = slot.Position?.Y ?? 0, + Rotation = slot.Rotation ?? "None", Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = slot.ItemKey ?? "", @@ -2996,6 +2997,7 @@ static AetheriaRuntimeLoadoutItemSlotCommit[] ToSlotCommits( { X = slot.Position?.X ?? 0, Y = slot.Position?.Y ?? 0, + Rotation = slot.Rotation ?? "None", Item = ToLoadoutItemCommit(slot.Item) }) .ToArray(); diff --git a/Aetheria.State.Smoke/Program.cs b/Aetheria.State.Smoke/Program.cs index e37567a78..c2a0ef42c 100644 --- a/Aetheria.State.Smoke/Program.cs +++ b/Aetheria.State.Smoke/Program.cs @@ -248,6 +248,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s new AetheriaLoadoutItemSlot { Position = new AetheriaGridCoord { X = 0, Y = 0 }, + Rotation = "Clockwise", Item = new AetheriaLoadoutItem { ItemKey = itemKey.ToString(), @@ -275,6 +276,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s new AetheriaEntityItemSlot { Position = new AetheriaGridCoord { X = 0, Y = 0 }, + Rotation = "CounterClockwise", ItemKey = itemKey.ToString(), Quality = 0.9, Durability = 0.8, @@ -292,6 +294,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s new AetheriaLoadoutItemSlot { Position = new AetheriaGridCoord { X = 2, Y = 3 }, + Rotation = "Half", Item = new AetheriaLoadoutItem { ItemKey = itemKey.ToString(), @@ -851,6 +854,7 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether if (loadout?.RootEntity.Hull.ItemKey != AetheriaCatalogKeys.ItemDefinitionFromLegacyId(itemLegacyId).ToString() || loadout.RootEntity.Equipment.Length != 1 || + loadout.RootEntity.Equipment[0].Rotation != "Clockwise" || loadout.RootEntity.Equipment[0].Item.Enabled || !loadout.RootEntity.Equipment[0].Item.OverrideShutdown || loadout.RootEntity.WeaponGroups.Length != 1) @@ -890,10 +894,12 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether entitySnapshot.Equipment[0].Quality != 0.9 || entitySnapshot.Equipment[0].Durability != 0.8 || entitySnapshot.Equipment[0].Quantity != 1 || + entitySnapshot.Equipment[0].Rotation != "CounterClockwise" || entitySnapshot.Equipment[0].Enabled || !entitySnapshot.Equipment[0].OverrideShutdown || entitySnapshot.CargoContents.Length != 1 || entitySnapshot.CargoContents[0].Items[0].Item.Quantity != 7 || + entitySnapshot.CargoContents[0].Items[0].Rotation != "Half" || entitySnapshot.DockingBayAssignments.Length != 1 || entitySnapshot.DockingBayAssignments[0] != -1 || entitySnapshot.Visibility != 12.75 || diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index d777297bf..961e08136 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -1275,6 +1275,8 @@ static void RequireDaemonPlayableRunGenerationAuthority(string root) "HasBehavior(candidate, controllerKind)", "for (var draw = 0; draw < 16 && pool.Count > 0; draw++)", "FindFit(interiorCells, width, height, item, occupied)", + "slots.Add(Slot(hardpoint.PositionX, hardpoint.PositionY, item.ItemKey, rotation))", + "Rotation = RotationName(rotation)", "AetheriaLoadoutGenerationReceipt", "Selections = selectedKeys.Select" }; diff --git a/Aetheria.State/AetheriaRuntimeStateMapper.cs b/Aetheria.State/AetheriaRuntimeStateMapper.cs index 7d53750da..62a87d63c 100644 --- a/Aetheria.State/AetheriaRuntimeStateMapper.cs +++ b/Aetheria.State/AetheriaRuntimeStateMapper.cs @@ -120,6 +120,7 @@ public static class AetheriaRuntimeStateMapper X = slot.X, Y = slot.Y }, + Rotation = slot.Rotation ?? "None", Item = ToLoadoutItem(slot.Item) }) .ToArray(); diff --git a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs index 50b84678c..165ae702e 100644 --- a/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs +++ b/Aetheria.State/Documents/AetheriaRuntimeStateDocuments.cs @@ -190,6 +190,9 @@ public sealed class AetheriaLoadoutItemSlot [Key(1)] public AetheriaLoadoutItem Item { get; set; } = new(); + + [Key(2)] + public string Rotation { get; set; } = "None"; } [MessagePackObject] @@ -681,6 +684,9 @@ public sealed class AetheriaEntityItemSlot [Key(7)] public double Temperature { get; set; } + + [Key(8)] + public string Rotation { get; set; } = "None"; } [MessagePackObject] diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCargoTransactions.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCargoTransactions.cs index 7325b68f8..e8557d4a8 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCargoTransactions.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCargoTransactions.cs @@ -56,7 +56,7 @@ namespace GameCult.Aetheria.State.Verse private static AetheriaRuntimeLoadoutItemSlotCommit Clone(AetheriaRuntimeLoadoutItemSlotCommit source, int quantity) => new() { - X = source.X, Y = source.Y, + X = source.X, Y = source.Y, Rotation = source.Rotation ?? "None", Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = source.Item.ItemKey, Quality = source.Item.Quality, Durability = source.Item.Durability, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index 6601b0ae6..b55909c46 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -845,16 +845,22 @@ namespace GameCult.Aetheria.State.Verse [MessagePackObject(true)] public sealed class AetheriaRuntimeLoadoutItemSlotSnapshot { - public AetheriaRuntimeLoadoutItemSlotSnapshot(int x, int y, AetheriaRuntimeLoadoutItemSnapshot item) + public AetheriaRuntimeLoadoutItemSlotSnapshot( + int x, + int y, + AetheriaRuntimeLoadoutItemSnapshot item, + string rotation = "None") { X = x; Y = y; Item = item; + Rotation = rotation ?? "None"; } public int X { get; set; } public int Y { get; set; } public AetheriaRuntimeLoadoutItemSnapshot Item { get; set; } + public string Rotation { get; set; } } [MessagePackObject(true)] @@ -1017,7 +1023,8 @@ namespace GameCult.Aetheria.State.Verse int quantity, bool enabled, bool overrideShutdown, - double temperature = 0) + double temperature = 0, + string rotation = "None") { X = x; Y = y; @@ -1028,6 +1035,7 @@ namespace GameCult.Aetheria.State.Verse Enabled = enabled; OverrideShutdown = overrideShutdown; Temperature = temperature; + Rotation = rotation ?? "None"; } public int X { get; set; } @@ -1039,6 +1047,7 @@ namespace GameCult.Aetheria.State.Verse public bool Enabled { get; set; } public bool OverrideShutdown { get; set; } public double Temperature { get; set; } + public string Rotation { get; set; } } [MessagePackObject(true)] diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index 9dcda89d0..4b62af76e 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -954,7 +954,9 @@ namespace GameCult.Aetheria.State.Verse var enabled = ReadFieldBool(ref reader, slotFields, 5, true); var overrideShutdown = ReadFieldBool(ref reader, slotFields, 6); var temperature = ReadFieldDouble(ref reader, slotFields, 7); - SkipRemaining(ref reader, slotFields, 8); + var rotation = ReadFieldString(ref reader, slotFields, 8); + if (string.IsNullOrWhiteSpace(rotation)) rotation = "None"; + SkipRemaining(ref reader, slotFields, 9); slots[slot] = new AetheriaRuntimeEntityItemSlotSnapshot( position.X, position.Y, @@ -964,7 +966,8 @@ namespace GameCult.Aetheria.State.Verse quantity <= 0 ? 1 : quantity, enabled, overrideShutdown, - temperature); + temperature, + rotation); } return slots; @@ -1532,8 +1535,10 @@ namespace GameCult.Aetheria.State.Verse var slotFields = reader.ReadArrayHeader(); var position = ReadFieldGridCoord(ref reader, slotFields, 0); var item = ReadFieldLoadoutItem(ref reader, slotFields, 1); - SkipRemaining(ref reader, slotFields, 2); - return new AetheriaRuntimeLoadoutItemSlotSnapshot(position.X, position.Y, item); + var rotation = ReadFieldString(ref reader, slotFields, 2); + if (string.IsNullOrWhiteSpace(rotation)) rotation = "None"; + SkipRemaining(ref reader, slotFields, 3); + return new AetheriaRuntimeLoadoutItemSlotSnapshot(position.X, position.Y, item, rotation); } private static GridCoord ReadFieldGridCoord(ref MessagePackReader reader, int fields, int index) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs index 5ed9238d4..b68ea5123 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs @@ -1983,6 +1983,7 @@ namespace GameCult.Aetheria.State.Verse { X = slot?.X ?? 0, Y = slot?.Y ?? 0, + Rotation = slot?.Rotation ?? "None", Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = item?.ItemKey ?? "", diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs index a66209528..2f12a600f 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeGameViewportDocuments.cs @@ -1031,7 +1031,8 @@ namespace GameCult.Aetheria.State.Verse slot.Item.Quantity, slot.Item.Enabled, slot.Item.OverrideShutdown, - slot.Item.Temperature)) + slot.Item.Temperature, + slot.Rotation ?? "None")) .ToArray(); } @@ -1059,7 +1060,8 @@ namespace GameCult.Aetheria.State.Verse slot.Item.Quantity, slot.Item.Enabled, slot.Item.OverrideShutdown, - slot.Item.Temperature))) + slot.Item.Temperature), + slot.Rotation ?? "None")) .ToArray(); } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs index c5831cfd0..866b6441d 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeSnapshotDocuments.cs @@ -150,6 +150,9 @@ namespace GameCult.Aetheria.State.Verse [Key(2)] public AetheriaRuntimeLoadoutItemCommit Item { get; set; } = new AetheriaRuntimeLoadoutItemCommit(); + + [Key(3)] + public string Rotation { get; set; } = "None"; } [MessagePackObject] @@ -420,6 +423,7 @@ namespace GameCult.Aetheria.State.Verse { X = slot.X, Y = slot.Y, + Rotation = slot.Rotation ?? "None", Item = CloneItem(slot.Item) }; } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 7240c48b0..bf676be83 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -1246,6 +1246,13 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. before hydrating row projections. No live caller can enumerate legacy catalog entries or request catalog-shaped single entries through `ItemManager`. + Equipment placement rotation is durable semantic state, not an occupancy-loop + temporary. The daemon generator emits the same quarter-turn used to reserve a + hardpoint, free-space fit, or cargo fit; loadout documents, runtime commits, + catalog and viewport snapshots, state mapping, and clone/transfer paths retain + it. This is the required input for daemon-owned thruster direction and torque; + Unity may visualize the resulting actuator state but cannot reconstruct a + missing rotation from prefab transforms. - Inputs: Unity gameplay code, legacy catalog files, typed state documents, and CultMesh server state. - Outputs: `Aetheria.State` emits `.cc` state and CultMesh documents. Runtime diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index 0b1e1a08a..2214e9fa6 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -17,7 +17,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity. |---|---|---|---|---|---|---| | Run/session state | `ServerShared`, `ActionGameManager` | Aetheria daemon/CultCache | provider/session advertisement | all clients | owner-exists; wired; projected; not proven | restart and multi-client convergence | | Entity identity/loadout | `ServerShared/Entity*` | daemon | typed entity and inventory surfaces | EveUnity/Electron | owner-exists; wired; projected; not proven | loadout round trip and reconnect | -| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, and hardpoints require matching semantic type, rotated footprint containment, and controller role; generation fills the largest compatible footprint before applying economic weighting, so small gear remains a valid large-mount fallback without displacing available station-scale gear; cargo/capacitor plus station docking equipment must fit remaining hull space; stations draw up to sixteen faction-available equipment rows and pack largest-first into the selected cargo bay's actual interior shape; ordinary ships begin without generated cargo, and the deleted scenario-string path can no longer silently invent non-catalog item identities; station cargo slots and receipts preserve the same canonical keys; zone generation derives one continuing deterministic RNG stream per availability corporation, so cross-faction calls cannot perturb another faction; every generated entity persists an immutable source/selection receipt that reaches Eve as `loadout.item` semantics; Terminus explicitly maps combat-role factions to catalog availability corporations | multi-zone generated-world parity and exact root-to-zone stream sequencing against the fossil | +| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, and hardpoints require matching semantic type, rotated footprint containment, and controller role; the exact placement rotation used by hardpoint/free-space/cargo fitting now survives generated state, durable loadouts, runtime commits, viewport snapshots, cloning, and reconnect instead of disappearing after occupancy reservation; generation fills the largest compatible footprint before applying economic weighting, so small gear remains a valid large-mount fallback without displacing available station-scale gear; cargo/capacitor plus station docking equipment must fit remaining hull space; stations draw up to sixteen faction-available equipment rows and pack largest-first into the selected cargo bay's actual interior shape; ordinary ships begin without generated cargo, and the deleted scenario-string path can no longer silently invent non-catalog item identities; station cargo slots and receipts preserve the same canonical keys; zone generation derives one continuing deterministic RNG stream per availability corporation, so cross-faction calls cannot perturb another faction; every generated entity persists an immutable source/selection receipt that reaches Eve as `loadout.item` semantics; Terminus explicitly maps combat-role factions to catalog availability corporations | multi-zone generated-world parity and exact root-to-zone stream sequencing against the fossil | | Flight intent | `Ship`, `Thruster`, `AetherDrive`, input paths | daemon intent + Ymir physics | analog movement/look/tractor | EveUnity input driver | movement intents feed Ymir-owned body integration; authored thrust allocation, torque, energy and heat remain simplified | prove original drive/equipment behavior ordering and runtime interpolation | | Collision | Unity physics and entity handlers | Ymir | collision/result facts | visual interpolation only | Ymir.Core world stepping wired for entity bodies, radial fields and contacts; missing-owner negative proof passes | publish authoritative collision events and prove Unity cannot write transforms back | | Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | simplified owner wired/projected; every authored instant lock behavior now evaluates on every daemon step independently of trigger/burst selection, so an idle weapon cannot retain a stale completed lock; acquisition and decay derive from facing, elapsed time, and observer-local contact confidence instead of granting an immediate perfect lock; target changes and invalid targets clear the daemon state; `weapon.lock.started`, `weapon.lock.acquired`, and `weapon.lock.lost` publish stable one-time chronology with target, weapon identity, reason, and prior/current progress through the generic feedback stream; the provider cockpit and generic combat reticle consume the same continuous selected-target lock without client smoothing or reconstruction | angular loss fixture, visibility/nearest/explicit targeting cases, provider audio/effect roles, and authored reticle parity |