Add Unity playable world projection proof

This commit is contained in:
Meta Vens 2026-07-09 17:56:30 +01:00
commit 09874ad026
16 changed files with 829 additions and 23 deletions

View file

@ -603,6 +603,16 @@ Recently cut:
Norn, and TeX remain sidecar plugin projections when a visual-novel surface
is lowered. The remaining EveUnity blocker is an owner-repo Unity screenshot
or frame-capture PNG, not absence of semantic scene projection evidence.
- Unity scene lowering now extracts a provider-authored playable ARPG world
model from the Aetheria daemon game surface without importing Aetheria runtime
types. The Aetheria fixture carries generic `world.scene3d`,
`world.entity3d`, asset-manifest, input-profile, camera, movement, focus, and
target command affordances; `EveUnitySceneSurfaceLowerer` exposes them as a
`PlayableWorld` projection, and the JSON capture artifact exports the same
`playableWorld` shape under `gamecult.eve.unity_scene_projection.v1`. The
next blocker is the live generic Unity client loop: subscribe to the Aetheria
daemon surface over CultMesh/CultNet, instantiate the projected world, emit
`gamecult.eve.command.v1` input, and wait for provider receipts/next frames.
- 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

View file

@ -474,9 +474,12 @@ Unity scene semantic capture evidence follows the same rule without pretending
it is a rendered Unity frame. `tools/eveunity/eveunity-scene-capture-artifact.mjs`
lowers the Aetheria world surface into `gamecult.eve.unity_scene_projection.v1`
and writes `artifacts/eveunity-scene-capture/latest/unity-scene-projection.json`.
It records scene graph projection, embedded slots, and sidecar plugin projection
metadata. The remaining blocker is Unity screenshot or frame-capture PNG
production from EveUnity.
It records scene graph projection, embedded slots, sidecar plugin projection
metadata, and a `playableWorld` extraction for generic `world.scene3d` /
`world.entity3d` ARPG data: state pointer, asset manifest, camera/input profile,
entity transforms, asset refs, and daemon command affordances. The remaining
blocker is a live generic Unity client loop 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,

View file

@ -152,6 +152,22 @@ Toolkit, Unity scene, Electron shell, and TUI lowerers are lowering targets;
Aetheria remains the owner of world state, assets, command acceptance, and
receipts.
The Unity scene lowerer now has a first playable-world extraction proof. The
fixture carries a generic `world.scene3d` node with a daemon state pointer,
asset manifest ref, third-person input/camera profile, movement/focus/target
command names, `world.entity3d` rows, and a `field.vector3d` volume field. The
Unity scene runtime lowers that into `playableWorld` projection evidence:
entity ids, kinds, labels, positions, radii, asset refs, controllability, and
command affordances. 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.
This is not yet the final playable client. The remaining cut is a generic
EveUnity scene client that subscribes to the live Aetheria daemon surface over
CultMesh/CultNet, instantiates the projected world in a Unity player, emits
input as `gamecult.eve.command.v1`, and proves daemon receipts drive the next
frame rather than renderer-local state.
## Non-Goals
Eve does not own provider gameplay rules.

View file

