summer-engine 3.4.0 → 3.5.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.
Files changed (290) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +5 -1
  3. package/.codex-plugin/plugin.json +5 -1
  4. package/.cursor-plugin/plugin.json +5 -1
  5. package/.factory-plugin/plugin.json +5 -1
  6. package/.opencode/INSTALL.md +8 -8
  7. package/.opencode/plugins/summer.js +2 -2
  8. package/AGENTS.md +7 -7
  9. package/CHANGELOG.md +37 -0
  10. package/GEMINI.md +1 -1
  11. package/NOTICE +1 -1
  12. package/README.md +35 -35
  13. package/assets/autopilot/run.sh +2 -2
  14. package/assets/navigation/web-routes.json +1 -1
  15. package/commands/gameskill.md +2 -2
  16. package/commands/summer.md +25 -25
  17. package/dist/cli/commands/open.d.ts +1 -1
  18. package/dist/cli/commands/run.d.ts +1 -1
  19. package/dist/cli/commands/run.js +1 -1
  20. package/dist/cli/commands/setup.js +24 -4
  21. package/dist/cli/commands/skills.js +2 -2
  22. package/dist/core/api-client.d.ts +5 -6
  23. package/dist/core/api-client.js +12 -15
  24. package/dist/core/auth.js +2 -2
  25. package/dist/core/capabilities/asset-import.d.ts +0 -1
  26. package/dist/core/capabilities/camera-view.d.ts +1 -1
  27. package/dist/core/capabilities/camera-view.js +2 -2
  28. package/dist/core/capabilities/capture-gameplay.d.ts +72 -7
  29. package/dist/core/capabilities/capture-gameplay.js +386 -25
  30. package/dist/core/capabilities/connect-signal.d.ts +1 -2
  31. package/dist/core/capabilities/connect-signal.js +1 -2
  32. package/dist/core/capabilities/console-read.d.ts +2 -2
  33. package/dist/core/capabilities/console-read.js +2 -2
  34. package/dist/core/capabilities/engine-receipt.d.ts +5 -7
  35. package/dist/core/capabilities/engine-receipt.js +7 -9
  36. package/dist/core/capabilities/export-engine-version.d.ts +7 -0
  37. package/dist/core/capabilities/export-engine-version.js +9 -0
  38. package/dist/core/capabilities/export-game.d.ts +4 -0
  39. package/dist/core/capabilities/export-game.js +53 -10
  40. package/dist/core/capabilities/export-presets.d.ts +1 -1
  41. package/dist/core/capabilities/export-presets.js +1 -1
  42. package/dist/core/capabilities/fabricate-mesh.d.ts +2 -3
  43. package/dist/core/capabilities/fabricate-mesh.js +2 -3
  44. package/dist/core/capabilities/frame-quality.d.ts +4 -4
  45. package/dist/core/capabilities/frame-quality.js +4 -4
  46. package/dist/core/capabilities/grep.d.ts +1 -1
  47. package/dist/core/capabilities/grep.js +1 -1
  48. package/dist/core/capabilities/image-generation.d.ts +12 -2
  49. package/dist/core/capabilities/image-generation.js +31 -2
  50. package/dist/core/capabilities/navigation/open.d.ts +1 -1
  51. package/dist/core/capabilities/navigation/open.js +1 -1
  52. package/dist/core/capabilities/navigation/render-product-map.js +1 -1
  53. package/dist/core/capabilities/navigation/targets.js +2 -2
  54. package/dist/core/capabilities/placement-batch.d.ts +1 -1
  55. package/dist/core/capabilities/placement-batch.js +2 -2
  56. package/dist/core/capabilities/play-determinism.d.ts +1 -1
  57. package/dist/core/capabilities/play-determinism.js +1 -1
  58. package/dist/core/capabilities/publish-build.d.ts +1 -1
  59. package/dist/core/capabilities/publish-build.js +21 -9
  60. package/dist/core/capabilities/publish-download.d.ts +1 -2
  61. package/dist/core/capabilities/publish-download.js +2 -3
  62. package/dist/core/capabilities/replace-node.d.ts +1 -3
  63. package/dist/core/capabilities/replace-node.js +1 -3
  64. package/dist/core/capabilities/runtime-control.d.ts +5 -7
  65. package/dist/core/capabilities/runtime-control.js +7 -9
  66. package/dist/core/capabilities/scene-batch.d.ts +1 -1
  67. package/dist/core/capabilities/scene-batch.js +1 -1
  68. package/dist/core/capabilities/scene-kind.js +1 -1
  69. package/dist/core/capabilities/scene-readback.d.ts +3 -4
  70. package/dist/core/capabilities/scene-readback.js +3 -4
  71. package/dist/core/capabilities/scene-script.d.ts +2 -2
  72. package/dist/core/capabilities/scene-script.js +2 -2
  73. package/dist/core/capabilities/seeing/shot-store.d.ts +1 -1
  74. package/dist/core/capabilities/seeing/shot-store.js +1 -1
  75. package/dist/core/capabilities/summer-bundle.d.ts +11 -3
  76. package/dist/core/capabilities/summer-bundle.js +21 -3
  77. package/dist/core/capabilities/surface-snap.d.ts +1 -1
  78. package/dist/core/capabilities/surface-snap.js +1 -1
  79. package/dist/core/capabilities/tool-dispatch.js +14 -3
  80. package/dist/core/capabilities/ui-control.d.ts +1 -2
  81. package/dist/core/capabilities/ui-control.js +1 -2
  82. package/dist/core/capabilities/world-snapshot.d.ts +1 -1
  83. package/dist/core/capabilities/world-snapshot.js +1 -1
  84. package/dist/core/capability-skew.d.ts +2 -3
  85. package/dist/core/capability-skew.js +4 -5
  86. package/dist/core/feedback/client.js +1 -1
  87. package/dist/core/launch-posture.d.ts +3 -3
  88. package/dist/core/launch-posture.js +3 -3
  89. package/dist/core/library-read.d.ts +7 -0
  90. package/dist/core/library-read.js +60 -9
  91. package/dist/core/library-search.d.ts +2 -0
  92. package/dist/core/templates.d.ts +1 -2
  93. package/dist/core/templates.js +1 -2
  94. package/dist/installer/agent-config.d.ts +25 -0
  95. package/dist/installer/agent-config.js +114 -3
  96. package/dist/installer/agent-table.d.ts +12 -0
  97. package/dist/installer/agent-table.js +9 -0
  98. package/dist/mcp/server.d.ts +2 -2
  99. package/dist/mcp/server.js +2 -2
  100. package/dist/mcp/tools/creator-tools.js +33 -4
  101. package/dist/mcp/tools/debug-tools.js +1 -1
  102. package/dist/mcp/tools/generate-tools.js +18 -37
  103. package/dist/mcp/tools/library-tools.d.ts +2 -1
  104. package/dist/mcp/tools/library-tools.js +1 -0
  105. package/dist/mcp/tools/navigation-tools.d.ts +1 -1
  106. package/dist/mcp/tools/navigation-tools.js +1 -1
  107. package/dist/mcp/tools/spatial-tools.js +1 -1
  108. package/dist/mcp/tools/visual-tools.js +2 -2
  109. package/dist/mcp/tools/with-engine.d.ts +1 -2
  110. package/dist/mcp/tools/with-engine.js +1 -2
  111. package/gemini-extension.json +5 -1
  112. package/hooks/session-start.sh +2 -2
  113. package/integrations/README.md +70 -76
  114. package/integrations/claude/README.md +0 -3
  115. package/integrations/codex/README.md +1 -2
  116. package/integrations/copilot-jetbrains/README.md +1 -1
  117. package/integrations/cursor/README.md +1 -2
  118. package/integrations/factory/README.md +15 -11
  119. package/integrations/mistral-vibe/README.md +1 -1
  120. package/integrations/opencode/README.md +1 -1
  121. package/integrations/roo-code/README.md +1 -1
  122. package/integrations/rovo-dev/README.md +1 -1
  123. package/integrations/trae/README.md +1 -1
  124. package/integrations/windsurf/README.md +3 -3
  125. package/library/references/collaborative-protocol/collaborative-protocol.md +5 -5
  126. package/library/references/gd-style/gd-style.md +4 -6
  127. package/library/references/godot-version/godot-version.md +3 -3
  128. package/library/references/headless-routing/headless-routing.md +2 -2
  129. package/library/references/mcp-fresh-chat-playbook/mcp-fresh-chat-playbook.md +2 -2
  130. package/library/references/mcp-tools-reference/mcp-tools-reference.md +16 -15
  131. package/library/references/product-map/product-map.md +1 -1
  132. package/library/references/public-surface-language/public-surface-language.md +1 -1
  133. package/library/references/summer-folder/summer-folder.md +1 -1
  134. package/library/skills/3d-lighting/SKILL.md +4 -4
  135. package/library/skills/README.md +16 -6
  136. package/library/skills/adaptive-music/SKILL.md +7 -7
  137. package/library/skills/ai-characters-in-games/SKILL.md +1 -1
  138. package/library/skills/ambient-bed/SKILL.md +12 -14
  139. package/library/skills/animated-loop/SKILL.md +42 -42
  140. package/library/skills/animation-tree/SKILL.md +6 -6
  141. package/library/skills/art-direction/SKILL.md +8 -11
  142. package/library/skills/asset-strategy/SKILL.md +3 -3
  143. package/library/skills/audio-direction/SKILL.md +8 -10
  144. package/library/skills/auto-fire-targeting/SKILL.md +4 -3
  145. package/library/skills/bounded-engine-runs/SKILL.md +6 -7
  146. package/library/skills/brainstorm-game/SKILL.md +5 -5
  147. package/library/skills/brainstorming/SKILL.md +9 -15
  148. package/library/skills/celeste-momentum-platforming/SKILL.md +8 -13
  149. package/library/skills/celeste-momentum-platforming/resource.yaml +1 -1
  150. package/library/skills/character-animation-wiring/SKILL.md +2 -2
  151. package/library/skills/character-model/SKILL.md +15 -26
  152. package/library/skills/character-movement/SKILL.md +1 -1
  153. package/library/skills/character-movement/references/movement-3d.md +1 -1
  154. package/library/skills/character-portrait/SKILL.md +4 -4
  155. package/library/skills/cinematic-cutscene/SKILL.md +38 -38
  156. package/library/skills/combat-basics/SKILL.md +1 -1
  157. package/library/skills/compatibility-renderer-traps/SKILL.md +2 -2
  158. package/library/skills/concept-art/SKILL.md +3 -3
  159. package/library/skills/create-asset-sheet/SKILL.md +12 -31
  160. package/library/skills/debug/SKILL.md +5 -4
  161. package/library/skills/debugging-game-feel/SKILL.md +1 -4
  162. package/library/skills/debugging-game-feel/resource.yaml +1 -1
  163. package/library/skills/design-level/SKILL.md +5 -5
  164. package/library/skills/design-mechanic/SKILL.md +6 -6
  165. package/library/skills/design-npc/SKILL.md +8 -15
  166. package/library/skills/diagnosing-perf-regressions/SKILL.md +5 -10
  167. package/library/skills/dispatching-parallel-agents/SKILL.md +1 -19
  168. package/library/skills/environment-kit/SKILL.md +5 -5
  169. package/library/skills/export-and-ship/SKILL.md +24 -25
  170. package/library/skills/export-and-ship/resource.yaml +2 -1
  171. package/library/skills/fabricating-assets/SKILL.md +1 -1
  172. package/library/skills/facial-and-lipsync/SKILL.md +5 -5
  173. package/library/skills/fps-controller/SKILL.md +17 -16
  174. package/library/skills/fps-controller/resource.yaml +0 -2
  175. package/library/skills/game-feel/SKILL.md +6 -7
  176. package/library/skills/gameskill/SKILL.md +3 -3
  177. package/library/skills/gdscript-patterns/SKILL.md +3 -3
  178. package/library/skills/generate-motion/SKILL.md +5 -22
  179. package/library/skills/godogen-visual-proof-game-generation/SKILL.md +72 -90
  180. package/library/skills/godogen-visual-proof-game-generation/resource.yaml +0 -1
  181. package/library/skills/grow-analytics/SKILL.md +21 -0
  182. package/library/skills/grow-analytics/resource.yaml +27 -0
  183. package/library/skills/headless-scripting/SKILL.md +8 -8
  184. package/library/skills/instantiate-asset-pack/SKILL.md +7 -3
  185. package/library/skills/investigating-bugs/SKILL.md +3 -11
  186. package/library/skills/investigating-bugs/condition-based-waiting.md +0 -10
  187. package/library/skills/investigating-bugs/root-cause-tracing.md +0 -8
  188. package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +15 -15
  189. package/library/skills/lumera-single-image-scene-reconstruction/resource.yaml +0 -1
  190. package/library/skills/make-game/SKILL.md +3 -5
  191. package/library/skills/make-game/references/2d-platformer.md +1 -1
  192. package/library/skills/multiplayer-project/SKILL.md +1 -1
  193. package/library/skills/multiplayer-testing/SKILL.md +4 -4
  194. package/library/skills/music-track/SKILL.md +40 -38
  195. package/library/skills/new-project/SKILL.md +4 -6
  196. package/library/skills/organic-model/SKILL.md +9 -9
  197. package/library/skills/pixel-art/SKILL.md +6 -6
  198. package/library/skills/play/SKILL.md +3 -4
  199. package/library/skills/playtesting-a-feature/SKILL.md +4 -6
  200. package/library/skills/procedural-animation/SKILL.md +10 -11
  201. package/library/skills/prop-model/SKILL.md +4 -4
  202. package/library/skills/psx-retro-rendering/SKILL.md +20 -20
  203. package/library/skills/psx-retro-rendering/resource.yaml +2 -3
  204. package/library/skills/publish-your-game/SKILL.md +21 -0
  205. package/library/skills/publish-your-game/resource.yaml +29 -0
  206. package/library/skills/realtime-wet-surfaces/SKILL.md +15 -15
  207. package/library/skills/realtime-wet-surfaces/resource.yaml +1 -2
  208. package/library/skills/remote-deploy/SKILL.md +20 -21
  209. package/library/skills/retarget/SKILL.md +19 -23
  210. package/library/skills/running-in-the-cloud/SKILL.md +5 -5
  211. package/library/skills/scene-composition/SKILL.md +5 -5
  212. package/library/skills/scene-hierarchy-design/SKILL.md +3 -3
  213. package/library/skills/scene-hierarchy-design/resource.yaml +0 -1
  214. package/library/skills/scene-scripting/SKILL.md +3 -3
  215. package/library/skills/scene-to-level/SKILL.md +28 -31
  216. package/library/skills/skill-create/SKILL.md +1 -1
  217. package/library/skills/skill-improve/SKILL.md +9 -9
  218. package/library/skills/skill-test/SKILL.md +10 -10
  219. package/library/skills/skintokens-auto-rigging/SKILL.md +18 -18
  220. package/library/skills/skintokens-auto-rigging/resource.yaml +3 -3
  221. package/library/skills/skybox-panorama/SKILL.md +8 -9
  222. package/library/skills/sound-effect/SKILL.md +19 -19
  223. package/library/skills/spatial-placement/SKILL.md +2 -2
  224. package/library/skills/spatial-placement/references/kit-placement-tools.md +12 -22
  225. package/library/skills/sprite-sheet/SKILL.md +10 -10
  226. package/library/skills/store-art/SKILL.md +23 -0
  227. package/library/skills/store-art/resource.yaml +30 -0
  228. package/library/skills/store-listing/SKILL.md +21 -0
  229. package/library/skills/store-listing/resource.yaml +25 -0
  230. package/library/skills/summer-analytics/SKILL.md +3 -3
  231. package/library/skills/summer-analytics/resource.yaml +1 -0
  232. package/library/skills/summer-friends/SKILL.md +10 -10
  233. package/library/skills/summer-leaderboards/SKILL.md +6 -6
  234. package/library/skills/summer-match-results/SKILL.md +13 -13
  235. package/library/skills/summer-matchmaking/SKILL.md +11 -11
  236. package/library/skills/summer-parties/SKILL.md +9 -9
  237. package/library/skills/summer-player-data/SKILL.md +12 -12
  238. package/library/skills/summer-store/SKILL.md +6 -6
  239. package/library/skills/summer-world-chat/SKILL.md +9 -9
  240. package/library/skills/summer-world-saves/SKILL.md +7 -7
  241. package/library/skills/text-to-motion/rigging-for-text-motion.md +1 -1
  242. package/library/skills/tileable-texture/SKILL.md +1 -1
  243. package/library/skills/trailer-shot/SKILL.md +36 -36
  244. package/library/skills/tune-performance/SKILL.md +13 -13
  245. package/library/skills/tune-performance/resource.yaml +1 -1
  246. package/library/skills/ui-graphics/SKILL.md +7 -7
  247. package/library/skills/use-widget-asset/SKILL.md +7 -7
  248. package/library/skills/using-summer/SKILL.md +5 -7
  249. package/library/skills/vehicle-model/SKILL.md +6 -7
  250. package/library/skills/vehicle-model/resource.yaml +2 -2
  251. package/library/skills/verification-before-completion/SKILL.md +5 -6
  252. package/library/skills/vfx-dissolve/SKILL.md +4 -4
  253. package/library/skills/vfx-fire/SKILL.md +6 -6
  254. package/library/skills/vfx-fire/_building-blocks/additive-billboard-particles.md +3 -3
  255. package/library/skills/vfx-hit-spark/SKILL.md +4 -4
  256. package/library/skills/vfx-hit-spark/_building-blocks/additive-billboard-particles.md +3 -3
  257. package/library/skills/vfx-lightning/SKILL.md +5 -5
  258. package/library/skills/vfx-lightning/_building-blocks/additive-billboard-particles.md +3 -3
  259. package/library/skills/vfx-lightning/_building-blocks/trauma-shake-snippet.md +9 -3
  260. package/library/skills/vfx-magic-glow/SKILL.md +4 -4
  261. package/library/skills/vfx-magic-glow/_building-blocks/additive-billboard-particles.md +3 -3
  262. package/library/skills/vfx-muzzle-flash/SKILL.md +5 -5
  263. package/library/skills/vfx-muzzle-flash/_building-blocks/additive-billboard-particles.md +3 -3
  264. package/library/skills/vfx-muzzle-flash/_building-blocks/trauma-shake-snippet.md +9 -3
  265. package/library/skills/vfx-smoke/SKILL.md +5 -5
  266. package/library/skills/vfx-water-ripple/SKILL.md +2 -2
  267. package/library/skills/vfx-water-ripple/_building-blocks/additive-billboard-particles.md +3 -3
  268. package/library/skills/voice-line/SKILL.md +9 -9
  269. package/library/skills/world-building-3d/references/spatial-tools.md +2 -2
  270. package/library/skills/writing-plans/SKILL.md +4 -4
  271. package/library/skills/writing-skills/SKILL.md +8 -8
  272. package/library/tools/capture-gameplay/resource.yaml +66 -1
  273. package/library/tools/creator-publish/resource.yaml +4 -4
  274. package/library/tools/creator-releases/resource.yaml +5 -5
  275. package/library/tools/export-game/resource.yaml +3 -0
  276. package/library/tools/generate-image/resource.yaml +4 -2
  277. package/library/tools/publish-build/resource.yaml +5 -1
  278. package/package.json +1 -1
  279. package/registry/generated/counts.json +2 -2
  280. package/registry/generated/gemini-extension.json +5 -1
  281. package/registry/generated/index.json +280 -137
  282. package/registry/generated/marketplace.claude.json +1 -1
  283. package/registry/generated/plugin.claude.json +5 -1
  284. package/registry/generated/plugin.codex.json +5 -1
  285. package/registry/generated/plugin.cursor.json +5 -1
  286. package/registry/generated/plugin.factory.json +5 -1
  287. package/registry/generated/skills-index.md +16 -6
  288. package/registry/generated/skills-registry.json +57 -5
  289. package/registry/schemas/domains.json +1 -1
  290. package/registry/schemas/skill.schema.json +5 -0
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, openSync, readdirSync, readFileSync, readSync, closeSync, statSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
2
3
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
4
  import { platform } from "node:os";
