summer-engine 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +27 -5
- package/.codex-plugin/plugin.json +28 -6
- package/.cursor-plugin/plugin.json +27 -5
- package/.factory-plugin/plugin.json +27 -5
- package/.opencode/plugins/summer.js +1 -1
- package/CHANGELOG.md +102 -0
- package/README.md +18 -3
- package/assets/audit/scene_audit.gd +4136 -0
- package/assets/seeing/seeing_probe.gd +1873 -0
- package/dist/cli/commands/skills.js +20 -2
- package/dist/core/api-client.d.ts +9 -1
- package/dist/core/api-client.js +21 -4
- package/dist/core/capabilities/agent-playbook.js +46 -0
- package/dist/core/capabilities/audit/accept.d.ts +55 -0
- package/dist/core/capabilities/audit/accept.js +147 -0
- package/dist/core/capabilities/audit/args.d.ts +95 -0
- package/dist/core/capabilities/audit/args.js +89 -0
- package/dist/core/capabilities/audit/audit.d.ts +56 -0
- package/dist/core/capabilities/audit/audit.js +287 -0
- package/dist/core/capabilities/audit/frame.d.ts +14 -0
- package/dist/core/capabilities/audit/frame.js +35 -0
- package/dist/core/capabilities/audit/gaps.d.ts +96 -0
- package/dist/core/capabilities/audit/gaps.js +257 -0
- package/dist/core/capabilities/audit/judge.d.ts +162 -0
- package/dist/core/capabilities/audit/judge.js +1134 -0
- package/dist/core/capabilities/audit/math.d.ts +110 -0
- package/dist/core/capabilities/audit/math.js +263 -0
- package/dist/core/capabilities/audit/summary.d.ts +74 -0
- package/dist/core/capabilities/audit/summary.js +212 -0
- package/dist/core/capabilities/board.d.ts +31 -0
- package/dist/core/capabilities/board.js +43 -0
- package/dist/core/capabilities/camera-view.d.ts +4 -2
- package/dist/core/capabilities/camera-view.js +6 -3
- package/dist/core/capabilities/connect-signal.d.ts +85 -0
- package/dist/core/capabilities/connect-signal.js +285 -0
- package/dist/core/capabilities/engine-fallbacks.d.ts +5 -0
- package/dist/core/capabilities/engine-fallbacks.js +5 -0
- package/dist/core/capabilities/engine-ops.d.ts +5 -1
- package/dist/core/capabilities/engine-ops.js +0 -3
- package/dist/core/capabilities/engine-receipt.d.ts +12 -0
- package/dist/core/capabilities/engine-receipt.js +26 -0
- package/dist/core/capabilities/file-read.d.ts +64 -0
- package/dist/core/capabilities/file-read.js +325 -0
- package/dist/core/capabilities/game-task-plan.js +4 -2
- package/dist/core/capabilities/grep.d.ts +60 -0
- package/dist/core/capabilities/grep.js +190 -0
- package/dist/core/capabilities/inspect-node.d.ts +56 -0
- package/dist/core/capabilities/inspect-node.js +225 -0
- package/dist/core/capabilities/inspect-resource.d.ts +61 -0
- package/dist/core/capabilities/inspect-resource.js +376 -0
- package/dist/core/capabilities/math3d.d.ts +28 -0
- package/dist/core/capabilities/math3d.js +88 -0
- package/dist/core/capabilities/motion-generation.d.ts +62 -0
- package/dist/core/capabilities/motion-generation.js +138 -0
- package/dist/core/capabilities/placement-batch.d.ts +102 -0
- package/dist/core/capabilities/placement-batch.js +483 -0
- package/dist/core/capabilities/placement-math.d.ts +70 -0
- package/dist/core/capabilities/placement-math.js +209 -0
- package/dist/core/capabilities/placement-script.d.ts +42 -0
- package/dist/core/capabilities/placement-script.js +1096 -0
- package/dist/core/capabilities/placement.d.ts +485 -0
- package/dist/core/capabilities/placement.js +1731 -0
- package/dist/core/capabilities/project-context.d.ts +33 -2
- package/dist/core/capabilities/project-context.js +87 -4
- package/dist/core/capabilities/replace-node.d.ts +91 -0
- package/dist/core/capabilities/replace-node.js +423 -0
- package/dist/core/capabilities/runtime-control.d.ts +18 -2
- package/dist/core/capabilities/runtime-control.js +67 -2
- package/dist/core/capabilities/scene-batch.d.ts +63 -0
- package/dist/core/capabilities/scene-batch.js +394 -0
- package/dist/core/capabilities/scene-readback.d.ts +36 -0
- package/dist/core/capabilities/scene-readback.js +54 -0
- package/dist/core/capabilities/seeing/args.d.ts +348 -0
- package/dist/core/capabilities/seeing/args.js +178 -0
- package/dist/core/capabilities/seeing/candidates.d.ts +127 -0
- package/dist/core/capabilities/seeing/candidates.js +282 -0
- package/dist/core/capabilities/seeing/math.d.ts +134 -0
- package/dist/core/capabilities/seeing/math.js +269 -0
- package/dist/core/capabilities/seeing/probe.d.ts +62 -0
- package/dist/core/capabilities/seeing/probe.js +188 -0
- package/dist/core/capabilities/seeing/scoring.d.ts +235 -0
- package/dist/core/capabilities/seeing/scoring.js +639 -0
- package/dist/core/capabilities/seeing/seeing.d.ts +233 -0
- package/dist/core/capabilities/seeing/seeing.js +1341 -0
- package/dist/core/capabilities/seeing/shot-store.d.ts +52 -0
- package/dist/core/capabilities/seeing/shot-store.js +204 -0
- package/dist/core/capabilities/surface-snap-mesh.d.ts +88 -0
- package/dist/core/capabilities/surface-snap-mesh.js +425 -0
- package/dist/core/capabilities/surface-snap.d.ts +77 -0
- package/dist/core/capabilities/surface-snap.js +461 -0
- package/dist/core/capabilities/tool-dispatch.js +231 -72
- package/dist/core/capabilities/tscn.d.ts +85 -0
- package/dist/core/capabilities/tscn.js +391 -0
- package/dist/core/capabilities/world-snapshot.d.ts +58 -0
- package/dist/core/capabilities/world-snapshot.js +150 -0
- package/dist/core/capability-skew.d.ts +5 -5
- package/dist/core/capability-skew.js +14 -6
- package/dist/core/headless/worker-engine-client.d.ts +1 -1
- package/dist/core/headless/worker-engine-client.js +2 -2
- package/dist/core/library-read.d.ts +37 -5
- package/dist/core/library-read.js +264 -33
- package/dist/core/library-search.d.ts +2 -2
- package/dist/installer/agent-config.js +5 -3
- package/dist/installer/agent-table.d.ts +16 -0
- package/dist/installer/agent-table.js +52 -17
- package/dist/mcp/server.js +9 -3
- package/dist/mcp/tools/asset-tools.js +21 -3
- package/dist/mcp/tools/audit-tools.d.ts +23 -0
- package/dist/mcp/tools/audit-tools.js +60 -0
- package/dist/mcp/tools/debug-tools.js +3 -1
- package/dist/mcp/tools/file-tools.js +17 -4
- package/dist/mcp/tools/generate-tools.js +74 -66
- package/dist/mcp/tools/library-tools.js +2 -0
- package/dist/mcp/tools/perception-tools.js +6 -19
- package/dist/mcp/tools/placement-tools.d.ts +24 -0
- package/dist/mcp/tools/placement-tools.js +91 -0
- package/dist/mcp/tools/project-tools.js +18 -15
- package/dist/mcp/tools/scene-tools.js +71 -83
- package/dist/mcp/tools/seeing-tools.d.ts +24 -0
- package/dist/mcp/tools/seeing-tools.js +64 -0
- package/dist/mcp/tools/spatial-tools.js +17 -9
- package/dist/mcp/tools/visual-tools.js +59 -7
- package/gemini-extension.json +26 -4
- package/integrations/amp/README.md +1 -1
- package/integrations/crush/README.md +1 -1
- package/integrations/grok-build/README.md +1 -1
- package/integrations/hermes/README.md +1 -1
- package/integrations/kimi-code/README.md +1 -1
- package/integrations/mistral-vibe/README.md +1 -1
- package/integrations/opencode/README.md +1 -1
- package/integrations/rovo-dev/README.md +1 -1
- package/integrations/vscode-copilot/README.md +1 -1
- package/integrations/warp/README.md +1 -1
- package/integrations/windsurf/README.md +1 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +59 -15
- package/library/references/mcp-tools-reference/resource.yaml +1 -1
- package/library/references/summer-folder/resource.yaml +1 -1
- package/library/references/summer-folder/summer-folder.md +1 -1
- package/library/skills/3d-lighting/SKILL.md +1 -1
- package/library/skills/README.md +36 -14
- package/library/skills/adaptive-music/SKILL.md +1 -1
- package/library/skills/agent-playtesting/SKILL.md +1 -1
- package/library/skills/ai-characters-in-games/SKILL.md +220 -0
- package/library/skills/ai-characters-in-games/resource.yaml +30 -0
- package/library/skills/ambient-bed/SKILL.md +1 -1
- package/library/skills/animated-loop/SKILL.md +1 -1
- package/library/skills/animation-tree/SKILL.md +1 -1
- package/library/skills/art-direction/SKILL.md +1 -1
- package/library/skills/asset-strategy/SKILL.md +2 -1
- package/library/skills/asset-strategy/resource.yaml +2 -1
- package/library/skills/audio-direction/SKILL.md +1 -1
- package/library/skills/auto-fire-targeting/SKILL.md +1 -1
- package/library/skills/brainstorm-game/SKILL.md +1 -1
- package/library/skills/brainstorming/SKILL.md +1 -1
- package/library/skills/browse-templates/SKILL.md +3 -3
- package/library/skills/browse-templates/resource.yaml +1 -1
- package/library/skills/camera-rigs/SKILL.md +128 -0
- package/library/skills/camera-rigs/resource.yaml +29 -0
- package/library/skills/celeste-momentum-platforming/SKILL.md +1 -1
- package/library/skills/character-animation-wiring/SKILL.md +1 -1
- package/library/skills/character-model/SKILL.md +1 -1
- package/library/skills/character-movement/SKILL.md +326 -0
- package/library/skills/character-movement/references/default-third-person-controller.gd +201 -0
- package/library/skills/character-movement/references/default-third-person-verification-probe.gd +154 -0
- package/library/skills/character-movement/references/movement-3d.md +136 -0
- package/library/skills/character-movement/references/platformer-feel.md +83 -0
- package/library/skills/character-movement/resource.yaml +40 -0
- package/library/skills/character-portrait/SKILL.md +1 -1
- package/library/skills/cinematic-cutscene/SKILL.md +1 -1
- package/library/skills/combat-basics/SKILL.md +159 -0
- package/library/skills/combat-basics/references/projectiles-and-aoe.md +100 -0
- package/library/skills/combat-basics/resource.yaml +34 -0
- package/library/skills/concept-art/SKILL.md +1 -1
- package/library/skills/create-asset-sheet/SKILL.md +1 -1
- package/library/skills/debug/SKILL.md +1 -1
- package/library/skills/debugging-game-feel/SKILL.md +1 -1
- package/library/skills/design-level/SKILL.md +1 -1
- package/library/skills/design-mechanic/SKILL.md +1 -1
- package/library/skills/design-npc/SKILL.md +1 -1
- package/library/skills/diagnosing-perf-regressions/SKILL.md +1 -1
- package/library/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/library/skills/driving-the-editor-ui/SKILL.md +1 -1
- package/library/skills/environment-kit/SKILL.md +1 -1
- package/library/skills/export-and-ship/SKILL.md +1 -1
- package/library/skills/fabricating-assets/SKILL.md +1 -1
- package/library/skills/facial-and-lipsync/SKILL.md +1 -1
- package/library/skills/fps-controller/SKILL.md +9 -7
- package/library/skills/fps-controller/resource.yaml +4 -1
- package/library/skills/game-feel/SKILL.md +1 -1
- package/library/skills/gameskill/SKILL.md +1 -1
- package/library/skills/gdscript-patterns/SKILL.md +1 -1
- package/library/skills/generate-motion/SKILL.md +9 -9
- package/library/skills/generate-motion/resource.yaml +2 -1
- package/library/skills/godogen-visual-proof-game-generation/SKILL.md +1 -1
- package/library/skills/headless-scripting/SKILL.md +1 -1
- package/library/skills/in-game-purchases/SKILL.md +254 -0
- package/library/skills/in-game-purchases/resource.yaml +29 -0
- package/library/skills/instantiate-asset-pack/SKILL.md +1 -1
- package/library/skills/investigating-bugs/SKILL.md +1 -1
- package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +1 -1
- package/library/skills/make-game/SKILL.md +25 -3
- package/library/skills/make-game/references/2d-platformer-polish.md +93 -0
- package/library/skills/make-game/references/2d-platformer.md +117 -0
- package/library/skills/make-game/references/2d-rpg-content.md +234 -0
- package/library/skills/make-game/references/2d-rpg.md +241 -0
- package/library/skills/make-game/references/2d-survivors-progression.md +201 -0
- package/library/skills/make-game/references/2d-survivors.md +237 -0
- package/library/skills/make-game/references/3d-fps-systems.md +216 -0
- package/library/skills/make-game/references/3d-fps.md +177 -0
- package/library/skills/make-game/references/3d-horror-systems.md +186 -0
- package/library/skills/make-game/references/3d-horror.md +201 -0
- package/library/skills/make-game/references/3d-third-person-systems.md +220 -0
- package/library/skills/make-game/references/3d-third-person.md +215 -0
- package/library/skills/make-game/resource.yaml +6 -1
- package/library/skills/multiplayer/SKILL.md +150 -0
- package/library/skills/multiplayer/resource.yaml +33 -0
- package/library/skills/multiplayer-movement/SKILL.md +510 -0
- package/library/skills/multiplayer-movement/files.md +501 -0
- package/library/skills/multiplayer-movement/resource.yaml +30 -0
- package/library/skills/multiplayer-project/SKILL.md +521 -0
- package/library/skills/multiplayer-project/files.md +448 -0
- package/library/skills/multiplayer-project/resource.yaml +36 -0
- package/library/skills/multiplayer-publish/SKILL.md +229 -0
- package/library/skills/multiplayer-publish/export-errors.md +89 -0
- package/library/skills/multiplayer-publish/resource.yaml +28 -0
- package/library/skills/multiplayer-state/SKILL.md +438 -0
- package/library/skills/multiplayer-state/files.md +592 -0
- package/library/skills/multiplayer-state/resource.yaml +35 -0
- package/library/skills/multiplayer-testing/SKILL.md +236 -0
- package/library/skills/multiplayer-testing/resource.yaml +39 -0
- package/library/skills/music-track/SKILL.md +1 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/new-project/SKILL.md +1 -1
- package/library/skills/organic-model/SKILL.md +1 -1
- package/library/skills/pixel-art/SKILL.md +1 -1
- package/library/skills/play/SKILL.md +1 -1
- package/library/skills/playtesting-a-feature/SKILL.md +1 -1
- package/library/skills/procedural-animation/SKILL.md +2 -2
- package/library/skills/procedural-animation/resource.yaml +1 -1
- package/library/skills/prop-model/SKILL.md +1 -1
- package/library/skills/psx-retro-rendering/SKILL.md +1 -1
- package/library/skills/realtime-wet-surfaces/SKILL.md +2 -2
- package/library/skills/remote-deploy/SKILL.md +1 -1
- package/library/skills/retarget/SKILL.md +1 -1
- package/library/skills/running-in-the-cloud/SKILL.md +1 -1
- package/library/skills/save-load/SKILL.md +156 -0
- package/library/skills/save-load/resource.yaml +27 -0
- package/library/skills/scene-composition/SKILL.md +1 -1
- package/library/skills/scene-hierarchy-design/SKILL.md +1 -1
- package/library/skills/scene-scripting/SKILL.md +1 -1
- package/library/skills/scene-to-level/SKILL.md +1 -1
- package/library/skills/skill-create/SKILL.md +1 -1
- package/library/skills/skill-improve/SKILL.md +1 -1
- package/library/skills/skill-test/SKILL.md +1 -1
- package/library/skills/skintokens-auto-rigging/SKILL.md +1 -1
- package/library/skills/skybox-panorama/SKILL.md +1 -1
- package/library/skills/sound-effect/SKILL.md +1 -1
- package/library/skills/spatial-placement/SKILL.md +134 -3
- package/library/skills/spatial-placement/references/kit-placement-tools.md +364 -0
- package/library/skills/spatial-placement/resource.yaml +16 -2
- package/library/skills/sprite-sheet/SKILL.md +1 -1
- package/library/skills/summer-analytics/SKILL.md +147 -0
- package/library/skills/summer-analytics/resource.yaml +28 -0
- package/library/skills/summer-friends/SKILL.md +226 -0
- package/library/skills/summer-friends/resource.yaml +28 -0
- package/library/skills/summer-leaderboards/SKILL.md +231 -0
- package/library/skills/summer-leaderboards/resource.yaml +30 -0
- package/library/skills/summer-match-results/SKILL.md +286 -0
- package/library/skills/summer-match-results/resource.yaml +32 -0
- package/library/skills/summer-matchmaking/SKILL.md +301 -0
- package/library/skills/summer-matchmaking/resource.yaml +32 -0
- package/library/skills/summer-parties/SKILL.md +253 -0
- package/library/skills/summer-parties/resource.yaml +30 -0
- package/library/skills/summer-player-data/SKILL.md +322 -0
- package/library/skills/summer-player-data/resource.yaml +30 -0
- package/library/skills/summer-store/SKILL.md +271 -0
- package/library/skills/summer-store/resource.yaml +29 -0
- package/library/skills/summer-world-chat/SKILL.md +195 -0
- package/library/skills/summer-world-chat/resource.yaml +30 -0
- package/library/skills/summer-world-saves/SKILL.md +284 -0
- package/library/skills/summer-world-saves/resource.yaml +29 -0
- package/library/skills/text-to-motion/SKILL.md +99 -0
- package/library/skills/text-to-motion/prompts-and-controls.md +34 -0
- package/library/skills/text-to-motion/resource.yaml +41 -0
- package/library/skills/text-to-motion/rigging-for-text-motion.md +54 -0
- package/library/skills/tileable-texture/SKILL.md +1 -1
- package/library/skills/trailer-shot/SKILL.md +1 -1
- package/library/skills/tune-performance/SKILL.md +1 -1
- package/library/skills/ui-basics/SKILL.md +10 -1
- package/library/skills/ui-graphics/SKILL.md +1 -1
- package/library/skills/use-widget-asset/SKILL.md +1 -1
- package/library/skills/using-summer/SKILL.md +2 -2
- package/library/skills/using-summer/resource.yaml +2 -2
- package/library/skills/vehicle-model/SKILL.md +1 -1
- package/library/skills/verification-before-completion/SKILL.md +1 -1
- package/library/skills/verifying-scenes/SKILL.md +76 -2
- package/library/skills/verifying-scenes/resource.yaml +13 -2
- package/library/skills/vfx-dissolve/SKILL.md +1 -1
- package/library/skills/vfx-fire/SKILL.md +1 -1
- package/library/skills/vfx-hit-spark/SKILL.md +1 -1
- package/library/skills/vfx-lightning/SKILL.md +1 -1
- package/library/skills/vfx-magic-glow/SKILL.md +1 -1
- package/library/skills/vfx-muzzle-flash/SKILL.md +1 -1
- package/library/skills/vfx-smoke/SKILL.md +1 -1
- package/library/skills/vfx-water-ripple/SKILL.md +1 -1
- package/library/skills/voice-line/SKILL.md +1 -1
- package/library/skills/web-and-touch-input/SKILL.md +38 -0
- package/library/skills/web-and-touch-input/resource.yaml +28 -0
- package/library/skills/web-compatibility-look/SKILL.md +62 -0
- package/library/skills/web-compatibility-look/resource.yaml +30 -0
- package/library/skills/world-building-3d/SKILL.md +8 -1
- package/library/skills/world-building-3d/references/spatial-tools.md +1 -1
- package/library/skills/world-building-3d/resource.yaml +2 -1
- package/library/skills/writing-plans/SKILL.md +1 -1
- package/library/skills/writing-skills/SKILL.md +1 -1
- package/library/templates/3d-lan-multiplayer-starter/resource.yaml +2 -2
- package/library/tools/attach-to-surface/resource.yaml +142 -0
- package/library/tools/batch/resource.yaml +9 -3
- package/library/tools/connect-ports/resource.yaml +120 -0
- package/library/tools/connect-signal/resource.yaml +3 -2
- package/library/tools/debug-views/resource.yaml +94 -0
- package/library/tools/frame-nodes/resource.yaml +104 -0
- package/library/tools/frame-shot/resource.yaml +134 -0
- package/library/tools/generate-motion/resource.yaml +44 -9
- package/library/tools/get-board/resource.yaml +50 -0
- package/library/tools/get-project-context/resource.yaml +13 -2
- package/library/tools/grep/resource.yaml +77 -0
- package/library/tools/inspect-asset/resource.yaml +71 -0
- package/library/tools/inspect-node/resource.yaml +9 -2
- package/library/tools/inspect-resource/resource.yaml +16 -5
- package/library/tools/instantiate-scene/resource.yaml +27 -2
- package/library/tools/measure/resource.yaml +96 -0
- package/library/tools/place-adjacent/resource.yaml +113 -0
- package/library/tools/play/resource.yaml +15 -2
- package/library/tools/raycast/resource.yaml +101 -0
- package/library/tools/read-file/resource.yaml +36 -2
- package/library/tools/read-library/resource.yaml +2 -2
- package/library/tools/repeat-along/resource.yaml +129 -0
- package/library/tools/replace-node/resource.yaml +11 -5
- package/library/tools/scene-audit/resource.yaml +144 -0
- package/library/tools/screenshot/resource.yaml +13 -2
- package/library/tools/search-assets/resource.yaml +18 -0
- package/library/tools/shot-sheet/resource.yaml +110 -0
- package/library/tools/snap-to-surface/resource.yaml +4 -1
- package/library/tools/world-snapshot/resource.yaml +24 -3
- package/library/tools/zoom/resource.yaml +105 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +17 -3
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +26 -4
- package/registry/generated/index.json +2154 -469
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +27 -5
- package/registry/generated/plugin.codex.json +28 -6
- package/registry/generated/plugin.cursor.json +27 -5
- package/registry/generated/plugin.factory.json +27 -5
- package/registry/generated/skills-index.md +36 -14
- package/registry/generated/skills-registry.json +421 -124
- package/registry/generated/templates-registry.json +2 -2
- package/library/skills/host-authoritative-state/SKILL.md +0 -332
- package/library/skills/host-authoritative-state/resource.yaml +0 -28
- package/library/skills/peer-to-peer-multiplayer/SKILL.md +0 -373
- package/library/skills/peer-to-peer-multiplayer/resource.yaml +0 -28
- package/library/skills/setup-multiplayer/SKILL.md +0 -336
- package/library/skills/setup-multiplayer/resource.yaml +0 -28
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scene-batch — summer_batch's scene-mutation path, ONE implementation for both
|
|
3
|
+
* faces (src/mcp/tools/scene-tools.ts and tool-dispatch.ts), plus the
|
|
4
|
+
* re-owning step every MCP path that sends ReparentNode shares.
|
|
5
|
+
*
|
|
6
|
+
* Two engine ops answer ok for changes SaveScene then leaves out of the file
|
|
7
|
+
* (engine modules/1summer_engine/editor/ops/scene_ops.cpp, engine 0.6.0):
|
|
8
|
+
*
|
|
9
|
+
* - ReparentNode (SceneOps::reparent_node) re-owns only the node it moves
|
|
10
|
+
* (`add_do_method(node, "set_owner", root)`). The remove_child before it
|
|
11
|
+
* runs Node::_propagate_validate_owner over the detached subtree and clears
|
|
12
|
+
* every owner that is not inside it — the scene root. PackedScene::pack
|
|
13
|
+
* skips unowned nodes, so Box/Toy/ToyPart moved under Cabinet saved as
|
|
14
|
+
* Cabinet/Box alone.
|
|
15
|
+
* - ConnectSignal (SceneOps::connect_signal) connects without
|
|
16
|
+
* CONNECT_PERSIST, so no [connection] line is written. A raw ConnectSignal
|
|
17
|
+
* is refused here before anything is sent; summer_connect_signal
|
|
18
|
+
* (connect-signal.ts) connects with the flag and reads the line back.
|
|
19
|
+
*
|
|
20
|
+
* No engine op sets an owner, except ReparentNode's own set_owner(root) on the
|
|
21
|
+
* node it moves. So a batch with ReparentNode runs as:
|
|
22
|
+
* 0. SaveScene, then read the saved .tscn: the nodes the scene root owns;
|
|
23
|
+
* 1. the ops in order; each ReparentNode travels in its own request together
|
|
24
|
+
* with an in-place ReparentNode (same parent, same live index, local
|
|
25
|
+
* transform kept) for every scene-owned descendant of the moved node,
|
|
26
|
+
* shallowest first — each of those re-owns one descendant;
|
|
27
|
+
* 2. the final SaveScene; then the file is read back and every moved node
|
|
28
|
+
* and re-owned descendant is looked up at its new path. Any one missing
|
|
29
|
+
* is failure_reason not_persisted (persisted:false), never success.
|
|
30
|
+
*/
|
|
31
|
+
import { resolveSingleOnlyOps } from "../capability-skew.js";
|
|
32
|
+
import { ToolInputError } from "../tool-errors.js";
|
|
33
|
+
import { asRecord, stringFrom } from "../util/json.js";
|
|
34
|
+
import { executeOpsChunked, safeProjectPath, sceneMutationOps } from "./engine-ops.js";
|
|
35
|
+
import { extractOpError } from "./engine-receipt.js";
|
|
36
|
+
import { isReadbackFailure, readSavedScene } from "./scene-readback.js";
|
|
37
|
+
import { isAtOrBelow, isSceneCreatedNode, joinNodePath, normalizeNodePath, parentNodePath } from "./tscn.js";
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Re-owning (shared with summer_replace_node)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
function depth(path) {
|
|
42
|
+
return path === "." ? 0 : path.split("/").length;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The in-place ReparentNode ops that give a moved node's descendants back to
|
|
46
|
+
* the scene root: each op moves one descendant onto the parent it already has
|
|
47
|
+
* (its own set_owner(root) is the only engine path that sets an owner),
|
|
48
|
+
* keeping the local transform and, when the live index is known, the sibling
|
|
49
|
+
* index. `descendants` are paths BEFORE the move; `rebase` maps one to its
|
|
50
|
+
* path after it. Shallowest first: re-parenting a node clears the owners
|
|
51
|
+
* below it again, so a child is only re-owned after its parent.
|
|
52
|
+
*/
|
|
53
|
+
export function reownInPlaceOps(descendants, rebase, liveIndex) {
|
|
54
|
+
const ordered = descendants
|
|
55
|
+
.map((path, order) => ({ path, order }))
|
|
56
|
+
.sort((a, b) => depth(a.path) - depth(b.path) ||
|
|
57
|
+
(liveIndex?.get(a.path) ?? a.order) - (liveIndex?.get(b.path) ?? b.order) ||
|
|
58
|
+
a.order - b.order);
|
|
59
|
+
return ordered.map(({ path }) => {
|
|
60
|
+
const index = liveIndex?.get(path);
|
|
61
|
+
return {
|
|
62
|
+
op: "ReparentNode",
|
|
63
|
+
path: rebase(path),
|
|
64
|
+
new_parent_path: rebase(parentNodePath(path) ?? "."),
|
|
65
|
+
keep_global_transform: false,
|
|
66
|
+
...(index !== undefined ? { new_index: index } : {}),
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Guards
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
/** summer_batch guard (both faces): a raw ConnectSignal is saved without its
|
|
74
|
+
* connection. Refuse it before anything is sent. Null when the ops are fine. */
|
|
75
|
+
export function rawConnectSignalRefusal(ops) {
|
|
76
|
+
if (!ops.some((op) => op.op === "ConnectSignal"))
|
|
77
|
+
return null;
|
|
78
|
+
return ("summer_batch does not send a raw ConnectSignal: the engine connects without CONNECT_PERSIST, so the receipt says ok " +
|
|
79
|
+
"but SaveScene leaves the connection out of the .tscn and it is gone on reload. Nothing was sent. " +
|
|
80
|
+
"Use summer_connect_signal: it connects with CONNECT_PERSIST and confirms the [connection] line in the saved file.");
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Which nodes the scene root owns, followed through the batch
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
/** Root-owned node paths, and the paths the batch must prove in the saved
|
|
86
|
+
* file, rewritten as the batch renames, moves and removes nodes. */
|
|
87
|
+
class OwnedPaths {
|
|
88
|
+
owned = new Set();
|
|
89
|
+
/** Moved and re-owned nodes the read-back must find, at their current path. */
|
|
90
|
+
expected = new Set();
|
|
91
|
+
constructor(initial) {
|
|
92
|
+
for (const path of initial)
|
|
93
|
+
if (path !== ".")
|
|
94
|
+
this.owned.add(path);
|
|
95
|
+
}
|
|
96
|
+
below(path) {
|
|
97
|
+
return [...this.owned].filter((p) => p !== path && isAtOrBelow(p, path));
|
|
98
|
+
}
|
|
99
|
+
move(from, to) {
|
|
100
|
+
if (from === "." || from === to)
|
|
101
|
+
return;
|
|
102
|
+
const rewrite = (paths) => new Set([...paths].map((p) => (isAtOrBelow(p, from) ? `${to}${p.slice(from.length)}` : p)));
|
|
103
|
+
this.owned = rewrite(this.owned);
|
|
104
|
+
this.expected = rewrite(this.expected);
|
|
105
|
+
}
|
|
106
|
+
remove(path) {
|
|
107
|
+
if (path === ".")
|
|
108
|
+
return;
|
|
109
|
+
for (const p of [...this.owned])
|
|
110
|
+
if (isAtOrBelow(p, path))
|
|
111
|
+
this.owned.delete(p);
|
|
112
|
+
for (const p of [...this.expected])
|
|
113
|
+
if (isAtOrBelow(p, path))
|
|
114
|
+
this.expected.delete(p);
|
|
115
|
+
}
|
|
116
|
+
/** Follow one op the engine applied (`result` is its receipt). */
|
|
117
|
+
apply(op, result) {
|
|
118
|
+
if (result?.ok === false)
|
|
119
|
+
return;
|
|
120
|
+
const meta = asRecord(result?.meta);
|
|
121
|
+
switch (op.op) {
|
|
122
|
+
case "AddNode":
|
|
123
|
+
case "InstantiateScene": {
|
|
124
|
+
const reported = stringFrom(meta?.nodePath);
|
|
125
|
+
const name = stringFrom(op.name);
|
|
126
|
+
const path = reported
|
|
127
|
+
? normalizeNodePath(reported)
|
|
128
|
+
: name
|
|
129
|
+
? joinNodePath(normalizeNodePath(String(op.parent ?? ".")), name)
|
|
130
|
+
: undefined;
|
|
131
|
+
if (path && path !== ".")
|
|
132
|
+
this.owned.add(path);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "RemoveNode":
|
|
136
|
+
this.remove(normalizeNodePath(String(op.path ?? "")));
|
|
137
|
+
break;
|
|
138
|
+
case "SetProp": {
|
|
139
|
+
if (op.key !== "name" || typeof op.value !== "string")
|
|
140
|
+
break;
|
|
141
|
+
const from = normalizeNodePath(String(op.path ?? ""));
|
|
142
|
+
const parent = parentNodePath(from);
|
|
143
|
+
if (parent !== null)
|
|
144
|
+
this.move(from, joinNodePath(parent, op.value));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
default:
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** The live subtree under `nodePath` (children in tree order), or null when
|
|
153
|
+
* the engine cannot answer for that node. */
|
|
154
|
+
async function readLiveSubtree(client, scenePath, nodePath, depthLimit) {
|
|
155
|
+
try {
|
|
156
|
+
const state = asRecord(await client.getSceneState(scenePath, { root: nodePath, depth: depthLimit, limit: 100_000 }));
|
|
157
|
+
if (!state || state.ok === false)
|
|
158
|
+
return null;
|
|
159
|
+
const data = asRecord(state.data);
|
|
160
|
+
// A path the engine cannot resolve falls back to the scene root: compare.
|
|
161
|
+
if (!data || normalizeNodePath(String(data.path ?? "")) !== nodePath)
|
|
162
|
+
return null;
|
|
163
|
+
const index = new Map();
|
|
164
|
+
const walk = (node) => {
|
|
165
|
+
const children = Array.isArray(node.children) ? node.children : [];
|
|
166
|
+
children.forEach((child, i) => {
|
|
167
|
+
const record = asRecord(child);
|
|
168
|
+
if (!record)
|
|
169
|
+
return;
|
|
170
|
+
index.set(normalizeNodePath(String(record.path ?? "")), i);
|
|
171
|
+
walk(record);
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
walk(data);
|
|
175
|
+
return { data, live: { index, complete: data.truncated !== true } };
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function readLiveChildNames(client, scenePath, nodePath) {
|
|
182
|
+
const read = await readLiveSubtree(client, scenePath, nodePath, 1);
|
|
183
|
+
if (!read)
|
|
184
|
+
return null;
|
|
185
|
+
const children = Array.isArray(read.data.children) ? read.data.children : [];
|
|
186
|
+
return children.map((child) => String(asRecord(child)?.name ?? ""));
|
|
187
|
+
}
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// The batch
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
function opResults(receipt) {
|
|
192
|
+
const results = asRecord(receipt)?.results;
|
|
193
|
+
return Array.isArray(results) ? results.map((r) => asRecord(r) ?? {}) : [];
|
|
194
|
+
}
|
|
195
|
+
function receiptList(receipt) {
|
|
196
|
+
const nested = asRecord(receipt)?.receipts;
|
|
197
|
+
return Array.isArray(nested) ? nested : [receipt];
|
|
198
|
+
}
|
|
199
|
+
const TOO_LARGE_HINT = "Move the nodes with summer_run_script instead (reparent, then ctx.set_owner_recursive on the moved node), and confirm the saved scene with summer_read_file.";
|
|
200
|
+
/**
|
|
201
|
+
* summer_batch's scene-mutation path. A raw ConnectSignal is refused (nothing
|
|
202
|
+
* sent); a batch without ReparentNode is sent exactly as before; a batch with
|
|
203
|
+
* ReparentNode keeps every scene-owned descendant of each moved node and is
|
|
204
|
+
* verified from the saved file.
|
|
205
|
+
*/
|
|
206
|
+
export async function executeSceneBatch(client, scenePath, ops, options = {}) {
|
|
207
|
+
const refusal = rawConnectSignalRefusal(ops);
|
|
208
|
+
if (refusal)
|
|
209
|
+
throw new ToolInputError(refusal);
|
|
210
|
+
const singleOnly = resolveSingleOnlyOps(client);
|
|
211
|
+
const send = (chunk) => client.executeIdentityBoundOps(chunk, { ...options, scenePath });
|
|
212
|
+
if (!ops.some((op) => op.op === "ReparentNode")) {
|
|
213
|
+
return executeOpsChunked(send, sceneMutationOps(ops), singleOnly);
|
|
214
|
+
}
|
|
215
|
+
const target = safeProjectPath(scenePath);
|
|
216
|
+
if (!target.endsWith(".tscn")) {
|
|
217
|
+
throw new ToolInputError("summer_batch with ReparentNode verifies the moved nodes in the saved text scene, so scenePath must be a .tscn file. " +
|
|
218
|
+
"For a binary .scn move the nodes with summer_run_script (reparent, then ctx.set_owner_recursive on the moved node).");
|
|
219
|
+
}
|
|
220
|
+
if (ops.some((op) => op.op === "Undo")) {
|
|
221
|
+
throw new ToolInputError("summer_batch does not combine Undo with ReparentNode: the moved nodes are re-owned and verified from the scene as it was before the batch, which an Undo changes. Send the Undo in its own summer_batch. Nothing was sent.");
|
|
222
|
+
}
|
|
223
|
+
const all = sceneMutationOps(ops);
|
|
224
|
+
// 0. Save, so the file is the editor state, and read which nodes the root owns.
|
|
225
|
+
const pre = await executeOpsChunked(send, [{ op: "SaveScene" }], singleOnly);
|
|
226
|
+
if (extractOpError(pre)) {
|
|
227
|
+
return { ...(asRecord(pre) ?? {}), note: "Nothing from the batch was sent: the SaveScene that precedes a ReparentNode batch failed." };
|
|
228
|
+
}
|
|
229
|
+
const before = await readSavedScene(client, target, TOO_LARGE_HINT);
|
|
230
|
+
if (isReadbackFailure(before))
|
|
231
|
+
return { ...before, note: "Nothing from the batch was sent." };
|
|
232
|
+
const tracked = new OwnedPaths(before.parsed.nodes.filter(isSceneCreatedNode).map((n) => n.path));
|
|
233
|
+
const receipts = [...receiptList(pre)];
|
|
234
|
+
const results = [];
|
|
235
|
+
const applied = [];
|
|
236
|
+
const reparented = [];
|
|
237
|
+
const warnings = [];
|
|
238
|
+
let last = asRecord(pre) ?? {};
|
|
239
|
+
const partial = (receipt, failedKind, notSent) => {
|
|
240
|
+
const envelope = asRecord(receipt) ?? {};
|
|
241
|
+
const base = (typeof envelope.error === "string" && envelope.error) || extractOpError(receipt) || `Engine request failed (${failedKind}).`;
|
|
242
|
+
const notSentKinds = notSent.map((op) => String(op.op ?? ""));
|
|
243
|
+
return {
|
|
244
|
+
...envelope,
|
|
245
|
+
ok: false,
|
|
246
|
+
status: "error",
|
|
247
|
+
persisted: false,
|
|
248
|
+
error: applied.length > 0
|
|
249
|
+
? `${base} NOTE: ${applied.length} earlier op(s) already applied (${applied.join(", ")}) — the scene is modified in the editor but NOT saved to disk.` +
|
|
250
|
+
(notSentKinds.length ? ` Not sent: ${notSentKinds.join(", ")}.` : "")
|
|
251
|
+
: base,
|
|
252
|
+
results,
|
|
253
|
+
receipts,
|
|
254
|
+
...(reparented.length ? { reparented } : {}),
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
/** Send `chunk`; the first `callerOps` entries are the caller's ops, the
|
|
258
|
+
* rest the workaround's. Null on success, else the failure envelope. */
|
|
259
|
+
const run = async (chunk, callerOps, rest) => {
|
|
260
|
+
const receipt = await executeOpsChunked(send, chunk, singleOnly);
|
|
261
|
+
receipts.push(...receiptList(receipt));
|
|
262
|
+
const rs = opResults(receipt);
|
|
263
|
+
results.push(...rs.slice(0, callerOps));
|
|
264
|
+
const error = extractOpError(receipt);
|
|
265
|
+
for (let i = 0; i < callerOps; i++) {
|
|
266
|
+
if (rs[i] && rs[i].ok !== false) {
|
|
267
|
+
tracked.apply(chunk[i], rs[i]);
|
|
268
|
+
applied.push(String(chunk[i].op ?? ""));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (error)
|
|
272
|
+
return partial(receipt, String(chunk[0]?.op ?? "batch"), rest);
|
|
273
|
+
last = asRecord(receipt) ?? last;
|
|
274
|
+
return null;
|
|
275
|
+
};
|
|
276
|
+
let pending = [];
|
|
277
|
+
for (let i = 0; i < all.length; i++) {
|
|
278
|
+
const op = all[i];
|
|
279
|
+
const from = normalizeNodePath(String(op.path ?? ""));
|
|
280
|
+
// The root cannot move; the engine refuses that op with its own error.
|
|
281
|
+
if (op.op !== "ReparentNode" || from === ".") {
|
|
282
|
+
pending.push(op);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (pending.length) {
|
|
286
|
+
const failed = await run(pending, pending.length, all.slice(i));
|
|
287
|
+
if (failed)
|
|
288
|
+
return failed;
|
|
289
|
+
pending = [];
|
|
290
|
+
}
|
|
291
|
+
const newParent = normalizeNodePath(String(op.new_parent_path ?? ""));
|
|
292
|
+
const name = from.split("/").pop();
|
|
293
|
+
const to = joinNodePath(newParent, name);
|
|
294
|
+
const descendants = tracked.below(from);
|
|
295
|
+
// add_child(node, true) renames on a name clash ("Box" -> "Box2"), so the
|
|
296
|
+
// new path could not be predicted, re-owned or verified.
|
|
297
|
+
if (newParent !== parentNodePath(from)) {
|
|
298
|
+
const siblings = await readLiveChildNames(client, target, newParent);
|
|
299
|
+
if (siblings?.includes(name)) {
|
|
300
|
+
return {
|
|
301
|
+
...partial({ error: `ReparentNode ${from} -> ${newParent} not sent: ${newParent} already has a child named "${name}", and the engine would rename the moved node, so its subtree could not be kept and verified. Rename one of them first (SetProp name), then retry.` }, "ReparentNode", all.slice(i)),
|
|
302
|
+
failure_reason: "name_collision",
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
let repairs = [];
|
|
307
|
+
let reowned = [];
|
|
308
|
+
if (descendants.length > 0) {
|
|
309
|
+
const live = await readLiveSubtree(client, target, from, 64);
|
|
310
|
+
let keep = descendants;
|
|
311
|
+
if (!live) {
|
|
312
|
+
warnings.push(`The live order under ${from} could not be read, so the re-owned descendants of ${from} may change sibling order.`);
|
|
313
|
+
}
|
|
314
|
+
else if (live.live.complete) {
|
|
315
|
+
const gone = descendants.filter((d) => !live.live.index.has(d));
|
|
316
|
+
if (gone.length)
|
|
317
|
+
warnings.push(`Not in the live tree, so not re-owned: ${gone.join(", ")}.`);
|
|
318
|
+
keep = descendants.filter((d) => live.live.index.has(d));
|
|
319
|
+
}
|
|
320
|
+
const rebase = (path) => (path === from ? to : `${to}${path.slice(from.length)}`);
|
|
321
|
+
repairs = reownInPlaceOps(keep, rebase, live?.live.index);
|
|
322
|
+
reowned = repairs.map((r) => String(r.path));
|
|
323
|
+
}
|
|
324
|
+
const failed = await run([op, ...repairs], 1, all.slice(i + 1));
|
|
325
|
+
if (failed)
|
|
326
|
+
return failed;
|
|
327
|
+
// The moved node's own receipt went into results; the in-place moves did not.
|
|
328
|
+
tracked.move(from, to);
|
|
329
|
+
tracked.owned.add(to);
|
|
330
|
+
tracked.expected.add(to);
|
|
331
|
+
for (const path of reowned)
|
|
332
|
+
tracked.expected.add(path);
|
|
333
|
+
reparented.push({ path: from, new_path: to, ...(reowned.length ? { reowned } : {}) });
|
|
334
|
+
}
|
|
335
|
+
if (pending.length) {
|
|
336
|
+
const failed = await run(pending, pending.length, []);
|
|
337
|
+
if (failed)
|
|
338
|
+
return failed;
|
|
339
|
+
}
|
|
340
|
+
// 2. The batch's SaveScene ran last. Read the file and find every moved node.
|
|
341
|
+
const saved = asRecord(last.scenePersistence);
|
|
342
|
+
const envelope = {
|
|
343
|
+
...last,
|
|
344
|
+
results,
|
|
345
|
+
requests: receipts.length,
|
|
346
|
+
receipts,
|
|
347
|
+
reparented,
|
|
348
|
+
workaround: "ReparentNode clears the scene owner below the moved node, so each scene-owned descendant was moved onto its own parent again (one in-place ReparentNode each) before the save.",
|
|
349
|
+
};
|
|
350
|
+
const after = await readSavedScene(client, target, TOO_LARGE_HINT);
|
|
351
|
+
if (isReadbackFailure(after)) {
|
|
352
|
+
return {
|
|
353
|
+
...envelope,
|
|
354
|
+
...after,
|
|
355
|
+
persisted: false,
|
|
356
|
+
verified: false,
|
|
357
|
+
...(saved ? { scenePersistence: { ...saved, verified: false } } : {}),
|
|
358
|
+
error: `${after.error} The batch was applied and saved, but the moved nodes could not be verified in the file.`,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const expected = [...tracked.expected.keys()];
|
|
362
|
+
const missing = expected.filter((path) => !after.parsed.nodes.some((n) => n.path === path && isSceneCreatedNode(n)));
|
|
363
|
+
const verification = {
|
|
364
|
+
verified: missing.length === 0,
|
|
365
|
+
read_back: target,
|
|
366
|
+
checked: expected,
|
|
367
|
+
...(missing.length ? { missing } : {}),
|
|
368
|
+
};
|
|
369
|
+
if (missing.length) {
|
|
370
|
+
return {
|
|
371
|
+
...envelope,
|
|
372
|
+
ok: false,
|
|
373
|
+
status: "error",
|
|
374
|
+
failure_reason: "not_persisted",
|
|
375
|
+
persisted: false,
|
|
376
|
+
verified: false,
|
|
377
|
+
...(saved ? { scenePersistence: { ...saved, verified: false } } : {}),
|
|
378
|
+
verification,
|
|
379
|
+
...(warnings.length ? { warnings } : {}),
|
|
380
|
+
error: `summer_batch ReparentNode did NOT persist (persisted:false): the saved ${target} has no ${missing.join(", ")}. ` +
|
|
381
|
+
"The editor still shows them, but they are not owned by the scene, so they are gone on reload. Do not report the move as done: " +
|
|
382
|
+
"read the saved file (summer_read_file), then move them again with summer_run_script (reparent, then ctx.set_owner_recursive on each node that has no instanced scene below it).",
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
...envelope,
|
|
387
|
+
ok: true,
|
|
388
|
+
persisted: true,
|
|
389
|
+
verified: true,
|
|
390
|
+
...(saved ? { scenePersistence: { ...saved, verified: true } } : {}),
|
|
391
|
+
verification,
|
|
392
|
+
...(warnings.length ? { warnings } : {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scene-readback — read a scene back from disk after SaveScene and parse it,
|
|
3
|
+
* ONE copy for every tool that proves a change from the saved file instead of
|
|
4
|
+
* trusting the engine receipt (summer_replace_node, summer_batch with
|
|
5
|
+
* ReparentNode, summer_connect_signal).
|
|
6
|
+
*
|
|
7
|
+
* The engine's `scenePersistence.persisted` only says SaveScene returned OK
|
|
8
|
+
* (ops_executor.cpp: scene_persistence["persisted"] =
|
|
9
|
+
* explicit_scene_save_succeeded); what the file holds is only known by reading
|
|
10
|
+
* it. The client renames that flag to `saved` (engine-receipt.ts
|
|
11
|
+
* honestSceneReceipt); `verified` is set only by tools that read back here.
|
|
12
|
+
*/
|
|
13
|
+
import { type JsonRecord } from "../util/json.js";
|
|
14
|
+
import { type ParsedTscn } from "./tscn.js";
|
|
15
|
+
/** The engine's /api/state/read-file window used for scene read-backs. */
|
|
16
|
+
export declare const SCENE_READ_LIMIT = 1000000;
|
|
17
|
+
export type ReadbackFailure = JsonRecord & {
|
|
18
|
+
ok: false;
|
|
19
|
+
error: string;
|
|
20
|
+
failure_reason: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function readbackFailure(failure_reason: string, error: string, extra?: JsonRecord): ReadbackFailure;
|
|
23
|
+
export declare function isReadbackFailure(value: unknown): value is ReadbackFailure;
|
|
24
|
+
export interface SavedSceneReader {
|
|
25
|
+
readProjectFile(path: string, maxBytes?: number): Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface SavedScene {
|
|
28
|
+
parsed: ParsedTscn;
|
|
29
|
+
sha256?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read `scenePath` as saved and parse it. `tooLargeHint` is appended to the
|
|
33
|
+
* scene_too_large failure (what to do instead when the file cannot be
|
|
34
|
+
* verified through the 1 MB read window).
|
|
35
|
+
*/
|
|
36
|
+
export declare function readSavedScene(client: SavedSceneReader, scenePath: string, tooLargeHint?: string): Promise<SavedScene | ReadbackFailure>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scene-readback — read a scene back from disk after SaveScene and parse it,
|
|
3
|
+
* ONE copy for every tool that proves a change from the saved file instead of
|
|
4
|
+
* trusting the engine receipt (summer_replace_node, summer_batch with
|
|
5
|
+
* ReparentNode, summer_connect_signal).
|
|
6
|
+
*
|
|
7
|
+
* The engine's `scenePersistence.persisted` only says SaveScene returned OK
|
|
8
|
+
* (ops_executor.cpp: scene_persistence["persisted"] =
|
|
9
|
+
* explicit_scene_save_succeeded); what the file holds is only known by reading
|
|
10
|
+
* it. The client renames that flag to `saved` (engine-receipt.ts
|
|
11
|
+
* honestSceneReceipt); `verified` is set only by tools that read back here.
|
|
12
|
+
*/
|
|
13
|
+
import { asRecord } from "../util/json.js";
|
|
14
|
+
import { parseTscn } from "./tscn.js";
|
|
15
|
+
/** The engine's /api/state/read-file window used for scene read-backs. */
|
|
16
|
+
export const SCENE_READ_LIMIT = 1_000_000;
|
|
17
|
+
export function readbackFailure(failure_reason, error, extra = {}) {
|
|
18
|
+
return { ok: false, failure_reason, error, ...extra };
|
|
19
|
+
}
|
|
20
|
+
export function isReadbackFailure(value) {
|
|
21
|
+
return (!!value &&
|
|
22
|
+
typeof value === "object" &&
|
|
23
|
+
value.ok === false &&
|
|
24
|
+
typeof value.failure_reason === "string");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read `scenePath` as saved and parse it. `tooLargeHint` is appended to the
|
|
28
|
+
* scene_too_large failure (what to do instead when the file cannot be
|
|
29
|
+
* verified through the 1 MB read window).
|
|
30
|
+
*/
|
|
31
|
+
export async function readSavedScene(client, scenePath, tooLargeHint = "") {
|
|
32
|
+
let read;
|
|
33
|
+
try {
|
|
34
|
+
read = asRecord(await client.readProjectFile(scenePath, SCENE_READ_LIMIT));
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
return readbackFailure("scene_unreadable", `Could not read ${scenePath} back: ${err instanceof Error ? err.message : String(err)}`);
|
|
38
|
+
}
|
|
39
|
+
if (!read || read.ok === false) {
|
|
40
|
+
return readbackFailure("scene_unreadable", `Could not read ${scenePath} back: ${String(read?.error ?? "no response")}`);
|
|
41
|
+
}
|
|
42
|
+
const data = asRecord(read.data);
|
|
43
|
+
if (typeof data?.content !== "string") {
|
|
44
|
+
return readbackFailure("scene_unreadable", `${scenePath} did not read back as text (binary .scn scenes cannot be verified).`);
|
|
45
|
+
}
|
|
46
|
+
if (data.truncated === true) {
|
|
47
|
+
return readbackFailure("scene_too_large", `${scenePath} is larger than the 1 MB read window, so the change could not be planned and verified from the saved file.` +
|
|
48
|
+
(tooLargeHint ? ` ${tooLargeHint}` : ""));
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
parsed: parseTscn(data.content),
|
|
52
|
+
...(typeof data.sha256 === "string" ? { sha256: data.sha256 } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|