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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +5 -1
- package/.codex-plugin/plugin.json +5 -1
- package/.cursor-plugin/plugin.json +5 -1
- package/.factory-plugin/plugin.json +5 -1
- package/.opencode/INSTALL.md +8 -8
- package/.opencode/plugins/summer.js +2 -2
- package/AGENTS.md +7 -7
- package/CHANGELOG.md +37 -0
- package/GEMINI.md +1 -1
- package/NOTICE +1 -1
- package/README.md +35 -35
- package/assets/autopilot/run.sh +2 -2
- package/assets/navigation/web-routes.json +1 -1
- package/commands/gameskill.md +2 -2
- package/commands/summer.md +25 -25
- package/dist/cli/commands/open.d.ts +1 -1
- package/dist/cli/commands/run.d.ts +1 -1
- package/dist/cli/commands/run.js +1 -1
- package/dist/cli/commands/setup.js +24 -4
- package/dist/cli/commands/skills.js +2 -2
- package/dist/core/api-client.d.ts +5 -6
- package/dist/core/api-client.js +12 -15
- package/dist/core/auth.js +2 -2
- package/dist/core/capabilities/asset-import.d.ts +0 -1
- package/dist/core/capabilities/camera-view.d.ts +1 -1
- package/dist/core/capabilities/camera-view.js +2 -2
- package/dist/core/capabilities/capture-gameplay.d.ts +72 -7
- package/dist/core/capabilities/capture-gameplay.js +386 -25
- package/dist/core/capabilities/connect-signal.d.ts +1 -2
- package/dist/core/capabilities/connect-signal.js +1 -2
- package/dist/core/capabilities/console-read.d.ts +2 -2
- package/dist/core/capabilities/console-read.js +2 -2
- package/dist/core/capabilities/engine-receipt.d.ts +5 -7
- package/dist/core/capabilities/engine-receipt.js +7 -9
- package/dist/core/capabilities/export-engine-version.d.ts +7 -0
- package/dist/core/capabilities/export-engine-version.js +9 -0
- package/dist/core/capabilities/export-game.d.ts +4 -0
- package/dist/core/capabilities/export-game.js +53 -10
- package/dist/core/capabilities/export-presets.d.ts +1 -1
- package/dist/core/capabilities/export-presets.js +1 -1
- package/dist/core/capabilities/fabricate-mesh.d.ts +2 -3
- package/dist/core/capabilities/fabricate-mesh.js +2 -3
- package/dist/core/capabilities/frame-quality.d.ts +4 -4
- package/dist/core/capabilities/frame-quality.js +4 -4
- package/dist/core/capabilities/grep.d.ts +1 -1
- package/dist/core/capabilities/grep.js +1 -1
- package/dist/core/capabilities/image-generation.d.ts +12 -2
- package/dist/core/capabilities/image-generation.js +31 -2
- package/dist/core/capabilities/navigation/open.d.ts +1 -1
- package/dist/core/capabilities/navigation/open.js +1 -1
- package/dist/core/capabilities/navigation/render-product-map.js +1 -1
- package/dist/core/capabilities/navigation/targets.js +2 -2
- package/dist/core/capabilities/placement-batch.d.ts +1 -1
- package/dist/core/capabilities/placement-batch.js +2 -2
- package/dist/core/capabilities/play-determinism.d.ts +1 -1
- package/dist/core/capabilities/play-determinism.js +1 -1
- package/dist/core/capabilities/publish-build.d.ts +1 -1
- package/dist/core/capabilities/publish-build.js +21 -9
- package/dist/core/capabilities/publish-download.d.ts +1 -2
- package/dist/core/capabilities/publish-download.js +2 -3
- package/dist/core/capabilities/replace-node.d.ts +1 -3
- package/dist/core/capabilities/replace-node.js +1 -3
- package/dist/core/capabilities/runtime-control.d.ts +5 -7
- package/dist/core/capabilities/runtime-control.js +7 -9
- package/dist/core/capabilities/scene-batch.d.ts +1 -1
- package/dist/core/capabilities/scene-batch.js +1 -1
- package/dist/core/capabilities/scene-kind.js +1 -1
- package/dist/core/capabilities/scene-readback.d.ts +3 -4
- package/dist/core/capabilities/scene-readback.js +3 -4
- package/dist/core/capabilities/scene-script.d.ts +2 -2
- package/dist/core/capabilities/scene-script.js +2 -2
- package/dist/core/capabilities/seeing/shot-store.d.ts +1 -1
- package/dist/core/capabilities/seeing/shot-store.js +1 -1
- package/dist/core/capabilities/summer-bundle.d.ts +11 -3
- package/dist/core/capabilities/summer-bundle.js +21 -3
- package/dist/core/capabilities/surface-snap.d.ts +1 -1
- package/dist/core/capabilities/surface-snap.js +1 -1
- package/dist/core/capabilities/tool-dispatch.js +14 -3
- package/dist/core/capabilities/ui-control.d.ts +1 -2
- package/dist/core/capabilities/ui-control.js +1 -2
- package/dist/core/capabilities/world-snapshot.d.ts +1 -1
- package/dist/core/capabilities/world-snapshot.js +1 -1
- package/dist/core/capability-skew.d.ts +2 -3
- package/dist/core/capability-skew.js +4 -5
- package/dist/core/feedback/client.js +1 -1
- package/dist/core/launch-posture.d.ts +3 -3
- package/dist/core/launch-posture.js +3 -3
- package/dist/core/library-read.d.ts +7 -0
- package/dist/core/library-read.js +60 -9
- package/dist/core/library-search.d.ts +2 -0
- package/dist/core/templates.d.ts +1 -2
- package/dist/core/templates.js +1 -2
- package/dist/installer/agent-config.d.ts +25 -0
- package/dist/installer/agent-config.js +114 -3
- package/dist/installer/agent-table.d.ts +12 -0
- package/dist/installer/agent-table.js +9 -0
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/tools/creator-tools.js +33 -4
- package/dist/mcp/tools/debug-tools.js +1 -1
- package/dist/mcp/tools/generate-tools.js +18 -37
- package/dist/mcp/tools/library-tools.d.ts +2 -1
- package/dist/mcp/tools/library-tools.js +1 -0
- package/dist/mcp/tools/navigation-tools.d.ts +1 -1
- package/dist/mcp/tools/navigation-tools.js +1 -1
- package/dist/mcp/tools/spatial-tools.js +1 -1
- package/dist/mcp/tools/visual-tools.js +2 -2
- package/dist/mcp/tools/with-engine.d.ts +1 -2
- package/dist/mcp/tools/with-engine.js +1 -2
- package/gemini-extension.json +5 -1
- package/hooks/session-start.sh +2 -2
- package/integrations/README.md +70 -76
- package/integrations/claude/README.md +0 -3
- package/integrations/codex/README.md +1 -2
- package/integrations/copilot-jetbrains/README.md +1 -1
- package/integrations/cursor/README.md +1 -2
- package/integrations/factory/README.md +15 -11
- package/integrations/mistral-vibe/README.md +1 -1
- package/integrations/opencode/README.md +1 -1
- package/integrations/roo-code/README.md +1 -1
- package/integrations/rovo-dev/README.md +1 -1
- package/integrations/trae/README.md +1 -1
- package/integrations/windsurf/README.md +3 -3
- package/library/references/collaborative-protocol/collaborative-protocol.md +5 -5
- package/library/references/gd-style/gd-style.md +4 -6
- package/library/references/godot-version/godot-version.md +3 -3
- package/library/references/headless-routing/headless-routing.md +2 -2
- package/library/references/mcp-fresh-chat-playbook/mcp-fresh-chat-playbook.md +2 -2
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +16 -15
- package/library/references/product-map/product-map.md +1 -1
- package/library/references/public-surface-language/public-surface-language.md +1 -1
- package/library/references/summer-folder/summer-folder.md +1 -1
- package/library/skills/3d-lighting/SKILL.md +4 -4
- package/library/skills/README.md +16 -6
- package/library/skills/adaptive-music/SKILL.md +7 -7
- package/library/skills/ai-characters-in-games/SKILL.md +1 -1
- package/library/skills/ambient-bed/SKILL.md +12 -14
- package/library/skills/animated-loop/SKILL.md +42 -42
- package/library/skills/animation-tree/SKILL.md +6 -6
- package/library/skills/art-direction/SKILL.md +8 -11
- package/library/skills/asset-strategy/SKILL.md +3 -3
- package/library/skills/audio-direction/SKILL.md +8 -10
- package/library/skills/auto-fire-targeting/SKILL.md +4 -3
- package/library/skills/bounded-engine-runs/SKILL.md +6 -7
- package/library/skills/brainstorm-game/SKILL.md +5 -5
- package/library/skills/brainstorming/SKILL.md +9 -15
- package/library/skills/celeste-momentum-platforming/SKILL.md +8 -13
- package/library/skills/celeste-momentum-platforming/resource.yaml +1 -1
- package/library/skills/character-animation-wiring/SKILL.md +2 -2
- package/library/skills/character-model/SKILL.md +15 -26
- package/library/skills/character-movement/SKILL.md +1 -1
- package/library/skills/character-movement/references/movement-3d.md +1 -1
- package/library/skills/character-portrait/SKILL.md +4 -4
- package/library/skills/cinematic-cutscene/SKILL.md +38 -38
- package/library/skills/combat-basics/SKILL.md +1 -1
- package/library/skills/compatibility-renderer-traps/SKILL.md +2 -2
- package/library/skills/concept-art/SKILL.md +3 -3
- package/library/skills/create-asset-sheet/SKILL.md +12 -31
- package/library/skills/debug/SKILL.md +5 -4
- package/library/skills/debugging-game-feel/SKILL.md +1 -4
- package/library/skills/debugging-game-feel/resource.yaml +1 -1
- package/library/skills/design-level/SKILL.md +5 -5
- package/library/skills/design-mechanic/SKILL.md +6 -6
- package/library/skills/design-npc/SKILL.md +8 -15
- package/library/skills/diagnosing-perf-regressions/SKILL.md +5 -10
- package/library/skills/dispatching-parallel-agents/SKILL.md +1 -19
- package/library/skills/environment-kit/SKILL.md +5 -5
- package/library/skills/export-and-ship/SKILL.md +24 -25
- package/library/skills/export-and-ship/resource.yaml +2 -1
- package/library/skills/fabricating-assets/SKILL.md +1 -1
- package/library/skills/facial-and-lipsync/SKILL.md +5 -5
- package/library/skills/fps-controller/SKILL.md +17 -16
- package/library/skills/fps-controller/resource.yaml +0 -2
- package/library/skills/game-feel/SKILL.md +6 -7
- package/library/skills/gameskill/SKILL.md +3 -3
- package/library/skills/gdscript-patterns/SKILL.md +3 -3
- package/library/skills/generate-motion/SKILL.md +5 -22
- package/library/skills/godogen-visual-proof-game-generation/SKILL.md +72 -90
- package/library/skills/godogen-visual-proof-game-generation/resource.yaml +0 -1
- package/library/skills/grow-analytics/SKILL.md +21 -0
- package/library/skills/grow-analytics/resource.yaml +27 -0
- package/library/skills/headless-scripting/SKILL.md +8 -8
- package/library/skills/instantiate-asset-pack/SKILL.md +7 -3
- package/library/skills/investigating-bugs/SKILL.md +3 -11
- package/library/skills/investigating-bugs/condition-based-waiting.md +0 -10
- package/library/skills/investigating-bugs/root-cause-tracing.md +0 -8
- package/library/skills/lumera-single-image-scene-reconstruction/SKILL.md +15 -15
- package/library/skills/lumera-single-image-scene-reconstruction/resource.yaml +0 -1
- package/library/skills/make-game/SKILL.md +3 -5
- package/library/skills/make-game/references/2d-platformer.md +1 -1
- package/library/skills/multiplayer-project/SKILL.md +1 -1
- package/library/skills/multiplayer-testing/SKILL.md +4 -4
- package/library/skills/music-track/SKILL.md +40 -38
- package/library/skills/new-project/SKILL.md +4 -6
- package/library/skills/organic-model/SKILL.md +9 -9
- package/library/skills/pixel-art/SKILL.md +6 -6
- package/library/skills/play/SKILL.md +3 -4
- package/library/skills/playtesting-a-feature/SKILL.md +4 -6
- package/library/skills/procedural-animation/SKILL.md +10 -11
- package/library/skills/prop-model/SKILL.md +4 -4
- package/library/skills/psx-retro-rendering/SKILL.md +20 -20
- package/library/skills/psx-retro-rendering/resource.yaml +2 -3
- package/library/skills/publish-your-game/SKILL.md +21 -0
- package/library/skills/publish-your-game/resource.yaml +29 -0
- package/library/skills/realtime-wet-surfaces/SKILL.md +15 -15
- package/library/skills/realtime-wet-surfaces/resource.yaml +1 -2
- package/library/skills/remote-deploy/SKILL.md +20 -21
- package/library/skills/retarget/SKILL.md +19 -23
- package/library/skills/running-in-the-cloud/SKILL.md +5 -5
- package/library/skills/scene-composition/SKILL.md +5 -5
- package/library/skills/scene-hierarchy-design/SKILL.md +3 -3
- package/library/skills/scene-hierarchy-design/resource.yaml +0 -1
- package/library/skills/scene-scripting/SKILL.md +3 -3
- package/library/skills/scene-to-level/SKILL.md +28 -31
- package/library/skills/skill-create/SKILL.md +1 -1
- package/library/skills/skill-improve/SKILL.md +9 -9
- package/library/skills/skill-test/SKILL.md +10 -10
- package/library/skills/skintokens-auto-rigging/SKILL.md +18 -18
- package/library/skills/skintokens-auto-rigging/resource.yaml +3 -3
- package/library/skills/skybox-panorama/SKILL.md +8 -9
- package/library/skills/sound-effect/SKILL.md +19 -19
- package/library/skills/spatial-placement/SKILL.md +2 -2
- package/library/skills/spatial-placement/references/kit-placement-tools.md +12 -22
- package/library/skills/sprite-sheet/SKILL.md +10 -10
- package/library/skills/store-art/SKILL.md +23 -0
- package/library/skills/store-art/resource.yaml +30 -0
- package/library/skills/store-listing/SKILL.md +21 -0
- package/library/skills/store-listing/resource.yaml +25 -0
- package/library/skills/summer-analytics/SKILL.md +3 -3
- package/library/skills/summer-analytics/resource.yaml +1 -0
- package/library/skills/summer-friends/SKILL.md +10 -10
- package/library/skills/summer-leaderboards/SKILL.md +6 -6
- package/library/skills/summer-match-results/SKILL.md +13 -13
- package/library/skills/summer-matchmaking/SKILL.md +11 -11
- package/library/skills/summer-parties/SKILL.md +9 -9
- package/library/skills/summer-player-data/SKILL.md +12 -12
- package/library/skills/summer-store/SKILL.md +6 -6
- package/library/skills/summer-world-chat/SKILL.md +9 -9
- package/library/skills/summer-world-saves/SKILL.md +7 -7
- package/library/skills/text-to-motion/rigging-for-text-motion.md +1 -1
- package/library/skills/tileable-texture/SKILL.md +1 -1
- package/library/skills/trailer-shot/SKILL.md +36 -36
- package/library/skills/tune-performance/SKILL.md +13 -13
- package/library/skills/tune-performance/resource.yaml +1 -1
- package/library/skills/ui-graphics/SKILL.md +7 -7
- package/library/skills/use-widget-asset/SKILL.md +7 -7
- package/library/skills/using-summer/SKILL.md +5 -7
- package/library/skills/vehicle-model/SKILL.md +6 -7
- package/library/skills/vehicle-model/resource.yaml +2 -2
- package/library/skills/verification-before-completion/SKILL.md +5 -6
- package/library/skills/vfx-dissolve/SKILL.md +4 -4
- package/library/skills/vfx-fire/SKILL.md +6 -6
- package/library/skills/vfx-fire/_building-blocks/additive-billboard-particles.md +3 -3
- package/library/skills/vfx-hit-spark/SKILL.md +4 -4
- package/library/skills/vfx-hit-spark/_building-blocks/additive-billboard-particles.md +3 -3
- package/library/skills/vfx-lightning/SKILL.md +5 -5
- package/library/skills/vfx-lightning/_building-blocks/additive-billboard-particles.md +3 -3
- package/library/skills/vfx-lightning/_building-blocks/trauma-shake-snippet.md +9 -3
- package/library/skills/vfx-magic-glow/SKILL.md +4 -4
- package/library/skills/vfx-magic-glow/_building-blocks/additive-billboard-particles.md +3 -3
- package/library/skills/vfx-muzzle-flash/SKILL.md +5 -5
- package/library/skills/vfx-muzzle-flash/_building-blocks/additive-billboard-particles.md +3 -3
- package/library/skills/vfx-muzzle-flash/_building-blocks/trauma-shake-snippet.md +9 -3
- package/library/skills/vfx-smoke/SKILL.md +5 -5
- package/library/skills/vfx-water-ripple/SKILL.md +2 -2
- package/library/skills/vfx-water-ripple/_building-blocks/additive-billboard-particles.md +3 -3
- package/library/skills/voice-line/SKILL.md +9 -9
- package/library/skills/world-building-3d/references/spatial-tools.md +2 -2
- package/library/skills/writing-plans/SKILL.md +4 -4
- package/library/skills/writing-skills/SKILL.md +8 -8
- package/library/tools/capture-gameplay/resource.yaml +66 -1
- package/library/tools/creator-publish/resource.yaml +4 -4
- package/library/tools/creator-releases/resource.yaml +5 -5
- package/library/tools/export-game/resource.yaml +3 -0
- package/library/tools/generate-image/resource.yaml +4 -2
- package/library/tools/publish-build/resource.yaml +5 -1
- package/package.json +1 -1
- package/registry/generated/counts.json +2 -2
- package/registry/generated/gemini-extension.json +5 -1
- package/registry/generated/index.json +280 -137
- package/registry/generated/marketplace.claude.json +1 -1
- package/registry/generated/plugin.claude.json +5 -1
- package/registry/generated/plugin.codex.json +5 -1
- package/registry/generated/plugin.cursor.json +5 -1
- package/registry/generated/plugin.factory.json +5 -1
- package/registry/generated/skills-index.md +16 -6
- package/registry/generated/skills-registry.json +57 -5
- package/registry/schemas/domains.json +1 -1
- 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
|
-
\
|
|
125
|
-
\
|
|
126
|
-
\t\
|
|
127
|
-
\t\
|
|
128
|
-
\t\
|
|
129
|
-
\t\
|
|
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
|
|
140
|
-
|
|
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
|
|
189
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
10
|
-
* GetConsoleOutput
|
|
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
|
|
10
|
-
* GetConsoleOutput
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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 (
|
|
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
|
-
*
|
|
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;
|