From 5c7a56bd3b2c9abaefe3a68a4e3582bf13bcb09e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 17 Jul 2026 06:40:51 +0100 Subject: [PATCH] Make trade delivery spatial and atomic --- Aetheria.State.Daemon.Smoke/Program.cs | 47 +++++++++++++------ .../Tests/DaemonRuntimeDocumentTests.cs | 26 +++++++++- .../AetheriaRuntimeDaemonOperations.cs | 23 +++++---- .../AetheriaRuntimeRefitTransactions.cs | 12 +++-- docs/aetheria-perfect-machine-map.md | 10 +++- docs/original-game-migration-matrix.md | 4 +- 6 files changed, 91 insertions(+), 31 deletions(-) diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index d1d1442d9..8d1523505 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -6802,12 +6802,26 @@ internal sealed class AetheriaDaemonYmirSmokeChecks private static void TradePurchaseDerivesAcceptanceFromDaemonState() { - var ore = CatalogItem("trade-ore"); ore.Price = 40; ore.Volume = 1; - var cargoBay = CatalogItem("trade-cargo-bay"); cargoBay.InteriorOccupiedCells = 10; + var ore = CatalogItem("trade-ore"); + ore.Price = 40; + ore.Volume = 1; + ore.Stackable = true; + ore.MaxStack = 4; + ore.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)]; + var cargoBay = CatalogItem("trade-cargo-bay"); + cargoBay.InteriorShapeWidth = 3; + cargoBay.InteriorShapeHeight = 1; + cargoBay.InteriorOccupiedCells = 3; + cargoBay.InteriorShapeCells = + [ + new AetheriaRuntimeShapeCell(0, 0), + new AetheriaRuntimeShapeCell(1, 0), + new AetheriaRuntimeShapeCell(2, 0) + ]; var catalog = new AetheriaRuntimeCatalogSnapshot([ore, cargoBay], [], []); var station = Entity(0, 0, "station"); station.Kind = "station"; - station.CargoContents = [Cargo((ore.ItemKey, 10, 2, 3))]; + station.CargoContents = [Cargo((ore.ItemKey, 4, 2, 3), (ore.ItemKey, 4, 3, 3))]; station.DockingBayAssignments = [1]; station.ChildEntityIndices = [1]; var ship = Entity(1, 0, "player"); @@ -6815,7 +6829,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 } }]; - ship.CargoContents = [Cargo()]; + ship.CargoContents = [Cargo((ore.ItemKey, 3, 0, 0))]; var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [station, ship] }; var run = new AetheriaRuntimeRunCheckpointCommit { @@ -6832,7 +6846,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks 1, run.CurrentEntityKey); command.TradePurchase.ItemKey = ore.ItemKey; - command.TradePurchase.Quantity = 5; + command.TradePurchase.Quantity = 4; command.TradePurchase.StationCargoIndex = 0; command.TradePurchase.TargetCargoIndex = 0; command.TradePurchase.SourceX = 2; @@ -6846,8 +6860,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks Require(AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(run.CurrentEntityKey, out _, out _), "trade smoke current entity key must be daemon-resolvable"); - RequireEqual(10, AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(ship, catalog, ore.ItemKey, 0), - "trade smoke cargo fixture must have room for the requested commodity"); + RequireEqual(0, AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(ship, catalog, ore.ItemKey, 0), + "the old volume-only capacity proxy must expose why it cannot own spatial stack placement"); var daemonUnitPrice = AetheriaRuntimeDaemonTradeItemQueries.TradeItemValue( ore, station.CargoContents[0].Items[0].Item, @@ -6861,9 +6875,14 @@ internal sealed class AetheriaDaemonYmirSmokeChecks Require(result.AppliedCommandIds.Contains(command.CommandId), "daemon must accept a valid commodity purchase regardless of forged compatibility opinions"); - RequireEqual(5000 - daemonUnitPrice * 5, run.Credits, + RequireEqual(5000 - daemonUnitPrice * 4, run.Credits, "daemon catalog price must own trade credit mutation"); - RequireEqual(5, CargoQuantity(ship, ore.ItemKey), "daemon cargo capacity and placement must own delivery"); + RequireEqual(7, CargoQuantity(ship, ore.ItemKey), "daemon cargo capacity and placement must own delivery"); + Require(ship.CargoContents.Single().Items + .Where(slot => slot.Item.ItemKey == ore.ItemKey) + .Select(slot => (slot.X, slot.Y, slot.Item.Quantity)) + .SequenceEqual([(0, 0, 4), (1, 0, 3)]), + "a purchase must fill an existing stack before placing the remainder in the first authored free cell"); RequireEqual(2, zone.Entities.Count, "forged ship-creation opinion must not materialize an entity"); AetheriaRuntimeDaemonCommandDocument Contender(string clientId, string commandId) @@ -6876,10 +6895,10 @@ internal sealed class AetheriaDaemonYmirSmokeChecks run.CurrentEntityKey); contender.CommandId = commandId; contender.TradePurchase.ItemKey = ore.ItemKey; - contender.TradePurchase.Quantity = 5; + contender.TradePurchase.Quantity = 4; contender.TradePurchase.StationCargoIndex = 0; contender.TradePurchase.TargetCargoIndex = 0; - contender.TradePurchase.SourceX = 2; + contender.TradePurchase.SourceX = 3; contender.TradePurchase.SourceY = 3; return contender; } @@ -6899,9 +6918,9 @@ internal sealed class AetheriaDaemonYmirSmokeChecks RequireEqual(AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable, contested.RejectedCommandReasons[secondClient.CommandId], "contested stock must retain the exact daemon-owned rejection reason"); - RequireEqual(creditsBeforeContest - daemonUnitPrice * 5, run.Credits, + RequireEqual(creditsBeforeContest - daemonUnitPrice * 4, run.Credits, "contested stock must debit the shared run wallet exactly once"); - RequireEqual(10, CargoQuantity(ship, ore.ItemKey), + RequireEqual(11, CargoQuantity(ship, ore.ItemKey), "contested stock must deliver exactly one winning stack quantity"); RequireEqual(0, CargoQuantity(station, ore.ItemKey), "the winning purchase must exhaust authoritative station stock"); @@ -6966,7 +6985,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "CultCache reconnect must preserve the single authoritative debit"); RequireEqual(0, CargoQuantity(durableStation, ore.ItemKey), "CultCache reconnect must not resurrect contested station stock"); - RequireEqual(10, CargoQuantity(durableShip, ore.ItemKey), + RequireEqual(11, CargoQuantity(durableShip, ore.ItemKey), "CultCache reconnect must preserve the winning cargo delivery exactly once"); RequireEqual("reconciled", AetheriaRuntimeDaemonReceiptProjector.Project( durableWinner, AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId).State, diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index 4c269cc9f..5c0178263 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -4584,12 +4584,34 @@ public class DaemonRuntimeDocumentTests run.CurrentEntityKey = "zone.0.entity.1"; run.Zones[0].Entities[0].DockingBayAssignments = new[] { 1 }; run.Zones[0].Entities[0].ChildEntityIndices = new[] { 1 }; + var ore = CatalogItem("ore", Array.Empty(), price: 40, stackable: true); + ore.MaxStack = 10; + ore.ShapeWidth = 1; + ore.ShapeHeight = 1; + ore.OccupiedCells = 1; + ore.ShapeCells = new[] { new AetheriaRuntimeShapeCell(0, 0) }; + var cargoBay = CatalogItem("trade-test-cargo-bay", Array.Empty()); + cargoBay.InteriorShapeWidth = 1; + cargoBay.InteriorShapeHeight = 1; + cargoBay.InteriorOccupiedCells = 1; + cargoBay.InteriorShapeCells = new[] { new AetheriaRuntimeShapeCell(0, 0) }; + run.Zones[0].Entities[1].CargoBays = new[] + { + new AetheriaRuntimeLoadoutItemSlotCommit + { + Item = new AetheriaRuntimeLoadoutItemCommit + { + ItemKey = cargoBay.ItemKey, + Quantity = 1 + } + } + }; var command = AetheriaRuntimeDaemonCommandDocument.Create( AetheriaRuntimeDaemonCommandKinds.TradePurchase, "codex", "session-trade", 30, - "zone.0.entity.0"); + "zone.0.entity.1"); command.TargetEntityKey = "zone.0.entity.1"; command.TextValue = "ore"; command.ScalarValue = 200; @@ -4612,7 +4634,7 @@ public class DaemonRuntimeDocumentTests new AetheriaRuntimeDaemonOperationContext { Catalog = new AetheriaRuntimeCatalogSnapshot( - new[] { CatalogItem("ore", Array.Empty(), price: 40) }, + new[] { ore, cargoBay }, Array.Empty(), Array.Empty()) }); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs index f33b654b4..c9cc01bf0 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs @@ -1083,27 +1083,32 @@ namespace GameCult.Aetheria.State.Verse Math.Max(0, purchase.TargetCargoIndex), out var targetEntity, out var targetCargoIndex, - out _) || - quantity > AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(targetEntity, catalog, itemKey, targetCargoIndex)) + out _)) { return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeTargetNoFit); } - if (!TryRemoveCargoItemQuantity( + if (!AetheriaRuntimeRefitTransactions.TryTransferCargo( stationEntity, stationCargoIndex, - itemKey, purchase.SourceX, purchase.SourceY, + targetEntity, + targetCargoIndex, + itemKey, quantity, - out var purchasedSlot)) + 0, + 0, + false, + catalog, + out var cargoReason)) { - return context.Reject(AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable); + return context.Reject( + string.Equals(cargoReason, AetheriaRuntimeDaemonRejectionReasons.InvalidCargoSource, StringComparison.Ordinal) + ? AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable + : AetheriaRuntimeDaemonRejectionReasons.TradeTargetNoFit); } - purchasedSlot.X = 0; - purchasedSlot.Y = 0; - AddCargoItem(targetEntity, targetCargoIndex, purchasedSlot); run.Credits -= totalPrice; return true; } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRefitTransactions.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRefitTransactions.cs index 7a60ac84c..74009709e 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRefitTransactions.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRefitTransactions.cs @@ -173,9 +173,15 @@ namespace GameCult.Aetheria.State.Verse if (remaining > 0) { - moved.Item.Quantity = remaining; - if (!TryPlaceCargoSlot(destinationItems, bay, item, moved, 0, 0, false, catalog)) - return Reject(AetheriaRuntimeDaemonRejectionReasons.CargoNoFit, out rejectionReason); + var maxStack = item.Stackable ? Math.Max(1, item.MaxStack) : 1; + while (remaining > 0) + { + var placed = CloneSlot(moved); + placed.Item.Quantity = Math.Min(remaining, maxStack); + if (!TryPlaceCargoSlot(destinationItems, bay, item, placed, 0, 0, false, catalog)) + return Reject(AetheriaRuntimeDaemonRejectionReasons.CargoNoFit, out rejectionReason); + remaining -= placed.Item.Quantity; + } } } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index a20243178..969345716 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -3045,7 +3045,15 @@ purchase reads the stock left by the preceding command in that frame, so two clients requesting the final stack produce one atomic debit/delivery and one typed `trade-stock-unavailable` rejection. World state and both committed command facts survive a hard CultCache close/reopen; generic Eve reconstructs -the reconciled and denied receipts without owning economy logic. Runtime loadout +the reconciled and denied receipts without owning economy logic. Commodity +delivery shares the inventory owner's cloned spatial transaction: existing +compatible stacks fill first, remaining quantity splits at the catalog maximum, +and each new stack claims the first fitting authored bay cells before stock, +cargo, or credits commit. The old volume-only capacity proxy is no longer a +trade writer. The fossil had no durable quote object or expiry transition; it +displayed current value and bought immediately, so purchase execution derives +the current authoritative value instead of adding a counterfeit quote owner. +Runtime loadout restore follows the same rule: UI requests restoration, and the daemon owns instantiation, credits, dock assignment, current entity, and checkpoint. Docked current-ship selection follows the same rule: UI requests selection, and the diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index 241f271aa..f9ff984ee 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -29,8 +29,8 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity. | Tractor/scoop | analog tractor and physical timed pickups | daemon + Ymir | tractor power, pickup body and receipt | pickup world entity plus beam/interaction feedback | fossil 2/s power ramp and authored forward radius/traction/distance feed pickup bodies into the shared Ymir world; ship contact collects exactly once when capacity permits or applies the fossil 25-unit rejection kick; Aetheria maps pickup collision only against ship bodies, so stations and other world bodies cannot become cargo writers; 30s daemon lifetime; instant cargo teleport deleted; current-zone pickups occupy ordinary rows in the shared Eve entity SoA body with stable logical identity and provider-owned assets; `pilot.scoop` advertises a generic `button-hold.v1` scalar contract, the released client sends `1` on press and `0` on release through `SetTractorPower`, both receipts reconcile, and daemon power returns exactly to zero; the provider build removes the fossil's embedded tractor object from ship prefabs, and `beam.presentation` attaches the sole standalone effect to the SoA ship; released warm witnesses prove successful exactly-once Ymir collection and controlled-player `cargo-capacity` rejection with unchanged cargo | polish provider beam shape/attachment | | Dock/undock | wormhole-first interact; first eligible bay; component prerequisites | daemon + Ymir placement | dock commands/context | camera/UI transition | daemon docking now preserves the fossil transaction: stable zone order selects the first entity with a real empty bay inside the fixed 25-unit XZ center radius; undock checks installed Cockpit, Thruster-or-AetherDrive, Reactor, then empty bay cargo in source order; rejection reasons survive committed command facts into generic Eve receipts; success removes parentage without moving, stopping, or rotating the ship. Accepted transitions additionally publish retained `ship.docked` / `ship.undocked` feedback with exact actor, parent, bay, frame and position; rejected operations publish no false transition. Docking clears held-group and transient weapon state, docked actors reject later fire commands, and combat subtracts docked child identities from both attacker iteration and target lookup, so stale input, forged pulses, and turret controllers cannot fire through a bay. Ymir world entities retain gravity/query bodies but do not invent ship/station rigidbody collision response that would make the 25-unit rule unreachable; ship/pickup Begin contacts remain enabled. The live generic Unity witness also proves advertised actions, bay assignment, dock-parent SoA inclusion and sensor sharing, camera retargeting, controlled-subject hiding, movement suppression, and restoration on undock | reconnect, docked inventory/trade surfaces, and authored dock-camera blend/effect lowering | | Wormhole travel | `ActionGameManager.EnterWormhole` plus `Ship` enter/exit state | daemon fixed-step transition plus Ymir exclusion | current-entity transition state and feedback chronology | provider-selected generic transition lowering | command acceptance no longer moves an entity immediately; player interact, explicit travel, and agent routing derive the same canonical adjacent-zone wormhole and proximity gate, then persist four-second `entering`, authoritative transfer, four-second `exiting`, and one-frame `completed` phases. The daemon owns smootherstep XZ motion, the fossil 1000-unit visual-depth lever, deterministic 20-unit exit direction, 50-unit exit radius, target-adjacency discovery, and exactly-once start/transfer/completion events. Flight and every world-physics implementation receive no transitioning body; Ymir resumes on the tick after completion. An autonomous three-zone route now hard-flushes and closes CultCache during the first `entering` phase, reopens into a fresh run graph and Ymir owner, preserves the assigned task/carrier/phase, crosses both edges, emits exactly two start/transfer/completion triplets, and never clones the worker | generic provider transition effect/camera lowering and live multi-zone pilot witness | -| Inventory/equipment | `Entity.ItemFits`, `TryEquip`, `TryUnequip`, `EquippedCargoBay.ItemFits`/`TryStore`, inventory panels + ServerShared | daemon atomic inventory transactions | typed inventory/equipment state, commands, exact receipts | generic UI | generated cargo and docking bays now occupy their typed collections instead of masquerading as ordinary equipment; docked equip/reposition/store validates the complete destination on cloned state before committing either side, uses authored hull interior and hardpoint geometry, forces hardpoint rotation, rejects occupied cells and deployed ships, preserves exact item instance fields, surviving equipment behavior/weapon state, and weapon-group membership, routes bay categories with aligned content/assignment arrays, and refuses to unequip nonempty/occupied bays. Cargo transfer now uses the same plan-first rule with typed bay interiors, rotated item footprints, deterministic free-cell search, exact stack maxima, and no partial source/destination mutation on rejection; exact reasons reach generic Eve receipts. The generic Eve equip request-to-transaction-to-CultCache-restart path is covered, including source non-resurrection | live generic refit control lowering, reconnect after rejected moves, commodity multi-stack chronology, and full fossil inventory feedback parity | -| Trade | trade UI/economy | daemon | stock, quote, purchase/sale commands | generic UI | daemon derives station, stock row, catalog item, current price, quantity, credits, target capacity, and hull-vs-cargo result; purchases ignore client opinions about price and destination kind. The sequential daemon reducer now serializes two client purchases for the same final stock in one frame: exactly one command debits/delivers, the other commits `trade-stock-unavailable`, and a hard CultCache close/reopen preserves exhausted stock, the single wallet debit, delivered cargo, and both generic Eve receipts. Docked sales identify only the source stack and quantity: the daemon derives station, instance-sensitive value, payout, deterministic typed stock placement, and commits cargo/credits atomically; generic Eve advertises available buy and sell actions. The generic Unity live witness receives an authoritative purchase receipt while paused | live sale receipt witness, quote expiry, and full trade surface parity | +| Inventory/equipment | `Entity.ItemFits`, `TryEquip`, `TryUnequip`, `EquippedCargoBay.ItemFits`/`TryStore`, inventory panels + ServerShared | daemon atomic inventory transactions | typed inventory/equipment state, commands, exact receipts | generic UI | generated cargo and docking bays now occupy their typed collections instead of masquerading as ordinary equipment; docked equip/reposition/store validates the complete destination on cloned state before committing either side, uses authored hull interior and hardpoint geometry, forces hardpoint rotation, rejects occupied cells and deployed ships, preserves exact item instance fields, surviving equipment behavior/weapon state, and weapon-group membership, routes bay categories with aligned content/assignment arrays, and refuses to unequip nonempty/occupied bays. Cargo transfer now uses the same plan-first rule with typed bay interiors, rotated item footprints, deterministic free-cell search, exact stack maxima, multi-stack splitting, and no partial source/destination mutation on rejection; exact reasons reach generic Eve receipts. The generic Eve equip request-to-transaction-to-CultCache-restart path is covered, including source non-resurrection | live generic refit control lowering, reconnect after rejected moves, and full fossil inventory feedback parity | +| Trade | trade UI/economy | daemon | stock, current value, purchase/sale commands | generic UI | daemon derives station, stock row, catalog item, current price, quantity, credits, target capacity, and hull-vs-cargo result; purchases ignore client opinions about price and destination kind. Commodity delivery now uses the same cloned spatial cargo plan as inventory transfer: it fills compatible partial stacks, splits the remainder at authored maximum stack size, finds deterministic free cells in the bay shape, and only then commits stock, cargo, and credits. The sequential daemon reducer serializes two client purchases for the same final stock in one frame: exactly one command debits/delivers, the other commits `trade-stock-unavailable`, and a hard CultCache close/reopen preserves exhausted stock, the single wallet debit, delivered cargo, and both generic Eve receipts. Docked sales identify only the source stack and quantity: the daemon derives station, instance-sensitive value, payout, deterministic typed stock placement, and commits cargo/credits atomically; generic Eve advertises available buy and sell actions. The fossil had no durable quote or expiry mechanic: its menu displayed current item value and mutated immediately, so Aetheria intentionally reprices at authoritative command execution rather than inventing quote authority. The generic Unity live witness receives an authoritative purchase receipt while paused | live sale receipt witness and full trade surface parity | | Action bar/input | input shell and rebinding UI | daemon advertises actions; client owns bindings | input capability and action descriptors | universal binding UI | contracts wired; consumable rows are catalog-filtered, expose daemon-owned cargo quantity, active-effect count, exact duration/remaining duration, and the fossil `remaining / duration` fill as `remaining-ratio.v1`. A depleted or non-stackable active consumable remains visible with unavailable command state until its effect ends, so a lowerer never queries Unity gameplay objects. Weapon groups advertise the fossil press/release lifecycle as generic `button-hold.v1` values routed to `SetWeaponGroupActive`; daemon entity state persists the active-group latch, press edges pulse instant/charged weapons once, constant weapons remain selected without client keepalives, release clears the latch, and the one-shot compatibility command is not the public input contract. Equipment actions derive from exact installed catalog payload/state pairs instead of blindly activating behavior zero: Switch and Trigger expose generic hold values, adjustable Thermotoggle exposes a canonical `scalar.v1` Kelvin target/current value, fixed/non-interactive rows are omitted, and offline rows retain identity as unavailable. Target nearest/previous/next/reticle/clear actions expose daemon-owned current selection, visible-hostile count, and availability; the default profile preserves fossil `R/T/Y/U`, restores the shared wormhole-first/then-dock interaction to `F`, and routes `view-direction.v1` world XYZ through the released generic runtime while target ranking remains daemon policy. Released EveUnity lowers provider action-bar suggestions with provider-owned icon asset keys, availability, one-shot, held, view-direction, and explicit scalar controls without importing Aetheria. Every value form translates through generic Eve requests into typed daemon commands under provider identity `aetheria` | generic slot editing/persistence, consumable quantity/progress chrome, provider styling, and live action-bar witness | | Pilot camera | ARPG follow/dock cameras | Eve view semantics + runtime | camera rig and semantic view id | EveUnity camera rig | daemon dock state selects distinct contracts instead of collapsing both fossil cameras: undocked flight advertises the entity-forward `Third Person Rig` framing (distance 30, screen 0.64/0.19, FOV 60, lens 1-4096) and the shared `aim.presentation` convergence point as its look-at; Aetheria translates the fossil Cinemachine top-origin screen Y of `0.81` to Eve's bottom-origin viewport Y of `0.19`; EveUnity solves native pose while preserving both optical aim and advertised entity framing, without provider types; the retained dock projection remains separate | reproduce the dock camera's separate follow/look-at subjects, blend timeline, noise, and reconnect state | | Render channels | Unity layers/culling masks | provider surface policy + runtime asset variant | semantic channel exclusions | EveUnity native layer mapping | wired, exclusive, and lowered: the pilot surface excludes the semantic `map` channel; the Unity asset variant maps `map` to its authored native layer; EveUnity subtracts that layer from the configured camera mask; portable state contains no Unity layer or mask; provider conversion preserves the source icon shader's unlit lighting model; the released witness proves the pilot camera excludes map glyphs while the map camera retains readable white glyphs without a scene light | preserve the channel proof while world presentation and transport continue to evolve |