4
5
  import { isAbsolute, join, resolve } from "node:path";
@@ -15,8 +16,22 @@ const DEFAULT_RESOLUTION = "1920x1080";
15
16
  const MAX_FRAMES = 10;
16
17
  /** The engine clamps --summer-verify-max to 240 (main.cpp SUMMER_VERIFY_MAX_SECONDS). */
17
18
  const ENGINE_MAX_SECONDS = 240;
19
+ /** Offscreen runs draw uncapped (~130 fps) while the verify backstop counts 60 frames a second; cap the frame rate so seconds stay seconds. */
20
+ const CAPTURE_MAX_FPS = 60;
21
+ /** Files the engine imports; a new or changed one means the .godot/ caches are stale. */
22
+ const IMPORTED_EXTENSIONS = new Set([
23
+ ".png", ".jpg", ".jpeg", ".webp", ".svg", ".tga", ".bmp", ".exr", ".hdr", ".ktx", ".dds",
24
+ ".glb", ".gltf", ".fbx", ".obj", ".blend", ".dae",
25
+ ".wav", ".ogg", ".mp3",
26
+ ".ttf", ".otf", ".woff", ".woff2", ".fnt", ".csv",
27
+ ]);
28
+ const SKIPPED_DIRS = new Set([".godot", ".git", ".summer", ".import", "node_modules", "build", "exports"]);
29
+ const MAX_SCANNED_FILES = 50_000;
18
30
  const IMPORT_TIMEOUT_MS = 5 * 60_000;
