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,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* replace-node — ONE implementation of `summer_replace_node` for both faces
|
|
3
|
+
* (src/mcp/tools/scene-tools.ts and tool-dispatch.ts), built from ops whose
|
|
4
|
+
* results persist, and verified against the SAVED scene file.
|
|
5
|
+
*
|
|
6
|
+
* Why not the engine's ReplaceNode op for scenes: SceneOps::replace_node
|
|
7
|
+
* (engine modules/1summer_engine/editor/ops/scene_ops.cpp, scene branch and
|
|
8
|
+
* the async _finish_replace_node) instantiates the new PackedScene and hands
|
|
9
|
+
* it to SceneTreeDock::replace_node — the editor's "Change Type" path — which
|
|
10
|
+
* ends in Node::replace_by (scene/main/node.cpp). replace_by finishes with
|
|
11
|
+
* `p_node->set_scene_file_path(get_scene_file_path())`: the new instance is
|
|
12
|
+
* stamped with the OLD node's scene path. The editor draws the new subtree,
|
|
13
|
+
* the receipt says ok, and SaveScene packs the node as
|
|
14
|
+
* `instance=ExtResource(<old scene>)` with a fresh unique_id — the change is
|
|
15
|
+
* gone on reload. A plain
|
|
16
|
+
* node replaced by a scene loses the other way (scene_file_path cleared, the
|
|
17
|
+
* instance's children owned by the instance root and dropped on save), and a
|
|
18
|
+
* type change of an instanced node stays an instance of the old scene.
|
|
19
|
+
*
|
|
20
|
+
* So the scene branch (and a type change of an instanced node) runs as:
|
|
21
|
+
* 0. SaveScene, so the file on disk is exactly the editor state;
|
|
22
|
+
* 1. read + parse the saved .tscn (which children the scene added under the
|
|
23
|
+
* node, which properties it overrides, its sibling index);
|
|
24
|
+
* 2. InstantiateScene / AddNode at the same parent under a temporary name;
|
|
25
|
+
* 3. SetProp every copyable property override (transform, visibility, ...);
|
|
26
|
+
* 4. ReparentNode each scene-added child onto the new node (local
|
|
27
|
+
* transforms kept), then re-parent each deeper scene-added descendant in
|
|
28
|
+
* place — the engine's ReparentNode re-owns only the node it moves, and
|
|
29
|
+
* remove_child clears the owner of everything below it;
|
|
30
|
+
* 5. MoveNode to the old sibling index, RemoveNode the old node, SetProp
|
|
31
|
+
* name back to the original name;
|
|
32
|
+
* 6. SaveScene, read the file back and check it: the node at the path
|
|
33
|
+
* instances the new scene (or has the new type), the parent and every
|
|
34
|
+
* scene-added descendant are there. persisted:false is reported as a
|
|
35
|
+
* failure, never as success.
|
|
36
|
+
* A type change of a plain node keeps the engine's ReplaceNode (it persists:
|
|
37
|
+
* replace_by copies an empty scene path) and gets the same read-back check.
|
|
38
|
+
*/
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
import { resolveSingleOnlyOps, missingEngineOpResult } from "../capability-skew.js";
|
|
41
|
+
import { ToolInputError } from "../tool-errors.js";
|
|
42
|
+
import { asRecord } from "../util/json.js";
|
|
43
|
+
import { executeOpsChunked, safeProjectPath, sceneMutationOps } from "./engine-ops.js";
|
|
44
|
+
import { extractOpError } from "./engine-receipt.js";
|
|
45
|
+
import { REPLACE_NODE_FALLBACK } from "./engine-fallbacks.js";
|
|
46
|
+
import { reownInPlaceOps } from "./scene-batch.js";
|
|
47
|
+
import { isReadbackFailure, readSavedScene as readSceneFile, readbackFailure } from "./scene-readback.js";
|
|
48
|
+
import { decodeGodotString, extResourceId, findTscnNode, isAtOrBelow, isSceneCreatedNode, joinNodePath, normalizeNodePath, normalizeResPath, quotedLiteral, } from "./tscn.js";
|
|
49
|
+
// Mirrors library/tools/replace-node/resource.yaml input_schema (parity-tested).
|
|
50
|
+
export const replaceNodeInputShape = {
|
|
51
|
+
scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
|
|
52
|
+
path: z.string().describe("Node path to replace, e.g. './House3/Front/G_f2_door'"),
|
|
53
|
+
type: z.string().optional().describe("New node type, e.g. 'RigidBody3D'. Give exactly one of type or scene."),
|
|
54
|
+
scene: z
|
|
55
|
+
.string()
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Scene or model to replace with, e.g. 'res://kit/wall_door_02.tscn'. Give exactly one of type or scene."),
|
|
58
|
+
};
|
|
59
|
+
export const replaceNodeInputSchema = z.object(replaceNodeInputShape).strict();
|
|
60
|
+
const failure = readbackFailure;
|
|
61
|
+
const isFailure = isReadbackFailure;
|
|
62
|
+
const TOO_LARGE_HINT = "Replace by hand: summer_remove_node, then summer_instantiate_scene with the same parent and name, then summer_set_prop for the transform.";
|
|
63
|
+
const readSavedScene = (client, scenePath) => readSceneFile(client, scenePath, TOO_LARGE_HINT);
|
|
64
|
+
/** Per-op results of a (possibly chunked) receipt. */
|
|
65
|
+
function opResults(receipt) {
|
|
66
|
+
const results = asRecord(receipt)?.results;
|
|
67
|
+
return Array.isArray(results) ? results.filter((r) => asRecord(r)) : [];
|
|
68
|
+
}
|
|
69
|
+
/** Live child names of `nodePath`, in tree order, from a targeted scene read
|
|
70
|
+
* rooted at that node; null when the engine cannot answer (an older build
|
|
71
|
+
* ignores `root` and answers the scene root instead — detected by path). */
|
|
72
|
+
async function readLiveChildren(client, scenePath, nodePath) {
|
|
73
|
+
try {
|
|
74
|
+
const state = asRecord(await client.getSceneState(scenePath, { root: nodePath, depth: 1, limit: 100_000 }));
|
|
75
|
+
if (!state || state.ok === false)
|
|
76
|
+
return null;
|
|
77
|
+
const data = asRecord(state.data);
|
|
78
|
+
if (!data || normalizeNodePath(String(data.path ?? "")) !== nodePath)
|
|
79
|
+
return null;
|
|
80
|
+
if (!Array.isArray(data.children))
|
|
81
|
+
return null;
|
|
82
|
+
return data.children.map((child) => String(asRecord(child)?.name ?? ""));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** A property override as a SetProp value, or why it cannot be copied. */
|
|
89
|
+
export function propSetValue(prop, parsed) {
|
|
90
|
+
const raw = prop.value.trim();
|
|
91
|
+
if (/\bSubResource\(/.test(raw)) {
|
|
92
|
+
return { reason: "scene-local sub_resource: it cannot be copied by reference; set it again on the new node" };
|
|
93
|
+
}
|
|
94
|
+
const ext = extResourceId(raw);
|
|
95
|
+
if (ext !== undefined) {
|
|
96
|
+
const path = parsed.extResources.get(ext)?.path;
|
|
97
|
+
return path ? { value: path } : { reason: `ext_resource ${ext} is not listed in the scene file` };
|
|
98
|
+
}
|
|
99
|
+
if (/\bExtResource\(/.test(raw))
|
|
100
|
+
return { reason: "the value embeds resource references; set it again on the new node" };
|
|
101
|
+
if (raw === "true")
|
|
102
|
+
return { value: true };
|
|
103
|
+
if (raw === "false")
|
|
104
|
+
return { value: false };
|
|
105
|
+
if (/^-?\d+(?:\.\d+)?(?:e[-+]?\d+)?$/i.test(raw))
|
|
106
|
+
return { value: Number(raw) };
|
|
107
|
+
const text = quotedLiteral(raw);
|
|
108
|
+
if (text !== undefined)
|
|
109
|
+
return { value: text };
|
|
110
|
+
const nodePath = /^NodePath\("((?:[^"\\]|\\.)*)"\)$/.exec(raw);
|
|
111
|
+
if (nodePath)
|
|
112
|
+
return { value: decodeGodotString(nodePath[1]) };
|
|
113
|
+
// Godot literal text (Transform3D(...), Vector3(...), Color(...), arrays,
|
|
114
|
+
// dictionaries): SetProp parses it with str_to_var.
|
|
115
|
+
return { value: raw };
|
|
116
|
+
}
|
|
117
|
+
function tempName(base, taken) {
|
|
118
|
+
const stem = `${base}_SummerReplace`;
|
|
119
|
+
let name = stem;
|
|
120
|
+
for (let i = 2; taken.has(name); i++)
|
|
121
|
+
name = `${stem}${i}`;
|
|
122
|
+
return name;
|
|
123
|
+
}
|
|
124
|
+
function siblingIndexInFile(parsed, node) {
|
|
125
|
+
return parsed.nodes.filter((n) => n.parent === node.parent && n.order < node.order).length;
|
|
126
|
+
}
|
|
127
|
+
function describeNode(node) {
|
|
128
|
+
if (node.instancePath)
|
|
129
|
+
return node.instancePath;
|
|
130
|
+
if (node.instance)
|
|
131
|
+
return `ExtResource("${node.instance}")`;
|
|
132
|
+
return `type ${node.type ?? "?"}`;
|
|
133
|
+
}
|
|
134
|
+
function verifyAgainstSaved(after, plan) {
|
|
135
|
+
const problems = [];
|
|
136
|
+
const warnings = [];
|
|
137
|
+
const node = findTscnNode(after, plan.target);
|
|
138
|
+
const saved = { read_back: plan.scenePath };
|
|
139
|
+
if (!node) {
|
|
140
|
+
problems.push(`the saved file has no node at ${plan.target}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
saved.node = describeNode(node);
|
|
144
|
+
saved.parent = node.parent;
|
|
145
|
+
if (plan.scene !== undefined) {
|
|
146
|
+
if (!node.instance) {
|
|
147
|
+
problems.push(`the saved node is a plain ${node.type ?? "node"}, not an instance of ${plan.scene}`);
|
|
148
|
+
}
|
|
149
|
+
else if (normalizeResPath(node.instancePath ?? "") !== normalizeResPath(plan.scene)) {
|
|
150
|
+
problems.push(`the saved node still instances ${describeNode(node)}, not ${plan.scene}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (node.instance) {
|
|
154
|
+
problems.push(`the saved node is still an instance of ${describeNode(node)}`);
|
|
155
|
+
}
|
|
156
|
+
else if (node.type !== plan.type) {
|
|
157
|
+
problems.push(`the saved node has type ${node.type ?? "?"}, not ${plan.type}`);
|
|
158
|
+
}
|
|
159
|
+
if (node.parent !== plan.parent) {
|
|
160
|
+
problems.push(`the saved node's parent is ${node.parent ?? "(none)"}, not ${plan.parent ?? "(none)"}`);
|
|
161
|
+
}
|
|
162
|
+
const index = siblingIndexInFile(after, node);
|
|
163
|
+
saved.index = index;
|
|
164
|
+
if (index !== plan.fileIndex) {
|
|
165
|
+
warnings.push(`sibling order changed: index ${index} among the saved siblings, was ${plan.fileIndex}`);
|
|
166
|
+
}
|
|
167
|
+
if (plan.oldTransform !== undefined) {
|
|
168
|
+
const now = node.props.find((p) => p.key === "transform")?.value;
|
|
169
|
+
const squash = (s) => s.replace(/\s+/g, "");
|
|
170
|
+
if (now === undefined) {
|
|
171
|
+
warnings.push("the saved node lists no transform override (it now uses the new scene's own root transform); check its placement");
|
|
172
|
+
}
|
|
173
|
+
else if (squash(now) !== squash(plan.oldTransform)) {
|
|
174
|
+
warnings.push(`the saved transform differs from the old one: ${now} (was ${plan.oldTransform})`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const missing = plan.keptNodes
|
|
179
|
+
.filter((kept) => !after.nodes.some((n) => n.path === kept.path && isSceneCreatedNode(n)))
|
|
180
|
+
.map((kept) => kept.path);
|
|
181
|
+
if (missing.length > 0)
|
|
182
|
+
problems.push(`children missing from the saved file: ${missing.join(", ")}`);
|
|
183
|
+
return { persisted: problems.length === 0, problems, warnings, saved };
|
|
184
|
+
}
|
|
185
|
+
function notPersistedFailure(plan, verification, extra) {
|
|
186
|
+
return failure("not_persisted", `summer_replace_node did NOT persist (persisted:false): ${verification.problems.join("; ")}. ` +
|
|
187
|
+
`The editor may show the change, but ${plan.scenePath} on disk does not, so it reverts on reload. Do not report it as done: ` +
|
|
188
|
+
"inspect the saved file (summer_read_file), then redo it with summer_remove_node + summer_instantiate_scene (same parent and name) + summer_set_prop transform.", { persisted: false, verified: false, scenePath: plan.scenePath, path: plan.target, verification: { ...verification.saved, problems: verification.problems, warnings: verification.warnings }, ...extra });
|
|
189
|
+
}
|
|
190
|
+
/** summer_batch guard (both faces): a raw ReplaceNode with `scene` is the op
|
|
191
|
+
* that saves the OLD scene reference. Refuse it before anything is sent and
|
|
192
|
+
* point at the tool that persists. Null when the ops are fine. */
|
|
193
|
+
export function rawSceneReplaceRefusal(ops) {
|
|
194
|
+
const raw = ops.find((op) => op.op === "ReplaceNode" && typeof op.scene === "string" && op.scene.trim() !== "");
|
|
195
|
+
if (!raw)
|
|
196
|
+
return null;
|
|
197
|
+
return ("summer_batch does not send a raw ReplaceNode with scene: on current engines it shows the new scene in the editor but saves the OLD scene reference, " +
|
|
198
|
+
"so the change reverts on reload. Nothing was sent. Use summer_replace_node (it verifies the saved file), or RemoveNode + InstantiateScene.");
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Replace the node at `path` in `scenePath` with `scene` (or a node of
|
|
202
|
+
* `type`) and prove the saved file holds the result.
|
|
203
|
+
*/
|
|
204
|
+
export async function replaceNodePersisted(client, args) {
|
|
205
|
+
const scenePath = safeProjectPath(args.scenePath);
|
|
206
|
+
if (!scenePath.endsWith(".tscn")) {
|
|
207
|
+
throw new ToolInputError("summer_replace_node verifies the result in the saved text scene, so scenePath must be a .tscn file. For a binary .scn use summer_remove_node + summer_instantiate_scene.");
|
|
208
|
+
}
|
|
209
|
+
const type = args.type?.trim() || undefined;
|
|
210
|
+
const scene = args.scene?.trim() ? safeProjectPath(args.scene) : undefined;
|
|
211
|
+
if ((type === undefined) === (scene === undefined)) {
|
|
212
|
+
throw new ToolInputError("Give exactly one of type (a node class) or scene (a res:// scene or model path).");
|
|
213
|
+
}
|
|
214
|
+
if (type !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(type)) {
|
|
215
|
+
throw new ToolInputError(`Invalid type "${type}": pass a plain node class name such as 'RigidBody3D'.`);
|
|
216
|
+
}
|
|
217
|
+
const target = normalizeNodePath(args.path);
|
|
218
|
+
const singleOnly = resolveSingleOnlyOps(client);
|
|
219
|
+
const send = (ops, options = {}) => executeOpsChunked((chunk) => client.executeIdentityBoundOps(chunk, { groupUndo: true, ...options, scenePath }), ops, singleOnly);
|
|
220
|
+
// 0. Save first: the plan is read from the file, so the file must be the editor state.
|
|
221
|
+
const preSave = await send([{ op: "SaveScene" }]);
|
|
222
|
+
if (extractOpError(preSave))
|
|
223
|
+
return asRecord(preSave) ?? { ok: false, error: "SaveScene failed" };
|
|
224
|
+
// 1. Read the scene as saved.
|
|
225
|
+
const before = await readSavedScene(client, scenePath);
|
|
226
|
+
if (isFailure(before))
|
|
227
|
+
return before;
|
|
228
|
+
const node = findTscnNode(before.parsed, target);
|
|
229
|
+
if (!node) {
|
|
230
|
+
return failure("node_not_found", `No node at "${target}" in ${scenePath} (paths are relative to the scene root, e.g. "World/Door"). Nothing was replaced; read the hierarchy with summer_get_scene_tree.`);
|
|
231
|
+
}
|
|
232
|
+
const keptNodes = before.parsed.nodes.filter((n) => n !== node && isAtOrBelow(n.path, target) && isSceneCreatedNode(n));
|
|
233
|
+
const plan = {
|
|
234
|
+
scenePath,
|
|
235
|
+
target,
|
|
236
|
+
parent: node.parent,
|
|
237
|
+
...(scene !== undefined ? { scene } : {}),
|
|
238
|
+
...(type !== undefined ? { type } : {}),
|
|
239
|
+
keptNodes,
|
|
240
|
+
fileIndex: siblingIndexInFile(before.parsed, node),
|
|
241
|
+
};
|
|
242
|
+
const from = describeNode(node);
|
|
243
|
+
// A type change of a plain node: the engine op persists. Verify it anyway.
|
|
244
|
+
if (type !== undefined && !node.instance && !node.instancePlaceholder) {
|
|
245
|
+
const receipt = await send(sceneMutationOps([{ op: "ReplaceNode", path: args.path, type }]));
|
|
246
|
+
if (extractOpError(receipt))
|
|
247
|
+
return asRecord(receipt) ?? { ok: false, error: "ReplaceNode failed" };
|
|
248
|
+
const after = await readSavedScene(client, scenePath);
|
|
249
|
+
if (isFailure(after))
|
|
250
|
+
return { ...after, applied: true, note: "ReplaceNode and SaveScene reported success, but the saved file could not be read back to verify it." };
|
|
251
|
+
const verification = verifyAgainstSaved(after.parsed, plan);
|
|
252
|
+
const extra = { method: "engine_replace_node", replaced: { from, to: `type ${type}` } };
|
|
253
|
+
if (!verification.persisted)
|
|
254
|
+
return notPersistedFailure(plan, verification, extra);
|
|
255
|
+
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
persisted: true,
|
|
258
|
+
verified: true,
|
|
259
|
+
scenePath,
|
|
260
|
+
path: target,
|
|
261
|
+
...extra,
|
|
262
|
+
verification: { ...verification.saved, ...(verification.warnings.length ? { warnings: verification.warnings } : {}) },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (node.parent === null) {
|
|
266
|
+
return failure("root_not_supported", `"${target}" is the scene root. Replacing the root with ${scene ?? `type ${type}`} cannot be expressed as instantiate-and-remove; ` +
|
|
267
|
+
"create a new scene instead (summer_create_scene), or change the root's own properties.");
|
|
268
|
+
}
|
|
269
|
+
const parent = node.parent;
|
|
270
|
+
// Capability pre-flight for every op this path sends.
|
|
271
|
+
for (const op of [scene !== undefined ? "InstantiateScene" : "AddNode", "SetProp", "ReparentNode", "MoveNode", "RemoveNode"]) {
|
|
272
|
+
const missing = missingEngineOpResult(client, op, REPLACE_NODE_FALLBACK);
|
|
273
|
+
if (missing)
|
|
274
|
+
return { ...missing };
|
|
275
|
+
}
|
|
276
|
+
// What the new node takes over, and what cannot travel.
|
|
277
|
+
const props = [];
|
|
278
|
+
const notCopied = [];
|
|
279
|
+
for (const prop of node.props) {
|
|
280
|
+
const converted = propSetValue(prop, before.parsed);
|
|
281
|
+
if ("value" in converted)
|
|
282
|
+
props.push({ key: prop.key, value: converted.value });
|
|
283
|
+
else
|
|
284
|
+
notCopied.push({ key: prop.key, reason: converted.reason });
|
|
285
|
+
}
|
|
286
|
+
const oldTransform = node.props.find((p) => p.key === "transform")?.value;
|
|
287
|
+
if (oldTransform !== undefined)
|
|
288
|
+
plan.oldTransform = oldTransform;
|
|
289
|
+
const directChildren = keptNodes.filter((n) => n.parent === target);
|
|
290
|
+
const deeper = keptNodes
|
|
291
|
+
.filter((n) => n.parent !== target)
|
|
292
|
+
.sort((a, b) => a.order - b.order)
|
|
293
|
+
.map((n) => n.path);
|
|
294
|
+
// Overrides of nodes the OLD instance creates (no type/instance, nearest
|
|
295
|
+
// scene-created ancestor is the old node) belong to the old scene and go.
|
|
296
|
+
const droppedOverrides = before.parsed.nodes
|
|
297
|
+
.filter((n) => !isSceneCreatedNode(n) && isAtOrBelow(n.path, target) && n !== node)
|
|
298
|
+
.filter((n) => !keptNodes.some((k) => n.path.startsWith(`${k.path}/`)))
|
|
299
|
+
.map((n) => n.path);
|
|
300
|
+
const editableDropped = before.parsed.editable.includes(target);
|
|
301
|
+
const connections = before.parsed.connections.filter((c) => c.from === target || c.to === target);
|
|
302
|
+
const notCarriedOver = {
|
|
303
|
+
...(notCopied.length ? { props: notCopied } : {}),
|
|
304
|
+
...(node.groups.length ? { groups: node.groups } : {}),
|
|
305
|
+
...(connections.length
|
|
306
|
+
? { connections: connections.map((c) => `${c.from}:${c.signal} -> ${c.to}:${c.method}`) }
|
|
307
|
+
: {}),
|
|
308
|
+
...(droppedOverrides.length ? { overrides_of_old_scene_nodes: droppedOverrides } : {}),
|
|
309
|
+
...(editableDropped ? { editable_children: true } : {}),
|
|
310
|
+
};
|
|
311
|
+
// 2. Create the replacement next to the old node under a temporary name.
|
|
312
|
+
const siblingNames = new Set(before.parsed.nodes.filter((n) => n.parent === parent).map((n) => n.name));
|
|
313
|
+
const liveSiblings = await readLiveChildren(client, scenePath, parent);
|
|
314
|
+
for (const name of liveSiblings ?? [])
|
|
315
|
+
siblingNames.add(name);
|
|
316
|
+
const tmp = tempName(node.name, siblingNames);
|
|
317
|
+
const createOp = scene !== undefined
|
|
318
|
+
? { op: "InstantiateScene", parent, scene, name: tmp }
|
|
319
|
+
: { op: "AddNode", parent, type: type, name: tmp };
|
|
320
|
+
const created = await send([createOp]);
|
|
321
|
+
if (extractOpError(created)) {
|
|
322
|
+
return { ...(asRecord(created) ?? {}), ok: false, note: `Nothing was replaced: creating the new node failed. ${scenePath} is unchanged.` };
|
|
323
|
+
}
|
|
324
|
+
const createdMeta = asRecord(opResults(created)[0]?.meta);
|
|
325
|
+
const newPath = normalizeNodePath(typeof createdMeta?.nodePath === "string" && createdMeta.nodePath ? createdMeta.nodePath : joinNodePath(parent, tmp));
|
|
326
|
+
// Remove the temporary node again (nothing else was changed yet).
|
|
327
|
+
const rollbackNew = async (reason, message) => {
|
|
328
|
+
const undo = await send([{ op: "RemoveNode", path: newPath }]);
|
|
329
|
+
const rolledBack = !extractOpError(undo);
|
|
330
|
+
return failure(reason, rolledBack
|
|
331
|
+
? `${message} The temporary node was removed again; the old node is untouched and ${scenePath} on disk is unchanged.`
|
|
332
|
+
: `${message} Removing the temporary node ${newPath} also failed: remove it with summer_remove_node. ${scenePath} on disk is unchanged (not saved).`, { persisted: false, rolled_back: rolledBack });
|
|
333
|
+
};
|
|
334
|
+
// Children of the new scene that would collide with the moved children.
|
|
335
|
+
const newChildren = await readLiveChildren(client, scenePath, newPath);
|
|
336
|
+
if (newChildren) {
|
|
337
|
+
const clashes = directChildren.filter((c) => newChildren.includes(c.name)).map((c) => c.name);
|
|
338
|
+
if (clashes.length > 0) {
|
|
339
|
+
return rollbackNew("child_name_collision", `The new ${scene ?? `type ${type}`} already has children named ${clashes.join(", ")}, the same names as children added under ${target}; ` +
|
|
340
|
+
"moving them would rename them. Rename those children first (summer_set_prop name), then retry.");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Live sibling index of the old node (an instanced or inherited parent has
|
|
344
|
+
// children the file does not list).
|
|
345
|
+
const liveIndex = liveSiblings ? liveSiblings.indexOf(node.name) : -1;
|
|
346
|
+
const oldIndex = liveIndex >= 0 ? liveIndex : plan.fileIndex;
|
|
347
|
+
// 3. Property overrides (each attempted; a failure is reported, not fatal).
|
|
348
|
+
const propsCopied = [];
|
|
349
|
+
if (props.length > 0) {
|
|
350
|
+
const receipt = await send(props.map((p) => ({ op: "SetProp", path: newPath, key: p.key, value: p.value })), { stopOnError: false });
|
|
351
|
+
const results = opResults(receipt);
|
|
352
|
+
props.forEach((p, i) => {
|
|
353
|
+
const r = results[i];
|
|
354
|
+
if (r && r.ok !== false)
|
|
355
|
+
propsCopied.push(p.key);
|
|
356
|
+
else
|
|
357
|
+
notCopied.push({ key: p.key, reason: String(r?.error ?? "SetProp failed") });
|
|
358
|
+
});
|
|
359
|
+
if (notCopied.length)
|
|
360
|
+
notCarriedOver.props = notCopied;
|
|
361
|
+
}
|
|
362
|
+
// 4 + 5. Move children, restore ownership below them, take the old slot, drop the old node, take its name.
|
|
363
|
+
const rebase = (path) => (path === target ? newPath : `${newPath}${path.slice(target.length)}`);
|
|
364
|
+
const structure = [
|
|
365
|
+
...directChildren.map((c) => ({ op: "ReparentNode", path: c.path, new_parent_path: newPath, keep_global_transform: false })),
|
|
366
|
+
...reownInPlaceOps(deeper, rebase),
|
|
367
|
+
{ op: "MoveNode", path: newPath, new_index: oldIndex },
|
|
368
|
+
{ op: "RemoveNode", path: target },
|
|
369
|
+
{ op: "SetProp", path: newPath, key: "name", value: node.name },
|
|
370
|
+
];
|
|
371
|
+
const moved = await send(structure);
|
|
372
|
+
if (extractOpError(moved)) {
|
|
373
|
+
const results = opResults(moved);
|
|
374
|
+
const applied = results.filter((r) => r.ok !== false).length;
|
|
375
|
+
const removedOld = results.some((r, i) => r.ok !== false && structure[i]?.op === "RemoveNode");
|
|
376
|
+
if (applied === 0) {
|
|
377
|
+
return rollbackNew("replace_failed", `Replacement stopped before anything moved: ${extractOpError(moved)}.`);
|
|
378
|
+
}
|
|
379
|
+
return failure("replace_partially_applied", `summer_replace_node stopped after ${applied} of ${structure.length} steps: ${extractOpError(moved)}. ` +
|
|
380
|
+
`Applied in the editor only, NOT saved (${scenePath} on disk still holds the original node). ` +
|
|
381
|
+
(removedOld
|
|
382
|
+
? `The old node is gone and the new one is at ${newPath}: rename it with summer_set_prop name "${node.name}", then summer_save_scene.`
|
|
383
|
+
: `The new node is at ${newPath} next to the old one: undo with Ctrl+Z in the editor, or move the children back and remove ${newPath}.`), { persisted: false, applied_steps: structure.slice(0, applied).map((op) => String(op.op)) });
|
|
384
|
+
}
|
|
385
|
+
// 6. Save, then read the file back and check it.
|
|
386
|
+
const saved = await send([{ op: "SaveScene" }]);
|
|
387
|
+
if (extractOpError(saved)) {
|
|
388
|
+
return {
|
|
389
|
+
...(asRecord(saved) ?? {}),
|
|
390
|
+
ok: false,
|
|
391
|
+
persisted: false,
|
|
392
|
+
note: `The replacement is applied in the editor but SaveScene failed, so ${scenePath} on disk still holds the original node. Fix the save error, then call summer_save_scene.`,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const after = await readSavedScene(client, scenePath);
|
|
396
|
+
if (isFailure(after))
|
|
397
|
+
return { ...after, persisted: false, note: "The replacement was applied and saved, but the file could not be read back to verify it." };
|
|
398
|
+
const verification = verifyAgainstSaved(after.parsed, plan);
|
|
399
|
+
const extra = {
|
|
400
|
+
method: "instantiate_move_remove",
|
|
401
|
+
replaced: { from, to: scene ?? `type ${type}` },
|
|
402
|
+
...(directChildren.length ? { moved_children: directChildren.map((c) => c.name) } : {}),
|
|
403
|
+
...(propsCopied.length ? { props_copied: propsCopied } : {}),
|
|
404
|
+
...(Object.keys(notCarriedOver).length ? { not_carried_over: notCarriedOver } : {}),
|
|
405
|
+
};
|
|
406
|
+
if (!verification.persisted)
|
|
407
|
+
return notPersistedFailure(plan, verification, extra);
|
|
408
|
+
const warnings = [...verification.warnings];
|
|
409
|
+
if (Object.keys(notCarriedOver).length) {
|
|
410
|
+
warnings.push("Some state of the old node did not carry over (see not_carried_over): re-apply what is still wanted.");
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
ok: true,
|
|
414
|
+
persisted: true,
|
|
415
|
+
verified: true,
|
|
416
|
+
scenePath,
|
|
417
|
+
path: target,
|
|
418
|
+
...extra,
|
|
419
|
+
verification: verification.saved,
|
|
420
|
+
...(warnings.length ? { warnings } : {}),
|
|
421
|
+
undo: "Sent as separate engine requests: undoing it in the editor takes one Ctrl+Z per step (create, properties, move/remove).",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
@@ -186,8 +186,8 @@ export declare const runtimeAnimateArgsSchema: z.ZodObject<{
|
|
|
186
186
|
instance?: string | undefined;
|
|
187
187
|
name?: string | undefined;
|
|
188
188
|
value?: string | number | boolean | undefined;
|
|
189
|
-
space?: "local" | "global" | "both" | undefined;
|
|
190
189
|
cmd?: "pause" | "play" | "stop" | "state" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
|
|
190
|
+
space?: "local" | "global" | "both" | undefined;
|
|
191
191
|
blend?: number | undefined;
|
|
192
192
|
state?: string | undefined;
|
|
193
193
|
position?: number | undefined;
|
|
@@ -205,8 +205,8 @@ export declare const runtimeAnimateArgsSchema: z.ZodObject<{
|
|
|
205
205
|
instance?: string | undefined;
|
|
206
206
|
name?: string | undefined;
|
|
207
207
|
value?: string | number | boolean | undefined;
|
|
208
|
-
space?: "local" | "global" | "both" | undefined;
|
|
209
208
|
cmd?: "pause" | "play" | "stop" | "state" | "seek" | "speed" | "travel" | "start" | "set_param" | "get_param" | undefined;
|
|
209
|
+
space?: "local" | "global" | "both" | undefined;
|
|
210
210
|
blend?: number | undefined;
|
|
211
211
|
state?: string | undefined;
|
|
212
212
|
position?: number | undefined;
|
|
@@ -473,6 +473,9 @@ export declare const playGameExtensionSchema: {
|
|
|
473
473
|
fixed_fps: z.ZodOptional<z.ZodNumber>;
|
|
474
474
|
time_scale: z.ZodOptional<z.ZodNumber>;
|
|
475
475
|
speed: z.ZodOptional<z.ZodNumber>;
|
|
476
|
+
players: z.ZodOptional<z.ZodNumber>;
|
|
477
|
+
spectators: z.ZodOptional<z.ZodNumber>;
|
|
478
|
+
queue: z.ZodOptional<z.ZodString>;
|
|
476
479
|
focus: z.ZodOptional<z.ZodBoolean>;
|
|
477
480
|
};
|
|
478
481
|
export interface PlayGameArgs {
|
|
@@ -486,7 +489,13 @@ export interface PlayGameArgs {
|
|
|
486
489
|
speed?: number;
|
|
487
490
|
/** true = today's toolbar-style launch (Game tab + focus). Absent/false = quiet. */
|
|
488
491
|
focus?: boolean;
|
|
492
|
+
/** Local Play: headless authority + this many clients (0 = one ordinary run). */
|
|
493
|
+
players?: number;
|
|
494
|
+
spectators?: number;
|
|
495
|
+
queue?: string;
|
|
489
496
|
}
|
|
497
|
+
/** True when the caller asked for Local Play parameters at all. */
|
|
498
|
+
export declare function playRequestsLocalPlay(args: PlayGameArgs): boolean;
|
|
490
499
|
/**
|
|
491
500
|
* Quiet is the default whenever an agent drives play — both faces of this tool
|
|
492
501
|
* (MCP and `summer tool`) ARE agent faces, so the only way to a focus-stealing
|
|
@@ -510,6 +519,13 @@ export declare function buildStopGameOp(instance: string): BuiltRuntimeOp;
|
|
|
510
519
|
* believe a parallel instance is up.
|
|
511
520
|
*/
|
|
512
521
|
export declare function withPlayInstanceEcho(result: unknown, args: PlayGameArgs): unknown;
|
|
522
|
+
export declare const PLAY_LOCAL_PLAY_NOT_SUPPORTED = "This Summer Engine build did not echo `local_play` in its PlayGame result \u2014 it predates Local Play and started ONE ordinary client, not a local authority plus the requested players. Update to an engine with Local Play (SummerEngine main after PR #415; restart it after updating).";
|
|
523
|
+
/**
|
|
524
|
+
* Local Play was requested with players > 0: an engine with Local Play echoes
|
|
525
|
+
* `local_play` (applied, or applied:false with a reason). No echo means an
|
|
526
|
+
* engine that ignored the keys and ran one client — say so.
|
|
527
|
+
*/
|
|
528
|
+
export declare function withLocalPlayEcho(result: unknown, args: PlayGameArgs): unknown;
|
|
513
529
|
export declare const PLAY_QUIET_NOT_SUPPORTED = "This Summer Engine build did not echo `agent_quiet` in its PlayGame result \u2014 it predates quiet play and has most likely switched the editor to the Game tab and taken focus. Update Summer Engine (restart it after updating) for launches that leave the user's screen alone.";
|
|
514
530
|
/**
|
|
515
531
|
* Quiet was requested: the engine echoes `agent_quiet` when it understood the
|
|
@@ -853,11 +853,32 @@ export const playGameExtensionSchema = {
|
|
|
853
853
|
.number()
|
|
854
854
|
.optional()
|
|
855
855
|
.describe("User time scale applied on session start, (0, 100] — e.g. 0.5 for half speed."),
|
|
856
|
+
players: z
|
|
857
|
+
.number()
|
|
858
|
+
.int()
|
|
859
|
+
.min(0)
|
|
860
|
+
.max(64)
|
|
861
|
+
.optional()
|
|
862
|
+
.describe("Local Play for a Summer multiplayer game (engine with Local Play): start the game's authority headless plus this many clients on this machine, each joining through the game's own Summer.client.join. The scenes and queue come from the project's summer.build.json WorldDefinition; no account, Docker or game flags are needed. Omit it and pass queue to start the queue's minPlayers from summer.build.json; 0 forces one ordinary run. The result's local_play block lists every process and persona, plus warnings when the roster is outside the queue's minPlayers..maxPlayers."),
|
|
863
|
+
spectators: z
|
|
864
|
+
.number()
|
|
865
|
+
.int()
|
|
866
|
+
.min(0)
|
|
867
|
+
.optional()
|
|
868
|
+
.describe("Local Play: also start this many spectator clients (only with players)."),
|
|
869
|
+
queue: z
|
|
870
|
+
.string()
|
|
871
|
+
.optional()
|
|
872
|
+
.describe("Local Play: the summer.build.json queue the local World stands in for (default: the first declared queue)."),
|
|
856
873
|
focus: z
|
|
857
874
|
.boolean()
|
|
858
875
|
.optional()
|
|
859
876
|
.describe("Default false = QUIET play (PlayGame agent:true): the editor does not switch to the Game tab or grab focus for the embedded game, so the user keeps working. Pass true to launch the way the toolbar Play button does (Game tab + focus) — only when the user is watching and asked to see it."),
|
|
860
877
|
};
|
|
878
|
+
/** True when the caller asked for Local Play parameters at all. */
|
|
879
|
+
export function playRequestsLocalPlay(args) {
|
|
880
|
+
return args.players !== undefined || args.spectators !== undefined || args.queue !== undefined;
|
|
881
|
+
}
|
|
861
882
|
/**
|
|
862
883
|
* Quiet is the default whenever an agent drives play — both faces of this tool
|
|
863
884
|
* (MCP and `summer tool`) ARE agent faces, so the only way to a focus-stealing
|
|
@@ -881,7 +902,8 @@ export function playNeedsOp(args) {
|
|
|
881
902
|
args.seed !== undefined ||
|
|
882
903
|
args.fixed_fps !== undefined ||
|
|
883
904
|
args.time_scale !== undefined ||
|
|
884
|
-
args.speed !== undefined
|
|
905
|
+
args.speed !== undefined ||
|
|
906
|
+
playRequestsLocalPlay(args));
|
|
885
907
|
}
|
|
886
908
|
/** True when the call addresses a non-main instance or an offscreen mode —
|
|
887
909
|
* the part of PlayGame that only a runtime-control engine understands. */
|
|
@@ -914,6 +936,26 @@ export function buildPlayGameOp(args) {
|
|
|
914
936
|
if (args.speed !== undefined && (typeof args.speed !== "number" || !Number.isFinite(args.speed) || args.speed <= 0 || args.speed > 100)) {
|
|
915
937
|
throw new ToolInputError("speed must be a number in (0, 100].");
|
|
916
938
|
}
|
|
939
|
+
if (playRequestsLocalPlay(args)) {
|
|
940
|
+
if (args.players !== undefined && (!isInt(args.players) || args.players < 0 || args.players > 64)) {
|
|
941
|
+
throw new ToolInputError("players must be an integer 0..64 (0 forces one ordinary run).");
|
|
942
|
+
}
|
|
943
|
+
if (args.spectators !== undefined && (!isInt(args.spectators) || args.spectators < 0)) {
|
|
944
|
+
throw new ToolInputError("spectators must be an integer >= 0.");
|
|
945
|
+
}
|
|
946
|
+
if ((args.players ?? 0) + (args.spectators ?? 0) > 64) {
|
|
947
|
+
throw new ToolInputError("players + spectators must be at most 64.");
|
|
948
|
+
}
|
|
949
|
+
if (args.spectators !== undefined && args.spectators > 0 && args.players === 0) {
|
|
950
|
+
throw new ToolInputError("spectators need players > 0.");
|
|
951
|
+
}
|
|
952
|
+
if (args.queue !== undefined && (typeof args.queue !== "string" || args.queue.trim().length === 0)) {
|
|
953
|
+
throw new ToolInputError("queue must be a queue name declared in summer.build.json.");
|
|
954
|
+
}
|
|
955
|
+
if (instance.length > 0 && instance !== "main") {
|
|
956
|
+
throw new ToolInputError("Local Play (players/spectators/queue) runs the editor's main game; it does not combine with an offscreen instance.");
|
|
957
|
+
}
|
|
958
|
+
}
|
|
917
959
|
const op = { op: "PlayGame" };
|
|
918
960
|
if (typeof args.scene === "string" && args.scene.trim().length > 0)
|
|
919
961
|
op.scene = args.scene.trim();
|
|
@@ -935,6 +977,12 @@ export function buildPlayGameOp(args) {
|
|
|
935
977
|
op.time_scale = args.time_scale;
|
|
936
978
|
if (args.speed !== undefined)
|
|
937
979
|
op.speed = args.speed;
|
|
980
|
+
if (args.players !== undefined)
|
|
981
|
+
op.players = args.players;
|
|
982
|
+
if (args.spectators !== undefined)
|
|
983
|
+
op.spectators = args.spectators;
|
|
984
|
+
if (args.queue !== undefined)
|
|
985
|
+
op.queue = args.queue.trim();
|
|
938
986
|
return { kind: "PlayGame", op, timeoutMs: PLAY_INSTANCE_TIMEOUT_MS };
|
|
939
987
|
}
|
|
940
988
|
export function buildStopGameOp(instance) {
|
|
@@ -963,6 +1011,23 @@ export function withPlayInstanceEcho(result, args) {
|
|
|
963
1011
|
warning: `This Summer Engine build did not echo \`instance\` in its PlayGame result — it predates instance-aware play and has most likely started the MAIN embedded game, ignoring instance:'${args.instance ?? ""}' / mode:'${args.mode ?? "embedded"}'. Verify with summer_is_running or summer_game_control action:'instances' before addressing that instance, and update Summer Engine for parallel instances.`,
|
|
964
1012
|
};
|
|
965
1013
|
}
|
|
1014
|
+
export const PLAY_LOCAL_PLAY_NOT_SUPPORTED = "This Summer Engine build did not echo `local_play` in its PlayGame result — it predates Local Play and started ONE ordinary client, not a local authority plus the requested players. Update to an engine with Local Play (SummerEngine main after PR #415; restart it after updating).";
|
|
1015
|
+
/**
|
|
1016
|
+
* Local Play was requested with players > 0: an engine with Local Play echoes
|
|
1017
|
+
* `local_play` (applied, or applied:false with a reason). No echo means an
|
|
1018
|
+
* engine that ignored the keys and ran one client — say so.
|
|
1019
|
+
*/
|
|
1020
|
+
export function withLocalPlayEcho(result, args) {
|
|
1021
|
+
if (!playRequestsLocalPlay(args) || args.players === 0 || !result || typeof result !== "object")
|
|
1022
|
+
return result;
|
|
1023
|
+
if (extractOpError(result))
|
|
1024
|
+
return result;
|
|
1025
|
+
const envelope = result;
|
|
1026
|
+
const payload = envelope.results?.[0] ?? envelope;
|
|
1027
|
+
if (payload.local_play && typeof payload.local_play === "object")
|
|
1028
|
+
return result;
|
|
1029
|
+
return { ...envelope, local_play_note: PLAY_LOCAL_PLAY_NOT_SUPPORTED };
|
|
1030
|
+
}
|
|
966
1031
|
export const PLAY_QUIET_NOT_SUPPORTED = "This Summer Engine build did not echo `agent_quiet` in its PlayGame result — it predates quiet play and has most likely switched the editor to the Game tab and taken focus. Update Summer Engine (restart it after updating) for launches that leave the user's screen alone.";
|
|
967
1032
|
/**
|
|
968
1033
|
* Quiet was requested: the engine echoes `agent_quiet` when it understood the
|
|
@@ -1007,5 +1072,5 @@ export async function playGame(client, args) {
|
|
|
1007
1072
|
return missing;
|
|
1008
1073
|
}
|
|
1009
1074
|
const result = await client.executeOps([op], undefined, timeoutMs);
|
|
1010
|
-
return withPlayPostureEcho(withPlayInstanceEcho(withRuntimeFailureHints(withOldEngineHint(result, "PlayGame", PLAY_INSTANCE_FALLBACK)), args), args);
|
|
1075
|
+
return withLocalPlayEcho(withPlayPostureEcho(withPlayInstanceEcho(withRuntimeFailureHints(withOldEngineHint(result, "PlayGame", PLAY_INSTANCE_FALLBACK)), args), args), args);
|
|
1011
1076
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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 { type CapabilityAdvertisingClient } from "../capability-skew.js";
|
|
32
|
+
import { type JsonRecord } from "../util/json.js";
|
|
33
|
+
/** The engine calls summer_batch's scene path needs — a structural subset of
|
|
34
|
+
* EngineApiClient so tests can hand in a fake engine. */
|
|
35
|
+
export interface SceneBatchClient extends CapabilityAdvertisingClient {
|
|
36
|
+
executeIdentityBoundOps(ops: Record<string, unknown>[], options?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
37
|
+
readProjectFile(path: string, maxBytes?: number): Promise<unknown>;
|
|
38
|
+
getSceneState(scenePath?: string, options?: {
|
|
39
|
+
depth?: number;
|
|
40
|
+
limit?: number;
|
|
41
|
+
root?: string;
|
|
42
|
+
}): Promise<unknown>;
|
|
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 declare function reownInPlaceOps(descendants: readonly string[], rebase: (path: string) => string, liveIndex?: ReadonlyMap<string, number>): JsonRecord[];
|
|
54
|
+
/** summer_batch guard (both faces): a raw ConnectSignal is saved without its
|
|
55
|
+
* connection. Refuse it before anything is sent. Null when the ops are fine. */
|
|
56
|
+
export declare function rawConnectSignalRefusal(ops: ReadonlyArray<Record<string, unknown>>): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* summer_batch's scene-mutation path. A raw ConnectSignal is refused (nothing
|
|
59
|
+
* sent); a batch without ReparentNode is sent exactly as before; a batch with
|
|
60
|
+
* ReparentNode keeps every scene-owned descendant of each moved node and is
|
|
61
|
+
* verified from the saved file.
|
|
62
|
+
*/
|
|
63
|
+
export declare function executeSceneBatch(client: SceneBatchClient, scenePath: string, ops: JsonRecord[], options?: JsonRecord): Promise<unknown>;
|