From eef8a5b7aa94aaedc05d6caffed71919023e9d6e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 18:14:33 +0100 Subject: [PATCH] Add Unity playable world presentation boundary --- docs/eve-migration-roadmap.md | 14 +- docs/parity-testing-harness.md | 9 +- docs/world-state-lowering.md | 15 +- .../Runtime/EveUnityPlayableWorldPresenter.cs | 154 ++++++++++++++ .../EveUnitySceneSurfaceLowererTests.cs | 201 ++++++++++++------ .../eve-runtime-capability.json | 4 + .../eveunity-scene-split-handoff.json | 6 +- .../run-eveunity-scene-lifecycle-smoke.ps1 | 2 +- ...un-eveunity-scene-provider-shell-smoke.ps1 | 12 +- ...ntime-owner-conformance-consumer-smoke.ps1 | 1 + tools/parity/parity-manifest.json | 10 +- 11 files changed, 346 insertions(+), 82 deletions(-) create mode 100644 runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index b8e1f2f..e600ee7 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -613,11 +613,15 @@ Recently cut: `EveUnitySceneClientSession` now consumes provider surface snapshots and emits movement/focus/target/action intents through the advertised command boundary, while `EveUnitySceneProviderConnection` defines the generic surface source - and command sink ports that a CultMesh/CultNet adapter must implement. The - Unity client boundary is generic before it learns live transport. The next - blocker is the live generic Unity player loop: implement that CultMesh/CultNet - adapter, instantiate the projected world in Unity, and wait for provider - receipts/next frames rather than renderer-local simulation. + and command sink ports that a CultMesh/CultNet adapter must implement. + `EveUnityPlayableWorldPresenter` now maps provider-authored entity rows and + asset refs into scene sink operations, including removal of entities absent + from later provider snapshots, without importing Aetheria entity/prefab + classes. The Unity client boundary is generic before it learns live transport. + The next blocker is the live generic Unity player loop: implement that + CultMesh/CultNet adapter, provide Unity `GameObject`/prefab-backed scene sink + and asset resolver implementations, and wait for provider receipts/next frames + rather than renderer-local simulation. - EveElectron capture lifecycle now carries the same kind of structured pending capture contract. `captureContract` names the Electron shell runtime, capture kind, PNG artifact pattern, conformance attachment point, required Aetheria diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 42463f5..91f3ff1 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -484,9 +484,12 @@ active surface and emits `gamecult.eve.command.v1` intents without Aetheria types. It also declares `providerSurfaceSource` through `EveUnitySceneProviderConnection`, `IEveUnitySceneProviderSurfaceSource`, and `IEveUnitySceneCommandSink`, so the live CultMesh/CultNet adapter has a narrow -runtime-owned port to implement. The remaining blocker is that live adapter, -Unity player instantiation, and Unity screenshot or frame-capture PNG production -from EveUnity. +runtime-owned port to implement. The runtime now also declares +`playableWorldScenePresentation`: `EveUnityPlayableWorldPresenter` maps +provider-authored entity rows and asset refs into scene sink operations while +removing entities absent from later provider snapshots. The remaining blocker is +that live adapter, Unity `GameObject`/prefab-backed scene sink and asset +resolver, and Unity screenshot or frame-capture PNG production from EveUnity. Runtime capture probe coverage is exported as `runtimeCaptureProbeCoverage[]`. Each record joins runtime status, capture owner, lifecycle capture contract, diff --git a/docs/world-state-lowering.md b/docs/world-state-lowering.md index e9fcfa5..571acaf 100644 --- a/docs/world-state-lowering.md +++ b/docs/world-state-lowering.md @@ -163,14 +163,19 @@ radii, asset refs, controllability, and command affordances. The generic `gamecult.eve.command.v1` intents through the advertised command boundary. `EveUnitySceneProviderConnection` adds the live-source port shape: `IEveUnitySceneProviderSurfaceSource` supplies current and updated snapshots, -while `IEveUnitySceneCommandSink` receives command envelopes. Aetheria-specific -names appear only as provider-authored data and command ids; the Unity client -code does not import Aetheria runtime types or apply movement locally. +while `IEveUnitySceneCommandSink` receives command envelopes. +`EveUnityPlayableWorldPresenter` maps the provider-authored entity rows and +asset refs into scene operations through `IEveUnityPlayableWorldSceneSink` and +`IEveUnityPlayableWorldAssetResolver`, including removal of entities that +disappear from later provider snapshots. Aetheria-specific names appear only as +provider-authored data and command ids; the Unity client code does not import +Aetheria runtime types, prefab classes, or apply movement locally. This is not yet the final playable client. The remaining cut is a live CultMesh/CultNet provider subscription adapter implementing those source/sink -ports, Unity player instantiation of the projected world, and proof that daemon -receipts drive the next frame rather than renderer-local state. +ports, Unity `GameObject`/prefab-backed implementations of the scene sink and +asset resolver, and proof that daemon receipts drive the next frame rather than +renderer-local state. ## Non-Goals diff --git a/runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs b/runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs new file mode 100644 index 0000000..b77695b --- /dev/null +++ b/runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + public interface IEveUnityPlayableWorldSceneSink + { + void ConfigureWorld(EveUnityPlayableWorldProjection world); + + void UpsertEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset); + + void RemoveEntity(string entityId); + } + + public interface IEveUnityPlayableWorldAssetResolver + { + EveUnityPlayableWorldAssetBinding Resolve(EveUnityPlayableWorldEntity entity); + } + + public sealed class EveUnityPlayableWorldPresenter + { + private readonly IEveUnityPlayableWorldSceneSink _sceneSink; + private readonly IEveUnityPlayableWorldAssetResolver _assetResolver; + private readonly HashSet _knownEntityIds = new HashSet(StringComparer.Ordinal); + + public EveUnityPlayableWorldPresenter( + IEveUnityPlayableWorldSceneSink sceneSink, + IEveUnityPlayableWorldAssetResolver assetResolver) + { + _sceneSink = sceneSink ?? throw new ArgumentNullException(nameof(sceneSink)); + _assetResolver = assetResolver ?? throw new ArgumentNullException(nameof(assetResolver)); + } + + public EveUnityPlayableWorldPresentation Apply(EveUnitySceneProjection projection) + { + if (projection == null) throw new ArgumentNullException(nameof(projection)); + if (projection.PlayableWorld == null) + throw new InvalidOperationException("Unity playable world presentation requires a playableWorld projection."); + + return Apply(projection.PlayableWorld); + } + + public EveUnityPlayableWorldPresentation Apply(EveUnityPlayableWorldProjection world) + { + if (world == null) throw new ArgumentNullException(nameof(world)); + + var nextEntityIds = new HashSet(StringComparer.Ordinal); + _sceneSink.ConfigureWorld(world); + + var upserted = 0; + foreach (var entity in world.Entities) + { + if (string.IsNullOrWhiteSpace(entity.EntityId)) + continue; + + nextEntityIds.Add(entity.EntityId); + _sceneSink.UpsertEntity(entity, _assetResolver.Resolve(entity)); + upserted++; + } + + var removed = 0; + var previousEntityIds = new List(_knownEntityIds); + foreach (var entityId in previousEntityIds) + { + if (nextEntityIds.Contains(entityId)) + continue; + + _sceneSink.RemoveEntity(entityId); + _knownEntityIds.Remove(entityId); + removed++; + } + + foreach (var entityId in nextEntityIds) + _knownEntityIds.Add(entityId); + + return new EveUnityPlayableWorldPresentation( + world.WorldRootId, + world.PlayerEntityId, + world.InputProfile, + world.CameraRig, + upserted, + removed, + _knownEntityIds.Count); + } + } + + public sealed class EveUnityPlayableWorldAssetBinding + { + public EveUnityPlayableWorldAssetBinding( + string assetRef, + string entityKind, + string presentationKind) + { + AssetRef = assetRef ?? ""; + EntityKind = entityKind ?? ""; + PresentationKind = presentationKind ?? ""; + } + + public string AssetRef { get; } + + public string EntityKind { get; } + + public string PresentationKind { get; } + } + + public sealed class EveUnityPlayableWorldPresentation + { + public EveUnityPlayableWorldPresentation( + string worldRootId, + string playerEntityId, + string inputProfile, + string cameraRig, + int upsertedEntities, + int removedEntities, + int activeEntities) + { + WorldRootId = worldRootId ?? ""; + PlayerEntityId = playerEntityId ?? ""; + InputProfile = inputProfile ?? ""; + CameraRig = cameraRig ?? ""; + UpsertedEntities = upsertedEntities; + RemovedEntities = removedEntities; + ActiveEntities = activeEntities; + } + + public string WorldRootId { get; } + + public string PlayerEntityId { get; } + + public string InputProfile { get; } + + public string CameraRig { get; } + + public int UpsertedEntities { get; } + + public int RemovedEntities { get; } + + public int ActiveEntities { get; } + } + + public sealed class EveUnityAssetRefResolver : IEveUnityPlayableWorldAssetResolver + { + public EveUnityPlayableWorldAssetBinding Resolve(EveUnityPlayableWorldEntity entity) + { + if (entity == null) throw new ArgumentNullException(nameof(entity)); + return new EveUnityPlayableWorldAssetBinding( + entity.AssetRef, + entity.EntityKind, + string.IsNullOrWhiteSpace(entity.AssetRef) ? "unity-generated-placeholder" : "provider-asset-ref"); + } + } +} diff --git a/runtimes/incubating/eve-unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/runtimes/incubating/eve-unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index d509abd..0094c00 100644 --- a/runtimes/incubating/eve-unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/runtimes/incubating/eve-unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -198,6 +198,46 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projectionUpdates, Is.EqualTo(2)); } + [Test] + public void PlayableWorldPresenterInstantiatesUpdatesAndDespawnsProviderEntities() + { + var lowerer = new EveUnitySceneSurfaceLowerer(); + var sink = new FakePlayableWorldSceneSink(); + var presenter = new EveUnityPlayableWorldPresenter(sink, new EveUnityAssetRefResolver()); + + var firstProjection = lowerer.Lower(PlayableArpgDocument(), Advertisement("aetheria.daemon.game")); + var firstPresentation = presenter.Apply(firstProjection); + + Assert.That(firstPresentation.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable")); + Assert.That(firstPresentation.PlayerEntityId, Is.EqualTo("player-vanguard")); + Assert.That(firstPresentation.InputProfile, Is.EqualTo("arpg-third-person")); + Assert.That(firstPresentation.CameraRig, Is.EqualTo("third-person-orbit")); + Assert.That(firstPresentation.UpsertedEntities, Is.EqualTo(3)); + Assert.That(firstPresentation.RemovedEntities, Is.EqualTo(0)); + Assert.That(firstPresentation.ActiveEntities, Is.EqualTo(3)); + Assert.That(sink.ConfiguredWorlds.Count, Is.EqualTo(1)); + Assert.That(sink.Upserts.Count, Is.EqualTo(3)); + Assert.That(sink.Upserts[0].entity.EntityId, Is.EqualTo("player-vanguard")); + Assert.That(sink.Upserts[0].entity.PositionX, Is.EqualTo(0f)); + Assert.That(sink.Upserts[0].asset.AssetRef, Is.EqualTo("cultmesh://aetheria/assets/map/entity/player")); + Assert.That(sink.Upserts[0].asset.PresentationKind, Is.EqualTo("provider-asset-ref")); + + var secondProjection = lowerer.Lower( + PlayableArpgDocument(includeRaider: false, playerPosition: "5,0,2"), + Advertisement("aetheria.daemon.game")); + var secondPresentation = presenter.Apply(secondProjection); + + Assert.That(secondPresentation.UpsertedEntities, Is.EqualTo(2)); + Assert.That(secondPresentation.RemovedEntities, Is.EqualTo(1)); + Assert.That(secondPresentation.ActiveEntities, Is.EqualTo(2)); + Assert.That(sink.ConfiguredWorlds.Count, Is.EqualTo(2)); + Assert.That(sink.RemovedEntityIds.Count, Is.EqualTo(1)); + Assert.That(sink.RemovedEntityIds[0], Is.EqualTo("raider-scout")); + Assert.That(sink.Upserts[3].entity.EntityId, Is.EqualTo("player-vanguard")); + Assert.That(sink.Upserts[3].entity.PositionX, Is.EqualTo(5f)); + Assert.That(sink.Upserts[3].entity.PositionZ, Is.EqualTo(2f)); + } + [Test] public void SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState() { @@ -323,8 +363,79 @@ namespace GameCult.Eve.UnityScene.Tests Array.Empty()); } - private static EveSurfaceDocument PlayableArpgDocument() + private static EveSurfaceDocument PlayableArpgDocument( + bool includeRaider = true, + string playerPosition = "0,0,0") { + var playableChildren = new List + { + PlayableEntity( + "aetheria.daemon.game.entity.player", + "player-vanguard", + "player", + "Vanguard", + "player", + "cultmesh://aetheria/assets/map/entity/player", + playerPosition, + "35", + "1.15", + true, + true, + "aetheria.daemon.focus", + "aetheria.daemon.move_intent", + "", + "") + }; + + if (includeRaider) + { + playableChildren.Add(PlayableEntity( + "aetheria.daemon.game.entity.raider", + "raider-scout", + "enemy", + "Raider Scout", + "raider", + "cultmesh://aetheria/assets/map/entity/ship", + "9,0,14", + "220", + "0.9", + true, + false, + "", + "", + "aetheria.daemon.target", + "")); + } + + playableChildren.Add(PlayableEntity( + "aetheria.daemon.game.entity.station", + "anchor-station", + "station", + "Anchor Station", + "neutral", + "cultmesh://aetheria/assets/map/entity/station", + "-18,0,6", + "0", + "3.4", + true, + false, + "aetheria.daemon.focus", + "", + "", + "")); + + playableChildren.Add(new EveSurfaceComponent( + "aetheria.daemon.game.flow", + "field.vector3d", + new Dictionary(StringComparer.Ordinal) + { + ["fieldId"] = "aetheria.zone.flow", + ["bind"] = "aetheria.daemon.soaView.flowField3d", + ["visualizer"] = "particles", + ["bounds"] = "-32,0,-32,32,24,32" + }, + Array.Empty())); + return new EveSurfaceDocument( "aetheria", "game.runtime", @@ -354,68 +465,7 @@ namespace GameCult.Eve.UnityScene.Tests ["targetCommand"] = "aetheria.daemon.target", ["actionCommand"] = "aetheria.daemon.use_equipment" }, - new[] - { - PlayableEntity( - "aetheria.daemon.game.entity.player", - "player-vanguard", - "player", - "Vanguard", - "player", - "cultmesh://aetheria/assets/map/entity/player", - "0,0,0", - "35", - "1.15", - true, - true, - "aetheria.daemon.focus", - "aetheria.daemon.move_intent", - "", - ""), - PlayableEntity( - "aetheria.daemon.game.entity.raider", - "raider-scout", - "enemy", - "Raider Scout", - "raider", - "cultmesh://aetheria/assets/map/entity/ship", - "9,0,14", - "220", - "0.9", - true, - false, - "", - "", - "aetheria.daemon.target", - ""), - PlayableEntity( - "aetheria.daemon.game.entity.station", - "anchor-station", - "station", - "Anchor Station", - "neutral", - "cultmesh://aetheria/assets/map/entity/station", - "-18,0,6", - "0", - "3.4", - true, - false, - "aetheria.daemon.focus", - "", - "", - ""), - new EveSurfaceComponent( - "aetheria.daemon.game.flow", - "field.vector3d", - new Dictionary(StringComparer.Ordinal) - { - ["fieldId"] = "aetheria.zone.flow", - ["bind"] = "aetheria.daemon.soaView.flowField3d", - ["visualizer"] = "particles", - ["bounds"] = "-32,0,-32,32,24,32" - }, - Array.Empty()) - }, + playableChildren, new[] { new CultMeshStateBindingDescriptor("run", "cultmesh://aetheria/run/current"), @@ -605,5 +655,30 @@ namespace GameCult.Eve.UnityScene.Tests Submitted.Add(request); } } + + private sealed class FakePlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink + { + public List ConfiguredWorlds { get; } = new List(); + + public List<(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)> Upserts { get; } = + new List<(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)>(); + + public List RemovedEntityIds { get; } = new List(); + + public void ConfigureWorld(EveUnityPlayableWorldProjection world) + { + ConfiguredWorlds.Add(world); + } + + public void UpsertEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset) + { + Upserts.Add((entity, asset)); + } + + public void RemoveEntity(string entityId) + { + RemovedEntityIds.Add(entityId); + } + } } } diff --git a/runtimes/incubating/eve-unity-scene/eve-runtime-capability.json b/runtimes/incubating/eve-unity-scene/eve-runtime-capability.json index 8f5ba2b..57ee493 100644 --- a/runtimes/incubating/eve-unity-scene/eve-runtime-capability.json +++ b/runtimes/incubating/eve-unity-scene/eve-runtime-capability.json @@ -20,6 +20,7 @@ "providerSurfaceSource", "sceneGraphProjection", "playableWorldProjection", + "playableWorldScenePresentation", "embeddedDocuments" ], "supportedPlugins": [ @@ -63,6 +64,7 @@ "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneClientSession.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", + "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", @@ -111,6 +113,7 @@ "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneClientSession.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", + "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", @@ -148,6 +151,7 @@ "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneClientSession.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", + "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", diff --git a/runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json b/runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json index 5e5e1e6..76c4c57 100644 --- a/runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json +++ b/runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json @@ -18,6 +18,7 @@ "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneClientSession.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", + "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", @@ -36,6 +37,7 @@ "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneClientSession.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", + "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "runtimes/incubating/eve-unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef", "tools/eveunity/eveunity-release-contract.mjs", "tools/eveunity/eveunity-release-contract.test.mjs", @@ -54,6 +56,7 @@ "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneClientSession.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", + "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", @@ -61,7 +64,7 @@ "runtimes/incubating/eve-unity-scene/eve-runtime-capability.json" ], "destinationOwner": "EveUnity", - "replacementProof": "EveUnity claims the provider-advertised unity-scene lowering target with provider-agnostic playable-world projection, scene graph projection, provider snapshot session, and command boundary evidence; live transport and capture remain separate lifecycle proofs." + "replacementProof": "EveUnity claims the provider-advertised unity-scene lowering target with provider-agnostic playable-world projection, scene graph projection, provider snapshot session, playable-world scene presentation, and command boundary evidence; live transport and capture remain separate lifecycle proofs." }, { "id": "unity-scene-sai-plugin-projection", @@ -150,6 +153,7 @@ "EveUnity owns a provider-agnostic scene graph lowerer that consumes advertised worldInteraction boundaries without importing Aetheria internals.", "EveUnity owns a provider-snapshot Unity scene client session that lowers Aetheria's surface and emits command intents without importing Aetheria project code.", "EveUnity owns provider surface source and command sink ports that a live CultMesh/CultNet adapter can implement without changing scene lowering.", + "EveUnity owns a playable-world presenter that maps provider-authored entity rows and asset refs into Unity scene operations without importing Aetheria entity or prefab classes.", "EveUnity owns the provider-agnostic playable-world scene lowerer outside Eve incubation.", "EveUnity owns the Unity scene Sai projection adapter outside Eve incubation without importing Sai story state or VN/Ink internals.", "EveUnity owns the Unity scene Norn projection adapter outside Eve incubation without importing Norn implementation state.", diff --git a/scripts/run-eveunity-scene-lifecycle-smoke.ps1 b/scripts/run-eveunity-scene-lifecycle-smoke.ps1 index 8eeee91..0d019ea 100644 --- a/scripts/run-eveunity-scene-lifecycle-smoke.ps1 +++ b/scripts/run-eveunity-scene-lifecycle-smoke.ps1 @@ -39,7 +39,7 @@ if ($manifest.incubation.splitHandoff.manifestPath -ne "runtimes/incubating/eve- throw "EveUnity scene manifest missing split handoff path" } -foreach ($feature in @("providerAdvertisements", "commandTransport", "providerSurfaceSession", "providerSurfaceSource", "sceneGraphProjection", "playableWorldProjection", "embeddedDocuments")) { +foreach ($feature in @("providerAdvertisements", "commandTransport", "providerSurfaceSession", "providerSurfaceSource", "sceneGraphProjection", "playableWorldProjection", "playableWorldScenePresentation", "embeddedDocuments")) { if (-not (@($manifest.supportedFeatures) -contains $feature)) { throw "EveUnity scene manifest missing provider-shell feature: $feature" } diff --git a/scripts/run-eveunity-scene-provider-shell-smoke.ps1 b/scripts/run-eveunity-scene-provider-shell-smoke.ps1 index e99e10a..02eba8a 100644 --- a/scripts/run-eveunity-scene-provider-shell-smoke.ps1 +++ b/scripts/run-eveunity-scene-provider-shell-smoke.ps1 @@ -54,7 +54,7 @@ foreach ($evidencePath in @($worldSurfaceLoweringClaim.evidencePaths)) { } } -foreach ($feature in @("providerAdvertisements", "commandTransport", "providerSurfaceSession", "providerSurfaceSource", "sceneGraphProjection", "playableWorldProjection")) { +foreach ($feature in @("providerAdvertisements", "commandTransport", "providerSurfaceSession", "providerSurfaceSource", "sceneGraphProjection", "playableWorldProjection", "playableWorldScenePresentation")) { if (-not (@($manifest.supportedFeatures) -contains $feature)) { throw "EveUnity scene provider shell missing supported feature: $feature" } @@ -66,6 +66,7 @@ $expectedFiles = @( "runtimes\incubating\eve-unity-scene\Runtime\EveUnitySceneSurfaceLowerer.cs", "runtimes\incubating\eve-unity-scene\Runtime\EveUnitySceneClientSession.cs", "runtimes\incubating\eve-unity-scene\Runtime\EveUnitySceneProviderConnection.cs", + "runtimes\incubating\eve-unity-scene\Runtime\EveUnityPlayableWorldPresenter.cs", "runtimes\incubating\eve-unity-scene\Runtime\SaiVisualNovelUnitySceneProjectionAdapter.cs", "runtimes\incubating\eve-unity-scene\Runtime\NornGraphUnitySceneProjectionAdapter.cs", "runtimes\incubating\eve-unity-scene\Runtime\TeXMathUnitySceneProjectionAdapter.cs", @@ -101,6 +102,13 @@ foreach ($symbol in @("IEveUnitySceneProviderSurfaceSource", "IEveUnitySceneComm } } +$playableWorldPresenterSource = Get-Content -LiteralPath (Join-Path $projectRoot "runtimes\incubating\eve-unity-scene\Runtime\EveUnityPlayableWorldPresenter.cs") -Raw +foreach ($symbol in @("IEveUnityPlayableWorldSceneSink", "IEveUnityPlayableWorldAssetResolver", "EveUnityPlayableWorldPresenter", "EveUnityPlayableWorldAssetBinding", "EveUnityPlayableWorldPresentation", "EveUnityAssetRefResolver", "ConfigureWorld", "UpsertEntity", "RemoveEntity", "provider-asset-ref", "unity-generated-placeholder")) { + if (-not $playableWorldPresenterSource.Contains($symbol)) { + throw "EveUnity playable world presenter missing symbol: $symbol" + } +} + $saiAdapterSource = Get-Content -LiteralPath (Join-Path $projectRoot "runtimes\incubating\eve-unity-scene\Runtime\SaiVisualNovelUnitySceneProjectionAdapter.cs") -Raw foreach ($symbol in @("SaiVisualNovelUnitySceneProjectionAdapter", "sai.vn", "vn.stage", "story.choose", "story.continue", "story.jump", "gamecult.eve.plugin_abi.v1", "sidecar-advertised-plugin-abi", "Sai")) { if (-not $saiAdapterSource.Contains($symbol)) { @@ -123,7 +131,7 @@ foreach ($symbol in @("TeXMathUnitySceneProjectionAdapter", "tex.math", "embed.t } $testSource = Get-Content -LiteralPath (Join-Path $projectRoot "runtimes\incubating\eve-unity-scene\Tests\Editor\EveUnitySceneSurfaceLowererTests.cs") -Raw -foreach ($symbol in @("LowerCarriesProviderAdvertisedWorldBoundary", "LowerBuildsProviderAgnosticSceneGraphFromSurfaceTree", "LowerExtractsPlayableArpgWorldFromGenericScene3dSurface", "GenericClientSessionConsumesAetheriaPlayableWorldSnapshotWithoutAetheriaTypes", "GenericProviderConnectionAppliesLiveSnapshotsAndSubmitsCommandsThroughSink", "FakeProviderSurfaceSource", "FakeCommandSink", "SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "playable-world-root", "playable-world-entity", "world-field-3d", "arpg-third-person", "cultmesh://aetheria/assets/manifest", "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", "aetheria.daemon.move_intent", "sai-vn-scene-stage", "norn-graph-scene-projection", "tex-math-scene-projection", "sai.vn", "norn.graph", "tex.math", "sidecar-advertised-plugin-abi", "aetheria.daemon.commands", "aetheria.eve_command_acceptance_status.v1")) { +foreach ($symbol in @("LowerCarriesProviderAdvertisedWorldBoundary", "LowerBuildsProviderAgnosticSceneGraphFromSurfaceTree", "LowerExtractsPlayableArpgWorldFromGenericScene3dSurface", "GenericClientSessionConsumesAetheriaPlayableWorldSnapshotWithoutAetheriaTypes", "GenericProviderConnectionAppliesLiveSnapshotsAndSubmitsCommandsThroughSink", "PlayableWorldPresenterInstantiatesUpdatesAndDespawnsProviderEntities", "FakeProviderSurfaceSource", "FakeCommandSink", "FakePlayableWorldSceneSink", "SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "playable-world-root", "playable-world-entity", "world-field-3d", "arpg-third-person", "cultmesh://aetheria/assets/manifest", "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", "aetheria.daemon.move_intent", "sai-vn-scene-stage", "norn-graph-scene-projection", "tex-math-scene-projection", "sai.vn", "norn.graph", "tex.math", "sidecar-advertised-plugin-abi", "aetheria.daemon.commands", "aetheria.eve_command_acceptance_status.v1")) { if (-not $testSource.Contains($symbol)) { throw "EveUnity scene provider shell tests missing symbol: $symbol" } diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index 1e704f2..997c236 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -68,6 +68,7 @@ node $consumerScript $consumerExport ` --expect-runtime-feature unity-scene:providerSurfaceSource ` --expect-runtime-feature unity-scene:sceneGraphProjection ` --expect-runtime-feature unity-scene:playableWorldProjection ` + --expect-runtime-feature unity-scene:playableWorldScenePresentation ` --expect-runtime-feature electron-shell:providerAdvertisements ` --expect-runtime-feature electron-shell:commandTransport ` --expect-runtime-feature electron-shell:surfaceTreeProjection ` diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index 7892f01..cf91ed4 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -1040,7 +1040,7 @@ "splitHandoffPath": "runtimes/incubating/eve-unity-scene/eveunity-scene-split-handoff.json", "requiredFixtures": ["aetheria-world", "eve-world-smoke"], "pluginFixtures": ["sai-vn"], - "supportedFeatures": ["providerAdvertisements", "commandTransport", "providerSurfaceSession", "providerSurfaceSource", "sceneGraphProjection", "playableWorldProjection", "embeddedDocuments"], + "supportedFeatures": ["providerAdvertisements", "commandTransport", "providerSurfaceSession", "providerSurfaceSource", "sceneGraphProjection", "playableWorldProjection", "playableWorldScenePresentation", "embeddedDocuments"], "supportedPlugins": [ { "pluginId": "sai.vn", @@ -1070,6 +1070,7 @@ "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneClientSession.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", + "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", @@ -1085,6 +1086,7 @@ "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneClientSession.cs", "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", + "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "runtimes/incubating/eve-unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", @@ -1120,6 +1122,10 @@ "path": "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneProviderConnection.cs", "contains": ["IEveUnitySceneProviderSurfaceSource", "IEveUnitySceneCommandSink", "EveUnitySceneProviderConnection", "SnapshotAvailable", "CurrentSnapshot", "Connect", "Refresh", "SubmitMoveIntent", "SubmitFocusIntent", "SubmitTargetIntent", "SubmitActionIntent", "Disconnect"] }, + { + "path": "runtimes/incubating/eve-unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", + "contains": ["IEveUnityPlayableWorldSceneSink", "IEveUnityPlayableWorldAssetResolver", "EveUnityPlayableWorldPresenter", "EveUnityPlayableWorldAssetBinding", "EveUnityPlayableWorldPresentation", "EveUnityAssetRefResolver", "ConfigureWorld", "UpsertEntity", "RemoveEntity", "provider-asset-ref", "unity-generated-placeholder"] + }, { "path": "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "contains": ["SaiVisualNovelUnitySceneProjectionAdapter", "sai.vn", "vn.stage", "story.choose", "story.continue", "story.jump", "gamecult.eve.plugin_abi.v1", "sidecar-advertised-plugin-abi", "Sai"] @@ -1134,7 +1140,7 @@ }, { "path": "runtimes/incubating/eve-unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs", - "contains": ["LowerCarriesProviderAdvertisedWorldBoundary", "LowerBuildsProviderAgnosticSceneGraphFromSurfaceTree", "LowerExtractsPlayableArpgWorldFromGenericScene3dSurface", "GenericClientSessionConsumesAetheriaPlayableWorldSnapshotWithoutAetheriaTypes", "GenericProviderConnectionAppliesLiveSnapshotsAndSubmitsCommandsThroughSink", "FakeProviderSurfaceSource", "FakeCommandSink", "SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "playable-world-root", "playable-world-entity", "world-field-3d", "arpg-third-person", "cultmesh://aetheria/assets/manifest", "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", "aetheria.daemon.move_intent", "sai-vn-scene-stage", "norn-graph-scene-projection", "tex-math-scene-projection", "norn.map", "cultmesh://aetheria/norn/map", "sai.vn", "norn.graph", "tex.math", "sidecar-advertised-plugin-abi", "aetheria.daemon.commands", "aetheria.eve_command_acceptance_status.v1"] + "contains": ["LowerCarriesProviderAdvertisedWorldBoundary", "LowerBuildsProviderAgnosticSceneGraphFromSurfaceTree", "LowerExtractsPlayableArpgWorldFromGenericScene3dSurface", "GenericClientSessionConsumesAetheriaPlayableWorldSnapshotWithoutAetheriaTypes", "GenericProviderConnectionAppliesLiveSnapshotsAndSubmitsCommandsThroughSink", "PlayableWorldPresenterInstantiatesUpdatesAndDespawnsProviderEntities", "FakeProviderSurfaceSource", "FakeCommandSink", "FakePlayableWorldSceneSink", "SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "playable-world-root", "playable-world-entity", "world-field-3d", "arpg-third-person", "cultmesh://aetheria/assets/manifest", "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", "aetheria.daemon.move_intent", "sai-vn-scene-stage", "norn-graph-scene-projection", "tex-math-scene-projection", "norn.map", "cultmesh://aetheria/norn/map", "sai.vn", "norn.graph", "tex.math", "sidecar-advertised-plugin-abi", "aetheria.daemon.commands", "aetheria.eve_command_acceptance_status.v1"] }, { "path": "tools/eveunity/eveunity-capture-contract.test.mjs",