19
31
  const PROBE_FILE = "capture_probe.gd";
32
+ const MAX_STEPS = 50;
33
+ const MAX_ARGS = 32;
34
+ const STEP_KINDS = ["press", "key", "action", "click", "drag", "wait", "shot"];
20
35
  function invalid(message, recovery) {
21
36
  return new BuildToolError("capture_args_invalid", message, recovery);
22
37
  }
@@ -54,6 +69,169 @@ function checkScene(project, scene) {
54
69
  }
55
70
  return value;
56
71
  }
72
+ function checkArgs(args) {
73
+ if (args === undefined)
74
+ return [];
75
+ if (!Array.isArray(args) || args.length > MAX_ARGS || args.some((arg) => typeof arg !== "string" || arg.length > 200 || /[\u0000\n\r]/.test(arg))) {
76
+ throw invalid(`args must be at most ${MAX_ARGS} single-line strings.`, 'Recovery: pass the game\'s own flags, e.g. ["--autostart", "--solo"].');
77
+ }
78
+ return args;
79
+ }
80
+ function point(value, where) {
81
+ if (!Array.isArray(value) || value.length !== 2 || !value.every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 7680)) {
82
+ throw invalid(`${where} must be [x, y] in frame pixels.`, "Recovery: pass two numbers from 0 to 7680, e.g. [960, 540] for the middle of a 1920x1080 frame.");
83
+ }
84
+ return [value[0], value[1]];
85
+ }
86
+ /** Validate the steps and turn them into the probe's one shape. */
87
+ export function probeSteps(steps) {
88
+ if (steps === undefined)
89
+ return [];
90
+ if (!Array.isArray(steps) || steps.length > MAX_STEPS) {
91
+ throw invalid(`steps must be a list of at most ${MAX_STEPS} steps.`, 'Recovery: pass e.g. [{"press":"Play"},{"wait":2000},{"shot":true}].');
92
+ }
93
+ const out = steps.map((raw, index) => {
94
+ const where = `steps[${index}]`;
95
+ const step = (raw && typeof raw === "object" ? raw : {});
96
+ const kinds = STEP_KINDS.filter((kind) => kind in step);
97
+ if (kinds.length !== 1) {
98
+ throw invalid(`${where} must have exactly one of ${STEP_KINDS.join(", ")}.`, 'Recovery: one step per object, e.g. {"press":"Play"} or {"key":"Space"}.');
99
+ }
100
+ const hold = boundedNumber(step.holdMs, 100, 0, 10_000, `${where}.holdMs`);
101
+ const name = (key) => {
102
+ const value = step[key];
103
+ if (typeof value !== "string" || !value.trim() || value.length > 100) {
104
+ throw invalid(`${where}.${key} must be a non-empty name.`, 'Recovery: e.g. {"press":"Play"}, {"key":"Space"}, {"action":"jump"}.');
105
+ }
106
+ return value.trim();
107
+ };
108
+ switch (kinds[0]) {
109
+ case "press":
110
+ return { type: "press", text: name("press"), timeout: boundedNumber(step.timeoutSeconds, 10, 0, 60, `${where}.timeoutSeconds`) };
111
+ case "key":
112
+ return { type: "key", text: name("key"), hold_ms: hold };
113
+ case "action":
114
+ return { type: "action", text: name("action"), hold_ms: hold };
115
+ case "click":
116
+ return { type: "click", at: point(step.click, `${where}.click`) };
117
+ case "drag": {
118
+ const drag = (step.drag && typeof step.drag === "object" ? step.drag : {});
119
+ return {
120
+ type: "drag",
121
+ at: point(drag.from, `${where}.drag.from`),
122
+ to: point(drag.to, `${where}.drag.to`),
123
+ ms: boundedNumber(drag.ms, 300, 0, 10_000, `${where}.drag.ms`),
124
+ };
125
+ }
126
+ case "wait":
127
+ return { type: "wait", ms: boundedNumber(step.wait, 0, 0, 60_000, `${where}.wait`) };
128
+ default:
129
+ if (step.shot !== true)
130
+ throw invalid(`${where}.shot must be true.`, 'Recovery: {"shot":true} saves a frame at that point.');
131
+ return { type: "shot" };
132
+ }
133
+ });
134
+ if (out.filter((step) => step.type === "shot").length > MAX_FRAMES) {
135
+ throw invalid(`at most ${MAX_FRAMES} shot steps.`, "Recovery: capture again for more frames.");
136
+ }
137
+ return out;
138
+ }
139
+ /** Seconds the steps can take at most (press waits for its button). */
140
+ function stepSeconds(steps) {
141
+ return steps.reduce((sum, step) => sum + (step.timeout ?? 0) + ((step.hold_ms ?? 0) + (step.ms ?? 0)) / 1000 + 0.5, 0);
142
+ }
143
+ /**
144
+ * Why one imported file needs importing again, or null when its import is
145
+ * current. Like the editor: the .import sidecar names the cached result, whose
146
+ * .md5 records the source's hash; the hash is only computed when the source is
147
+ * newer than that record.
148
+ */
149
+ function staleImport(project, path) {
150
+ const relative = path.slice(project.length + 1);
151
+ let sidecar;
152
+ try {
153
+ sidecar = readFileSync(`${path}.import`, "utf8");
154
+ }
155
+ catch {
156
+ return `${relative} has not been imported`;
157
+ }
158
+ const dest = /^path(?:\.[a-z0-9_]+)?="res:\/\/\.godot\/imported\/([^"]+)"/m.exec(sidecar)?.[1];
159
+ // importer "keep" or "skip": nothing is cached.
160
+ if (!dest)
161
+ return null;
162
+ const record = join(project, ".godot", "imported", dest.replace(/\.[^.]+$/, ".md5"));
163
+ let recorded;
164
+ try {
165
+ recorded = { text: readFileSync(record, "utf8"), mtimeMs: statSync(record).mtimeMs };
166
+ }
167
+ catch {
168
+ return `${relative} has no import cache`;
169
+ }
170
+ if (statSync(path).mtimeMs <= recorded.mtimeMs)
171
+ return null;
172
+ const sourceMd5 = /source_md5="([0-9a-f]+)"/.exec(recorded.text)?.[1];
173
+ return sourceMd5 === createHash("md5").update(readFileSync(path)).digest("hex") ? null : `${relative} changed after the last import`;
174
+ }
175
+ function isLfsPointer(path) {
176
+ try {
177
+ if (statSync(path).size > 1024)
178
+ return false;
179
+ const fd = openSync(path, "r");
180
+ const buffer = Buffer.alloc(64);
181
+ readSync(fd, buffer, 0, 64, 0);
182
+ closeSync(fd);
183
+ return buffer.toString("utf8").startsWith("version https://git-lfs");
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ }
189
+ /**
190
+ * Whether the project's import caches are stale: an imported file (texture,
191
+ * model, sound, font) that has no .import sidecar or cache yet, or whose
192
+ * content changed since it was imported (a pull, or LFS files fetched after
193
+ * the first import). Also names files that are still Git LFS pointers, which no
194
+ * import can fix.
195
+ */
196
+ export function importState(project) {
197
+ if (!existsSync(join(project, ".godot")))
198
+ return { needed: true, reason: "the project was never imported", lfsPointers: [] };
199
+ const lfsPointers = [];
200
+ let reason;
201
+ let scanned = 0;
202
+ const walk = (dir) => {
203
+ let entries;
204
+ try {
205
+ entries = readdirSync(dir, { withFileTypes: true });
206
+ }
207
+ catch {
208
+ return;
209
+ }
210
+ for (const entry of entries) {
211
+ if (scanned >= MAX_SCANNED_FILES)
212
+ return;
213
+ const path = join(dir, entry.name);
214
+ if (entry.isDirectory()) {
215
+ if (!SKIPPED_DIRS.has(entry.name) && !existsSync(join(path, ".gdignore")))
216
+ walk(path);
217
+ continue;
218
+ }
219
+ const dot = entry.name.lastIndexOf(".");
220
+ if (!entry.isFile() || dot < 0 || !IMPORTED_EXTENSIONS.has(entry.name.slice(dot).toLowerCase()))
221
+ continue;
222
+ scanned++;
223
+ if (isLfsPointer(path)) {
224
+ if (lfsPointers.length < 10)
225
+ lfsPointers.push(path.slice(project.length + 1));
226
+ continue;
227
+ }
228
+ if (!reason)
229
+ reason = staleImport(project, path) ?? undefined;
230
+ }
231
+ };
232
+ walk(project);
233
+ return { needed: reason !== undefined, ...(reason ? { reason } : {}), lfsPointers };
234
+ }
57
235
  function stamp(date) {
58
236
  return date.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").replace("T", "-");
59
237
  }
@@ -77,14 +255,42 @@ export function pngSize(bytes) {
77
255
  return null;
78
256
  return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
79
257
  }
258
+ /** A GDScript string literal (JSON escapes are valid GDScript escapes). */
259
+ const gdString = (text) => JSON.stringify(text);
260
+ const gdFloat = (value) => (Number.isInteger(value) ? `${value}.0` : String(value));
261
+ const gdVector = (point) => `Vector2(${gdFloat(point[0])}, ${gdFloat(point[1])})`;
262
+ /** One step as a typed GDScript call: no Variant reads, so strict projects load it. */
263
+ function stepLine(step, index) {
264
+ switch (step.type) {
265
+ case "press":
266
+ return `await _press(${index}, ${gdString(step.text)}, ${gdFloat(step.timeout)})`;
267
+ case "key":
268
+ return `await _key_named(${index}, ${gdString(step.text)}, ${step.hold_ms})`;
269
+ case "action":
270
+ return `await _action_named(${index}, ${gdString(step.text)}, ${step.hold_ms})`;
271
+ case "click":
272
+ return `await _click(${gdVector(step.at)})`;
273
+ case "drag":
274
+ return `await _drag(${gdVector(step.at)}, ${gdVector(step.to)}, ${step.ms})`;
275
+ case "wait":
276
+ return `await get_tree().create_timer(${gdFloat(step.ms / 1000)}, true).timeout`;
277
+ default:
278
+ return "await _shot()";
279
+ }
280
+ }
80
281
  /**
81
282
  * The stock probe. Self-contained (extends Node, no base class to copy into the
82
283
  * project); follows the verify-instance contract of assets/autopilot/probe_base.gd:
83
284
  * the engine sets summer_out_dir and summer_max_seconds before _ready(), the
84
285
  * first frames are empty until the scene has drawn twice, results.json is
85
- * always written.
286
+ * always written. The steps are generated as typed calls, and nothing reads a
287
+ * Variant, so projects that treat every GDScript warning as an error
288
+ * (untyped_declaration, unsafe_call_argument, unsafe_cast, integer_division)
289
+ * still load it.
86
290
  */
87
- export function captureProbeSource(frames, waitSeconds, intervalSeconds) {
291
+ export function captureProbeSource(frames, waitSeconds, intervalSeconds, steps = []) {
292
+ const shotSteps = steps.some((step) => step.type === "shot");
293
+ const body = steps.length ? steps.map((step, index) => `\t${stepLine(step, index)}`).join("\n") : "\tpass";
88
294
  return `# Written by summer_capture_gameplay. Safe to delete.
89
295
  # Fully typed: projects that treat GDScript warnings as errors must still load it.
90
296
  extends Node
@@ -95,11 +301,13 @@ var summer_max_seconds: int = 20
95
301
  const FRAMES: int = ${frames}
96
302
  const WAIT_SECONDS: float = ${waitSeconds.toFixed(3)}
97
303
  const INTERVAL_SECONDS: float = ${intervalSeconds.toFixed(3)}
304
+ const SHOT_STEPS: bool = ${shotSteps}
98
305
 
99
- var _shots: Array = []
100
- var _warnings: Array = []
306
+ var _shots: Array[Dictionary] = []
307
+ var _warnings: Array[String] = []
101
308
  var _start_ms: int = 0
102
309
  var _done: bool = false
310
+ var _pressed_seen: bool = false
103
311
 
104
312
  func _ready() -> void:
105
313
  \tprocess_mode = Node.PROCESS_MODE_ALWAYS
@@ -121,23 +329,159 @@ func _run() -> void:
121
329
  \t\t_finish(true)
122
330
  \t\treturn
123
331
  \tawait get_tree().create_timer(WAIT_SECONDS, true).timeout
124
- \tfor i: int in range(FRAMES):
125
- \t\tif i > 0:
126
- \t\t\tawait get_tree().create_timer(INTERVAL_SECONDS, true).timeout
127
- \t\tawait RenderingServer.frame_post_draw
128
- \t\tvar img: Image = get_viewport().get_texture().get_image()
129
- \t\tvar file_name: String = "shot-%02d.png" % (i + 1)
130
- \t\tif img == null:
131
- \t\t\t_warnings.append("%s: the viewport returned no image." % file_name)
132
- \t\t\tcontinue
133
- \t\tif img.save_png(summer_out_dir.path_join(file_name)) == OK:
134
- \t\t\t_shots.append({"file": file_name, "width": img.get_width(), "height": img.get_height()})
135
- \t\telse:
136
- \t\t\t_warnings.append("%s: could not write the PNG." % file_name)
332
+ \tawait _steps()
333
+ \tif not SHOT_STEPS:
334
+ \t\tfor i: int in range(FRAMES):
335
+ \t\t\tif i > 0:
336
+ \t\t\t\tawait get_tree().create_timer(INTERVAL_SECONDS, true).timeout
337
+ \t\t\tawait _shot()
137
338
  \t_finish(true)
138
339
 
139
- func _errors() -> Array:
140
- \tvar errors: Array = []
340
+ func _steps() -> void:
341
+ ${body}
342
+
343
+ func _shot() -> void:
344
+ \tawait RenderingServer.frame_post_draw
345
+ \tvar img: Image = get_viewport().get_texture().get_image()
346
+ \tvar file_name: String = "shot-%02d.png" % (_shots.size() + 1)
347
+ \tif img == null:
348
+ \t\t_warnings.append("%s: the viewport returned no image." % file_name)
349
+ \t\treturn
350
+ \tif img.save_png(summer_out_dir.path_join(file_name)) == OK:
351
+ \t\t_shots.append({"file": file_name, "width": img.get_width(), "height": img.get_height()})
352
+ \telse:
353
+ \t\t_warnings.append("%s: could not write the PNG." % file_name)
354
+
355
+ # Clicks and drags take frame pixels: the saved PNG's own coordinates. The frame
356
+ # is the root viewport; the final transform maps it to the window, where input
357
+ # events land, so a stretched canvas still gets the click where the frame shows it.
358
+ func _to_window(frame_pos: Vector2) -> Vector2:
359
+ \treturn get_viewport().get_final_transform() * frame_pos
360
+
361
+ func _mouse_button(frame_pos: Vector2, pressed: bool) -> void:
362
+ \tvar ev: InputEventMouseButton = InputEventMouseButton.new()
363
+ \tev.button_index = MOUSE_BUTTON_LEFT
364
+ \tev.pressed = pressed
365
+ \tev.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
366
+ \tev.position = _to_window(frame_pos)
367
+ \tev.global_position = ev.position
368
+ \tInput.parse_input_event(ev)
369
+
370
+ func _mouse_move(frame_pos: Vector2, relative: Vector2, held: bool) -> void:
371
+ \tvar ev: InputEventMouseMotion = InputEventMouseMotion.new()
372
+ \tev.position = _to_window(frame_pos)
373
+ \tev.global_position = ev.position
374
+ \tev.relative = relative
375
+ \tev.button_mask = MOUSE_BUTTON_MASK_LEFT if held else 0
376
+ \tInput.parse_input_event(ev)
377
+
378
+ func _click(frame_pos: Vector2) -> void:
379
+ \t_mouse_move(frame_pos, Vector2.ZERO, false)
380
+ \tawait get_tree().process_frame
381
+ \t_mouse_button(frame_pos, true)
382
+ \tawait get_tree().create_timer(0.05, true).timeout
383
+ \t_mouse_button(frame_pos, false)
384
+ \tawait get_tree().process_frame
385
+
386
+ func _drag(from: Vector2, to: Vector2, ms: int) -> void:
387
+ \t_mouse_move(from, Vector2.ZERO, false)
388
+ \t_mouse_button(from, true)
389
+ \tvar moves: int = maxi(2, roundi(ms / 16.0))
390
+ \tvar last: Vector2 = from
391
+ \tfor j: int in range(1, moves + 1):
392
+ \t\tawait get_tree().create_timer(ms / 1000.0 / moves, true).timeout
393
+ \t\tvar here: Vector2 = from.lerp(to, float(j) / moves)
394
+ \t\t_mouse_move(here, here - last, true)
395
+ \t\tlast = here
396
+ \t_mouse_button(to, false)
397
+ \tawait get_tree().process_frame
398
+
399
+ func _key_named(index: int, key_name: String, hold_ms: int) -> void:
400
+ \tvar keycode: Key = OS.find_keycode_from_string(key_name)
401
+ \tif keycode == KEY_NONE:
402
+ \t\t_warnings.append('steps[%d]: no key is named "%s" (use names like Space, Enter, Escape, A, Up).' % [index, key_name])
403
+ \t\treturn
404
+ \tvar ev: InputEventKey = InputEventKey.new()
405
+ \tev.keycode = keycode
406
+ \tev.physical_keycode = keycode
407
+ \tev.pressed = true
408
+ \tInput.parse_input_event(ev)
409
+ \tawait get_tree().create_timer(hold_ms / 1000.0, true).timeout
410
+ \tvar up: InputEventKey = InputEventKey.new()
411
+ \tup.keycode = keycode
412
+ \tup.physical_keycode = keycode
413
+ \tup.pressed = false
414
+ \tInput.parse_input_event(up)
415
+ \tawait get_tree().process_frame
416
+
417
+ func _action_named(index: int, action: String, hold_ms: int) -> void:
418
+ \tif not InputMap.has_action(StringName(action)):
419
+ \t\t_warnings.append('steps[%d]: the project has no input action "%s".' % [index, action])
420
+ \t\treturn
421
+ \tvar ev: InputEventAction = InputEventAction.new()
422
+ \tev.action = StringName(action)
423
+ \tev.pressed = true
424
+ \tInput.parse_input_event(ev)
425
+ \tawait get_tree().create_timer(hold_ms / 1000.0, true).timeout
426
+ \tvar up: InputEventAction = InputEventAction.new()
427
+ \tup.action = StringName(action)
428
+ \tup.pressed = false
429
+ \tInput.parse_input_event(up)
430
+ \tawait get_tree().process_frame
431
+
432
+ # A visible, enabled button: exact text, then node name, then partial text, then partial name.
433
+ func _find_button(label: String) -> BaseButton:
434
+ \tvar want: String = label.strip_edges().to_lower()
435
+ \tvar best: BaseButton = null
436
+ \tvar best_rank: int = 99
437
+ \tfor node: Node in get_tree().root.find_children("*", "BaseButton", true, false):
438
+ \t\tvar button: BaseButton = node as BaseButton
439
+ \t\tif button == null or button.disabled or not button.is_visible_in_tree():
440
+ \t\t\tcontinue
441
+ \t\tvar text: String = ""
442
+ \t\tvar as_button: Button = button as Button
443
+ \t\tif as_button != null:
444
+ \t\t\ttext = tr(as_button.text).strip_edges().to_lower()
445
+ \t\tvar node_name: String = String(button.name).to_lower()
446
+ \t\tvar rank: int = 99
447
+ \t\tif text == want:
448
+ \t\t\trank = 0
449
+ \t\telif node_name == want:
450
+ \t\t\trank = 1
451
+ \t\telif text.contains(want):
452
+ \t\t\trank = 2
453
+ \t\telif node_name.contains(want):
454
+ \t\t\trank = 3
455
+ \t\tif rank < best_rank:
456
+ \t\t\tbest = button
457
+ \t\t\tbest_rank = rank
458
+ \treturn best
459
+
460
+ func _on_pressed_seen() -> void:
461
+ \t_pressed_seen = true
462
+
463
+ # Click the button's centre like a player; emit pressed when the click did not reach it.
464
+ func _press(index: int, label: String, timeout: float) -> void:
465
+ \tvar button: BaseButton = _find_button(label)
466
+ \tvar deadline: int = Time.get_ticks_msec() + roundi(timeout * 1000.0)
467
+ \twhile button == null and Time.get_ticks_msec() < deadline:
468
+ \t\tawait get_tree().create_timer(0.1, true).timeout
469
+ \t\tbutton = _find_button(label)
470
+ \tif button == null:
471
+ \t\t_warnings.append('steps[%d]: no visible, enabled button "%s" within %.1f s.' % [index, label, timeout])
472
+ \t\treturn
473
+ \t_pressed_seen = false
474
+ \tbutton.pressed.connect(_on_pressed_seen, CONNECT_ONE_SHOT)
475
+ \tvar centre: Vector2 = button.get_global_transform_with_canvas() * (button.size / 2.0)
476
+ \tawait _click(centre)
477
+ \tif not _pressed_seen and is_instance_valid(button):
478
+ \t\tif button.pressed.is_connected(_on_pressed_seen):
479
+ \t\t\tbutton.pressed.disconnect(_on_pressed_seen)
480
+ \t\tbutton.pressed.emit()
481
+ \tawait get_tree().process_frame
482
+
483
+ func _errors() -> Array[String]:
484
+ \tvar errors: Array[String] = []
141
485
  \tvar path: String = summer_out_dir.path_join("errors.log")
142
486
  \tif FileAccess.file_exists(path):
143
487
  \t\tvar f: FileAccess = FileAccess.open(path, FileAccess.READ)
@@ -171,6 +515,8 @@ export async function captureGameplay(input, overrides = {}) {
171
515
  const waitSeconds = boundedNumber(input.waitSeconds, 3, 0, 120, "waitSeconds");
172
516
  const intervalSeconds = boundedNumber(input.intervalSeconds, 1, 0.1, 60, "intervalSeconds");
173
517
  const scene = checkScene(project, input.scene);
518
+ const userArgs = checkArgs(input.args);
519
+ const steps = probeSteps(input.steps);
174
520
  const started = deps.now();
175
521
  const binary = deps.findBinary();
176
522
  if (!binary) {
@@ -185,19 +531,25 @@ export async function captureGameplay(input, overrides = {}) {
185
531
  }
186
532
  await mkdir(out, { recursive: true });
187
533
  await rm(join(out, "results.json"), { force: true });
188
- // A never-opened checkout has no .godot/ caches; the game would boot into
189
- // missing-import errors. Build them once, headless (same as tests/autopilot).
534
+ // A never-opened checkout has no .godot/ caches, and a pull (or LFS files
535
+ // fetched later) leaves them stale; the game would boot into missing or old
536
+ // imports. Import headless first (same as tests/autopilot).
190
537
  let imported = false;
191
- if (!existsSync(join(project, ".godot"))) {
538
+ const importCheck = importState(project);
539
+ if (importCheck.needed) {
192
540
  const run = await deps.run(binary, ["--headless", "--import", "--disable-crash-handler", "--path", project], IMPORT_TIMEOUT_MS);
193
541
  if (run.timedOut || !existsSync(join(project, ".godot"))) {
194
542
  throw new BuildToolError("import_failed", "Summer Engine could not import the project's assets before the capture.", "Recovery: open the project once in Summer Engine (or run the engine with --headless --import), then capture again.", undefined, { output: outputTail(run.output) });
195
543
  }
196
544
  imported = true;
197
545
  }
546
+ const importReason = importCheck.reason;
198
547
  const probe = join(out, PROBE_FILE);
199
- await writeFile(probe, captureProbeSource(frames, waitSeconds, intervalSeconds));
200
- const maxSeconds = Math.min(ENGINE_MAX_SECONDS, Math.ceil(waitSeconds + (frames - 1) * intervalSeconds + 30));
548
+ await writeFile(probe, captureProbeSource(frames, waitSeconds, intervalSeconds, steps));
549
+ // The engine's verify backstop counts frames, not seconds; with the frame rate
550
+ // capped at 60 twice the planned run leaves room for slow loads.
551
+ const plannedSeconds = waitSeconds + (frames - 1) * intervalSeconds + stepSeconds(steps) + 15;
552
+ const maxSeconds = Math.min(ENGINE_MAX_SECONDS, Math.ceil(2 * plannedSeconds));
201
553
  const args = [
202
554
  "--disable-crash-handler",
203
555
  // No local HTTP API or ~/.summer discovery files for this throwaway run.
@@ -206,13 +558,18 @@ export async function captureGameplay(input, overrides = {}) {
206
558
  project,
207
559
  "--resolution",
208
560
  resolution.text,
561
+ "--max-fps",
562
+ String(CAPTURE_MAX_FPS),
209
563
  ...(scene ? ["--scene", scene] : []),
564
+ // The verify instance is offscreen already; the posture flag says so explicitly.
565
+ "--summer-offscreen",
210
566
  "--summer-verify",
211
567
  probe,
212
568
  "--summer-verify-out",
213
569
  out,
214
570
  "--summer-verify-max",
215
571
  String(maxSeconds),
572
+ ...(userArgs.length ? ["--", ...userArgs] : []),
216
573
  ];
217
574
  let run;
218
575
  try {
@@ -259,6 +616,9 @@ export async function captureGameplay(input, overrides = {}) {
259
616
  captured.push({ path, ...size });
260
617
  }
261
618
  const warnings = [...(results.frame_warnings ?? [])];
619
+ if (importCheck.lfsPointers.length) {
620
+ warnings.push(`These files are Git LFS pointers, not the real files, so the game shows them missing: ${importCheck.lfsPointers.join(", ")}. Run "git lfs pull" in the project, then capture again.`);
621
+ }
262
622
  if (results.finished === false)
263
623
  warnings.push("The game hit its time limit before every frame was saved.");
264
624
  const off = captured.filter((frame) => frame.width !== resolution.width || frame.height !== resolution.height);
@@ -276,6 +636,7 @@ export async function captureGameplay(input, overrides = {}) {
276
636
  out,
277
637
  engine: binary,
278
638
  imported,
639
+ ...(imported && importReason ? { importReason } : {}),
279
640
  durationMs: deps.now().getTime() - started.getTime(),
280
641
  ...(warnings.length ? { warnings } : {}),
281
642
  next: "Look at each frame before using it. For the store: upload with summer_upload_image_begin/complete and set them as desktop.screenshots or mobile.screenshots (summer_store_set_art). The game's own HUD may show; add no captions, logos or overlays.",
@@ -3,8 +3,7 @@
3
3
  * (src/mcp/tools/scene-tools.ts and tool-dispatch.ts) that leaves the
4
4
  * connection in the saved scene, verified from the file.
5
5
  *
6
- * Why not the engine's ConnectSignal op: SceneOps::connect_signal (engine
7
- * modules/1summer_engine/editor/ops/scene_ops.cpp) builds
6
+ * Why not the engine's ConnectSignal op: it builds
8
7
  * `Callable(receiver, method)` and registers
9
8
  * `add_do_method(emitter, "connect", signal, cb)` — no CONNECT_PERSIST, and the
10
9
  * op reads no flags argument. PackedScene::pack writes only persistent
@@ -3,8 +3,7 @@
3
3
  * (src/mcp/tools/scene-tools.ts and tool-dispatch.ts) that leaves the
4
4
  * connection in the saved scene, verified from the file.
5
5
  *
6
- * Why not the engine's ConnectSignal op: SceneOps::connect_signal (engine
7
- * modules/1summer_engine/editor/ops/scene_ops.cpp) builds
6
+ * Why not the engine's ConnectSignal op: it builds
8
7
  * `Callable(receiver, method)` and registers
9
8
  * `add_do_method(emitter, "connect", signal, cb)` — no CONNECT_PERSIST, and the
10
9
  * op reads no flags argument. PackedScene::pack writes only persistent
@@ -6,8 +6,8 @@
6
6
  * Output panel (EditorLog) never received them: a running game's errors are
7
7
  * collected by the debugger (summer_get_debugger_errors / the `debugger`
8
8
  * section of summer_get_diagnostics). The engine types console lines straight
9
- * from EditorLog::MSG_TYPE (error / warning / std / editor — debug_ops.cpp
10
- * GetConsoleOutput), so dropping `std` lines by default hides nothing that
9
+ * from EditorLog::MSG_TYPE (error / warning / std / editor) in
10
+ * GetConsoleOutput, so dropping `std` lines by default hides nothing that
11
11
  * was an error; the trap is treating this tool's count as the post-play
12
12
  * verdict at all. The note travels with the result because tool descriptions
13
13
  * are read once and results are read every time.
@@ -6,8 +6,8 @@
6
6
  * Output panel (EditorLog) never received them: a running game's errors are
7
7
  * collected by the debugger (summer_get_debugger_errors / the `debugger`
8
8
  * section of summer_get_diagnostics). The engine types console lines straight
9
- * from EditorLog::MSG_TYPE (error / warning / std / editor — debug_ops.cpp
10
- * GetConsoleOutput), so dropping `std` lines by default hides nothing that
9
+ * from EditorLog::MSG_TYPE (error / warning / std / editor) in
10
+ * GetConsoleOutput, so dropping `std` lines by default hides nothing that
11
11
  * was an error; the trap is treating this tool's count as the post-play
12
12
  * verdict at all. The note travels with the result because tool descriptions
13
13
  * are read once and results are read every time.
@@ -26,10 +26,9 @@ export declare function getFailureReason(value: {
26
26
  * Decide whether an engine result envelope represents a FAILURE, and if so
27
27
  * return a model-visible message. Returns null only for genuine success.
28
28
  *
29
- * Guards the two web bug classes (publicsummerengine cf17134f + contract.ts
30
- * `isFailureSignal`):
29
+ * Guards the two bug classes the web platform's tool layer once had:
31
30
  * - a failure `terminalState` (anything other than applied/no_op) is a failure
32
- * even when results[] is absent — the cf17134f "no-results envelope looked
31
+ * even when results[] is absent — the "no-results envelope looked
33
32
  * applied" masking. The poll loop surfaces timed_out/etc. here.
34
33
  * - an explicit ok:false / status:"error" / failed op inside results[].
35
34
  *
@@ -37,9 +36,8 @@ export declare function getFailureReason(value: {
37
36
  */
38
37
  export declare function extractOpError(result: unknown): string | null;
39
38
  /**
40
- * The engine stamps `scenePersistence.persisted` on every scene-targeted batch
41
- * (ops_executor.cpp: scene_persistence["persisted"] =
42
- * explicit_scene_save_succeeded). It only means SaveScene returned OK, not
39
+ * The engine stamps `scenePersistence.persisted` on every scene-targeted batch.
40
+ * It only means SaveScene returned OK, not
43
41
  * that the file holds the change: a ReparentNode subtree whose owners the
44
42
  * engine cleared, or a ConnectSignal made without CONNECT_PERSIST, is missing
45
43
  * from a file whose save "persisted". Rename it to
@@ -50,7 +48,7 @@ export declare function extractOpError(result: unknown): string | null;
50
48
  export declare function honestSceneReceipt<T>(result: T): T;
51
49
  /**
52
50
  * An older engine answers an unknown op with a per-op "unknown op: <Kind>"
53
- * (ops_executor.cpp fallthrough). Amend the envelope's error so the model gets
51
+ * Amend the envelope's error so the model gets
54
52
  * the upgrade path instead of retrying, and stamp `failure_reason:
55
53
  * "engine_lacks_op"` (+ `op`) so the result is detectable the same way as the
56
54
  * capability pre-flight's MissingOpResult: programmatic callers read the
@@ -9,7 +9,7 @@ export function getFailureReason(value) {
9
9
  ? value.failure_reason
10
10
  : undefined;
11
11
  }
12
- // 0.5.34 Block E contract (publicsummerengine src/lib/tools/contract.ts §0.1).
12
+ // Engine 0.5.34 receipt contract, shared with the web platform's tool layer.
13
13
  // The async lifecycle (async-op-lifecycle.ts pollOpToTerminal) merges
14
14
  // terminalState/errorClass onto the apply dict it returns. ONLY these two are
15
15
  // "applied something / applied nothing-on-purpose" — every other terminal state
@@ -33,10 +33,9 @@ const TERMINAL_STATE_MESSAGES = {
33
33
  * Decide whether an engine result envelope represents a FAILURE, and if so
34
34
  * return a model-visible message. Returns null only for genuine success.
35
35
  *
36
- * Guards the two web bug classes (publicsummerengine cf17134f + contract.ts
37
- * `isFailureSignal`):
36
+ * Guards the two bug classes the web platform's tool layer once had:
38
37
  * - a failure `terminalState` (anything other than applied/no_op) is a failure
39
- * even when results[] is absent — the cf17134f "no-results envelope looked
38
+ * even when results[] is absent — the "no-results envelope looked
40
39
  * applied" masking. The poll loop surfaces timed_out/etc. here.
41
40
  * - an explicit ok:false / status:"error" / failed op inside results[].
42
41
  *
@@ -97,9 +96,8 @@ export function extractOpError(result) {
97
96
  return null;
98
97
  }
99
98
  /**
100
- * The engine stamps `scenePersistence.persisted` on every scene-targeted batch
101
- * (ops_executor.cpp: scene_persistence["persisted"] =
102
- * explicit_scene_save_succeeded). It only means SaveScene returned OK, not
99
+ * The engine stamps `scenePersistence.persisted` on every scene-targeted batch.
100
+ * It only means SaveScene returned OK, not
103
101
  * that the file holds the change: a ReparentNode subtree whose owners the
104
102
  * engine cleared, or a ConnectSignal made without CONNECT_PERSIST, is missing
105
103
  * from a file whose save "persisted". Rename it to
@@ -123,11 +121,11 @@ export function honestSceneReceipt(result) {
123
121
  return (out ?? result);
124
122
  }
125
123
  /** The per-op text an older engine answers for a Kind its dispatch ladder does
126
- * not know (ops_executor.cpp fallthrough: `unknown op: <Kind>`). */
124
+ * not know (`unknown op: <Kind>`). */
127
125
  const UNKNOWN_OP_PATTERN = /unknown op/i;
128
126
  /**
129
127
  * An older engine answers an unknown op with a per-op "unknown op: <Kind>"
130
- * (ops_executor.cpp fallthrough). Amend the envelope's error so the model gets
128
+ * Amend the envelope's error so the model gets
131
129
  * the upgrade path instead of retrying, and stamp `failure_reason:
132
130
  * "engine_lacks_op"` (+ `op`) so the result is detectable the same way as the
133
131
  * capability pre-flight's MissingOpResult: programmatic callers read the
@@ -4,6 +4,13 @@
4
4
  * summer_export_game writes and fails after a full engine start.
5
5
  */
6
6
  export declare const EXPORT_MIN_ENGINE_VERSION = "0.7.0";
7
+ /** Bundle targets that need a newer engine than EXPORT_MIN_ENGINE_VERSION. */
8
+ export declare const TARGET_MIN_ENGINE_VERSION: Readonly<Record<string, string>>;
9
+ /** The requested targets this engine version cannot export; an unreadable version skips none. */
10
+ export declare function targetsNeedingNewerEngine(targets: readonly string[], version: string | null): Array<{
11
+ target: string;
12
+ needs: string;
13
+ }>;
7
14
  export declare const ENGINE_UPDATE_HOW = "run \"summer install --yes\" (or download it from summerengine.com/download)";
8
15
  /** Installed Summer version of an engine binary, or null when this system cannot read it. */
9
16
  export declare function installedEngineVersion(binary: string): string | null;