diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 3ac9666d9..342995ce8 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -1230,6 +1230,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "docking must not manufacture a bay assignment on an entity with no docking-bay equipment"); RequireEqual(-1, outOfRange.DockingBayAssignments.Single(), "a client scalar must not widen the daemon-owned fossil docking radius"); + var dockedEvent = selectionRun.GameEvents.Single(value => value.Kind == "ship.docked"); + Require(dockedEvent.SourceEntityIndex == actor.EntityIndex && + dockedEvent.TargetEntityIndex == firstEligible.EntityIndex && + dockedEvent.ScalarValue == 0 && dockedEvent.SubjectKey == selectionRun.CurrentEntityKey, + "accepted docking must publish one retained actor/parent/bay transition fact"); var disarmed = actor.WeaponStates.Single(); RequireEqual(0, actor.ActiveWeaponGroups.Count, "docking must clear the daemon-owned held weapon-group latch"); @@ -1305,6 +1310,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "undock rejection must retain the exact daemon-owned fossil reason"); Require(run.Zones[0].Entities[1].DockingBayAssignments.Contains(0), "a rejected undock must leave parentage and bay assignment untouched"); + Require(!run.GameEvents.Any(value => value.Kind == "ship.undocked"), + "a rejected undock must not publish a false presentation transition"); return result; } @@ -1369,6 +1376,21 @@ internal sealed class AetheriaDaemonYmirSmokeChecks "undocking must preserve the ship pose, velocity, and direction instead of applying an invented launch"); Require(!successRun.Zones[0].Entities[1].DockingBayAssignments.Contains(0), "successful undocking must release the exact occupied bay"); + var undockedEvent = successRun.GameEvents.Single(value => value.Kind == "ship.undocked"); + Require(undockedEvent.SourceEntityIndex == successShip.EntityIndex && + undockedEvent.TargetEntityIndex == successRun.Zones[0].Entities[1].EntityIndex && + undockedEvent.ScalarValue == 0, + "accepted undocking must retain the released parent and bay after state removal"); + var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build( + new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = successRun }, + new AetheriaRuntimeDaemonHealthDocument(), + AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"), + catalog: catalog); + Require(Flatten(surface.Surface.Root).Any(node => node.Kind == "feedback.event" && + node.Props["eventKind"] == "ship.undocked" && + node.Props["sourceEntityIndex"] == "0" && node.Props["targetEntityIndex"] == "1" && + node.Props["scalarValue"] == "0"), + "generic Eve feedback must expose authoritative docking chronology and bay identity"); } private static void DockedEntitiesCannotAttackOrBeTargeted() diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs index 23ac23e84..1fe13b7d0 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs @@ -1544,6 +1544,7 @@ namespace GameCult.Aetheria.State.Verse TargetEntityKey = command.TargetEntityKey ?? "", Dock = true }); + AppendDockingEvent(run, command, "ship.docked", actor, command.TargetEntityKey ?? ""); return true; } @@ -1580,6 +1581,21 @@ namespace GameCult.Aetheria.State.Verse if (string.IsNullOrWhiteSpace(actor)) return context.Reject(AetheriaRuntimeDaemonRejectionReasons.InvalidDockActor); + var parentKey = TryResolveDockParent(run, actor, out var resolvedParentKey, out var resolvedParent) + ? resolvedParentKey + : ""; + var undockBayIndex = -1; + if (TryResolveEntity(run, actor, out _, out var resolvedActorIndex, out _) && resolvedParent != null) + { + var assignments = resolvedParent.DockingBayAssignments ?? Array.Empty(); + for (var index = 0; index < assignments.Count; index++) + if (assignments[index] == resolvedActorIndex) + { + undockBayIndex = index; + break; + } + } + if (!ApplyUndockState(run, actor, context)) return false; @@ -1588,9 +1604,48 @@ namespace GameCult.Aetheria.State.Verse ActorEntityKey = actor, Undock = true }); + AppendDockingEvent(run, command, "ship.undocked", actor, parentKey, undockBayIndex); return true; } + private static void AppendDockingEvent( + AetheriaRuntimeRunCheckpointCommit run, + AetheriaRuntimeDaemonCommandDocument command, + string kind, + string actorEntityKey, + string parentEntityKey, + int knownBayIndex = -1) + { + if (!TryResolveEntity(run, actorEntityKey, out var zoneIndex, out var actorIndex, out var actor) || + !TryResolveEntity(run, parentEntityKey, out _, out var parentIndex, out var parent)) + return; + var bayIndex = knownBayIndex; + if (bayIndex < 0) + { + var assignments = parent.DockingBayAssignments ?? Array.Empty(); + for (var index = 0; index < assignments.Count; index++) + if (assignments[index] == actorIndex) + { + bayIndex = index; + break; + } + } + AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit + { + EventId = $"command:{command.CommandId}:{kind}", + Kind = kind, + FrameId = command.ObservedFrameId, + ZoneIndex = zoneIndex, + SourceEntityIndex = actorIndex, + TargetEntityIndex = parentIndex, + SubjectKey = actorEntityKey, + Reason = parentEntityKey, + ScalarValue = bayIndex, + PositionX = actor.PositionX, + PositionZ = actor.PositionZ + }); + } + private static bool ApplyInteractIntent( AetheriaRuntimeRunCheckpointCommit run, AetheriaRuntimeDaemonCommandDocument command, diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 230f44ce0..a75f48aa7 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -3076,11 +3076,13 @@ Docking ownership follows the fossil's entity and `ActionGameManager` path: bodies remain independently collidable with ships. - Outputs: exact parent/assignment mutation, cleared held-group and transient weapon state on dock, a world-combat participant set containing neither - docked attackers nor docked targets, docking intent, and an authoritative - rejection reason carried by the command fact and Eve receipt. + docked attackers nor docked targets, docking intent, one retained + `ship.docked` or `ship.undocked` fact carrying actor, parent, bay, frame and + position, and an authoritative rejection reason carried by the command fact + and Eve receipt. - Derived state: camera target, subject visibility, movement suppression, menu - copy, and effects derive from dock parentage plus the receipt. They do not - decide whether docking happened. + copy, audio, and effects derive from dock parentage plus retained feedback + chronology. They do not decide whether docking happened. - Forbidden writers: Unity `TryDock`/`TryUndock`, camera transitions, client proximity, command-supplied failure text, and renderer-local component checks cannot mutate or explain docking truth. Stale input state, forged fire @@ -3096,8 +3098,9 @@ Docking ownership follows the fossil's entity and `ActionGameManager` path: circle radii make the authored center-distance rule unreachable. - Verification layer: operation tests cover every ordered rejection, bay cargo, first-eligible selection, weapon disarm, docked fire-command rejection, - preserved pose/velocity/direction, exact receipt reason, entity pass-through - during home approach, and retained ship/pickup contact collection. Simulation + preserved pose/velocity/direction, exact receipt reason, exactly one accepted + transition fact, no false event on rejected undock, Eve feedback projection, + entity pass-through during home approach, and retained ship/pickup contact collection. Simulation smoke proves both docked attacker and docked target subtraction despite stale state and a forged pulse; the live witness remains responsible for camera/effect timing. diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index e3e59214d..b5cbc028a 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -27,7 +27,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity. | Weapons | ServerShared behaviors + Unity effects | daemon plus Ymir geometry/kinematics | action capability, weapon state, shot/damage receipts | effects/audio/HUD | instant and constant resource/timing lifecycles are daemon-owned; charged weapons accept one semantic request, spool without requiring a solution, hold at full charge, commit automatically when a solution appears, and perform deterministic once-per-second malfunction hazard checks after the authored safe hold interval with increasing persisted risk; successful instant, constant, charged, deployable, and turret-controlled transactions publish authored transient visibility and consume the owning equipment's temperature-derived wear, with charged visibility applying its authored multiplier and turret wear remaining exact-weapon scoped; GuidedWeapon and Launcher retain ordinary authoritative shot resolution while publishing their complete fossil steering profile as derived presentation; generic Eve `weapon.state` exposes charge hold/risk and feedback exposes readiness, commit, refusal, and malfunction chronology; committed direct damage now enters the shared daemon armor-cell transaction | remaining impact-distribution parity, generic guided/effect lowering, and Unity effect reconciliation | | Destruction/drops | hull death, 25% equipment rolls, unconditional cargo drops, 25-unit launch, 30-second pickups | daemon | destruction identity, death/drop events, typed pickups | provider-advertised destruction effect and pickups | owner-exists; wired; exclusive; projected: direct and deployable lethal damage commit one deterministic destruction transaction, clear live references, retain only a non-rendered identity tombstone, and emit exact pickups/events; residual cell damage destroys installed equipment before hull, disables its behaviors immediately, and cockpit destruction kills with its fossil cause before scalar hull depletion; the reserved client `DestroyEntity` command is rejected; Aetheria advertises its original big explosion as `effect.feedback.entity.destroyed`, lowered generically by EveUnity | multiplayer reconnect chronology | | 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. 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 chronology | +| 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. Smoke serializes and rehydrates mid-entry and proves agents traverse multi-edge routes through the same command boundary | 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 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 | live generic refit controls, reconnect after accepted and 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. 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, contested stock, and full trade surface parity | @@ -38,7 +38,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity. | Celestial world | `ZoneRenderer`, body/asteroid prefab scripts | daemon render projection plus provider assets | ordinary generic Eve entity SoA rows | EveUnity generic entity/prefab lowerer | wired, exclusive, projected, and lowered: daemon orbit and terrain queries emit stable presentation-only identities for suns, planets, gas giants, and live asteroid instances; provider packaging extracts only each advertised visual child and strips fossil scripts/physics; released EveUnity `0.3.51` applies semantic scale around provider-authored local scale and contains no celestial or Aetheria branch. The released warm witness records eleven bodies and eighteen asteroids with provider refs and enabled renderers, including seven celestial instances intersecting the pilot frustum | body material/light readability, close-range composition matching the fossil, asteroid variation, and Electron lowering | | Fields/fog/gravity | zone renderer and shaders | daemon/plugin semantics | Fields plugin documents | EveUnity fields lowerer | daemon publishes canonical gravity/fog splat layers through `field.volume3d`; Eve Fields `0.2.3` owns portable `PowerPulse` and `AnimatedRadialCosine` semantics; Aetheria emits the fossil global animated simplex/cellular/ambient stack, body-owned radial waves, and separate negative gameplay `gravity.height` and positive visual `fog.surface_height`; the shared camera-relative lattice spans 1536 world units as 256 six-unit Stardust cells, rasterizes gravity at eight texels per cell into 2048 texels, and snaps the viewport by one six-unit cell; daemon entities follow fossil terrain height plus hull grid offset after Ymir XZ motion; released EveUnity `0.3.58` lowers the authored topology, one shared snapped viewport frame for fog and Stardust, provider shader ABI, pre-postprocess URP pass, provider-requested TAA, provider-material temporal coverage inputs, authoritative flow time, the historical positive-Z Unity shader camera transform, and float32 packed-output diagnostics without provider code. The provider bundle runs the actual Stardust compute shader across X, Y, and diagonal one-cell remaps and proves 705 overlapping world cells remain bit-identical after buffer reassignment; an asymmetric GPU fixture proves positive world X samples positive texture X instead of retaining the deleted fossil gravity camera's two-axis reversal; its source gate rejects replacement of temporal dithering with a fixed alpha threshold. A separate gravity-fog gate requires raymarch and temporal reprojection to share the historical positive-Z camera convention. The flight post-process bundle gate verifies serialized ACES, exposure/contrast, bloom, vignette, and film-grain components | restore readable patch structure, gravity waves, surrounding world geometry, and final luminance/composition parity; visual parity remains explicitly open | | HUD/schematic | ARPG HUD and `SchematicDisplay` | Eve composition from daemon facts | semantic status/components | EveUnity UI | provider-authored transparent cockpit is wired, projected, and lowered through generic UI Toolkit panes, metrics, and progress components for hull, shield, capacitor, temperature, cargo, weapon cooldown/groups, selected target, target lock, target shield, and target hull. The daemon now publishes the original spatial schematic semantics as a generic `ship.schematic` subtree for both self and selected target: explicit occupied hull cells with current/maximum armor, catalog-backed rotated equipment footprints, durability, thermal range/performance, online state, wear, and nested weapon ammo/cooldown/charge/reload facts. Refit, damage, and presentation share the same rotation geometry; Unity objects do not derive schematic truth | generic spatial schematic lowering, warning timelines, dynamic action state, and composited live capture | -| Hit/lock/warnings | `ActionGameManager` feedback | daemon event facts | transient feedback stream | VFX/UI/audio | bounded deduplicated game-event ledger and Eve `feedback.stream` wired for pickup outcomes, projectile launch/impact, entity damage, alive-to-dead destruction, authoritative weapon reload chronology, lock start/acquired/lost chronology, and episode-deduplicated `weapon.fire.refused` facts for insufficient energy or missing ammunition; every lock transition carries authored weapon identity, target, reason, and prior/current progress, while reconnect priming prevents retained events from replaying as new feedback | add thermal and docking event families, negative transaction fixtures, provider lock audio/effect roles, and broader runtime effect consumption | +| Hit/lock/warnings | `ActionGameManager` feedback | daemon event facts | transient feedback stream | VFX/UI/audio | bounded deduplicated game-event ledger and Eve `feedback.stream` wired for pickup outcomes, projectile launch/impact, entity damage, alive-to-dead destruction, thermal-risk crossings, accepted dock/undock transitions, authoritative weapon reload chronology, lock start/acquired/lost chronology, and episode-deduplicated `weapon.fire.refused` facts for insufficient energy or missing ammunition; docking facts retain exact actor/parent/bay identity and rejected operations emit none; every lock transition carries authored weapon identity, target, reason, and prior/current progress, while reconnect priming prevents retained events from replaying as new feedback | broader negative transaction fixtures, provider lock/docking audio and effect roles, and broader runtime effect consumption | | Assets | `Assets/Resources` | Aetheria provider | asset catalog + CultMesh CDN | runtime cache/load | provider manifests name deterministic bundle-internal paths and now project each typed hull prefab through the same asset key in Eve scene and SoA identity state; the authoring build derives presentation-only prefabs, strips non-Eve gameplay scripts, externalizes embedded effects, preserves source material semantics, and verifies the loaded bundle ABI; released EveUnity `0.3.57` resolves Unity's lowercase bundle names against authored-case advertised keys. The released cold witness begins with an empty cache, transfers the 56,168,017-byte body through `cultmesh.content.v1`, atomically promotes its exact SHA-256 with no partial, selects `SharedFileMapping`, and lowers the shield-free hull while preserving pilot/map isolation | provider-to-client network zero-copy, shared-memory delivery, and GPU zero-copy remain open; the managed network session still copies and fragments payload bytes before verified promotion | | Save/resume | original persistence paths | daemon/CultCache | published current state | clients rehydrate | owner-exists; not parity-proven | mid-loop restart | | Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | live minimum-loop proof: generic Unity discovers the provider, lowers the pilot world/assets, single-steps dock and undock while paused, buys daemon-priced station stock, single-steps Ymir salvage collection, resumes, moves, targets, fires, and presents authoritative receipts/shot outcome; the released full-session profile starts with no pickup, destroys a deterministic target, observes its canonical cargo drop, tractors it into a Ymir Begin contact, and proves exactly one `ymir-fact:` collection receipt plus cargo `0` to `1` | richer trade content, docking transitions, and sustained combat encounters |