summer-engine 3.2.1 → 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 +61 -0
- package/README.md +7 -4
- 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/login.js +12 -0
- package/dist/cli/commands/publish.js +2 -1
- package/dist/cli/commands/tool.d.ts +7 -0
- package/dist/cli/commands/tool.js +36 -1
- package/dist/core/auth.js +3 -0
- package/dist/core/capabilities/audit/args.d.ts +14 -14
- 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 +12 -0
- package/dist/core/capabilities/engine-run.js +61 -0
- 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 +107 -0
- package/dist/core/capabilities/export-game.js +445 -0
- package/dist/core/capabilities/export-presets.d.ts +50 -0
- package/dist/core/capabilities/export-presets.js +177 -0
- package/dist/core/capabilities/export-templates.d.ts +118 -0
- package/dist/core/capabilities/export-templates.js +291 -0
- 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 +77 -0
- package/dist/core/capabilities/publish-build.js +500 -0
- package/dist/core/capabilities/publish-download.d.ts +37 -0
- package/dist/core/capabilities/publish-download.js +168 -0
- package/dist/core/capabilities/runtime-control.d.ts +10 -10
- package/dist/core/capabilities/summer-bundle.d.ts +49 -0
- package/dist/core/capabilities/summer-bundle.js +156 -0
- package/dist/core/capabilities/tool-dispatch.js +43 -1
- package/dist/core/capabilities/ui-control.d.ts +2 -2
- 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/core/library-read.d.ts +2 -2
- package/dist/core/oauth.d.ts +78 -0
- package/dist/core/oauth.js +309 -0
- package/dist/core/util/zip-write.d.ts +18 -0
- package/dist/core/util/zip-write.js +112 -0
- package/dist/core/util/zip.d.ts +22 -0
- package/dist/core/util/zip.js +140 -0
- package/dist/mcp/hosted-mount.d.ts +50 -0
- package/dist/mcp/hosted-mount.js +200 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.js +14 -6
- package/dist/mcp/tools/creator-tools.js +64 -1
- 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 +7 -3
- 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 +4 -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/multiplayer-publish/SKILL.md +5 -1
- package/library/skills/navigate-summer/SKILL.md +1 -1
- package/library/skills/web-compatibility-look/resource.yaml +1 -0
- package/library/tools/capture-gameplay/resource.yaml +73 -0
- package/library/tools/creator-publish/resource.yaml +4 -3
- package/library/tools/creator-releases/resource.yaml +1 -1
- package/library/tools/export-game/resource.yaml +72 -0
- package/library/tools/export-templates/resource.yaml +63 -0
- package/library/tools/generate-image/resource.yaml +15 -1
- package/library/tools/open/resource.yaml +1 -1
- package/library/tools/publish-build/resource.yaml +78 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +4 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +241 -19
- 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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { runEngine, type EngineRun } from "./engine-run.js";
|
|
2
|
+
import { SUMMER_GAMES_PRESET, type EnsuredPreset, type StoreTarget } from "./export-presets.js";
|
|
3
|
+
import { type TemplatesDependencies } from "./export-templates.js";
|
|
4
|
+
import { type SummerBundle } from "./summer-bundle.js";
|
|
5
|
+
export { runEngine, SUMMER_GAMES_PRESET, type EngineRun };
|
|
6
|
+
/**
|
|
7
|
+
* summer_export_game: run the installed Summer Engine headless and return the
|
|
8
|
+
* file the Summer Games store takes for the chosen targets.
|
|
9
|
+
*
|
|
10
|
+
* <engine> --headless --summer-no-api --path <project> --export-release "<preset>" <out>
|
|
11
|
+
*
|
|
12
|
+
* format "bundle" (default): the summer.bundle.v1 .zip for build-publications
|
|
13
|
+
* (summer_publish_build). Summer runs its client.pck on its own templates in
|
|
14
|
+
* the Summer Games apps, so no template and no signing. Without targets the
|
|
15
|
+
* engine's in-memory "summer.games" preset is used as is (every platform
|
|
16
|
+
* summer.build.json declares); with targets a named summer.games preset ticks
|
|
17
|
+
* exactly those (export-presets.ts).
|
|
18
|
+
*
|
|
19
|
+
* format "download" (one target: web, macos or windows): what the store's
|
|
20
|
+
* versions intake takes. web is a static HTML5 .zip (index.html at the root)
|
|
21
|
+
* built on Summer's WebGPU/JSPI Forward+ template; macos is the .app zipped
|
|
22
|
+
* (macos-universal, ad hoc signed, as the store asks for no signing); windows
|
|
23
|
+
* is the .exe with its pack embedded, zipped (windows-x64). These need the
|
|
24
|
+
* matching export template (summer_export_templates).
|
|
25
|
+
*
|
|
26
|
+
* --headless opens no window.
|
|
27
|
+
*/
|
|
28
|
+
export declare const LAST_EXPORT_FILE = "last-export.json";
|
|
29
|
+
export type ExportFormat = "bundle" | "download";
|
|
30
|
+
export interface ExportGameInput {
|
|
31
|
+
/** Project root (has project.godot). Defaults to the MCP's --project, then the working directory. */
|
|
32
|
+
project?: string;
|
|
33
|
+
/** Output .zip path. Defaults to <project>/.summer/exports/<name>-<time>.zip. */
|
|
34
|
+
out?: string;
|
|
35
|
+
/** Export with debug enabled (--export-debug). */
|
|
36
|
+
debug?: boolean;
|
|
37
|
+
/** Export preset name; the engine's own is "summer.games". Bundle format without targets only. */
|
|
38
|
+
preset?: string;
|
|
39
|
+
/** Store platforms: macos, windows, ios, android, web. */
|
|
40
|
+
targets?: string[];
|
|
41
|
+
/** "bundle" (store build for the Summer apps) or "download" (web build, native download). Default: bundle; web is always download. */
|
|
42
|
+
format?: string;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface ExportGameResult {
|
|
46
|
+
ok: true;
|
|
47
|
+
format: ExportFormat;
|
|
48
|
+
path: string;
|
|
49
|
+
sha256: string;
|
|
50
|
+
sizeBytes: number;
|
|
51
|
+
project: string;
|
|
52
|
+
preset: string;
|
|
53
|
+
/** The targets asked for, when any were. */
|
|
54
|
+
targets?: StoreTarget[];
|
|
55
|
+
/** What happened to export_presets.cfg for the preset this export used. */
|
|
56
|
+
presetChange?: EnsuredPreset["change"];
|
|
57
|
+
debug: boolean;
|
|
58
|
+
engine: string;
|
|
59
|
+
durationMs: number;
|
|
60
|
+
/** Bundle format: the bundle manifest. */
|
|
61
|
+
bundle?: Omit<SummerBundle, "hostedBuild" | "files"> & {
|
|
62
|
+
fileCount: number;
|
|
63
|
+
};
|
|
64
|
+
/** Download format: the store versions platform id (web, macos-universal, windows-x64). */
|
|
65
|
+
storePlatform?: string;
|
|
66
|
+
/** Download format: files in the zip. */
|
|
67
|
+
fileCount?: number;
|
|
68
|
+
/** Download format: the project icon the export used, or null. */
|
|
69
|
+
icon?: string | null;
|
|
70
|
+
/** Download format: what signing the file carries. */
|
|
71
|
+
signing?: string;
|
|
72
|
+
/** Files in the project folder the export created, changed or removed (export_presets.cfg, project.godot, ...). */
|
|
73
|
+
projectChanges?: ProjectChange[];
|
|
74
|
+
/** Things the store will still refuse or warn about, in plain words. */
|
|
75
|
+
warnings?: string[];
|
|
76
|
+
next: string;
|
|
77
|
+
}
|
|
78
|
+
export interface ProjectChange {
|
|
79
|
+
file: string;
|
|
80
|
+
change: "created" | "modified" | "deleted";
|
|
81
|
+
}
|
|
82
|
+
export interface LastExport {
|
|
83
|
+
path: string;
|
|
84
|
+
project: string;
|
|
85
|
+
sha256: string;
|
|
86
|
+
sizeBytes: number;
|
|
87
|
+
exportedAt: string;
|
|
88
|
+
/** Download exports: the store versions platform the file is for (summer_publish_build reads it). */
|
|
89
|
+
storePlatform?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface ExportGameDependencies {
|
|
92
|
+
findBinary: () => string | null;
|
|
93
|
+
/** Installed Summer version of the binary (Info.plist, sq.version), or null. */
|
|
94
|
+
engineVersion: (binary: string) => string | null;
|
|
95
|
+
run: (binary: string, args: string[], timeoutMs: number) => Promise<EngineRun>;
|
|
96
|
+
now: () => Date;
|
|
97
|
+
/** Template lookup for download exports (overridable in tests). */
|
|
98
|
+
templates?: Partial<TemplatesDependencies>;
|
|
99
|
+
}
|
|
100
|
+
export declare function resolveExportProject(project?: string): string;
|
|
101
|
+
/** One value from project.godot ([section] key=value), unquoted. */
|
|
102
|
+
export declare function projectSetting(project: string, section: string, key: string): string | null;
|
|
103
|
+
/** Lowercase ascii slug for file names and the macOS bundle id. */
|
|
104
|
+
export declare function slugify(name: string): string;
|
|
105
|
+
export declare function exportGame(input: ExportGameInput, overrides?: Partial<ExportGameDependencies>): Promise<ExportGameResult>;
|
|
106
|
+
/** The file the last summer_export_game wrote (bundle or download), when it is still on disk. */
|
|
107
|
+
export declare function readLastExport(): Promise<LastExport | null>;
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { platform } from "node:os";
|
|
5
|
+
import { engineSelectionFromEnv } from "../engine.js";
|
|
6
|
+
import { findEngineBinary } from "../engine-install.js";
|
|
7
|
+
import { readStoreJson, writeStoreJson } from "../store.js";
|
|
8
|
+
import { writeZip } from "../util/zip-write.js";
|
|
9
|
+
import { engineErrorLines, runEngine } from "./engine-run.js";
|
|
10
|
+
import { assertExportEngine, installedEngineVersion } from "./export-engine-version.js";
|
|
11
|
+
import { BUNDLE_TARGETS, DOWNLOAD_TARGETS, SUMMER_GAMES_PRESET, bundlePresetSpec, ensurePreset, normalizeTargets, variantString, } from "./export-presets.js";
|
|
12
|
+
import { engineTemplateInfo, findInstalledTemplate, templateFileFor } from "./export-templates.js";
|
|
13
|
+
import { BuildToolError, hashFile, readSummerBundle } from "./summer-bundle.js";
|
|
14
|
+
export { runEngine, SUMMER_GAMES_PRESET };
|
|
15
|
+
/**
|
|
16
|
+
* summer_export_game: run the installed Summer Engine headless and return the
|
|
17
|
+
* file the Summer Games store takes for the chosen targets.
|
|
18
|
+
*
|
|
19
|
+
* <engine> --headless --summer-no-api --path <project> --export-release "<preset>" <out>
|
|
20
|
+
*
|
|
21
|
+
* format "bundle" (default): the summer.bundle.v1 .zip for build-publications
|
|
22
|
+
* (summer_publish_build). Summer runs its client.pck on its own templates in
|
|
23
|
+
* the Summer Games apps, so no template and no signing. Without targets the
|
|
24
|
+
* engine's in-memory "summer.games" preset is used as is (every platform
|
|
25
|
+
* summer.build.json declares); with targets a named summer.games preset ticks
|
|
26
|
+
* exactly those (export-presets.ts).
|
|
27
|
+
*
|
|
28
|
+
* format "download" (one target: web, macos or windows): what the store's
|
|
29
|
+
* versions intake takes. web is a static HTML5 .zip (index.html at the root)
|
|
30
|
+
* built on Summer's WebGPU/JSPI Forward+ template; macos is the .app zipped
|
|
31
|
+
* (macos-universal, ad hoc signed, as the store asks for no signing); windows
|
|
32
|
+
* is the .exe with its pack embedded, zipped (windows-x64). These need the
|
|
33
|
+
* matching export template (summer_export_templates).
|
|
34
|
+
*
|
|
35
|
+
* --headless opens no window.
|
|
36
|
+
*/
|
|
37
|
+
export const LAST_EXPORT_FILE = "last-export.json";
|
|
38
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60_000;
|
|
39
|
+
/** Store platform ids of the versions intake (summer-platform creatorstore). */
|
|
40
|
+
const STORE_VERSION_PLATFORM = {
|
|
41
|
+
web: "web",
|
|
42
|
+
macos: "macos-universal",
|
|
43
|
+
windows: "windows-x64",
|
|
44
|
+
};
|
|
45
|
+
/** Web zip limits of the store (summer-platform internal/creatorstore/webzip.go). */
|
|
46
|
+
const WEB_LIMITS = { zipBytes: 500 * 1024 * 1024, fileBytes: 200 * 1024 * 1024, files: 2000 };
|
|
47
|
+
/** Games without a server run on the phone apps only (summer-platform gameplatforms.StandaloneTargets). */
|
|
48
|
+
const STANDALONE_TARGETS = ["ios", "android"];
|
|
49
|
+
/** Engine support per bundle target, for the recovery text when a target is missing. */
|
|
50
|
+
const TARGET_ENGINE_NOTE = {
|
|
51
|
+
android: "Android in a summer.games bundle needs a Summer Engine newer than 0.7.0.",
|
|
52
|
+
};
|
|
53
|
+
const defaultDependencies = {
|
|
54
|
+
findBinary: () => findEngineBinary(platform()),
|
|
55
|
+
engineVersion: installedEngineVersion,
|
|
56
|
+
run: runEngine,
|
|
57
|
+
now: () => new Date(),
|
|
58
|
+
};
|
|
59
|
+
export function resolveExportProject(project) {
|
|
60
|
+
const root = resolve(project?.trim() || engineSelectionFromEnv()?.projectPath || process.cwd());
|
|
61
|
+
if (!existsSync(join(root, "project.godot"))) {
|
|
62
|
+
throw new BuildToolError("project_not_found", `${root} is not a Summer project (no project.godot).`, "Recovery: pass project with the folder that holds project.godot.");
|
|
63
|
+
}
|
|
64
|
+
return root;
|
|
65
|
+
}
|
|
66
|
+
function stamp(date) {
|
|
67
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").replace("T", "-");
|
|
68
|
+
}
|
|
69
|
+
/** <project>/.summer/exports/, ignored by the editor and by git. */
|
|
70
|
+
async function defaultExportDir(project) {
|
|
71
|
+
const dir = join(project, ".summer", "exports");
|
|
72
|
+
await mkdir(dir, { recursive: true });
|
|
73
|
+
for (const [name, body] of [
|
|
74
|
+
[".gdignore", ""],
|
|
75
|
+
[".gitignore", "*\n"],
|
|
76
|
+
]) {
|
|
77
|
+
const file = join(dir, name);
|
|
78
|
+
if (!existsSync(file))
|
|
79
|
+
await writeFile(file, body);
|
|
80
|
+
}
|
|
81
|
+
return dir;
|
|
82
|
+
}
|
|
83
|
+
function outputTail(output) {
|
|
84
|
+
return output.trim().split("\n").slice(-40).join("\n");
|
|
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
|
+
}
|
|
149
|
+
/** One value from project.godot ([section] key=value), unquoted. */
|
|
150
|
+
export function projectSetting(project, section, key) {
|
|
151
|
+
let current = "";
|
|
152
|
+
for (const line of readFileSync(join(project, "project.godot"), "utf8").split(/\r?\n/)) {
|
|
153
|
+
const header = /^\[([^\]]+)\]\s*$/.exec(line);
|
|
154
|
+
if (header) {
|
|
155
|
+
current = header[1];
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (current !== section || !line.startsWith(`${key}=`))
|
|
159
|
+
continue;
|
|
160
|
+
const value = line.slice(key.length + 1).trim();
|
|
161
|
+
const quoted = /^"((?:[^"\\]|\\.)*)"$/.exec(value);
|
|
162
|
+
return quoted ? quoted[1].replace(/\\(["\\])/g, "$1") : value;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/** Lowercase ascii slug for file names and the macOS bundle id. */
|
|
167
|
+
export function slugify(name) {
|
|
168
|
+
return (name
|
|
169
|
+
.normalize("NFKD")
|
|
170
|
+
.replace(/[^\x00-\x7f]/g, "")
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
173
|
+
.replace(/^-+|-+$/g, "")
|
|
174
|
+
.slice(0, 60) || "game");
|
|
175
|
+
}
|
|
176
|
+
function resolveFormat(input, targets) {
|
|
177
|
+
const asked = input.format?.trim();
|
|
178
|
+
if (asked && asked !== "bundle" && asked !== "download") {
|
|
179
|
+
throw new BuildToolError("export_args_invalid", `Unknown format ${asked}.`, 'Recovery: use "bundle" or "download".');
|
|
180
|
+
}
|
|
181
|
+
if (asked === "download" || (!asked && targets?.includes("web"))) {
|
|
182
|
+
if (!targets || targets.length !== 1 || !DOWNLOAD_TARGETS.includes(targets[0])) {
|
|
183
|
+
throw new BuildToolError("export_args_invalid", "A download export makes one file for one target: web, macos or windows.", 'Recovery: pass exactly one of targets ["web"], ["macos"] or ["windows"]; iOS and Android are store builds only (format "bundle").');
|
|
184
|
+
}
|
|
185
|
+
return "download";
|
|
186
|
+
}
|
|
187
|
+
const notBundle = (targets ?? []).filter((target) => !BUNDLE_TARGETS.includes(target));
|
|
188
|
+
if (notBundle.length) {
|
|
189
|
+
throw new BuildToolError("export_args_invalid", `A summer.games bundle has no ${notBundle.join(", ")} client: the store takes web as a separate web build.`, 'Recovery: export web on its own with targets ["web"] (format "download").');
|
|
190
|
+
}
|
|
191
|
+
if (input.preset?.trim() && targets) {
|
|
192
|
+
throw new BuildToolError("export_args_invalid", "Pass targets or preset, not both: targets picks its own summer.games preset.", "Recovery: drop preset, or drop targets.");
|
|
193
|
+
}
|
|
194
|
+
return "bundle";
|
|
195
|
+
}
|
|
196
|
+
/** The rendering method the web build runs: the .web override, else the project's (Godot's default is Forward+). */
|
|
197
|
+
function webRenderer(project) {
|
|
198
|
+
return (projectSetting(project, "rendering", "renderer/rendering_method.web") ??
|
|
199
|
+
projectSetting(project, "rendering", "renderer/rendering_method") ??
|
|
200
|
+
"forward_plus");
|
|
201
|
+
}
|
|
202
|
+
function downloadPresetSpec(target, slug) {
|
|
203
|
+
if (target === "web") {
|
|
204
|
+
return {
|
|
205
|
+
name: "Summer download Web",
|
|
206
|
+
platform: "Web",
|
|
207
|
+
options: {
|
|
208
|
+
// Summer's WebGPU/JSPI template: Forward+ or Mobile in the browser,
|
|
209
|
+
// no threads (so summer.games can frame it), no GDExtension.
|
|
210
|
+
"variant/runtime_profile": variantString("summer-web-jspi-v1"),
|
|
211
|
+
"variant/extensions_support": "false",
|
|
212
|
+
"variant/thread_support": "false",
|
|
213
|
+
"html/export_icon": "true",
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (target === "macos") {
|
|
218
|
+
return {
|
|
219
|
+
name: "Summer download macOS",
|
|
220
|
+
platform: "macOS",
|
|
221
|
+
options: {
|
|
222
|
+
"binary_format/architecture": variantString("universal"),
|
|
223
|
+
"application/bundle_identifier": variantString(`games.summer.${slug}`),
|
|
224
|
+
// Built-in ad hoc signing works from every OS; the store needs no
|
|
225
|
+
// Developer ID or notarization.
|
|
226
|
+
"codesign/codesign": "1",
|
|
227
|
+
"notarization/notarization": "0",
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
name: "Summer download Windows",
|
|
233
|
+
platform: "Windows Desktop",
|
|
234
|
+
options: {
|
|
235
|
+
"binary_format/architecture": variantString("x86_64"),
|
|
236
|
+
// One .exe: the store takes one archive per platform.
|
|
237
|
+
"binary_format/embed_pck": "true",
|
|
238
|
+
"codesign/enable": "false",
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function listFiles(root) {
|
|
243
|
+
const files = [];
|
|
244
|
+
const walk = async (dir) => {
|
|
245
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
246
|
+
const path = join(dir, entry.name);
|
|
247
|
+
if (entry.isDirectory())
|
|
248
|
+
await walk(path);
|
|
249
|
+
else if (entry.isFile())
|
|
250
|
+
files.push(path);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
await walk(root);
|
|
254
|
+
return files.sort();
|
|
255
|
+
}
|
|
256
|
+
async function runExport(deps, binary, project, preset, out, debug, timeoutMs, notes) {
|
|
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];
|
|
259
|
+
let run;
|
|
260
|
+
try {
|
|
261
|
+
run = await deps.run(binary, args, timeoutMs);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
throw new BuildToolError("engine_launch_failed", `Summer Engine could not start: ${error instanceof Error ? error.message : String(error)}.`, 'Recovery: run "summer doctor" and check the engine install.');
|
|
265
|
+
}
|
|
266
|
+
if (run.timedOut) {
|
|
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) });
|
|
268
|
+
}
|
|
269
|
+
if (run.code !== 0 || !existsSync(out)) {
|
|
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) });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export async function exportGame(input, overrides = {}) {
|
|
276
|
+
const deps = { ...defaultDependencies, ...overrides };
|
|
277
|
+
const project = resolveExportProject(input.project);
|
|
278
|
+
const targets = input.targets?.length ? normalizeTargets(input.targets) : undefined;
|
|
279
|
+
const format = resolveFormat(input, targets);
|
|
280
|
+
const debug = input.debug === true;
|
|
281
|
+
const started = deps.now();
|
|
282
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
283
|
+
let out = null;
|
|
284
|
+
if (input.out?.trim()) {
|
|
285
|
+
out = isAbsolute(input.out) ? input.out : resolve(project, input.out);
|
|
286
|
+
if (extname(out).toLowerCase() !== ".zip") {
|
|
287
|
+
throw new BuildToolError("export_path_invalid", `The export path ${out} must end in .zip: the store takes a .zip.`, "Recovery: pass out ending in .zip, or omit it.");
|
|
288
|
+
}
|
|
289
|
+
await mkdir(dirname(out), { recursive: true });
|
|
290
|
+
}
|
|
291
|
+
const binary = deps.findBinary();
|
|
292
|
+
if (!binary) {
|
|
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.');
|
|
294
|
+
}
|
|
295
|
+
// Before any preset is written: an old engine cannot read them.
|
|
296
|
+
assertExportEngine(binary, deps.engineVersion(binary));
|
|
297
|
+
if (format === "download") {
|
|
298
|
+
return exportDownload(deps, binary, project, targets[0], out, debug, timeoutMs, started);
|
|
299
|
+
}
|
|
300
|
+
out ??= join(await defaultExportDir(project), `${basename(project)}-${stamp(started)}.zip`);
|
|
301
|
+
const before = existsSync(out) ? statSync(out).mtimeMs : null;
|
|
302
|
+
const projectBefore = await snapshotProjectFiles(project);
|
|
303
|
+
const ensured = targets ? await ensurePreset(project, bundlePresetSpec(targets)) : undefined;
|
|
304
|
+
const preset = ensured?.name ?? (input.preset?.trim() || SUMMER_GAMES_PRESET);
|
|
305
|
+
const notes = (targets ?? []).flatMap((target) => TARGET_ENGINE_NOTE[target] ?? []);
|
|
306
|
+
await withProjectChanges(project, projectBefore, () => runExport(deps, binary, project, preset, out, debug, timeoutMs, notes));
|
|
307
|
+
const projectChanges = await projectChangesSince(project, projectBefore, out);
|
|
308
|
+
if (before !== null && statSync(out).mtimeMs === before) {
|
|
309
|
+
throw new BuildToolError("export_failed", "Summer Engine exited without writing a new bundle.", "Recovery: read the engine output and export again.");
|
|
310
|
+
}
|
|
311
|
+
const bundle = await readSummerBundle(out);
|
|
312
|
+
const missing = (targets ?? []).filter((target) => !bundle.targetPlatforms.includes(target));
|
|
313
|
+
if (missing.length) {
|
|
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 });
|
|
315
|
+
}
|
|
316
|
+
const warnings = projectChangeWarnings(projectChanges);
|
|
317
|
+
const desktopOnly = bundle.hosted ? [] : bundle.targetPlatforms.filter((target) => !STANDALONE_TARGETS.includes(target));
|
|
318
|
+
if (desktopOnly.length) {
|
|
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(", ")}. ` +
|
|
320
|
+
'Export targets ["ios","android"] for the store build, and macOS or Windows as a download (format "download").');
|
|
321
|
+
}
|
|
322
|
+
const { sha256, sizeBytes } = await hashFile(out);
|
|
323
|
+
const last = { path: out, project, sha256, sizeBytes, exportedAt: deps.now().toISOString() };
|
|
324
|
+
await writeStoreJson(LAST_EXPORT_FILE, last);
|
|
325
|
+
const { hostedBuild: _hostedBuild, files, ...summary } = bundle;
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
format,
|
|
329
|
+
path: out,
|
|
330
|
+
sha256,
|
|
331
|
+
sizeBytes,
|
|
332
|
+
project,
|
|
333
|
+
preset,
|
|
334
|
+
...(targets ? { targets, presetChange: ensured.change } : {}),
|
|
335
|
+
debug,
|
|
336
|
+
engine: binary,
|
|
337
|
+
durationMs: deps.now().getTime() - started.getTime(),
|
|
338
|
+
bundle: { ...summary, fileCount: files.length },
|
|
339
|
+
...(projectChanges.length ? { projectChanges } : {}),
|
|
340
|
+
...(warnings.length ? { warnings } : {}),
|
|
341
|
+
next: "Upload it to the game's store listing with summer_publish_build (gameId, clientVersion).",
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function exportDownload(deps, binary, project, target, requestedOut, debug, timeoutMs, started) {
|
|
345
|
+
const name = projectSetting(project, "application", "config/name") || basename(project);
|
|
346
|
+
const slug = slugify(name);
|
|
347
|
+
const storePlatform = STORE_VERSION_PLATFORM[target];
|
|
348
|
+
if (target === "web") {
|
|
349
|
+
const renderer = webRenderer(project);
|
|
350
|
+
if (renderer !== "forward_plus" && renderer !== "mobile") {
|
|
351
|
+
throw new BuildToolError("web_renderer_unsupported", `The project renders with ${renderer}; Summer's web template runs Forward+ or Mobile only.`, 'Recovery: set Project Settings > Rendering > Renderer > Rendering Method (or its .web override) to "forward_plus", then export again.');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// Name the template the engine will need, before a long export fails on it.
|
|
355
|
+
const templateFile = templateFileFor(target, debug);
|
|
356
|
+
try {
|
|
357
|
+
const info = await engineTemplateInfo({ findBinary: () => binary, run: deps.run, ...deps.templates });
|
|
358
|
+
if (!findInstalledTemplate(info, templateFile)) {
|
|
359
|
+
throw new BuildToolError("export_template_missing", `The ${target} export needs the template ${templateFile}, and Summer Engine has none in ${info.searchDirs.join(" or ")}.`, `Recovery: call summer_export_templates with action "install" and platforms ["${target}"]${debug ? " and includeDebug true" : ""}, then export again.`, undefined, { template: templateFile, folder: info.folder });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
// Only a missing template stops here; if the version probe fails, the
|
|
364
|
+
// engine's own export check still names what is wrong.
|
|
365
|
+
if (error instanceof BuildToolError && error.code === "export_template_missing")
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
const spec = downloadPresetSpec(target, slug);
|
|
369
|
+
const projectBefore = await snapshotProjectFiles(project);
|
|
370
|
+
const ensured = await ensurePreset(project, spec);
|
|
371
|
+
const exportsDir = await defaultExportDir(project);
|
|
372
|
+
const work = join(exportsDir, `${slug}-${target}-${stamp(started)}`);
|
|
373
|
+
await rm(work, { recursive: true, force: true });
|
|
374
|
+
await mkdir(work, { recursive: true });
|
|
375
|
+
const out = requestedOut ?? join(exportsDir, `${slug}-${storePlatform}-${stamp(started)}.zip`);
|
|
376
|
+
const warnings = [];
|
|
377
|
+
let fileCount = 1;
|
|
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.");
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
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;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
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));
|
|
413
|
+
const icon = projectSetting(project, "application", "config/icon");
|
|
414
|
+
if (!icon)
|
|
415
|
+
warnings.push("The project has no icon (Project Settings > Application > Config > Icon); the export uses the Summer default.");
|
|
416
|
+
const { sha256, sizeBytes } = await hashFile(out);
|
|
417
|
+
const last = { path: out, project, sha256, sizeBytes, exportedAt: deps.now().toISOString(), storePlatform };
|
|
418
|
+
await writeStoreJson(LAST_EXPORT_FILE, last);
|
|
419
|
+
return {
|
|
420
|
+
ok: true,
|
|
421
|
+
format: "download",
|
|
422
|
+
path: out,
|
|
423
|
+
sha256,
|
|
424
|
+
sizeBytes,
|
|
425
|
+
project,
|
|
426
|
+
preset: spec.name,
|
|
427
|
+
targets: [target],
|
|
428
|
+
presetChange: ensured.change,
|
|
429
|
+
debug,
|
|
430
|
+
engine: binary,
|
|
431
|
+
durationMs: deps.now().getTime() - started.getTime(),
|
|
432
|
+
storePlatform,
|
|
433
|
+
fileCount,
|
|
434
|
+
icon,
|
|
435
|
+
signing: target === "macos" ? "ad hoc (built-in), not notarized" : "none",
|
|
436
|
+
...(projectChanges.length ? { projectChanges } : {}),
|
|
437
|
+
...(warnings.length ? { warnings } : {}),
|
|
438
|
+
next: `Upload it as the game's ${storePlatform} store version with summer_publish_build (gameId, clientVersion); it reads the platform from this export.`,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
/** The file the last summer_export_game wrote (bundle or download), when it is still on disk. */
|
|
442
|
+
export async function readLastExport() {
|
|
443
|
+
const last = await readStoreJson(LAST_EXPORT_FILE).catch(() => null);
|
|
444
|
+
return last && typeof last.path === "string" && existsSync(last.path) ? last : null;
|
|
445
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store targets for summer_export_game, and the export presets that make them.
|
|
3
|
+
*
|
|
4
|
+
* The Summer Games store has two intakes (summer-platform):
|
|
5
|
+
* - build-publications takes ONE summer.bundle.v1 .zip per Build. Summer runs
|
|
6
|
+
* its client.pck on its own client templates (the Summer Games iOS and
|
|
7
|
+
* Android apps, the Summer desktop app on macOS and Windows), so the creator
|
|
8
|
+
* needs no export template and signs nothing. A target only picks the
|
|
9
|
+
* platforms the bundle declares and the texture formats the pack carries
|
|
10
|
+
* (the summer.games preset option platforms/<id>).
|
|
11
|
+
* - store versions take a web build (a static HTML5 .zip played in an iframe)
|
|
12
|
+
* and native desktop downloads (macos-universal, windows-x64). These are
|
|
13
|
+
* ordinary exports and need the matching export template.
|
|
14
|
+
*
|
|
15
|
+
* Presets are kept in export_presets.cfg under fixed names, so the creator
|
|
16
|
+
* sees them in the Export dialog: "summer.games ios", "summer.games
|
|
17
|
+
* ios+android", "Summer download Web", ... Only the options listed here are
|
|
18
|
+
* ever rewritten; every other preset and setting stays byte-for-byte. The
|
|
19
|
+
* engine adds its default "summer.games" preset only while the file has no
|
|
20
|
+
* summer.games preset, so the default is written alongside the first named
|
|
21
|
+
* one.
|
|
22
|
+
*/
|
|
23
|
+
export declare const STORE_TARGETS: readonly ["macos", "windows", "ios", "android", "web"];
|
|
24
|
+
export type StoreTarget = (typeof STORE_TARGETS)[number];
|
|
25
|
+
/** Targets a summer.games bundle can declare (the engine's CLIENT_PLATFORMS). */
|
|
26
|
+
export declare const BUNDLE_TARGETS: readonly StoreTarget[];
|
|
27
|
+
/** Targets the store takes as a download or web build (store versions). */
|
|
28
|
+
export declare const DOWNLOAD_TARGETS: readonly StoreTarget[];
|
|
29
|
+
/** Engine summer.games platform and default preset name (summer_games_export_platform.h PLATFORM_NAME). */
|
|
30
|
+
export declare const SUMMER_GAMES_PLATFORM = "summer.games";
|
|
31
|
+
export declare const SUMMER_GAMES_PRESET = "summer.games";
|
|
32
|
+
/** Canonical order, without repeats. */
|
|
33
|
+
export declare function normalizeTargets(targets: readonly string[]): StoreTarget[];
|
|
34
|
+
/** A preset the MCP owns: its name, export platform and the options it sets (Godot variant text). */
|
|
35
|
+
export interface PresetSpec {
|
|
36
|
+
name: string;
|
|
37
|
+
platform: string;
|
|
38
|
+
customFeatures?: string;
|
|
39
|
+
options: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
/** Variant text for a String option. */
|
|
42
|
+
export declare function variantString(value: string): string;
|
|
43
|
+
export declare function bundlePresetSpec(targets: readonly StoreTarget[]): PresetSpec;
|
|
44
|
+
export interface EnsuredPreset {
|
|
45
|
+
name: string;
|
|
46
|
+
/** "created", "updated" (owned options rewritten) or "unchanged". */
|
|
47
|
+
change: "created" | "updated" | "unchanged";
|
|
48
|
+
}
|
|
49
|
+
/** Make sure export_presets.cfg has this preset with these option values. */
|
|
50
|
+
export declare function ensurePreset(project: string, spec: PresetSpec): Promise<EnsuredPreset>;
|