@ -32,6 +32,7 @@ namespace GameCult.Eve.UnityScene
advertisedSurface.WorldInteraction.CommandBoundary,
advertisedSurface.WorldInteraction.ReceiptSchema,
advertisedSurface.WorldInteraction.Ownership,
BuildPlayableWorld(document.Surface.Root),
BuildSceneGraph(document.Surface.Root));
}
@ -93,6 +94,103 @@ namespace GameCult.Eve.UnityScene
children);
}
private static EveUnityPlayableWorldProjection? BuildPlayableWorld(EveSurfaceComponent root)
{
var worldRoot = FindFirst(root, component => string.Equals(component.Kind, "world.scene3d", StringComparison.Ordinal));
if (worldRoot == null)
return null;
var entities = new List<EveUnityPlayableWorldEntity>();
foreach (var component in Flatten(worldRoot))
{
if (string.Equals(component.Kind, "world.entity3d", StringComparison.Ordinal))
entities.Add(BuildPlayableEntity(component));
}
return new EveUnityPlayableWorldProjection(
worldRoot.Id,
worldRoot.GetProp("statePointerId", worldRoot.GetProp("worldStatePointerId")),
worldRoot.GetProp("assetManifest", worldRoot.GetProp("assetManifestUri")),
worldRoot.GetProp("inputProfile"),
worldRoot.GetProp("cameraRig"),
worldRoot.GetProp("playerEntityId"),
worldRoot.GetProp("movementCommand"),
worldRoot.GetProp("focusCommand"),
worldRoot.GetProp("targetCommand"),
worldRoot.GetProp("actionCommand"),
entities);
}
private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component)
{
var position = ParseVector3(component.GetProp("position"));
return new EveUnityPlayableWorldEntity(
component.Id,
component.GetProp("entityId", component.Id),
component.GetProp("entityKind", component.GetProp("kind")),
component.GetProp("label", component.GetProp("name")),
component.GetProp("faction"),
component.GetProp("assetRef", component.GetProp("meshRef", component.GetProp("prefabRef"))),
position.x,
position.y,
position.z,
ParseFloat(component.GetProp("rotationY", component.GetProp("yaw")), 0f),
ParseFloat(component.GetProp("radius"), 0f),
ParseBoolean(component.GetProp("selectable")),
ParseBoolean(component.GetProp("controllable")),
component.GetProp("focusCommand"),
component.GetProp("moveCommand"),
component.GetProp("targetCommand"),
component.GetProp("actionCommand"));
}
private static EveSurfaceComponent? FindFirst(
EveSurfaceComponent component,
Predicate<EveSurfaceComponent> predicate)
{
if (predicate(component))
return component;
foreach (var child in component.Children)
{
var found = FindFirst(child, predicate);
if (found != null)
return found;
}
return null;
}
private static IEnumerable<EveSurfaceComponent> Flatten(EveSurfaceComponent component)
{
yield return component;
foreach (var child in component.Children)
{
foreach (var nested in Flatten(child))
yield return nested;
}
}
private static (float x, float y, float z) ParseVector3(string value)
{
var parts = (value ?? "").Split(',');
return (
parts.Length > 0 ? ParseFloat(parts[0], 0f) : 0f,
parts.Length > 1 ? ParseFloat(parts[1], 0f) : 0f,
parts.Length > 2 ? ParseFloat(parts[2], 0f) : 0f);
}
private static float ParseFloat(string value, float fallback)
{
return float.TryParse(value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var parsed)
? parsed
: fallback;
}
private static bool ParseBoolean(string value)
{
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
string.Equals(value, "1", StringComparison.Ordinal);
}
private static EveUnityScenePluginProjection? BuildPluginProjection(EveSurfaceComponent component)
{
if (SaiVisualNovelAdapter.CanProject(component))
@ -123,6 +221,13 @@ namespace GameCult.Eve.UnityScene
{
if (string.IsNullOrWhiteSpace(componentKind))
return "empty";
if (string.Equals(componentKind, "world.scene3d", StringComparison.Ordinal))
return "playable-world-root";
if (string.Equals(componentKind, "world.entity3d", StringComparison.Ordinal))
return "playable-world-entity";
if (string.Equals(componentKind, "field.vector3d", StringComparison.Ordinal) ||
string.Equals(componentKind, "field.scalar3d", StringComparison.Ordinal))
return "world-field-3d";
if (string.Equals(componentKind, "vn.stage", StringComparison.Ordinal))
return "sai-vn-scene-stage";
if (string.Equals(componentKind, "panel.dialogue", StringComparison.Ordinal) ||
@ -140,7 +245,8 @@ namespace GameCult.Eve.UnityScene
return "plugin-placeholder";
if (string.Equals(componentKind, "surface.slot", StringComparison.Ordinal))
return "embedded-surface-slot";
if (componentKind.StartsWith("world.", StringComparison.Ordinal))
if (componentKind.StartsWith("world.", StringComparison.Ordinal) ||
componentKind.StartsWith("field.", StringComparison.Ordinal))
return "world-projection-node";
if (componentKind.StartsWith("text.", StringComparison.Ordinal) || string.Equals(componentKind, "label", StringComparison.Ordinal))
return "scene-label";
@ -157,6 +263,7 @@ namespace GameCult.Eve.UnityScene
string commandBoundary,
string receiptSchema,
string ownership,
EveUnityPlayableWorldProjection? playableWorld,
EveUnitySceneNode root)
{
ProviderId = providerId ?? "";
@ -165,6 +272,7 @@ namespace GameCult.Eve.UnityScene
CommandBoundary = commandBoundary ?? "";
ReceiptSchema = receiptSchema ?? "";
Ownership = ownership ?? "";
PlayableWorld = playableWorld;
Root = root ?? throw new ArgumentNullException(nameof(root));
}
@ -180,9 +288,139 @@ namespace GameCult.Eve.UnityScene
public string Ownership { get; }
public EveUnityPlayableWorldProjection? PlayableWorld { get; }
public EveUnitySceneNode Root { get; }
}
public sealed class EveUnityPlayableWorldProjection
{
public EveUnityPlayableWorldProjection(
string worldRootId,
string statePointerId,
string assetManifest,
string inputProfile,
string cameraRig,
string playerEntityId,
string movementCommand,
string focusCommand,
string targetCommand,
string actionCommand,
IReadOnlyList<EveUnityPlayableWorldEntity> entities)
{
WorldRootId = worldRootId ?? "";
StatePointerId = statePointerId ?? "";
AssetManifest = assetManifest ?? "";
InputProfile = inputProfile ?? "";
CameraRig = cameraRig ?? "";
PlayerEntityId = playerEntityId ?? "";
MovementCommand = movementCommand ?? "";
FocusCommand = focusCommand ?? "";
TargetCommand = targetCommand ?? "";
ActionCommand = actionCommand ?? "";
Entities = entities ?? Array.Empty<EveUnityPlayableWorldEntity>();
}
public string WorldRootId { get; }
public string StatePointerId { get; }
public string AssetManifest { get; }
public string InputProfile { get; }
public string CameraRig { get; }
public string PlayerEntityId { get; }
public string MovementCommand { get; }
public string FocusCommand { get; }
public string TargetCommand { get; }
public string ActionCommand { get; }
public int EntityCount => Entities.Count;
public IReadOnlyList<EveUnityPlayableWorldEntity> Entities { get; }
}
public sealed class EveUnityPlayableWorldEntity
{
public EveUnityPlayableWorldEntity(
string nodeId,
string entityId,
string entityKind,
string label,
string faction,
string assetRef,
float positionX,
float positionY,
float positionZ,
float rotationY,
float radius,
bool selectable,
bool controllable,
string focusCommand,
string moveCommand,
string targetCommand,
string actionCommand)
{
NodeId = nodeId ?? "";
EntityId = entityId ?? "";
EntityKind = entityKind ?? "";
Label = label ?? "";
Faction = faction ?? "";
AssetRef = assetRef ?? "";
PositionX = positionX;
PositionY = positionY;
PositionZ = positionZ;
RotationY = rotationY;
Radius = radius;
Selectable = selectable;
Controllable = controllable;
FocusCommand = focusCommand ?? "";
MoveCommand = moveCommand ?? "";
TargetCommand = targetCommand ?? "";
ActionCommand = actionCommand ?? "";
}
public string NodeId { get; }
public string EntityId { get; }
public string EntityKind { get; }
public string Label { get; }
public string Faction { get; }
public string AssetRef { get; }
public float PositionX { get; }
public float PositionY { get; }
public float PositionZ { get; }
public float RotationY { get; }
public float Radius { get; }
public bool Selectable { get; }
public bool Controllable { get; }
public string FocusCommand { get; }
public string MoveCommand { get; }
public string TargetCommand { get; }
public string ActionCommand { get; }
}
public sealed class EveUnitySceneNode
{
public EveUnitySceneNode(

View file

@ -57,6 +57,43 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(nornNode.PluginProjection.SemanticOwner, Is.EqualTo("Norn"));
}
[Test]
public void LowerExtractsPlayableArpgWorldFromGenericScene3dSurface()
{
var lowerer = new EveUnitySceneSurfaceLowerer();
var projection = lowerer.Lower(PlayableArpgDocument(), Advertisement("aetheria.daemon.game"));
Assert.That(projection.Root.Children[0].SceneObjectKind, Is.EqualTo("playable-world-root"));
Assert.That(projection.Root.Children[0].Children[0].SceneObjectKind, Is.EqualTo("playable-world-entity"));
Assert.That(projection.Root.Children[0].Children[3].SceneObjectKind, Is.EqualTo("world-field-3d"));
Assert.That(projection.PlayableWorld, Is.Not.Null);
Assert.That(projection.PlayableWorld!.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable"));
Assert.That(projection.PlayableWorld.StatePointerId, Is.EqualTo("cultmesh://aetheria/run/current"));
Assert.That(projection.PlayableWorld.AssetManifest, Is.EqualTo("cultmesh://aetheria/assets/manifest"));
Assert.That(projection.PlayableWorld.InputProfile, Is.EqualTo("arpg-third-person"));
Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("third-person-orbit"));
Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard"));
Assert.That(projection.PlayableWorld.MovementCommand, Is.EqualTo("aetheria.daemon.move_intent"));
Assert.That(projection.PlayableWorld.FocusCommand, Is.EqualTo("aetheria.daemon.focus"));
Assert.That(projection.PlayableWorld.TargetCommand, Is.EqualTo("aetheria.daemon.target"));
Assert.That(projection.PlayableWorld.EntityCount, Is.EqualTo(3));
var player = FindEntity(projection.PlayableWorld, "player-vanguard");
Assert.That(player.EntityKind, Is.EqualTo("player"));
Assert.That(player.AssetRef, Is.EqualTo("cultmesh://aetheria/assets/map/entity/player"));
Assert.That(player.PositionX, Is.EqualTo(0f));
Assert.That(player.PositionY, Is.EqualTo(0f));
Assert.That(player.PositionZ, Is.EqualTo(0f));
Assert.That(player.Controllable, Is.True);
Assert.That(player.MoveCommand, Is.EqualTo("aetheria.daemon.move_intent"));
var raider = FindEntity(projection.PlayableWorld, "raider-scout");
Assert.That(raider.EntityKind, Is.EqualTo("enemy"));
Assert.That(raider.PositionX, Is.EqualTo(9f));
Assert.That(raider.PositionZ, Is.EqualTo(14f));
Assert.That(raider.TargetCommand, Is.EqualTo("aetheria.daemon.target"));
}
[Test]
public void SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState()
{
@ -182,6 +219,164 @@ namespace GameCult.Eve.UnityScene.Tests
Array.Empty<EveCommandTemplate>());
}
private static EveSurfaceDocument PlayableArpgDocument()
{
return new EveSurfaceDocument(
"aetheria",
"game.runtime",
"Aetheria daemon game",
1,
"2026-07-09T00:00:00Z",
new EveSurfaceTree(
"aetheria.daemon.game",
new EveSurfaceComponent(
"aetheria.daemon.game.root",
"surface",
new Dictionary<string, string>(StringComparer.Ordinal),
new[]
{
new EveSurfaceComponent(
"aetheria.daemon.game.playable",
"world.scene3d",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["statePointerId"] = "cultmesh://aetheria/run/current",
["assetManifest"] = "cultmesh://aetheria/assets/manifest",
["inputProfile"] = "arpg-third-person",
["cameraRig"] = "third-person-orbit",
["playerEntityId"] = "player-vanguard",
["movementCommand"] = "aetheria.daemon.move_intent",
["focusCommand"] = "aetheria.daemon.focus",
["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<string, string>(StringComparer.Ordinal)
{
["fieldId"] = "aetheria.zone.flow",
["bind"] = "aetheria.daemon.soaView.flowField3d",
["visualizer"] = "particles",
["bounds"] = "-32,0,-32,32,24,32"
},
Array.Empty<EveSurfaceComponent>())
},
new[]
{
new CultMeshStateBindingDescriptor("run", "cultmesh://aetheria/run/current"),
new CultMeshStateBindingDescriptor("assets", "cultmesh://aetheria/assets/manifest")
})
}),
Array.Empty<EveStyleToken>()),
new[]
{
new EveCommandTemplate(CultMesh.OperationBinding("aetheria.daemon.commands", "Aetheria daemon commands"))
});
}
private static EveSurfaceComponent PlayableEntity(
string nodeId,
string entityId,
string entityKind,
string label,
string faction,
string assetRef,
string position,
string rotationY,
string radius,
bool selectable,
bool controllable,
string focusCommand,
string moveCommand,
string targetCommand,
string actionCommand)
{
return new EveSurfaceComponent(
nodeId,
"world.entity3d",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["entityId"] = entityId,
["entityKind"] = entityKind,
["label"] = label,
["faction"] = faction,
["assetRef"] = assetRef,
["position"] = position,
["rotationY"] = rotationY,
["radius"] = radius,
["selectable"] = selectable ? "true" : "false",
["controllable"] = controllable ? "true" : "false",
["focusCommand"] = focusCommand,
["moveCommand"] = moveCommand,
["targetCommand"] = targetCommand,
["actionCommand"] = actionCommand
},
Array.Empty<EveSurfaceComponent>());
}
private static EveUnityPlayableWorldEntity FindEntity(
EveUnityPlayableWorldProjection playableWorld,
string entityId)
{
foreach (var entity in playableWorld.Entities)
{
if (string.Equals(entity.EntityId, entityId, StringComparison.Ordinal))
return entity;
}
throw new AssertionException($"Playable entity not found: {entityId}");
}
private static EveSurfaceDocument SaiDocument()
{
return new EveSurfaceDocument(

View file

@ -17,6 +17,7 @@
"providerAdvertisements",
"commandTransport",
"sceneGraphProjection",
"playableWorldProjection",
"embeddedDocuments"
],
"supportedPlugins": [
@ -54,7 +55,7 @@
"targetId": "unity-scene",
"surfaceKinds": ["interactive-world"],
"projectionKinds": ["provider-authored-world-surface"],
"supportLevel": "unity-scene-graph-command-surface",
"supportLevel": "unity-playable-world-scene-command-surface",
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
"evidencePaths": [
"runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs",

View file

@ -55,7 +55,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 scene graph projection and command boundary evidence; capture remains a separate lifecycle proof."
"replacementProof": "EveUnity claims the provider-advertised unity-scene lowering target with provider-agnostic playable-world projection, scene graph projection, and command boundary evidence; live transport and capture remain separate lifecycle proofs."
},
{
"id": "unity-scene-sai-plugin-projection",
@ -116,7 +116,7 @@
"scripts/run-eveunity-scene-capture-smoke.ps1"
],
"destinationOwner": "EveUnity",
"replacementProof": "EveUnity builds a provider-advertisement-backed Unity scene capture request and deterministic gamecult.eve.unity_scene_projection.v1 JSON projection artifact now; the split is not complete until EveUnity attaches Unity scene screenshot or frame-capture PNG artifacts to conformance output for at least one provider-authored world surface."
"replacementProof": "EveUnity builds a provider-advertisement-backed Unity scene capture request and deterministic gamecult.eve.unity_scene_projection.v1 JSON projection artifact with playableWorld evidence now; the split is not complete until EveUnity attaches Unity scene screenshot or frame-capture PNG artifacts to conformance output for at least one provider-authored world surface."
}
],
"contractInputs": [
@ -140,12 +140,13 @@
"EveUnity publishes a Unity scene runtime capability manifest.",
"EveUnity builds the provider-agnostic Unity scene UPM package artifact.",
"EveUnity owns a provider-agnostic scene graph lowerer that consumes advertised worldInteraction boundaries without importing Aetheria internals.",
"EveUnity owns the provider-agnostic scene graph lowerer outside Eve incubation.",
"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.",
"EveUnity owns the Unity scene TeX projection adapter outside Eve incubation without importing TeX implementation state.",
"EveUnity lowers at least one non-Aetheria provider-authored world surface into Unity scene objects.",
"EveUnity lowers Aetheria's game world surface through provider advertisements without importing Aetheria internals.",
"EveUnity lowers Aetheria's game world surface into generic playableWorld projection data through provider advertisements without importing Aetheria internals.",
"A generic EveUnity scene client subscribes to the live Aetheria daemon surface over CultMesh/CultNet without Aetheria project code.",
"Unity scene input emits gamecult.eve.command.v1 intents and renders provider receipts.",
"Unity scene screenshot or frame-capture PNG artifacts are attached to conformance output."
]

