Add Unity playable world projection proof
This commit is contained in:
parent
b03ddc1083
commit
09874ad026
16 changed files with 829 additions and 23 deletions
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue