summer-engine 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -2
- package/.codex-plugin/plugin.json +5 -3
- package/.cursor-plugin/plugin.json +4 -2
- package/.factory-plugin/plugin.json +4 -2
- package/CHANGELOG.md +36 -0
- package/README.md +2 -1
- package/assets/api-docs.json.gz +0 -0
- package/dist/cli/commands/install.d.ts +10 -0
- package/dist/cli/commands/install.js +55 -10
- package/dist/cli/commands/tool.d.ts +7 -0
- package/dist/cli/commands/tool.js +36 -1
- package/dist/core/capabilities/capture-gameplay.d.ts +69 -0
- package/dist/core/capabilities/capture-gameplay.js +283 -0
- package/dist/core/capabilities/creator.js +1 -1
- package/dist/core/capabilities/doctor.d.ts +9 -1
- package/dist/core/capabilities/doctor.js +47 -4
- package/dist/core/capabilities/engine-run.d.ts +4 -0
- package/dist/core/capabilities/engine-run.js +33 -5
- package/dist/core/capabilities/export-engine-version.d.ts +15 -0
- package/dist/core/capabilities/export-engine-version.js +24 -0
- package/dist/core/capabilities/export-game.d.ts +9 -1
- package/dist/core/capabilities/export-game.js +112 -36
- package/dist/core/capabilities/image-generation.d.ts +9 -0
- package/dist/core/capabilities/image-generation.js +19 -1
- package/dist/core/capabilities/publish-build.d.ts +14 -1
- package/dist/core/capabilities/publish-build.js +74 -22
- package/dist/core/capabilities/tool-dispatch.js +10 -0
- package/dist/core/config.js +2 -2
- package/dist/core/engine-install.d.ts +16 -0
- package/dist/core/engine-install.js +32 -2
- package/dist/core/hosted-mcp.d.ts +115 -0
- package/dist/core/hosted-mcp.js +164 -0
- package/dist/core/launch-posture.d.ts +3 -0
- package/dist/core/launch-posture.js +15 -2
- package/dist/mcp/hosted-mount.d.ts +13 -59
- package/dist/mcp/hosted-mount.js +24 -30
- package/dist/mcp/server.js +3 -2
- package/dist/mcp/tools/creator-tools.js +13 -3
- package/dist/mcp/tools/generate-tools.js +12 -2
- package/gemini-extension.json +3 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +3 -2
- package/library/skills/README.md +6 -4
- package/library/skills/bounded-engine-runs/SKILL.md +96 -0
- package/library/skills/bounded-engine-runs/resource.yaml +29 -0
- package/library/skills/compatibility-renderer-traps/SKILL.md +78 -0
- package/library/skills/compatibility-renderer-traps/resource.yaml +28 -0
- package/library/skills/concept-art/SKILL.md +2 -2
- package/library/skills/export-and-ship/SKILL.md +2 -0
- package/library/skills/headless-scripting/resource.yaml +1 -0
- package/library/skills/in-game-purchases/SKILL.md +123 -118
- package/library/skills/in-game-purchases/resource.yaml +2 -2
- package/library/skills/web-compatibility-look/resource.yaml +1 -0
- package/library/tools/capture-gameplay/resource.yaml +73 -0
- package/library/tools/generate-image/resource.yaml +15 -1
- package/library/tools/publish-build/resource.yaml +1 -1
- package/package.json +1 -1
- package/registry/generated/aliases.json +1 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +125 -12
- package/registry/generated/marketplace.claude.json +2 -2
- package/registry/generated/plugin.claude.json +4 -2
- package/registry/generated/plugin.codex.json +5 -3
- package/registry/generated/plugin.cursor.json +4 -2
- package/registry/generated/plugin.factory.json +4 -2
- package/registry/generated/skills-index.md +6 -4
- package/registry/generated/skills-registry.json +28 -1
|
@@ -6,7 +6,8 @@ import { engineSelectionFromEnv } from "../engine.js";
|
|
|
6
6
|
import { findEngineBinary } from "../engine-install.js";
|
|
7
7
|
import { readStoreJson, writeStoreJson } from "../store.js";
|
|
8
8
|
import { writeZip } from "../util/zip-write.js";
|
|
9
|
-
import { runEngine } from "./engine-run.js";
|
|
9
|
+
import { engineErrorLines, runEngine } from "./engine-run.js";
|
|
10
|
+
import { assertExportEngine, installedEngineVersion } from "./export-engine-version.js";
|
|
10
11
|
import { BUNDLE_TARGETS, DOWNLOAD_TARGETS, SUMMER_GAMES_PRESET, bundlePresetSpec, ensurePreset, normalizeTargets, variantString, } from "./export-presets.js";
|
|
11
12
|
import { engineTemplateInfo, findInstalledTemplate, templateFileFor } from "./export-templates.js";
|
|
12
13
|
import { BuildToolError, hashFile, readSummerBundle } from "./summer-bundle.js";
|
|
@@ -15,7 +16,7 @@ export { runEngine, SUMMER_GAMES_PRESET };
|
|
|
15
16
|
* summer_export_game: run the installed Summer Engine headless and return the
|
|
16
17
|
* file the Summer Games store takes for the chosen targets.
|
|
17
18
|
*
|
|
18
|
-
* <engine> --headless --path <project> --export-release "<preset>" <out>
|
|
19
|
+
* <engine> --headless --summer-no-api --path <project> --export-release "<preset>" <out>
|
|
19
20
|
*
|
|
20
21
|
* format "bundle" (default): the summer.bundle.v1 .zip for build-publications
|
|
21
22
|
* (summer_publish_build). Summer runs its client.pck on its own templates in
|
|
@@ -51,6 +52,7 @@ const TARGET_ENGINE_NOTE = {
|
|
|
51
52
|
};
|
|
52
53
|
const defaultDependencies = {
|
|
53
54
|
findBinary: () => findEngineBinary(platform()),
|
|
55
|
+
engineVersion: installedEngineVersion,
|
|
54
56
|
run: runEngine,
|
|
55
57
|
now: () => new Date(),
|
|
56
58
|
};
|
|
@@ -81,6 +83,69 @@ async function defaultExportDir(project) {
|
|
|
81
83
|
function outputTail(output) {
|
|
82
84
|
return output.trim().split("\n").slice(-40).join("\n");
|
|
83
85
|
}
|
|
86
|
+
/** size and mtime of every file at the top of the project folder (project.godot, export_presets.cfg, ...). */
|
|
87
|
+
async function snapshotProjectFiles(project) {
|
|
88
|
+
const files = new Map();
|
|
89
|
+
for (const entry of await readdir(project, { withFileTypes: true })) {
|
|
90
|
+
if (!entry.isFile())
|
|
91
|
+
continue;
|
|
92
|
+
const info = await stat(join(project, entry.name)).catch(() => null);
|
|
93
|
+
if (info)
|
|
94
|
+
files.set(entry.name, `${info.size}:${info.mtimeMs}`);
|
|
95
|
+
}
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
async function projectChangesSince(project, before, output) {
|
|
99
|
+
const after = await snapshotProjectFiles(project);
|
|
100
|
+
const changes = [];
|
|
101
|
+
for (const [file, value] of after) {
|
|
102
|
+
if (output && resolve(project, file) === resolve(output))
|
|
103
|
+
continue;
|
|
104
|
+
if (!before.has(file))
|
|
105
|
+
changes.push({ file, change: "created" });
|
|
106
|
+
else if (before.get(file) !== value)
|
|
107
|
+
changes.push({ file, change: "modified" });
|
|
108
|
+
}
|
|
109
|
+
for (const file of before.keys())
|
|
110
|
+
if (!after.has(file))
|
|
111
|
+
changes.push({ file, change: "deleted" });
|
|
112
|
+
return changes.sort((a, b) => a.file.localeCompare(b.file));
|
|
113
|
+
}
|
|
114
|
+
function projectChangeWarnings(changes) {
|
|
115
|
+
return changes.some((entry) => entry.file === "project.godot" && entry.change === "modified")
|
|
116
|
+
? ["Summer Engine rewrote project.godot while it exported (an engine update cleans up old settings; the previous file is project.godot.bak). Review the change and commit or revert it."]
|
|
117
|
+
: [];
|
|
118
|
+
}
|
|
119
|
+
/** Re-throw an export failure with the project files it already changed. */
|
|
120
|
+
async function withProjectChanges(project, before, work) {
|
|
121
|
+
try {
|
|
122
|
+
return await work();
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (!(error instanceof BuildToolError))
|
|
126
|
+
throw error;
|
|
127
|
+
const changes = await projectChangesSince(project, before).catch(() => []);
|
|
128
|
+
if (!changes.length)
|
|
129
|
+
throw error;
|
|
130
|
+
throw new BuildToolError(error.code, error.message.replace(` ${error.recovery}`, ""), error.recovery, error.status, { ...error.detail, projectChanges: changes });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Plain recovery for what the engine printed; the generic preset hint only when a preset is what it names. */
|
|
134
|
+
function exportFailureRecovery(errors, preset) {
|
|
135
|
+
const text = errors.join("\n");
|
|
136
|
+
if (/export preset/i.test(text) && /(invalid|not found|no such|unknown)/i.test(text)) {
|
|
137
|
+
return `Recovery: this engine has no "${preset}" export preset, which means it predates summer.games exports: update Summer Engine to 0.7.0+ (run "summer install --yes"), then export again.`;
|
|
138
|
+
}
|
|
139
|
+
if (/source graph|client pack|authority|domain/i.test(text)) {
|
|
140
|
+
return ("Recovery: this is a hosted game whose source graph puts a resource the client loads in a server-only (authority) domain. " +
|
|
141
|
+
"Open the source graph summer.build.json names (source_graph, usually source-domains.json) and narrow the authority root to the server-only files " +
|
|
142
|
+
"(for example the authority scene and its script), so the scenes and resources the client starts (such as the network composition .tres) sit under a shared or client root. " +
|
|
143
|
+
"The multiplayer-project skill explains the domains. Then export again.");
|
|
144
|
+
}
|
|
145
|
+
return errors.length
|
|
146
|
+
? "Recovery: fix what the engine names above and export again."
|
|
147
|
+
: "Recovery: the engine printed no ERROR line; read output, fix what it names, and export again.";
|
|
148
|
+
}
|
|
84
149
|
/** One value from project.godot ([section] key=value), unquoted. */
|
|
85
150
|
export function projectSetting(project, section, key) {
|
|
86
151
|
let current = "";
|
|
@@ -189,7 +254,8 @@ async function listFiles(root) {
|
|
|
189
254
|
return files.sort();
|
|
190
255
|
}
|
|
191
256
|
async function runExport(deps, binary, project, preset, out, debug, timeoutMs, notes) {
|
|
192
|
-
|
|
257
|
+
// --summer-no-api: an export needs no local HTTP API; nothing binds 127.0.0.1:6550 or writes discovery files.
|
|
258
|
+
const args = ["--headless", "--summer-no-api", "--path", project, debug ? "--export-debug" : "--export-release", preset, out];
|
|
193
259
|
let run;
|
|
194
260
|
try {
|
|
195
261
|
run = await deps.run(binary, args, timeoutMs);
|
|
@@ -201,10 +267,9 @@ async function runExport(deps, binary, project, preset, out, debug, timeoutMs, n
|
|
|
201
267
|
throw new BuildToolError("export_timeout", `The export did not finish within ${Math.round(timeoutMs / 1000)} seconds and was stopped.`, "Recovery: a first export imports every asset; retry with a larger timeoutSeconds.", undefined, { output: outputTail(run.output) });
|
|
202
268
|
}
|
|
203
269
|
if (run.code !== 0 || !existsSync(out)) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
].join(" "), undefined, { output: outputTail(run.output) });
|
|
270
|
+
const errors = run.errors?.length ? run.errors : engineErrorLines(run.output);
|
|
271
|
+
const named = errors.slice(0, 3).map((line) => line.replace(/^(SCRIPT )?ERROR:\s*/, ""));
|
|
272
|
+
throw new BuildToolError("export_failed", `Summer Engine did not export the game (exit ${run.code ?? run.signal})${named.length ? `: ${named.join(" | ")}` : "."}`, [exportFailureRecovery(errors, preset), ...notes].join(" "), undefined, { errors, output: outputTail(run.output) });
|
|
208
273
|
}
|
|
209
274
|
}
|
|
210
275
|
export async function exportGame(input, overrides = {}) {
|
|
@@ -227,15 +292,19 @@ export async function exportGame(input, overrides = {}) {
|
|
|
227
292
|
if (!binary) {
|
|
228
293
|
throw new BuildToolError("engine_not_installed", "Summer Engine is not installed on this machine.", 'Recovery: run "summer install", or set SUMMER_BIN to the engine executable, then export again.');
|
|
229
294
|
}
|
|
295
|
+
// Before any preset is written: an old engine cannot read them.
|
|
296
|
+
assertExportEngine(binary, deps.engineVersion(binary));
|
|
230
297
|
if (format === "download") {
|
|
231
298
|
return exportDownload(deps, binary, project, targets[0], out, debug, timeoutMs, started);
|
|
232
299
|
}
|
|
233
300
|
out ??= join(await defaultExportDir(project), `${basename(project)}-${stamp(started)}.zip`);
|
|
234
301
|
const before = existsSync(out) ? statSync(out).mtimeMs : null;
|
|
302
|
+
const projectBefore = await snapshotProjectFiles(project);
|
|
235
303
|
const ensured = targets ? await ensurePreset(project, bundlePresetSpec(targets)) : undefined;
|
|
236
304
|
const preset = ensured?.name ?? (input.preset?.trim() || SUMMER_GAMES_PRESET);
|
|
237
305
|
const notes = (targets ?? []).flatMap((target) => TARGET_ENGINE_NOTE[target] ?? []);
|
|
238
|
-
await runExport(deps, binary, project, preset, out, debug, timeoutMs, notes);
|
|
306
|
+
await withProjectChanges(project, projectBefore, () => runExport(deps, binary, project, preset, out, debug, timeoutMs, notes));
|
|
307
|
+
const projectChanges = await projectChangesSince(project, projectBefore, out);
|
|
239
308
|
if (before !== null && statSync(out).mtimeMs === before) {
|
|
240
309
|
throw new BuildToolError("export_failed", "Summer Engine exited without writing a new bundle.", "Recovery: read the engine output and export again.");
|
|
241
310
|
}
|
|
@@ -244,7 +313,7 @@ export async function exportGame(input, overrides = {}) {
|
|
|
244
313
|
if (missing.length) {
|
|
245
314
|
throw new BuildToolError("export_target_unsupported", `This Summer Engine exported the bundle without ${missing.join(", ")}: its summer.games preset has no such platform.`, ["Recovery: update Summer Engine and export again, or export without those targets.", ...missing.flatMap((target) => TARGET_ENGINE_NOTE[target] ?? [])].join(" "), undefined, { exportedPlatforms: bundle.targetPlatforms, path: out });
|
|
246
315
|
}
|
|
247
|
-
const warnings =
|
|
316
|
+
const warnings = projectChangeWarnings(projectChanges);
|
|
248
317
|
const desktopOnly = bundle.hosted ? [] : bundle.targetPlatforms.filter((target) => !STANDALONE_TARGETS.includes(target));
|
|
249
318
|
if (desktopOnly.length) {
|
|
250
319
|
warnings.push(`This game has no server, and the store runs games without a server on iPhone and Android only: summer_publish_build will refuse ${desktopOnly.join(", ")}. ` +
|
|
@@ -267,6 +336,7 @@ export async function exportGame(input, overrides = {}) {
|
|
|
267
336
|
engine: binary,
|
|
268
337
|
durationMs: deps.now().getTime() - started.getTime(),
|
|
269
338
|
bundle: { ...summary, fileCount: files.length },
|
|
339
|
+
...(projectChanges.length ? { projectChanges } : {}),
|
|
270
340
|
...(warnings.length ? { warnings } : {}),
|
|
271
341
|
next: "Upload it to the game's store listing with summer_publish_build (gameId, clientVersion).",
|
|
272
342
|
};
|
|
@@ -296,6 +366,7 @@ async function exportDownload(deps, binary, project, target, requestedOut, debug
|
|
|
296
366
|
throw error;
|
|
297
367
|
}
|
|
298
368
|
const spec = downloadPresetSpec(target, slug);
|
|
369
|
+
const projectBefore = await snapshotProjectFiles(project);
|
|
299
370
|
const ensured = await ensurePreset(project, spec);
|
|
300
371
|
const exportsDir = await defaultExportDir(project);
|
|
301
372
|
const work = join(exportsDir, `${slug}-${target}-${stamp(started)}`);
|
|
@@ -304,37 +375,41 @@ async function exportDownload(deps, binary, project, target, requestedOut, debug
|
|
|
304
375
|
const out = requestedOut ?? join(exportsDir, `${slug}-${storePlatform}-${stamp(started)}.zip`);
|
|
305
376
|
const warnings = [];
|
|
306
377
|
let fileCount = 1;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
else {
|
|
316
|
-
const engineOut = join(work, target === "web" ? "index.html" : `${slug}.exe`);
|
|
317
|
-
await runExport(deps, binary, project, spec.name, engineOut, debug, timeoutMs, []);
|
|
318
|
-
const files = await listFiles(work);
|
|
319
|
-
const entries = files.map((file) => ({ name: relative(work, file).split(sep).join("/"), source: file }));
|
|
320
|
-
if (target === "web") {
|
|
321
|
-
const sizes = await Promise.all(files.map(async (file) => (await stat(file)).size));
|
|
322
|
-
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
323
|
-
const tooBig = entries.find((_entry, index) => sizes[index] > WEB_LIMITS.fileBytes);
|
|
324
|
-
if (entries.length > WEB_LIMITS.files || total > WEB_LIMITS.zipBytes || tooBig) {
|
|
325
|
-
throw new BuildToolError("web_build_too_large", `The web build has ${entries.length} files and ${Math.round(total / 1048576)} MiB${tooBig ? `; ${tooBig.name} is over 200 MiB` : ""}. The store takes at most ${WEB_LIMITS.files} files, 500 MiB, 200 MiB per file.`, "Recovery: exclude unused assets from the export (Project > Export > Resources) or compress textures, then export again.");
|
|
326
|
-
}
|
|
378
|
+
await withProjectChanges(project, projectBefore, async () => {
|
|
379
|
+
try {
|
|
380
|
+
if (target === "macos") {
|
|
381
|
+
// The macOS exporter writes the .app inside a .zip itself.
|
|
382
|
+
const engineOut = join(work, `${slug}.zip`);
|
|
383
|
+
await runExport(deps, binary, project, spec.name, engineOut, debug, timeoutMs, []);
|
|
384
|
+
await rename(engineOut, out);
|
|
385
|
+
warnings.push("macOS shows players an unidentified-developer prompt for an ad hoc signed app; the store accepts it as is.");
|
|
327
386
|
}
|
|
328
387
|
else {
|
|
329
|
-
|
|
388
|
+
const engineOut = join(work, target === "web" ? "index.html" : `${slug}.exe`);
|
|
389
|
+
await runExport(deps, binary, project, spec.name, engineOut, debug, timeoutMs, []);
|
|
390
|
+
const files = await listFiles(work);
|
|
391
|
+
const entries = files.map((file) => ({ name: relative(work, file).split(sep).join("/"), source: file }));
|
|
392
|
+
if (target === "web") {
|
|
393
|
+
const sizes = await Promise.all(files.map(async (file) => (await stat(file)).size));
|
|
394
|
+
const total = sizes.reduce((sum, size) => sum + size, 0);
|
|
395
|
+
const tooBig = entries.find((_entry, index) => sizes[index] > WEB_LIMITS.fileBytes);
|
|
396
|
+
if (entries.length > WEB_LIMITS.files || total > WEB_LIMITS.zipBytes || tooBig) {
|
|
397
|
+
throw new BuildToolError("web_build_too_large", `The web build has ${entries.length} files and ${Math.round(total / 1048576)} MiB${tooBig ? `; ${tooBig.name} is over 200 MiB` : ""}. The store takes at most ${WEB_LIMITS.files} files, 500 MiB, 200 MiB per file.`, "Recovery: exclude unused assets from the export (Project > Export > Resources) or compress textures, then export again.");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
warnings.push("The .exe is not code signed; Windows SmartScreen may warn players. The store accepts it as is.");
|
|
402
|
+
}
|
|
403
|
+
await writeZip(out, entries);
|
|
404
|
+
fileCount = entries.length;
|
|
330
405
|
}
|
|
331
|
-
await writeZip(out, entries);
|
|
332
|
-
fileCount = entries.length;
|
|
333
406
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
407
|
+
finally {
|
|
408
|
+
await rm(work, { recursive: true, force: true });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
const projectChanges = await projectChangesSince(project, projectBefore, out);
|
|
412
|
+
warnings.unshift(...projectChangeWarnings(projectChanges));
|
|
338
413
|
const icon = projectSetting(project, "application", "config/icon");
|
|
339
414
|
if (!icon)
|
|
340
415
|
warnings.push("The project has no icon (Project Settings > Application > Config > Icon); the export uses the Summer default.");
|
|
@@ -358,6 +433,7 @@ async function exportDownload(deps, binary, project, target, requestedOut, debug
|
|
|
358
433
|
fileCount,
|
|
359
434
|
icon,
|
|
360
435
|
signing: target === "macos" ? "ad hoc (built-in), not notarized" : "none",
|
|
436
|
+
...(projectChanges.length ? { projectChanges } : {}),
|
|
361
437
|
...(warnings.length ? { warnings } : {}),
|
|
362
438
|
next: `Upload it as the game's ${storePlatform} store version with summer_publish_build (gameId, clientVersion); it reads the platform from this export.`,
|
|
363
439
|
};
|
|
@@ -6,19 +6,28 @@ export declare const imageGenerationArgsSchema: z.ZodObject<{
|
|
|
6
6
|
style: z.ZodDefault<z.ZodString>;
|
|
7
7
|
referenceImageUrl: z.ZodOptional<z.ZodString>;
|
|
8
8
|
removeBackground: z.ZodOptional<z.ZodBoolean>;
|
|
9
|
+
aspectRatio: z.ZodOptional<z.ZodEnum<["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "5:4", "4:5", "21:9"]>>;
|
|
10
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
9
12
|
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
10
13
|
}, "strip", z.ZodTypeAny, {
|
|
11
14
|
prompt: string;
|
|
12
15
|
model: string;
|
|
13
16
|
style: string;
|
|
17
|
+
width?: number | undefined;
|
|
18
|
+
height?: number | undefined;
|
|
14
19
|
options?: Record<string, any> | undefined;
|
|
15
20
|
referenceImageUrl?: string | undefined;
|
|
16
21
|
removeBackground?: boolean | undefined;
|
|
22
|
+
aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | undefined;
|
|
17
23
|
}, {
|
|
18
24
|
prompt: string;
|
|
25
|
+
width?: number | undefined;
|
|
26
|
+
height?: number | undefined;
|
|
19
27
|
options?: Record<string, any> | undefined;
|
|
20
28
|
model?: string | undefined;
|
|
21
29
|
style?: string | undefined;
|
|
22
30
|
referenceImageUrl?: string | undefined;
|
|
23
31
|
removeBackground?: boolean | undefined;
|
|
32
|
+
aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3" | "5:4" | "4:5" | "21:9" | undefined;
|
|
24
33
|
}>;
|
|
@@ -15,8 +15,26 @@ export const imageGenerationArgsSchema = z.object({
|
|
|
15
15
|
.optional()
|
|
16
16
|
.describe("Source image URL for img2img / edit mode. The prompt describes how to transform this image."),
|
|
17
17
|
removeBackground: z.boolean().optional().describe("Remove the background server-side and return an alpha PNG for sprites, icons, or isolated objects."),
|
|
18
|
+
aspectRatio: z
|
|
19
|
+
.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "5:4", "4:5", "21:9"])
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Aspect ratio of the output (default 1:1). The result reports the real width and height."),
|
|
22
|
+
width: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.min(64)
|
|
26
|
+
.max(4096)
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Smallest output width in pixels; pass with height. 1920 with height 1080 for store key art, 1080 with 1920 for a tall cover."),
|
|
29
|
+
height: z
|
|
30
|
+
.number()
|
|
31
|
+
.int()
|
|
32
|
+
.min(64)
|
|
33
|
+
.max(4096)
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Smallest output height in pixels; pass with width."),
|
|
18
36
|
options: z
|
|
19
37
|
.record(z.any())
|
|
20
38
|
.optional()
|
|
21
|
-
.describe("
|
|
39
|
+
.describe("Extra model params (seed, negative_prompt, ...). The result lists any it did not use in ignoredOptions."),
|
|
22
40
|
});
|
|
@@ -32,7 +32,7 @@ export interface Store {
|
|
|
32
32
|
key?: string;
|
|
33
33
|
}): Promise<T>;
|
|
34
34
|
}
|
|
35
|
-
export declare function storeError(status: number, code: string, message: string | undefined): BuildToolError;
|
|
35
|
+
export declare function storeError(status: number, code: string, message: string | undefined, requestId?: string): BuildToolError;
|
|
36
36
|
export declare function createStore(baseUrl: string, token: string, deps: PublishBuildDependencies): Store;
|
|
37
37
|
/** Whole-file and per-part SHA-256 ("sha256:<hex>") in one pass. */
|
|
38
38
|
export declare function hashParts(path: string, partSize: number): Promise<{
|
|
@@ -57,6 +57,19 @@ export declare function uploadParts(sign: (partNumbers: number[]) => Promise<Gra
|
|
|
57
57
|
partCount: number;
|
|
58
58
|
}, deps: PublishBuildDependencies, parts?: number[]): Promise<void>;
|
|
59
59
|
export declare function normalizeClientVersion(value?: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Before the person confirms, prove the target is real: the game exists for
|
|
62
|
+
* this account, and its store page lists the platforms the export targets.
|
|
63
|
+
*/
|
|
64
|
+
export declare function checkStoreGame(gameId: string, exportTargets: string[], deps: PublishBuildDependencies): Promise<{
|
|
65
|
+
game: {
|
|
66
|
+
gameId: string;
|
|
67
|
+
name: string | null;
|
|
68
|
+
status: string | null;
|
|
69
|
+
targets: string[];
|
|
70
|
+
};
|
|
71
|
+
warnings: string[];
|
|
72
|
+
}>;
|
|
60
73
|
export type PublishBuildResult = Record<string, unknown> & {
|
|
61
74
|
ok: true;
|
|
62
75
|
status: string;
|
|
@@ -61,9 +61,11 @@ export const defaultDependencies = {
|
|
|
61
61
|
now: Date.now,
|
|
62
62
|
token: () => getStoreAccessToken(),
|
|
63
63
|
};
|
|
64
|
-
export function storeError(status, code, message) {
|
|
64
|
+
export function storeError(status, code, message, requestId) {
|
|
65
65
|
if (status === 401) {
|
|
66
|
-
|
|
66
|
+
// The token was sent and refused: signing in again gives the same token
|
|
67
|
+
// shape and the same answer, so it is not the first thing to try.
|
|
68
|
+
return new BuildToolError("store_auth_refused", `The Summer store did not accept this sign-in${message ? `: ${message.replace(/\.$/, "")}` : ""}.`, 'Recovery: run "summer doctor" (Store line) to see the store\'s answer. Signing in again does not change a refusal of a valid sign-in. Until the store accepts agent sign-in for this account, upload the .zip in Studio (Store > your game > Exports).', status, requestId ? { requestId } : undefined);
|
|
67
69
|
}
|
|
68
70
|
if (status === 403 && code === "owner_approval_required") {
|
|
69
71
|
return new BuildToolError("owner_approval_required", message || "Only the game's owner can do this.", "Recovery: nothing went live. Submit with summer_store_submit and give the owner the approval link it returns; a live game's new download is finished by the owner in Studio.", status);
|
|
@@ -104,7 +106,8 @@ export function createStore(baseUrl, token, deps) {
|
|
|
104
106
|
const body = (json && typeof json === "object" ? json : {});
|
|
105
107
|
if (!response.ok) {
|
|
106
108
|
const error = body.error && typeof body.error === "object" ? body.error : {};
|
|
107
|
-
|
|
109
|
+
const requestId = [error.requestId, body.requestId, response.headers.get("x-request-id")].find((value) => typeof value === "string" && value);
|
|
110
|
+
throw storeError(response.status, typeof error.code === "string" ? error.code : `http_${response.status}`, typeof error.message === "string" ? error.message : undefined, requestId);
|
|
108
111
|
}
|
|
109
112
|
return body;
|
|
110
113
|
},
|
|
@@ -260,18 +263,68 @@ function targetPlatforms(bundle) {
|
|
|
260
263
|
// The Build declares exactly what the bundle declares (declaration_mismatch otherwise).
|
|
261
264
|
return bundle.targetPlatforms;
|
|
262
265
|
}
|
|
263
|
-
/**
|
|
264
|
-
async function
|
|
265
|
-
let games;
|
|
266
|
+
/** The signed-in person's store games, or why the store did not list them. */
|
|
267
|
+
async function listGames(deps) {
|
|
266
268
|
try {
|
|
267
|
-
const store = createStore(await resolveGatewayUrl(), await deps
|
|
269
|
+
const store = createStore(await resolveGatewayUrl(), await storeToken(deps), deps);
|
|
268
270
|
const page = await store.call("GET", "games");
|
|
269
|
-
games
|
|
271
|
+
return { games: (page.items ?? []).map((game) => ({ id: game.id, name: game.name, slug: game.slug, status: game.status, targets: game.supportedPlatforms ?? [] })) };
|
|
270
272
|
}
|
|
271
|
-
catch {
|
|
272
|
-
|
|
273
|
+
catch (error) {
|
|
274
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/** gameId is required: the error carries the person's games, or why the store did not list them. */
|
|
278
|
+
async function listGamesOrThrow(deps) {
|
|
279
|
+
const listed = await listGames(deps);
|
|
280
|
+
if ("games" in listed) {
|
|
281
|
+
throw new BuildToolError("game_required", listed.games.length
|
|
282
|
+
? "gameId is required: pick one of your store games below (the export is uploaded to one existing game)."
|
|
283
|
+
: "gameId is required, and this account has no store game yet.", listed.games.length
|
|
284
|
+
? "Recovery: call again with gameId from the list."
|
|
285
|
+
: "Recovery: create the store game first (summer_store_create_game), then call again with its gameId.", undefined, { games: listed.games });
|
|
286
|
+
}
|
|
287
|
+
throw new BuildToolError("game_required", `gameId is required, and the store did not list your games: ${listed.error}`, "Recovery: copy the game id from Studio (the store page URL) and retry.", undefined, { listError: listed.error });
|
|
288
|
+
}
|
|
289
|
+
async function storeToken(deps) {
|
|
290
|
+
try {
|
|
291
|
+
return await deps.token();
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error instanceof OAuthError)
|
|
295
|
+
throw new BuildToolError(error.code, error.message.replace(` ${error.recovery}`, ""), error.recovery);
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Before the person confirms, prove the target is real: the game exists for
|
|
301
|
+
* this account, and its store page lists the platforms the export targets.
|
|
302
|
+
*/
|
|
303
|
+
export async function checkStoreGame(gameId, exportTargets, deps) {
|
|
304
|
+
const store = createStore(await resolveGatewayUrl(), await storeToken(deps), deps);
|
|
305
|
+
let game;
|
|
306
|
+
try {
|
|
307
|
+
game = await store.call("GET", `games/${seg(gameId)}`);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (error instanceof BuildToolError && error.status === 404) {
|
|
311
|
+
const listed = await listGames(deps);
|
|
312
|
+
throw new BuildToolError("game_not_found", "games" in listed
|
|
313
|
+
? `There is no store game "${gameId}" for this account, so nothing can be uploaded to it.`
|
|
314
|
+
: `The store did not find game "${gameId}", and did not list your games either: ${listed.error}`, "games" in listed && listed.games.length
|
|
315
|
+
? "Recovery: call again with gameId from the list."
|
|
316
|
+
: "Recovery: create the store game first (summer_store_create_game), or copy its id from Studio (the store page URL).", 404, "games" in listed ? { games: listed.games } : undefined);
|
|
317
|
+
}
|
|
318
|
+
throw error;
|
|
273
319
|
}
|
|
274
|
-
|
|
320
|
+
const targets = Array.isArray(game.supportedPlatforms) ? game.supportedPlatforms.filter((value) => typeof value === "string") : [];
|
|
321
|
+
// The store page speaks catalog names (ios, android, macos, windows, linux, web) or store ids (macos-universal, ...).
|
|
322
|
+
const catalog = new Set(targets.map((target) => target.replace(/-(universal|x64)$/, "")));
|
|
323
|
+
const unlisted = exportTargets.filter((target) => !catalog.has(target));
|
|
324
|
+
const warnings = unlisted.length
|
|
325
|
+
? [`The store page of "${game.name ?? gameId}" does not list ${unlisted.join(", ")} (it lists ${targets.join(", ") || "no platforms"}). Players on ${unlisted.join(", ")} will not see this build until the store page lists them: add them with summer_store_update_listing.`]
|
|
326
|
+
: [];
|
|
327
|
+
return { game: { gameId, name: typeof game.name === "string" ? game.name : null, status: typeof game.status === "string" ? game.status : null, targets }, warnings };
|
|
275
328
|
}
|
|
276
329
|
export async function publishBuild(input, overrides = {}) {
|
|
277
330
|
const deps = { ...defaultDependencies, ...overrides };
|
|
@@ -288,7 +341,11 @@ export async function publishBuild(input, overrides = {}) {
|
|
|
288
341
|
const clientVersion = normalizeClientVersion(input.clientVersion);
|
|
289
342
|
if (!gameId)
|
|
290
343
|
return listGamesOrThrow(deps);
|
|
291
|
-
|
|
344
|
+
const download = { gameId, file, platform, clientVersion, confirm: input.confirm, waitSeconds: input.waitSeconds, face: input.face };
|
|
345
|
+
if (input.confirm === true)
|
|
346
|
+
return publishDownload(download, deps);
|
|
347
|
+
const preview = await publishDownload(download, deps);
|
|
348
|
+
return { ...preview, game: (await checkStoreGame(gameId, [], deps)).game };
|
|
292
349
|
}
|
|
293
350
|
if (extname(file).toLowerCase() !== ".zip") {
|
|
294
351
|
throw new BuildToolError("export_format_unsupported", `${file} is not a summer.games .zip export.`, "Recovery: export with summer_export_game and upload that .zip.");
|
|
@@ -313,23 +370,18 @@ export async function publishBuild(input, overrides = {}) {
|
|
|
313
370
|
if (!gameId)
|
|
314
371
|
return listGamesOrThrow(deps);
|
|
315
372
|
if (input.confirm !== true) {
|
|
373
|
+
const checked = await checkStoreGame(gameId, platforms, deps);
|
|
316
374
|
return {
|
|
317
375
|
ok: true,
|
|
318
376
|
status: "confirmation_required",
|
|
319
377
|
target,
|
|
320
|
-
|
|
378
|
+
game: checked.game,
|
|
379
|
+
...(checked.warnings.length ? { warnings: checked.warnings } : {}),
|
|
380
|
+
next: `Show the user this exact game (name and id), file, digest, size, version and publish choice. Only after they approve, call again with confirm=true. Nothing was uploaded.`,
|
|
321
381
|
};
|
|
322
382
|
}
|
|
323
383
|
const audit = { at: new Date(deps.now()).toISOString(), operation: "publish_build", face: input.face, ...target };
|
|
324
|
-
|
|
325
|
-
try {
|
|
326
|
-
token = await deps.token();
|
|
327
|
-
}
|
|
328
|
-
catch (error) {
|
|
329
|
-
if (error instanceof OAuthError)
|
|
330
|
-
throw new BuildToolError(error.code, error.message.replace(` ${error.recovery}`, ""), error.recovery);
|
|
331
|
-
throw error;
|
|
332
|
-
}
|
|
384
|
+
const token = await storeToken(deps);
|
|
333
385
|
const store = createStore(await resolveGatewayUrl(), token, deps);
|
|
334
386
|
await appendStoreJsonLine("creator-audit.jsonl", { ...audit, outcome: "started" });
|
|
335
387
|
// Same keys Studio derives: the same bytes, version and scene are one declaration.
|
|
@@ -49,6 +49,7 @@ import { shapeEngineLogResponse } from "../log-filters.js";
|
|
|
49
49
|
import { listCreatorReleases, publishCreator, } from "./creator.js";
|
|
50
50
|
import { exportGame } from "./export-game.js";
|
|
51
51
|
import { exportTemplates } from "./export-templates.js";
|
|
52
|
+
import { captureGameplay } from "./capture-gameplay.js";
|
|
52
53
|
import { publishBuild } from "./publish-build.js";
|
|
53
54
|
import { CONFIG_KEYS, getConfigValue, isConfigKey, readSummerConfig, resolveGatewayUrl, setConfigValue, unsetConfigValue, } from "../config.js";
|
|
54
55
|
import { createDebugReportArtifact } from "./debug-report.js";
|
|
@@ -630,6 +631,15 @@ export const TOOL_DISPATCH = [
|
|
|
630
631
|
summerVersion: optStr(args, "summerVersion"),
|
|
631
632
|
});
|
|
632
633
|
}),
|
|
634
|
+
entry("summer_capture_gameplay", "Capture real gameplay frames offscreen, without a running editor", false, (args) => captureGameplay({
|
|
635
|
+
project: optStr(args, "project"),
|
|
636
|
+
scene: optStr(args, "scene"),
|
|
637
|
+
resolution: optStr(args, "resolution"),
|
|
638
|
+
frames: optNumberOrUndefined(args, "frames"),
|
|
639
|
+
waitSeconds: optNumberOrUndefined(args, "waitSeconds"),
|
|
640
|
+
intervalSeconds: optNumberOrUndefined(args, "intervalSeconds"),
|
|
641
|
+
out: optStr(args, "out"),
|
|
642
|
+
})),
|
|
633
643
|
entry("summer_publish_build", "Upload a summer.games export to your store game (confirm-gated)", false, (args) => publishBuild({
|
|
634
644
|
gameId: optStr(args, "gameId"),
|
|
635
645
|
file: optStr(args, "file"),
|
package/dist/core/config.js
CHANGED
|
@@ -30,7 +30,7 @@ function validateGatewayUrl(value) {
|
|
|
30
30
|
}
|
|
31
31
|
const local = parsed.hostname === "localhost" ||
|
|
32
32
|
parsed.hostname === "127.0.0.1" ||
|
|
33
|
-
parsed.hostname === "::1";
|
|
33
|
+
parsed.hostname === "[::1]";
|
|
34
34
|
if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:")) {
|
|
35
35
|
throw new Error("gateway.url must use HTTPS (HTTP is allowed only for localhost). Recovery: set an HTTPS URL and retry.");
|
|
36
36
|
}
|
|
@@ -49,7 +49,7 @@ function validateCreatorApiUrl(value) {
|
|
|
49
49
|
}
|
|
50
50
|
const local = parsed.hostname === "localhost" ||
|
|
51
51
|
parsed.hostname === "127.0.0.1" ||
|
|
52
|
-
parsed.hostname === "::1";
|
|
52
|
+
parsed.hostname === "[::1]";
|
|
53
53
|
if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:")) {
|
|
54
54
|
throw new Error("creator.apiUrl must use HTTPS (HTTP is allowed only for localhost). Recovery: set an HTTPS URL and retry.");
|
|
55
55
|
}
|
|
@@ -58,6 +58,22 @@ export declare function linuxEngineInstallDir(): string;
|
|
|
58
58
|
export declare function macEnginePaths(env?: NodeJS.ProcessEnv): string[];
|
|
59
59
|
export declare function windowsEnginePaths(env?: NodeJS.ProcessEnv): string[];
|
|
60
60
|
export declare function linuxEnginePaths(): string[];
|
|
61
|
+
/** Where `summer install` records the engine it installed last
|
|
62
|
+
* (~/.summer/engine-install.json), so an install with --path is found later
|
|
63
|
+
* without SUMMER_BIN. */
|
|
64
|
+
export declare const ENGINE_INSTALL_FILE = "engine-install.json";
|
|
65
|
+
export interface RecordedEngineInstall {
|
|
66
|
+
schemaVersion: 1;
|
|
67
|
+
/** The engine executable (on macOS the one inside Summer.app). */
|
|
68
|
+
binary: string;
|
|
69
|
+
version: string | null;
|
|
70
|
+
installedAt: string;
|
|
71
|
+
}
|
|
72
|
+
export declare function recordEngineInstall(binary: string, version: string | null): Promise<void>;
|
|
73
|
+
/** The engine `summer install` installed last, or null (sync: findEngineBinary is). */
|
|
74
|
+
export declare function recordedEngineBinary(): string | null;
|
|
75
|
+
/** The bundle summer install writes on macOS: <path>/Summer.app, or the path itself when it names a .app. */
|
|
76
|
+
export declare function macInstallDestination(customPath?: string): string;
|
|
61
77
|
export declare function engineBinaryCandidates(os?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string[];
|
|
62
78
|
/** First existing engine binary for this machine, or null. SUMMER_BIN /
|
|
63
79
|
* SUMMER_ENGINE_BINARY wins when set and present; a set-but-missing override
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, statSync } from "fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
2
2
|
import { homedir, platform } from "os";
|
|
3
3
|
import { basename, join } from "path";
|
|
4
|
+
import { getSummerDir, writeStoreJson } from "./store.js";
|
|
4
5
|
/**
|
|
5
6
|
* Where each platform's engine binary lives, shared by `summer install`,
|
|
6
7
|
* `summer run`, and `summer doctor` so they can never disagree about what
|
|
@@ -95,6 +96,31 @@ export function windowsEnginePaths(env = process.env) {
|
|
|
95
96
|
export function linuxEnginePaths() {
|
|
96
97
|
return [join(linuxEngineInstallDir(), LINUX_ENGINE_BINARY_NAME)];
|
|
97
98
|
}
|
|
99
|
+
/** Where `summer install` records the engine it installed last
|
|
100
|
+
* (~/.summer/engine-install.json), so an install with --path is found later
|
|
101
|
+
* without SUMMER_BIN. */
|
|
102
|
+
export const ENGINE_INSTALL_FILE = "engine-install.json";
|
|
103
|
+
export async function recordEngineInstall(binary, version) {
|
|
104
|
+
const record = { schemaVersion: 1, binary, version, installedAt: new Date().toISOString() };
|
|
105
|
+
await writeStoreJson(ENGINE_INSTALL_FILE, record);
|
|
106
|
+
}
|
|
107
|
+
/** The engine `summer install` installed last, or null (sync: findEngineBinary is). */
|
|
108
|
+
export function recordedEngineBinary() {
|
|
109
|
+
try {
|
|
110
|
+
const record = JSON.parse(readFileSync(join(getSummerDir(), ENGINE_INSTALL_FILE), "utf8"));
|
|
111
|
+
return record.schemaVersion === 1 && typeof record.binary === "string" && record.binary ? record.binary : null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** The bundle summer install writes on macOS: <path>/Summer.app, or the path itself when it names a .app. */
|
|
118
|
+
export function macInstallDestination(customPath) {
|
|
119
|
+
if (!customPath?.trim())
|
|
120
|
+
return "/Applications/Summer.app";
|
|
121
|
+
const trimmed = customPath.trim().replace(/\/+$/, "");
|
|
122
|
+
return /\.app$/i.test(trimmed) ? trimmed : join(trimmed, "Summer.app");
|
|
123
|
+
}
|
|
98
124
|
export function engineBinaryCandidates(os = platform(), env = process.env) {
|
|
99
125
|
const override = engineBinaryOverride(env)?.path;
|
|
100
126
|
const platformPaths = os === "darwin"
|
|
@@ -102,7 +128,11 @@ export function engineBinaryCandidates(os = platform(), env = process.env) {
|
|
|
102
128
|
: os === "linux"
|
|
103
129
|
? linuxEnginePaths()
|
|
104
130
|
: windowsEnginePaths(env);
|
|
105
|
-
|
|
131
|
+
// The engine summer install put in place last wins over the default
|
|
132
|
+
// locations (an install with --path next to an older /Applications copy).
|
|
133
|
+
const recorded = recordedEngineBinary();
|
|
134
|
+
const paths = recorded ? [recorded, ...platformPaths.filter((path) => path !== recorded)] : platformPaths;
|
|
135
|
+
return override ? [override, ...paths] : paths;
|
|
106
136
|
}
|
|
107
137
|
/** First existing engine binary for this machine, or null. SUMMER_BIN /
|
|
108
138
|
* SUMMER_ENGINE_BINARY wins when set and present; a set-but-missing override
|