View file

@ -49,11 +49,156 @@
"type": "string",
"minLength": 1
},
"playableWorld": {
"$ref": "#/$defs/playableWorld"
},
"root": {
"$ref": "#/$defs/sceneNode"
}
},
"$defs": {
"vector3": {
"type": "object",
"required": ["x", "y", "z"],
"properties": {
"x": {
"type": "number"
},
"y": {
"type": "number"
},
"z": {
"type": "number"
}
},
"additionalProperties": true
},
"playableEntity": {
"type": "object",
"required": [
"nodeId",
"entityId",
"entityKind",
"assetRef",
"position",
"selectable",
"controllable"
],
"properties": {
"nodeId": {
"type": "string",
"minLength": 1
},
"entityId": {
"type": "string",
"minLength": 1
},
"entityKind": {
"type": "string"
},
"label": {
"type": "string"
},
"faction": {
"type": "string"
},
"assetRef": {
"type": "string"
},
"position": {
"$ref": "#/$defs/vector3"
},
"rotationY": {
"type": "number"
},
"radius": {
"type": "number"
},
"selectable": {
"type": "boolean"
},
"controllable": {
"type": "boolean"
},
"focusCommand": {
"type": "string"
},
"moveCommand": {
"type": "string"
},
"targetCommand": {
"type": "string"
},
"actionCommand": {
"type": "string"
}
},
"additionalProperties": true
},
"playableWorld": {
"type": "object",
"required": [
"worldRootId",
"statePointerId",
"assetManifest",
"inputProfile",
"cameraRig",
"playerEntityId",
"movementCommand",
"entityCount",
"entities"
],
"properties": {
"worldRootId": {
"type": "string",
"minLength": 1
},
"statePointerId": {
"type": "string",
"minLength": 1
},
"assetManifest": {
"type": "string",
"minLength": 1
},
"inputProfile": {
"type": "string",
"minLength": 1
},
"cameraRig": {
"type": "string",
"minLength": 1
},
"playerEntityId": {
"type": "string",
"minLength": 1
},
"movementCommand": {
"type": "string",
"minLength": 1
},
"focusCommand": {
"type": "string"
},
"targetCommand": {
"type": "string"
},
"actionCommand": {
"type": "string"
},
"entityCount": {
"type": "number"
},
"entities": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/playableEntity"
}
}
},
"additionalProperties": true
},
"embeddedDocument": {
"type": "object",
"required": [

View file

@ -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", "sceneGraphProjection", "embeddedDocuments")) {
foreach ($feature in @("providerAdvertisements", "commandTransport", "sceneGraphProjection", "playableWorldProjection", "embeddedDocuments")) {
if (-not (@($manifest.supportedFeatures) -contains $feature)) {
throw "EveUnity scene manifest missing provider-shell feature: $feature"
}
@ -92,7 +92,7 @@ $worldSurfaceLoweringClaim = $worldSurfaceLoweringClaims | Where-Object { $_.tar
if (-not $worldSurfaceLoweringClaim) {
throw "EveUnity scene manifest missing unity-scene world-surface lowering claim"
}
if ($worldSurfaceLoweringClaim.supportLevel -ne "unity-scene-graph-command-surface") {
if ($worldSurfaceLoweringClaim.supportLevel -ne "unity-playable-world-scene-command-surface") {
throw "Unexpected EveUnity scene world-surface support level: $($worldSurfaceLoweringClaim.supportLevel)"
}
if ($worldSurfaceLoweringClaim.ownership -ne "runtime-lowers-provider-world-surface-without-owning-world-state") {

View file

@ -41,7 +41,7 @@ if (-not (@($worldSurfaceLoweringClaim.surfaceKinds) -contains "interactive-worl
if (-not (@($worldSurfaceLoweringClaim.projectionKinds) -contains "provider-authored-world-surface")) {
throw "EveUnity scene world-surface claim must support provider-authored-world-surface projections"
}
if ($worldSurfaceLoweringClaim.supportLevel -ne "unity-scene-graph-command-surface") {
if ($worldSurfaceLoweringClaim.supportLevel -ne "unity-playable-world-scene-command-surface") {
throw "Unexpected EveUnity scene support level: $($worldSurfaceLoweringClaim.supportLevel)"
}
if ($worldSurfaceLoweringClaim.ownership -ne "runtime-lowers-provider-world-surface-without-owning-world-state") {
@ -54,7 +54,7 @@ foreach ($evidencePath in @($worldSurfaceLoweringClaim.evidencePaths)) {
}
}
foreach ($feature in @("providerAdvertisements", "commandTransport", "sceneGraphProjection")) {
foreach ($feature in @("providerAdvertisements", "commandTransport", "sceneGraphProjection", "playableWorldProjection")) {
if (-not (@($manifest.supportedFeatures) -contains $feature)) {
throw "EveUnity scene provider shell missing supported feature: $feature"
}
@ -79,7 +79,7 @@ foreach ($relativePath in $expectedFiles) {
}
$lowererSource = Get-Content -LiteralPath (Join-Path $projectRoot "runtimes\incubating\eve-unity-scene\Runtime\EveUnitySceneSurfaceLowerer.cs") -Raw
foreach ($symbol in @("EveUnitySceneSurfaceLowerer", "EveUnitySceneProviderSurfaceAdvertisement", "WorldInteraction", "CommandBoundary", "ReceiptSchema", "EveSurfaceCommandRequest", "BuildSceneGraph", "BuildPluginProjection", "SaiVisualNovelUnitySceneProjectionAdapter", "TeXMathUnitySceneProjectionAdapter", "EveUnityScenePluginProjection", "EveUnitySceneNode", "SceneObjectKind", "unity-scene")) {
foreach ($symbol in @("EveUnitySceneSurfaceLowerer", "EveUnitySceneProviderSurfaceAdvertisement", "WorldInteraction", "CommandBoundary", "ReceiptSchema", "EveSurfaceCommandRequest", "BuildSceneGraph", "BuildPlayableWorld", "EveUnityPlayableWorldProjection", "EveUnityPlayableWorldEntity", "BuildPluginProjection", "SaiVisualNovelUnitySceneProjectionAdapter", "TeXMathUnitySceneProjectionAdapter", "EveUnityScenePluginProjection", "EveUnitySceneNode", "SceneObjectKind", "playable-world-root", "unity-scene")) {
if (-not $lowererSource.Contains($symbol)) {
throw "EveUnity scene provider shell lowerer missing symbol: $symbol"
}
@ -107,7 +107,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", "SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "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", "SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "playable-world-root", "playable-world-entity", "world-field-3d", "arpg-third-person", "cultmesh://aetheria/assets/manifest", "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"
}

View file

@ -65,6 +65,7 @@ node $consumerScript $consumerExport `
--expect-runtime-feature unity-scene:providerAdvertisements `
--expect-runtime-feature unity-scene:commandTransport `
--expect-runtime-feature unity-scene:sceneGraphProjection `
--expect-runtime-feature unity-scene:playableWorldProjection `
--expect-runtime-feature electron-shell:providerAdvertisements `
--expect-runtime-feature electron-shell:commandTransport `
--expect-runtime-feature electron-shell:surfaceTreeProjection `
@ -85,7 +86,7 @@ node $consumerScript $consumerExport `
--expect-runtime-world-field unity-uitoolkit:unity-uitoolkit:supportLevel:ui-toolkit-semantic-command-surface `
--expect-runtime-world-field unity-uitoolkit:unity-uitoolkit:ownership:runtime-lowers-provider-world-surface-without-owning-world-state `
--expect-runtime-world-target unity-scene:unity-scene `
--expect-runtime-world-field unity-scene:unity-scene:supportLevel:unity-scene-graph-command-surface `
--expect-runtime-world-field unity-scene:unity-scene:supportLevel:unity-playable-world-scene-command-surface `
--expect-runtime-world-field unity-scene:unity-scene:ownership:runtime-lowers-provider-world-surface-without-owning-world-state `
--expect-runtime-world-target electron-shell:electron-shell `
--expect-runtime-world-field electron-shell:electron-shell:supportLevel:electron-shell-surface-tree-command-surface `

View file

@ -42,6 +42,7 @@ export function buildUnitySceneProjection(surfaceDocument, providerAdvertisement
if (document.surfaceId !== selected.surfaceId) {
throw new Error(`Surface document ${document.surfaceId} does not match advertised Unity scene surface ${selected.surfaceId}.`);
}
const playableWorld = buildPlayableWorld(document.root);
return {
type: "unity-scene-projection",
@ -53,6 +54,7 @@ export function buildUnitySceneProjection(surfaceDocument, providerAdvertisement
commandBoundary: selected.worldInteraction.commandBoundary,
receiptSchema: selected.worldInteraction.receiptSchema,
ownership: selected.worldInteraction.ownership,
...(playableWorld ? { playableWorld } : {}),
root: buildSceneNode(document.root),
};
}
@ -153,6 +155,9 @@ function normalizeEmbeddedDocuments(value) {
function sceneObjectKind(componentKind) {
if (!componentKind) return "empty";
if (componentKind === "world.scene3d") return "playable-world-root";
if (componentKind === "world.entity3d") return "playable-world-entity";
if (componentKind === "field.vector3d" || componentKind === "field.scalar3d") return "world-field-3d";
if (componentKind === "vn.stage") return "sai-vn-scene-stage";
if (componentKind === "panel.dialogue" || componentKind === "text.dialogue") return "sai-vn-scene-dialogue";
if (componentKind === "rail.actions") return "sai-vn-scene-action-rail";
@ -166,6 +171,87 @@ function sceneObjectKind(componentKind) {
return "scene-node";
}
function buildPlayableWorld(root) {
const world = findComponent(root, component => firstString(component?.kind) === "world.scene3d");
if (!world) return null;
const props = objectValue(world.props);
const entities = flattenComponents(world)
.filter(component => firstString(component.kind) === "world.entity3d")
.map(component => buildPlayableEntity(component));
return {
worldRootId: firstString(world.id),
statePointerId: firstString(props.statePointerId, props.worldStatePointerId),
assetManifest: firstString(props.assetManifest, props.assetManifestUri),
inputProfile: firstString(props.inputProfile),
cameraRig: firstString(props.cameraRig),
playerEntityId: firstString(props.playerEntityId),
movementCommand: firstString(props.movementCommand),
focusCommand: firstString(props.focusCommand),
targetCommand: firstString(props.targetCommand),
actionCommand: firstString(props.actionCommand),
entityCount: entities.length,
entities,
};
}
function buildPlayableEntity(component) {
const props = objectValue(component.props);
const [x, y, z] = parseVector3(firstString(props.position));
return {
nodeId: firstString(component.id),
entityId: firstString(props.entityId, component.id),
entityKind: firstString(props.entityKind, props.kind),
label: firstString(props.label, props.name),
faction: firstString(props.faction),
assetRef: firstString(props.assetRef, props.meshRef, props.prefabRef),
position: { x, y, z },
rotationY: parseNumber(firstString(props.rotationY, props.yaw), 0),
radius: parseNumber(firstString(props.radius), 0),
selectable: parseBoolean(props.selectable),
controllable: parseBoolean(props.controllable),
focusCommand: firstString(props.focusCommand),
moveCommand: firstString(props.moveCommand),
targetCommand: firstString(props.targetCommand),
actionCommand: firstString(props.actionCommand),
};
}
function findComponent(component, predicate) {
const source = objectValue(component);
if (predicate(source)) return source;
for (const child of Array.isArray(source.children) ? source.children : []) {
const found = findComponent(child, predicate);
if (found) return found;
}
return null;
}
function flattenComponents(component) {
const source = objectValue(component);
return [
source,
...((Array.isArray(source.children) ? source.children : []).flatMap(flattenComponents)),
];
}
function parseVector3(value) {
const values = String(value || "")
.split(",")
.map(part => parseNumber(part.trim(), 0));
return [values[0] || 0, values[1] || 0, values[2] || 0];
}
function parseNumber(value, fallback) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
function parseBoolean(value) {
if (typeof value === "boolean") return value;
if (typeof value === "number") return value !== 0;
return String(value || "").toLowerCase() === "true";
}
function buildPluginProjection(componentKind, component) {
const props = objectValue(component.props);
if (componentKind === "vn.stage") {

View file

@ -34,7 +34,19 @@ test("builds Unity scene projection capture artifact from provider advertisement
assert.equal(projection.artifactKind, "json-projection");
assert.equal(projection.captureKind, "unity-scene-projection-json");
assert.equal(projection.root.id, "aetheria.daemon.game.root");
assert.equal(projection.root.children[1].children[0].sceneObjectKind, "world-projection-node");
assert.equal(projection.playableWorld.worldRootId, "aetheria.daemon.game.playable");
assert.equal(projection.playableWorld.statePointerId, "cultmesh://aetheria/run/current");
assert.equal(projection.playableWorld.assetManifest, "cultmesh://aetheria/assets/manifest");
assert.equal(projection.playableWorld.inputProfile, "arpg-third-person");
assert.equal(projection.playableWorld.cameraRig, "third-person-orbit");
assert.equal(projection.playableWorld.playerEntityId, "player-vanguard");
assert.equal(projection.playableWorld.movementCommand, "aetheria.daemon.move_intent");
assert.equal(projection.playableWorld.entityCount, 3);
const player = projection.playableWorld.entities.find(entity => entity.entityId === "player-vanguard");
assert.equal(player.assetRef, "cultmesh://aetheria/assets/map/entity/player");
assert.deepEqual(player.position, { x: 0, y: 0, z: 0 });
assert.equal(player.controllable, true);
assert.equal(projection.root.children[1].children[0].sceneObjectKind, "playable-world-root");
});
test("builds Unity scene capture artifact for a generic world provider", () => {

View file

@ -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", "sceneGraphProjection", "embeddedDocuments"],
"supportedFeatures": ["providerAdvertisements", "commandTransport", "sceneGraphProjection", "playableWorldProjection", "embeddedDocuments"],
"supportedPlugins": [
{
"pluginId": "sai.vn",
@ -1064,7 +1064,7 @@
"targetId": "unity-scene",
"surfaceKinds": ["interactive-world"],
"projectionKinds": ["provider-authored-world-surface"],
"supportLevel": "unity-scene-graph-command-surface",
"supportLevel": "unity-playable-world-scene-command-surface",
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
"evidencePaths": [
"runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs",
@ -1106,7 +1106,7 @@
"expectedSourceSymbols": [
{
"path": "runtimes/incubating/eve-unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs",
"contains": ["EveUnitySceneSurfaceLowerer", "EveUnitySceneProviderSurfaceAdvertisement", "WorldInteraction", "CommandBoundary", "ReceiptSchema", "EveSurfaceCommandRequest", "BuildSceneGraph", "BuildEmbeddedDocumentSlots", "BuildPluginProjection", "SaiVisualNovelUnitySceneProjectionAdapter", "TeXMathUnitySceneProjectionAdapter", "EveUnitySceneEmbeddedDocumentSlot", "EveUnityScenePluginProjection", "EveUnitySceneNode", "SceneObjectKind", "unity-scene"]
"contains": ["EveUnitySceneSurfaceLowerer", "EveUnitySceneProviderSurfaceAdvertisement", "WorldInteraction", "CommandBoundary", "ReceiptSchema", "EveSurfaceCommandRequest", "BuildSceneGraph", "BuildPlayableWorld", "EveUnityPlayableWorldProjection", "EveUnityPlayableWorldEntity", "BuildEmbeddedDocumentSlots", "BuildPluginProjection", "SaiVisualNovelUnitySceneProjectionAdapter", "TeXMathUnitySceneProjectionAdapter", "EveUnitySceneEmbeddedDocumentSlot", "EveUnityScenePluginProjection", "EveUnitySceneNode", "SceneObjectKind", "playable-world-root", "unity-scene"]
},
{
"path": "runtimes/incubating/eve-unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs",
@ -1122,7 +1122,7 @@
},
{
"path": "runtimes/incubating/eve-unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs",
"contains": ["LowerCarriesProviderAdvertisedWorldBoundary", "LowerBuildsProviderAgnosticSceneGraphFromSurfaceTree", "SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "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", "SaiVisualNovelLowersThroughRuntimeProjectionAdapterWithoutOwningStoryState", "CommandIntentCarriesAdvertisedBoundaryWithoutOwningReceipts", "world-projection-node", "playable-world-root", "playable-world-entity", "world-field-3d", "arpg-third-person", "cultmesh://aetheria/assets/manifest", "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",
@ -1130,11 +1130,11 @@
},
{
"path": "tools/eveunity/eveunity-scene-capture-artifact.mjs",
"contains": ["buildUnitySceneCaptureArtifact", "buildUnitySceneProjection", "gamecult.eve.unity_scene_projection.v1", "unity-scene-projection-json", "writeUnitySceneCaptureArtifact", "request-output"]
"contains": ["buildUnitySceneCaptureArtifact", "buildUnitySceneProjection", "buildPlayableWorld", "gamecult.eve.unity_scene_projection.v1", "unity-scene-projection-json", "writeUnitySceneCaptureArtifact", "request-output"]
},
{
"path": "tools/eveunity/eveunity-scene-capture-artifact.test.mjs",
"contains": ["builds Unity scene projection capture artifact", "builds Unity scene capture artifact for a generic world provider", "marks Sai required and Norn TeX optional sidecar plugin projections", "gamecult.eve.unity_scene_projection.v1", "unity-scene-projection-json", "optional-nested", "eve.world-smoke.commands"]
"contains": ["builds Unity scene projection capture artifact", "builds Unity scene capture artifact for a generic world provider", "marks Sai required and Norn TeX optional sidecar plugin projections", "playableWorld", "arpg-third-person", "aetheria.daemon.move_intent", "gamecult.eve.unity_scene_projection.v1", "unity-scene-projection-json", "optional-nested", "eve.world-smoke.commands"]
},
{
"path": "tools/eveunity/eveunity-release-contract.test.mjs",

View file

@ -11,6 +11,11 @@
"purpose": "Proves Aetheria can expose daemon-owned interactive world surfaces through Eve without moving game truth into Eve.",
"exitCriteria": "Move to Aetheria when generated level state, assets, command receipts, and scenario conformance become product-owned fixtures.",
"asserts": [
"world.scene3d playable ARPG root",
"world.entity3d provider-authored entity rows",
"field.vector3d world projection",
"daemon asset manifest refs",
"generic Unity playableWorld projection",
"field.surface2d world projection",
"daemon command boundary",
"provider-owned bindings",

View file

@ -47,6 +47,99 @@
"direction": "horizontal"
},
"children": [
{
"id": "aetheria.daemon.game.playable",
"kind": "world.scene3d",
"props": {
"label": "Daemon ARPG View",
"statePointerId": "cultmesh://aetheria/run/current",
"assetManifest": "cultmesh://aetheria/assets/manifest",
"inputProfile": "arpg-third-person",
"cameraRig": "third-person-orbit",
"playerEntityId": "player-vanguard",
"movementCommand": "aetheria.daemon.move_intent",
"focusCommand": "aetheria.daemon.focus",
"targetCommand": "aetheria.daemon.target",
"actionCommand": "aetheria.daemon.use_equipment"
},
"stateBindings": [
{
"target": "run",
"pointerId": "cultmesh://aetheria/run/current"
},
{
"target": "assets",
"pointerId": "cultmesh://aetheria/assets/manifest"
}
],
"children": [
{
"id": "aetheria.daemon.game.entity.player",
"kind": "world.entity3d",
"props": {
"entityId": "player-vanguard",
"entityKind": "player",
"label": "Vanguard",
"faction": "player",
"assetRef": "cultmesh://aetheria/assets/map/entity/player",
"position": "0,0,0",
"rotationY": "35",
"radius": "1.15",
"selectable": "true",
"controllable": "true",
"focusCommand": "aetheria.daemon.focus",
"moveCommand": "aetheria.daemon.move_intent"
},
"children": []
},
{
"id": "aetheria.daemon.game.entity.raider",
"kind": "world.entity3d",
"props": {
"entityId": "raider-scout",
"entityKind": "enemy",
"label": "Raider Scout",
"faction": "raider",
"assetRef": "cultmesh://aetheria/assets/map/entity/ship",
"position": "9,0,14",
"rotationY": "220",
"radius": "0.9",
"selectable": "true",
"targetCommand": "aetheria.daemon.target"
},
"children": []
},
{
"id": "aetheria.daemon.game.entity.station",
"kind": "world.entity3d",
"props": {
"entityId": "anchor-station",
"entityKind": "station",
"label": "Anchor Station",
"faction": "neutral",
"assetRef": "cultmesh://aetheria/assets/map/entity/station",
"position": "-18,0,6",
"rotationY": "0",
"radius": "3.4",
"selectable": "true",
"focusCommand": "aetheria.daemon.focus"
},
"children": []
},
{
"id": "aetheria.daemon.game.flow",
"kind": "field.vector3d",
"props": {
"label": "Volumetric Flow",
"fieldId": "aetheria.zone.flow",
"bind": "aetheria.daemon.soaView.flowField3d",
"visualizer": "particles",
"bounds": "-32,0,-32,32,24,32"
},
"children": []
}
]
},
{
"id": "aetheria.daemon.game.gravity",
"kind": "field.surface2d",