summer-engine 4.3.0 → 4.6.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 +3 -1
- package/.codex-plugin/plugin.json +5 -3
- package/.cursor-plugin/plugin.json +4 -2
- package/.factory-plugin/plugin.json +4 -2
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +100 -0
- package/README.md +67 -27
- package/commands/start.md +19 -15
- package/commands/summer.md +1 -0
- package/dist/cli/command-usage.d.ts +3 -1
- package/dist/cli/command-usage.js +10 -4
- package/dist/cli/commands/create.d.ts +1 -25
- package/dist/cli/commands/create.js +10 -138
- package/dist/cli/commands/dev.d.ts +66 -0
- package/dist/cli/commands/dev.js +234 -0
- package/dist/cli/commands/install.d.ts +15 -58
- package/dist/cli/commands/install.js +31 -481
- package/dist/cli/commands/login.js +1 -1
- package/dist/cli/commands/ops.d.ts +17 -1
- package/dist/cli/commands/ops.js +97 -9
- package/dist/cli/commands/preview.d.ts +58 -0
- package/dist/cli/commands/preview.js +181 -0
- package/dist/cli/commands/run.d.ts +1 -17
- package/dist/cli/commands/run.js +17 -206
- package/dist/cli/commands/setup.js +175 -32
- package/dist/cli/index.js +5 -0
- package/dist/core/agent-host.d.ts +3 -0
- package/dist/core/agent-host.js +1 -1
- package/dist/core/api-client.d.ts +10 -2
- package/dist/core/api-client.js +13 -8
- package/dist/core/capabilities/agent-playbook.js +1 -0
- package/dist/core/capabilities/apps.d.ts +9 -9
- package/dist/core/capabilities/asset-import.d.ts +4 -1
- package/dist/core/capabilities/asset-import.js +15 -6
- package/dist/core/capabilities/capture-gameplay.js +1 -1
- package/dist/core/capabilities/connect-signal.d.ts +2 -2
- package/dist/core/capabilities/content-make/index.d.ts +81 -0
- package/dist/core/capabilities/content-make/index.js +706 -0
- package/dist/core/capabilities/content-make/media.d.ts +57 -0
- package/dist/core/capabilities/content-make/media.js +172 -0
- package/dist/core/capabilities/content-make/plan.d.ts +130 -0
- package/dist/core/capabilities/content-make/plan.js +433 -0
- package/dist/core/capabilities/content-make/render.d.ts +64 -0
- package/dist/core/capabilities/content-make/render.js +370 -0
- package/dist/core/capabilities/content-make/study.d.ts +13 -0
- package/dist/core/capabilities/content-make/study.js +165 -0
- package/dist/core/capabilities/content-make/types.d.ts +173 -0
- package/dist/core/capabilities/content-make/types.js +6 -0
- package/dist/core/capabilities/content-make/voice.d.ts +7 -0
- package/dist/core/capabilities/content-make/voice.js +27 -0
- package/dist/core/capabilities/events.d.ts +2 -2
- package/dist/core/capabilities/export-game.d.ts +4 -1
- package/dist/core/capabilities/export-game.js +4 -3
- package/dist/core/capabilities/export-presets.d.ts +2 -1
- package/dist/core/capabilities/export-presets.js +2 -1
- package/dist/core/capabilities/go-live.d.ts +14 -0
- package/dist/core/capabilities/go-live.js +408 -0
- package/dist/core/capabilities/image-generation.d.ts +6 -6
- package/dist/core/capabilities/item-manifest.d.ts +92 -0
- package/dist/core/capabilities/item-manifest.js +247 -0
- package/dist/core/capabilities/placement.d.ts +7 -7
- package/dist/core/capabilities/projects-local.d.ts +68 -0
- package/dist/core/capabilities/projects-local.js +172 -0
- package/dist/core/capabilities/publish-build.d.ts +13 -2
- package/dist/core/capabilities/publish-build.js +87 -21
- package/dist/core/capabilities/publish-download.js +1 -1
- package/dist/core/capabilities/runtime-control.d.ts +7 -2
- package/dist/core/capabilities/runtime-control.js +27 -1
- package/dist/core/capabilities/seeing/args.d.ts +2 -2
- package/dist/core/capabilities/store-checks.d.ts +45 -0
- package/dist/core/capabilities/store-checks.js +85 -0
- package/dist/core/capabilities/store-details.d.ts +37 -0
- package/dist/core/capabilities/store-details.js +94 -0
- package/dist/core/capabilities/summer-bundle.d.ts +21 -2
- package/dist/core/capabilities/summer-bundle.js +54 -7
- package/dist/core/capabilities/sync-items.d.ts +89 -0
- package/dist/core/capabilities/sync-items.js +352 -0
- package/dist/core/capabilities/tool-dispatch.js +66 -6
- package/dist/core/capabilities/video-ogv.d.ts +49 -0
- package/dist/core/capabilities/video-ogv.js +260 -0
- package/dist/core/capability-skew.js +2 -2
- package/dist/core/cli-usage-beacon.js +26 -2
- package/dist/core/cli-usage.d.ts +5 -2
- package/dist/core/cli-usage.js +8 -2
- package/dist/core/engine-install.d.ts +53 -0
- package/dist/core/engine-install.js +93 -0
- package/dist/core/engine-installer.d.ts +134 -0
- package/dist/core/engine-installer.js +760 -0
- package/dist/core/engine-launch.d.ts +126 -0
- package/dist/core/engine-launch.js +398 -0
- package/dist/core/engine-self-start.d.ts +63 -0
- package/dist/core/engine-self-start.js +282 -0
- package/dist/core/engine.d.ts +7 -1
- package/dist/core/engine.js +9 -2
- package/dist/core/error-codes.d.ts +4 -1
- package/dist/core/error-codes.js +7 -3
- package/dist/core/library-read.d.ts +2 -2
- package/dist/core/library-search.d.ts +2 -2
- package/dist/core/local-project.d.ts +50 -0
- package/dist/core/local-project.js +234 -0
- package/dist/core/ops/areas.js +9 -3
- package/dist/core/ops/catalog.d.ts +9 -0
- package/dist/core/ops/catalog.js +56 -4
- package/dist/core/ops/engine-answer.d.ts +4 -1
- package/dist/core/ops/engine-answer.js +16 -3
- package/dist/core/ops/engine.js +4 -4
- package/dist/core/ops/save-files.d.ts +6 -1
- package/dist/core/ops/save-files.js +67 -50
- package/dist/core/ops/search.js +9 -2
- package/dist/core/preview/input.d.ts +35 -0
- package/dist/core/preview/input.js +105 -0
- package/dist/core/preview/launch-config.d.ts +52 -0
- package/dist/core/preview/launch-config.js +137 -0
- package/dist/core/preview/page.d.ts +22 -0
- package/dist/core/preview/page.js +232 -0
- package/dist/core/preview/preview-source.d.ts +179 -0
- package/dist/core/preview/preview-source.js +350 -0
- package/dist/core/preview/server.d.ts +26 -0
- package/dist/core/preview/server.js +218 -0
- package/dist/core/telemetry.d.ts +20 -16
- package/dist/core/telemetry.js +92 -16
- package/dist/core/templates.d.ts +20 -0
- package/dist/core/templates.js +300 -3
- package/dist/core/tool-profiles.d.ts +2 -2
- package/dist/core/tool-profiles.js +1 -0
- package/dist/core/usage-fields.d.ts +41 -0
- package/dist/core/usage-fields.js +150 -0
- package/dist/core/util/sleep.d.ts +3 -0
- package/dist/core/util/sleep.js +12 -0
- package/dist/installer/agent-config.d.ts +29 -0
- package/dist/installer/agent-config.js +75 -11
- package/dist/installer/agent-detect.d.ts +44 -0
- package/dist/installer/agent-detect.js +166 -0
- package/dist/installer/agent-table.d.ts +19 -0
- package/dist/installer/agent-table.js +130 -0
- package/dist/installer/setup-error.d.ts +19 -0
- package/dist/installer/setup-error.js +34 -0
- package/dist/installer/version-check.d.ts +5 -0
- package/dist/installer/version-check.js +21 -0
- package/dist/mcp/ops-surface.d.ts +7 -0
- package/dist/mcp/ops-surface.js +40 -3
- package/dist/mcp/server.d.ts +14 -4
- package/dist/mcp/server.js +42 -8
- package/dist/mcp/tool-surface.d.ts +8 -0
- package/dist/mcp/tool-surface.js +19 -2
- package/dist/mcp/tools/creator-tools.js +92 -14
- package/dist/mcp/tools/debug-tools.js +2 -0
- package/dist/mcp/tools/local-project-tools.d.ts +6 -0
- package/dist/mcp/tools/local-project-tools.js +33 -0
- package/dist/mcp/tools/navigation-tools.js +2 -1
- package/dist/mcp/tools/project-tools.js +5 -8
- package/dist/mcp/tools/seeing-tools.js +1 -1
- package/dist/mcp/tools/visual-tools.js +1 -1
- package/dist/mcp/tools/with-engine.js +1 -1
- package/dist/project-memory/agent-instructions.js +11 -3
- package/gemini-extension.json +3 -1
- package/library/references/mcp-tools-reference/mcp-tools-reference.md +18 -5
- package/library/skills/README.md +12 -8
- package/library/skills/animated-loop/SKILL.md +10 -16
- package/library/skills/cinematic-cutscene/SKILL.md +7 -18
- package/library/skills/headless-scripting/SKILL.md +7 -4
- package/library/skills/in-game-purchases/SKILL.md +25 -8
- package/library/skills/in-game-purchases/resource.yaml +2 -1
- package/library/skills/launch-ready/SKILL.md +351 -0
- package/library/skills/launch-ready/resource.yaml +45 -0
- package/library/skills/make-content/SKILL.md +202 -0
- package/library/skills/make-content/resource.yaml +32 -0
- package/library/skills/make-game/SKILL.md +6 -4
- package/library/skills/make-game/resource.yaml +2 -1
- package/library/skills/navigate-summer/SKILL.md +4 -2
- package/library/skills/new-project/SKILL.md +15 -22
- package/library/skills/new-project/resource.yaml +2 -1
- package/library/skills/painted-25d-world/SKILL.md +366 -0
- package/library/skills/painted-25d-world/_building-blocks/blender-route.md +33 -0
- package/library/skills/painted-25d-world/_building-blocks/blockout_builder.gd +149 -0
- package/library/skills/painted-25d-world/_building-blocks/capture_paint_view.gd +32 -0
- package/library/skills/painted-25d-world/_building-blocks/paint_projection.gdshader +23 -0
- package/library/skills/painted-25d-world/_building-blocks/paintcam_check.gd +25 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_door.gd +17 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_location.gd +56 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_player.gd +125 -0
- package/library/skills/painted-25d-world/_building-blocks/painted_world.gd +111 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/cam.py +57 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/detail4k.py +81 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/overlay.py +17 -0
- package/library/skills/painted-25d-world/_building-blocks/tools/preview.py +60 -0
- package/library/skills/painted-25d-world/resource.yaml +39 -0
- package/library/skills/publish-your-game/SKILL.md +13 -14
- package/library/skills/publish-your-game/resource.yaml +4 -3
- package/library/skills/release-checklist/SKILL.md +188 -0
- package/library/skills/release-checklist/resource.yaml +40 -0
- package/library/skills/release-readiness/SKILL.md +1 -1
- package/library/skills/release-readiness/resource.yaml +1 -1
- package/library/skills/store-art/SKILL.md +1 -1
- package/library/skills/store-art/resource.yaml +1 -1
- package/library/skills/store-listing/SKILL.md +1 -1
- package/library/skills/store-listing/resource.yaml +1 -1
- package/library/skills/summer-cli/SKILL.md +2 -0
- package/library/skills/summer-store/SKILL.md +295 -39
- package/library/skills/summer-store/resource.yaml +9 -3
- package/library/skills/trailer-shot/SKILL.md +3 -3
- package/library/templates/README.md +5 -0
- package/library/tools/content-make/resource.yaml +132 -0
- package/library/tools/frame-shot/GUIDE.md +1 -1
- package/library/tools/go-live/resource.yaml +67 -0
- package/library/tools/import-from-url/GUIDE.md +1 -0
- package/library/tools/import-from-url-batch/GUIDE.md +1 -1
- package/library/tools/play/GUIDE.md +2 -0
- package/library/tools/projects-create-local/resource.yaml +55 -0
- package/library/tools/projects-open-local/resource.yaml +51 -0
- package/library/tools/publish-build/resource.yaml +3 -3
- package/library/tools/screenshot/GUIDE.md +1 -1
- package/library/tools/store-set-details/resource.yaml +114 -0
- package/library/tools/sync-items/resource.yaml +72 -0
- package/package.json +1 -1
- package/registry/generated/aliases.json +6 -0
- package/registry/generated/counts.json +3 -3
- package/registry/generated/gemini-extension.json +3 -1
- package/registry/generated/index.json +463 -37
- package/registry/generated/marketplace.claude.json +1 -1
- package/registry/generated/plugin.claude-directory.json +4 -2
- package/registry/generated/plugin.claude.json +3 -1
- 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/server.json +2 -2
- package/registry/generated/skills-index.md +12 -8
- package/registry/generated/skills-registry.json +62 -5
- package/registry/operations/legacy-names.json +40 -0
- package/registry/operations/public-catalog.json +10144 -387
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one page `summer preview` serves. Self-contained: inline style and
|
|
3
|
+
* script, no fonts, no CDN, no request leaves 127.0.0.1 (the server's
|
|
4
|
+
* Content-Security-Policy enforces it), and no token anywhere (the server
|
|
5
|
+
* holds the engine connection).
|
|
6
|
+
*
|
|
7
|
+
* Loop: fetch /api/frame, show it, wait the interval the server derived from
|
|
8
|
+
* the measured capture time, repeat. Hidden tab = no requests. Each frame
|
|
9
|
+
* response carries the status (x-summer-status) so one request a frame is all
|
|
10
|
+
* the page makes. The first frame after the page loads or comes back into
|
|
11
|
+
* view, and the Refresh button, ask for a fresh one (?fresh=1): game frames
|
|
12
|
+
* are otherwise about 20 s apart, and the header says how old the frame is.
|
|
13
|
+
* While the game runs, keys and clicks on the frame go to it; a key release is
|
|
14
|
+
* always sent, so no key stays held in the game. A page framed inside another
|
|
15
|
+
* page sends no input.
|
|
16
|
+
*
|
|
17
|
+
* For agents checking the page: <body data-state> is connecting, no_editor,
|
|
18
|
+
* editor or game; #status holds the status line; #refresh fetches a fresh
|
|
19
|
+
* frame; the page logs nothing to the console in normal operation, so a
|
|
20
|
+
* console error is a real one.
|
|
21
|
+
*/
|
|
22
|
+
export const PREVIEW_PAGE_HTML = `<!doctype html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8">
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
27
|
+
<title>Summer preview</title>
|
|
28
|
+
<style>
|
|
29
|
+
:root { --cobalt: #0047ab; --ink: #111; --muted: #6b6b6b; --line: #e6e6e6; }
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
html, body { height: 100%; margin: 0; }
|
|
32
|
+
body { display: flex; flex-direction: column; background: #fff; color: var(--ink);
|
|
33
|
+
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
|
34
|
+
header, footer { display: flex; align-items: center; gap: 12px; padding: 8px 14px; min-height: 36px; }
|
|
35
|
+
header { border-bottom: 1px solid var(--line); }
|
|
36
|
+
footer { border-top: 1px solid var(--line); color: var(--muted); }
|
|
37
|
+
.brand { color: var(--cobalt); font-weight: 700; }
|
|
38
|
+
#project { font-weight: 600; }
|
|
39
|
+
#scene { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
40
|
+
.spacer { flex: 1; }
|
|
41
|
+
#source { color: #fff; background: var(--cobalt); border-radius: 4px; padding: 1px 8px; font-weight: 600; }
|
|
42
|
+
#source:empty { display: none; }
|
|
43
|
+
#rate { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
44
|
+
#refresh { font: inherit; color: var(--cobalt); background: #fff; border: 1px solid var(--line); border-radius: 4px; padding: 1px 8px; cursor: pointer; }
|
|
45
|
+
body[data-state="no_editor"] #refresh, body[data-state="connecting"] #refresh { display: none; }
|
|
46
|
+
main { flex: 1; min-height: 0; position: relative; display: flex; align-items: center; justify-content: center; outline: none; }
|
|
47
|
+
#frame { width: 100%; height: 100%; object-fit: contain; display: block; user-select: none; -webkit-user-drag: none; }
|
|
48
|
+
body[data-state="game"] #frame { cursor: crosshair; }
|
|
49
|
+
#empty { display: none; text-align: center; max-width: 520px; padding: 24px; }
|
|
50
|
+
#empty h1 { font-size: 20px; margin: 0 0 8px; }
|
|
51
|
+
#empty code { display: inline-block; margin-top: 8px; padding: 6px 10px; border: 1px solid var(--line); border-radius: 4px; color: var(--cobalt); }
|
|
52
|
+
body[data-state="no_editor"] #frame, body[data-state="connecting"] #frame { display: none; }
|
|
53
|
+
body[data-state="no_editor"] #empty, body[data-state="connecting"] #empty { display: block; }
|
|
54
|
+
#status { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
55
|
+
</style>
|
|
56
|
+
</head>
|
|
57
|
+
<body data-state="connecting">
|
|
58
|
+
<header>
|
|
59
|
+
<span class="brand">Summer</span>
|
|
60
|
+
<span id="project"></span>
|
|
61
|
+
<span id="scene"></span>
|
|
62
|
+
<span class="spacer"></span>
|
|
63
|
+
<span id="source"></span>
|
|
64
|
+
<span id="rate"></span>
|
|
65
|
+
<button id="refresh" type="button" title="Capture a fresh frame now">Refresh</button>
|
|
66
|
+
</header>
|
|
67
|
+
<main id="stage" tabindex="0">
|
|
68
|
+
<img id="frame" alt="Live frame from Summer Engine">
|
|
69
|
+
<div id="empty">
|
|
70
|
+
<h1 id="empty-title">Looking for Summer Engine</h1>
|
|
71
|
+
<div id="empty-text"></div>
|
|
72
|
+
</div>
|
|
73
|
+
</main>
|
|
74
|
+
<footer><span id="status">Looking for Summer Engine</span></footer>
|
|
75
|
+
<script>
|
|
76
|
+
(() => {
|
|
77
|
+
const $ = (id) => document.getElementById(id);
|
|
78
|
+
const frame = $("frame");
|
|
79
|
+
const stage = $("stage");
|
|
80
|
+
const framed = window.top !== window.self;
|
|
81
|
+
let status = null;
|
|
82
|
+
let timer = null;
|
|
83
|
+
let loop = 0;
|
|
84
|
+
let lastUrl = null;
|
|
85
|
+
let note = null;
|
|
86
|
+
const shown = [];
|
|
87
|
+
|
|
88
|
+
function render() {
|
|
89
|
+
const s = status;
|
|
90
|
+
if (!s) return;
|
|
91
|
+
const message = typeof s.message === "string" ? s.message : "";
|
|
92
|
+
document.body.dataset.state = s.state;
|
|
93
|
+
document.body.dataset.source = s.frame ? s.frame.source : "";
|
|
94
|
+
$("project").textContent = s.project || "";
|
|
95
|
+
$("scene").textContent = s.scene || "";
|
|
96
|
+
$("source").textContent = s.state === "game" ? "Game" : s.state === "editor" ? "Editor" : "";
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
while (shown.length > 2 && now - shown[0] > 12000) shown.shift();
|
|
99
|
+
const fps = shown.length > 1 ? (shown.length - 1) / ((shown[shown.length - 1] - shown[0]) / 1000) : 0;
|
|
100
|
+
const rate = [];
|
|
101
|
+
if (s.frame && s.frame.source === "game") rate.push("frame " + Math.max(0, Math.round((now - s.frame.capturedAt) / 1000)) + " s old");
|
|
102
|
+
else if (s.frame && fps) rate.push(fps.toFixed(1) + " fps");
|
|
103
|
+
if (s.frame && s.latencyMs != null) rate.push(s.latencyMs + " ms per frame");
|
|
104
|
+
$("rate").textContent = rate.join(" · ");
|
|
105
|
+
$("status").textContent = note && now < note.until ? note.text : message;
|
|
106
|
+
const parts = message.split(": run ");
|
|
107
|
+
$("empty-title").textContent = s.state === "connecting" ? "Looking for Summer Engine" : parts[0];
|
|
108
|
+
const empty = $("empty-text");
|
|
109
|
+
empty.textContent = "";
|
|
110
|
+
if (parts[1]) {
|
|
111
|
+
const code = document.createElement("code");
|
|
112
|
+
code.textContent = parts[1];
|
|
113
|
+
empty.append("Run this in the project folder:", document.createElement("br"), code);
|
|
114
|
+
}
|
|
115
|
+
document.title = (s.project ? s.project + " · " : "") + "Summer preview";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function show(blob) {
|
|
119
|
+
const url = URL.createObjectURL(blob);
|
|
120
|
+
const next = new Image();
|
|
121
|
+
next.src = url;
|
|
122
|
+
try { await next.decode(); } catch (_) {}
|
|
123
|
+
frame.src = url;
|
|
124
|
+
if (lastUrl) URL.revokeObjectURL(lastUrl);
|
|
125
|
+
lastUrl = url;
|
|
126
|
+
shown.push(Date.now());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readStatus(value) {
|
|
130
|
+
return value && typeof value === "object" && typeof value.state === "string" ? value : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function tick(id, fresh) {
|
|
134
|
+
if (id !== loop || document.hidden) return;
|
|
135
|
+
const started = performance.now();
|
|
136
|
+
let wait = 2000;
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(fresh ? "/api/frame?fresh=1" : "/api/frame", { cache: "no-store" });
|
|
139
|
+
const header = res.headers.get("x-summer-status");
|
|
140
|
+
let next = null;
|
|
141
|
+
if (res.status === 200) {
|
|
142
|
+
if (header) next = readStatus(JSON.parse(decodeURIComponent(header)));
|
|
143
|
+
await show(await res.blob());
|
|
144
|
+
} else {
|
|
145
|
+
next = readStatus(await res.json().catch(() => null));
|
|
146
|
+
}
|
|
147
|
+
if (next) {
|
|
148
|
+
status = next;
|
|
149
|
+
wait = status.intervalMs || 2000;
|
|
150
|
+
}
|
|
151
|
+
} catch (_) {
|
|
152
|
+
status = { state: "no_editor", message: "The preview server stopped: run npx -y summer-engine@latest preview", frame: null };
|
|
153
|
+
}
|
|
154
|
+
try { render(); } catch (_) {}
|
|
155
|
+
if (id !== loop || document.hidden) return;
|
|
156
|
+
timer = setTimeout(() => tick(id, false), Math.max(0, wait - (performance.now() - started)));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function start() {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
loop += 1;
|
|
162
|
+
if (!document.hidden) tick(loop, true);
|
|
163
|
+
}
|
|
164
|
+
document.addEventListener("visibilitychange", start);
|
|
165
|
+
$("refresh").addEventListener("click", start);
|
|
166
|
+
setInterval(() => { if (!document.hidden) { try { render(); } catch (_) {} } }, 1000);
|
|
167
|
+
|
|
168
|
+
function framePoint(event) {
|
|
169
|
+
const rect = frame.getBoundingClientRect();
|
|
170
|
+
const w = frame.naturalWidth, h = frame.naturalHeight;
|
|
171
|
+
if (!w || !h) return null;
|
|
172
|
+
const scale = Math.min(rect.width / w, rect.height / h);
|
|
173
|
+
const x = (event.clientX - rect.left - (rect.width - w * scale) / 2) / scale;
|
|
174
|
+
const y = (event.clientY - rect.top - (rect.height - h * scale) / 2) / scale;
|
|
175
|
+
return x < 0 || y < 0 || x > w || y > h ? null : { x: x, y: y };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function inputOn() {
|
|
179
|
+
return !framed && status && status.input;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Releases go out even while input is off, so a key never stays held.
|
|
183
|
+
async function send(inputs, release) {
|
|
184
|
+
if (framed || (!release && !inputOn())) return;
|
|
185
|
+
try {
|
|
186
|
+
const res = await fetch("/api/input", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ inputs: inputs }) });
|
|
187
|
+
const body = await res.json();
|
|
188
|
+
note = body.ok || release ? null : { text: "Input not sent: " + body.error, until: Date.now() + 4000 };
|
|
189
|
+
} catch (_) {
|
|
190
|
+
note = { text: "Input not sent: the preview server did not answer", until: Date.now() + 4000 };
|
|
191
|
+
}
|
|
192
|
+
try { render(); } catch (_) {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
frame.addEventListener("click", (event) => {
|
|
196
|
+
const point = framePoint(event);
|
|
197
|
+
stage.focus();
|
|
198
|
+
if (point && status && status.state === "game") send([{ kind: "click", x: point.x, y: point.y, button: 1 }], false);
|
|
199
|
+
});
|
|
200
|
+
frame.addEventListener("contextmenu", (event) => {
|
|
201
|
+
if (!inputOn() || status.state !== "game") return;
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
const point = framePoint(event);
|
|
204
|
+
if (point) send([{ kind: "click", x: point.x, y: point.y, button: 2 }], false);
|
|
205
|
+
});
|
|
206
|
+
const held = new Set();
|
|
207
|
+
function onKey(event, pressed) {
|
|
208
|
+
if (pressed) {
|
|
209
|
+
if (!inputOn() || event.metaKey) return;
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
if (held.has(event.code)) return;
|
|
212
|
+
held.add(event.code);
|
|
213
|
+
send([{ kind: "key", code: event.code, pressed: true }], false);
|
|
214
|
+
} else if (held.delete(event.code)) {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
send([{ kind: "key", code: event.code, pressed: false }], true);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
stage.addEventListener("keydown", (event) => onKey(event, true));
|
|
220
|
+
stage.addEventListener("keyup", (event) => onKey(event, false));
|
|
221
|
+
stage.addEventListener("blur", () => {
|
|
222
|
+
const codes = Array.from(held);
|
|
223
|
+
held.clear();
|
|
224
|
+
if (codes.length) send(codes.map((code) => ({ kind: "key", code: code, pressed: false })), true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
start();
|
|
228
|
+
})();
|
|
229
|
+
</script>
|
|
230
|
+
</body>
|
|
231
|
+
</html>
|
|
232
|
+
`;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `summer preview` shows: the running game's frames, or the editor
|
|
3
|
+
* viewport when no game runs, read through the same local API client every
|
|
4
|
+
* engine tool uses (core/api-client.ts). The server (./server.ts) asks for a
|
|
5
|
+
* frame only while a page is visible and asking, so a closed or hidden tab
|
|
6
|
+
* costs the engine nothing.
|
|
7
|
+
*
|
|
8
|
+
* Capture path, measured on Summer Engine 0.7.2 (2026-10-19, M-series Mac):
|
|
9
|
+
* - editor viewport, GET /api/snapshot/viewport (1280x671 JPEG, ~47 KB):
|
|
10
|
+
* p50 45 ms with the editor in front, 100-150 ms with it in the background
|
|
11
|
+
* (an unfocused editor sleeps ~100 ms per main-loop pass);
|
|
12
|
+
* - running game, GET /api/snapshot/game (1280x720 JPEG): 1.25-1.35 s, the
|
|
13
|
+
* same for GameProbe at any max_dim, and requests queue one after another.
|
|
14
|
+
* The game renders, reads back and PNG-encodes a full-size frame on its
|
|
15
|
+
* main thread: captured back to back, a game at 117 fps fell to 13 fps;
|
|
16
|
+
* - IsGameRunning 13 ms; a game snapshot with no game 12 ms (not_running).
|
|
17
|
+
* So the pace comes from the measured capture time, never a fixed rate: one
|
|
18
|
+
* frame every max(200 ms, share x the recent capture time). The editor's
|
|
19
|
+
* share is 2 (4-5 fps while the view changes, 2 fps once two frames in a row
|
|
20
|
+
* come back identical). The game's share is 16: a frame about every 20 s, so
|
|
21
|
+
* the game is held for well under a tenth of the time and a person playing
|
|
22
|
+
* it does not feel the preview. A check that needs the game as it is now asks
|
|
23
|
+
* for a fresh frame (/api/frame?fresh=1, the page's first frame, its Refresh
|
|
24
|
+
* button), at most one every 2 s.
|
|
25
|
+
*
|
|
26
|
+
* Status is read on a slower clock: whether a game runs (1 s), the project
|
|
27
|
+
* and scene names (5 s), and a fresh connection attempt while no editor
|
|
28
|
+
* answers (2 s). An editor restart (new token or port) is followed.
|
|
29
|
+
*/
|
|
30
|
+
import type { EngineSnapshot, SnapshotOptions } from "../api-client.js";
|
|
31
|
+
import { type EngineCapabilities } from "../capability-skew.js";
|
|
32
|
+
import { type PreviewInput } from "./input.js";
|
|
33
|
+
/** The engine calls the preview makes: a structural subset of EngineApiClient. */
|
|
34
|
+
export interface PreviewEngine {
|
|
35
|
+
viewportSnapshot(options?: SnapshotOptions): Promise<EngineSnapshot>;
|
|
36
|
+
gameSnapshot(options?: SnapshotOptions): Promise<EngineSnapshot>;
|
|
37
|
+
executeOps(ops: Record<string, unknown>[], options?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
38
|
+
getProjectState(): Promise<unknown>;
|
|
39
|
+
getSceneState(): Promise<unknown>;
|
|
40
|
+
credentialsChanged(): Promise<boolean>;
|
|
41
|
+
getEngineCapabilities?(): EngineCapabilities | undefined;
|
|
42
|
+
getEngineVersion?(): string | undefined;
|
|
43
|
+
}
|
|
44
|
+
/** Fastest pace: 5 frames a second. */
|
|
45
|
+
export declare const MIN_FRAME_INTERVAL_MS = 200;
|
|
46
|
+
/** Slowest pace while a frame still comes back. */
|
|
47
|
+
export declare const MAX_FRAME_INTERVAL_MS = 30000;
|
|
48
|
+
/** Capture may take at most 1/share of the time: the editor's viewport
|
|
49
|
+
* read is light; a game frame stalls the game while it is read. */
|
|
50
|
+
export declare const LATENCY_SHARE: Readonly<Record<"game" | "editor", number>>;
|
|
51
|
+
/** Editor pace once the view has stopped changing (2 fps). */
|
|
52
|
+
export declare const EDITOR_IDLE_INTERVAL_MS = 500;
|
|
53
|
+
/** Identical frames in a row before the editor counts as idle. */
|
|
54
|
+
export declare const IDLE_AFTER_SAME_FRAMES = 2;
|
|
55
|
+
/** Fresh frames on demand: at most one capture start this often. */
|
|
56
|
+
export declare const FRESH_MIN_GAP_MS = 2000;
|
|
57
|
+
/** Pace of the page while no editor answers. */
|
|
58
|
+
export declare const NO_EDITOR_INTERVAL_MS = 2000;
|
|
59
|
+
export declare const PLAY_CHECK_MS = 1000;
|
|
60
|
+
export declare const META_CHECK_MS = 5000;
|
|
61
|
+
export declare const RECONNECT_MS = 2000;
|
|
62
|
+
export declare const LINK_CHECK_MS = 2000;
|
|
63
|
+
/** After a running game's frame failed for a reason other than "not running",
|
|
64
|
+
* show the editor and try the game again after this long. */
|
|
65
|
+
export declare const GAME_RETRY_MS = 10000;
|
|
66
|
+
/** A request that comes sooner than this share of the pace after the last
|
|
67
|
+
* capture started gets that capture's frame again: two tabs, or the page and
|
|
68
|
+
* an agent's own request, never double the engine's work. */
|
|
69
|
+
export declare const SHARE_FRAME_FRACTION = 0.75;
|
|
70
|
+
export declare const NO_EDITOR_MESSAGE = "Editor not running: run npx -y summer-engine@latest dev";
|
|
71
|
+
export declare const GAME_NOT_RUNNING_MESSAGE = "Game not running: showing the editor";
|
|
72
|
+
export type PreviewState = "connecting" | "no_editor" | "editor" | "game";
|
|
73
|
+
export interface PreviewFrame {
|
|
74
|
+
seq: number;
|
|
75
|
+
source: "game" | "editor";
|
|
76
|
+
bytes: Buffer;
|
|
77
|
+
mime: string;
|
|
78
|
+
width: number;
|
|
79
|
+
height: number;
|
|
80
|
+
/** The game window's size when the engine scaled the frame down. */
|
|
81
|
+
sourceWidth?: number;
|
|
82
|
+
sourceHeight?: number;
|
|
83
|
+
capturedAt: number;
|
|
84
|
+
latencyMs: number;
|
|
85
|
+
}
|
|
86
|
+
export interface PreviewStatus {
|
|
87
|
+
service: "summer-preview";
|
|
88
|
+
state: PreviewState;
|
|
89
|
+
/** The one status line the page shows. */
|
|
90
|
+
message: string;
|
|
91
|
+
project: string | null;
|
|
92
|
+
scene: string | null;
|
|
93
|
+
engineVersion: string | null;
|
|
94
|
+
frame: {
|
|
95
|
+
seq: number;
|
|
96
|
+
source: "game" | "editor";
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
capturedAt: number;
|
|
100
|
+
latencyMs: number;
|
|
101
|
+
} | null;
|
|
102
|
+
/** Average capture time of the source being shown, ms. */
|
|
103
|
+
latencyMs: number | null;
|
|
104
|
+
/** How long the page waits between frames (from the measured latency). */
|
|
105
|
+
intervalMs: number;
|
|
106
|
+
/** Keys (and clicks on a game frame) reach the running game. */
|
|
107
|
+
input: boolean;
|
|
108
|
+
}
|
|
109
|
+
export type InputResult = {
|
|
110
|
+
ok: true;
|
|
111
|
+
sent: number;
|
|
112
|
+
} | {
|
|
113
|
+
ok: false;
|
|
114
|
+
error: string;
|
|
115
|
+
};
|
|
116
|
+
export interface PreviewSourceOptions {
|
|
117
|
+
connect: () => Promise<PreviewEngine>;
|
|
118
|
+
now?: () => number;
|
|
119
|
+
/** Where failure details go; what the page sees stays short. */
|
|
120
|
+
log?: (line: string) => void;
|
|
121
|
+
}
|
|
122
|
+
/** The pace for a `source` capture that takes `latencyMs`; `idle` = the
|
|
123
|
+
* editor view has stopped changing. */
|
|
124
|
+
export declare function frameIntervalMs(latencyMs: number | null, source?: "game" | "editor", idle?: boolean): number;
|
|
125
|
+
/** Schedule a batch frame by frame: a key pressed and released (or released
|
|
126
|
+
* and pressed again) in the same batch gets the second event one frame later,
|
|
127
|
+
* so a fast tap still reads as a press in the game. */
|
|
128
|
+
export declare function scheduleInputBatch(events: Record<string, unknown>[]): Record<string, unknown>[];
|
|
129
|
+
export declare class PreviewSource {
|
|
130
|
+
private readonly connectEngine;
|
|
131
|
+
private readonly now;
|
|
132
|
+
private readonly log;
|
|
133
|
+
private client;
|
|
134
|
+
private attempted;
|
|
135
|
+
private lastConnectAt;
|
|
136
|
+
private lastLinkCheckAt;
|
|
137
|
+
private linkError;
|
|
138
|
+
private playing;
|
|
139
|
+
private playingScene;
|
|
140
|
+
private lastPlayCheckAt;
|
|
141
|
+
private project;
|
|
142
|
+
private scene;
|
|
143
|
+
private lastMetaAt;
|
|
144
|
+
private gameBlockedUntil;
|
|
145
|
+
private gameNote;
|
|
146
|
+
private inputSupported;
|
|
147
|
+
private latency;
|
|
148
|
+
private current;
|
|
149
|
+
private seq;
|
|
150
|
+
private inflight;
|
|
151
|
+
private lastCaptureAt;
|
|
152
|
+
private sameFrames;
|
|
153
|
+
private inputChain;
|
|
154
|
+
private inputQueue;
|
|
155
|
+
constructor(options: PreviewSourceOptions);
|
|
156
|
+
/** The current state, without asking the engine anything. */
|
|
157
|
+
status(): PreviewStatus;
|
|
158
|
+
/** The newest frame: captured now, or the last one when another viewer
|
|
159
|
+
* asked moments ago. `fresh` captures now unless a capture started in the
|
|
160
|
+
* last FRESH_MIN_GAP_MS. Null while no editor answers. One capture at a time. */
|
|
161
|
+
frame(options?: {
|
|
162
|
+
fresh?: boolean;
|
|
163
|
+
}): Promise<PreviewFrame | null>;
|
|
164
|
+
/** Send clicks and keys to the running game, also while its frames cannot
|
|
165
|
+
* be read (a key release must never be lost). Calls are sent one at a time,
|
|
166
|
+
* in order; input that arrives meanwhile goes out as one script. */
|
|
167
|
+
sendInput(inputs: PreviewInput[]): Promise<InputResult>;
|
|
168
|
+
private pace;
|
|
169
|
+
private state;
|
|
170
|
+
private message;
|
|
171
|
+
private drop;
|
|
172
|
+
/** The connected client, connecting or following a restart as needed. */
|
|
173
|
+
private engine;
|
|
174
|
+
/** Whether a game runs (and which scene). False when the editor is gone. */
|
|
175
|
+
private refreshPlaying;
|
|
176
|
+
private refreshMeta;
|
|
177
|
+
private capture;
|
|
178
|
+
private read;
|
|
179
|
+
}
|