@playcraft/cli 0.1.66 → 0.1.68
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/dist/assets/client.js +4 -2
- package/dist/assets/messages.js +2 -0
- package/dist/audio-source/messages.js +35 -0
- package/dist/commands/assets.js +1 -1
- package/dist/commands/audio-program.js +82 -0
- package/dist/commands/audio-source.js +109 -0
- package/dist/commands/audio.js +13 -4
- package/dist/commands/build.js +4 -2
- package/dist/commands/tools-context.js +46 -0
- package/dist/commands/tools.js +2 -0
- package/dist/project-skills/builtin-import.js +69 -22
- package/dist/project-skills/commands.js +11 -2
- package/dist/project-skills/lifecycle.js +1 -1
- package/dist/project-skills/local-store.js +90 -31
- package/dist/project-skills/messages.js +16 -0
- package/dist/project-skills/migrate-lock.js +166 -0
- package/dist/project-skills/remote-cache.js +36 -5
- package/dist/utils/agent-api-client.js +18 -1
- package/dist/utils/agent-generation-provenance.js +8 -2
- package/dist/utils/audio-export.js +249 -0
- package/dist/utils/audio-processor.js +22 -99
- package/dist/utils/execution-context-bridge.js +28 -0
- package/dist/workspace-runtime/batch/control.js +13 -2
- package/dist/workspace-runtime/batch/generate.js +43 -2
- package/dist/workspace-runtime/batch/prepare.js +4 -0
- package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
- package/dist/workspace-runtime/batch/source-capture.js +6 -8
- package/dist/workspace-runtime/codex/app-server.js +26 -2
- package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
- package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
- package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +15 -2
- package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
- package/dist/workspace-runtime/previews/finalize.js +1 -1
- package/dist/workspace-runtime/previews/output.js +4 -1
- package/dist/workspace-runtime/previews/publish.js +5 -2
- package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
- package/dist/workspace-runtime/serve.js +6 -0
- package/dist/workspace-runtime/server/accept-operation.js +85 -13
- package/dist/workspace-runtime/server/audio-assets.js +257 -0
- package/dist/workspace-runtime/server/audio-export.js +136 -0
- package/dist/workspace-runtime/server/directory-access.js +222 -67
- package/dist/workspace-runtime/server/dispatch.js +252 -29
- package/dist/workspace-runtime/server/http.js +97 -3
- package/dist/workspace-runtime/server/operation-record.js +1 -1
- package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
- package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
- package/dist/workspace-runtime/workspaces/asset-library-import.js +27 -7
- package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
- package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
- package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +80 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-import.js +149 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-package.js +26 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-artifact-audit.js +90 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-browser.js +194 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-build-inputs.js +122 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-engine.js +39 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-package.js +289 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-variant-build.js +217 -0
- package/dist/workspace-runtime/workspaces/audio-assets/references.js +107 -0
- package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
- package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
- package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
- package/dist/workspace-runtime/workspaces/audio-assets/service.js +459 -0
- package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
- package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
- package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
- package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
- package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
- package/dist/workspace-runtime/workspaces/files.js +3 -5
- package/dist/workspace-runtime/workspaces/git.js +7 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
- package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
- package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
- package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
- package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
- package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
- package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
- package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
- package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
- package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
- package/dist/workspace-runtime/workspaces/skills.js +48 -13
- package/package.json +9 -4
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
- package/project-template/vite.config.ts +1 -1
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-template-v2/.playcraft/skills.lock.json +9 -2
- package/project-template-v2/vite.config.ts +1 -1
- package/project-templates/empty/AGENTS.md +2 -0
- package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-templates/phaser/vite.config.ts +1 -1
- package/project-templates/playable-template-phaser/vite.config.ts +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-templates/threejs/vite.config.ts +1 -1
- package/templates/preview-runtime/v1/README.md +12 -0
- package/templates/preview-runtime/v1/build-preview.mjs +17 -8
- package/templates/preview-runtime/v1/manifest.json +9 -6
- package/templates/preview-runtime/v1/release-guard.mjs +1 -1
- package/templates/preview-runtime/v1/runtime.d.mts +1 -3
- package/templates/preview-runtime/v1/runtime.d.ts +1 -3
- package/templates/preview-runtime/v1/runtime.mjs +1142 -257
- package/templates/preview-runtime/v1/types/host.d.ts +29 -0
- package/templates/preview-runtime/v1/types/index.d.ts +6 -0
- package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
|
@@ -1,3 +1,231 @@
|
|
|
1
|
+
// ../preview-runtime/src/protocol.ts
|
|
2
|
+
var PROTOCOL_VERSION = 1;
|
|
3
|
+
var MAX_IMAGE_RESOURCE_BYTES = 32 * 1024 * 1024;
|
|
4
|
+
var ResourceLoadError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ResourceLoadError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var SupersededResourceError = class extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("A newer image value replaced this resource request");
|
|
13
|
+
this.name = "SupersededResourceError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var ProtocolError = class extends Error {
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = "ProtocolError";
|
|
21
|
+
}
|
|
22
|
+
code;
|
|
23
|
+
};
|
|
24
|
+
var forbidden = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
25
|
+
var record = (value) => !!value && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
26
|
+
var exactKeys = (value, allowed) => {
|
|
27
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
28
|
+
throw new ProtocolError("INVALID", "Unexpected message field");
|
|
29
|
+
};
|
|
30
|
+
var string = (v) => typeof v === "string" && v.length > 0 && v.length <= 1024;
|
|
31
|
+
var revision = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
32
|
+
function assertJson(value, depth = 0) {
|
|
33
|
+
if (depth > 64) throw new ProtocolError("LIMIT", "Configuration nesting exceeds limit");
|
|
34
|
+
if (value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value))
|
|
35
|
+
return;
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
for (const child of value) assertJson(child, depth + 1);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (record(value)) {
|
|
41
|
+
for (const [key, child] of Object.entries(value)) {
|
|
42
|
+
if (forbidden.has(key)) throw new ProtocolError("INVALID", "Unsafe configuration key");
|
|
43
|
+
assertJson(child, depth + 1);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
throw new ProtocolError("INVALID", "Configuration must contain finite JSON data");
|
|
48
|
+
}
|
|
49
|
+
function clone(value) {
|
|
50
|
+
return structuredClone(value);
|
|
51
|
+
}
|
|
52
|
+
function freeze(value) {
|
|
53
|
+
if (value && typeof value === "object") {
|
|
54
|
+
Object.freeze(value);
|
|
55
|
+
for (const child of Object.values(value)) freeze(child);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function fingerprint(value) {
|
|
60
|
+
if (Array.isArray(value)) return "[" + value.map(fingerprint).join(",") + "]";
|
|
61
|
+
if (record(value))
|
|
62
|
+
return "{" + Object.keys(value).sort().map((k) => JSON.stringify(k) + ":" + fingerprint(value[k])).join(",") + "}";
|
|
63
|
+
return JSON.stringify(value);
|
|
64
|
+
}
|
|
65
|
+
function assertHello(value) {
|
|
66
|
+
if (!record(value) || value.protocolVersion !== 1 || !string(value.sessionId) || !revision(value.frameEpoch) || !string(value.schemaVersion) || !string(value.configContractVersion) || !string(value.locale) || !record(value.scope))
|
|
67
|
+
throw new ProtocolError("IDENTITY", "Invalid runtime session binding");
|
|
68
|
+
if (!["projectId", "branchName", "themeId"].every(
|
|
69
|
+
(k) => string(value.scope[k])
|
|
70
|
+
))
|
|
71
|
+
throw new ProtocolError("IDENTITY", "Invalid configuration scope");
|
|
72
|
+
exactKeys(value.scope, ["projectId", "branchName", "themeId"]);
|
|
73
|
+
}
|
|
74
|
+
function assertIdentity(value) {
|
|
75
|
+
assertHello(value);
|
|
76
|
+
if (!string(value.runtimeInstanceId))
|
|
77
|
+
throw new ProtocolError("IDENTITY", "Missing runtime instance");
|
|
78
|
+
}
|
|
79
|
+
function sameIdentity(a, b) {
|
|
80
|
+
try {
|
|
81
|
+
assertIdentity(b);
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return [
|
|
86
|
+
"protocolVersion",
|
|
87
|
+
"sessionId",
|
|
88
|
+
"frameEpoch",
|
|
89
|
+
"runtimeInstanceId",
|
|
90
|
+
"schemaVersion",
|
|
91
|
+
"configContractVersion",
|
|
92
|
+
"locale"
|
|
93
|
+
].every((k) => a[k] === b[k]) && fingerprint(a.scope) === fingerprint(b.scope);
|
|
94
|
+
}
|
|
95
|
+
function assertBinding(expected, actual) {
|
|
96
|
+
if (fingerprint(expected.scope) !== fingerprint(actual.scope) || expected.locale !== actual.locale || expected.schemaVersion !== actual.schemaVersion || expected.configContractVersion !== actual.configContractVersion)
|
|
97
|
+
throw new ProtocolError("CONTRACT", "Scope, locale, schema or configuration contract mismatch");
|
|
98
|
+
}
|
|
99
|
+
function assertDocuments(value) {
|
|
100
|
+
assertJson(value);
|
|
101
|
+
if (!record(value) || Object.values(value).some((v) => !record(v)))
|
|
102
|
+
throw new ProtocolError("INVALID", "Expected raw document objects");
|
|
103
|
+
}
|
|
104
|
+
var identityKeys = [
|
|
105
|
+
"protocolVersion",
|
|
106
|
+
"sessionId",
|
|
107
|
+
"frameEpoch",
|
|
108
|
+
"runtimeInstanceId",
|
|
109
|
+
"scope",
|
|
110
|
+
"schemaVersion",
|
|
111
|
+
"configContractVersion",
|
|
112
|
+
"locale"
|
|
113
|
+
];
|
|
114
|
+
function assertImageResourceRequest(value) {
|
|
115
|
+
assertIdentity(value);
|
|
116
|
+
const request = value;
|
|
117
|
+
exactKeys(value, [
|
|
118
|
+
...identityKeys,
|
|
119
|
+
"draftRevision",
|
|
120
|
+
"documentId",
|
|
121
|
+
"path",
|
|
122
|
+
"value",
|
|
123
|
+
"resourceSequence",
|
|
124
|
+
"expectedContentHash"
|
|
125
|
+
]);
|
|
126
|
+
if (!revision(request.draftRevision) || !string(request.documentId) || forbidden.has(request.documentId) || !Array.isArray(request.path) || !request.path.length || request.path.length > 32 || request.path.some((part) => !string(part) || forbidden.has(part)) || !string(request.value) || (request.resourceSequence !== void 0 || request.expectedContentHash !== void 0) && (!revision(request.resourceSequence) || request.resourceSequence === 0 || request.expectedContentHash !== void 0 && (typeof request.expectedContentHash !== "string" || !/^[a-f0-9]{64}$/.test(request.expectedContentHash))))
|
|
127
|
+
throw new ProtocolError("INVALID", "Invalid image resource request");
|
|
128
|
+
}
|
|
129
|
+
function assertImageRefresh(value) {
|
|
130
|
+
assertIdentity(value);
|
|
131
|
+
const request = value;
|
|
132
|
+
exactKeys(value, [
|
|
133
|
+
...identityKeys,
|
|
134
|
+
"draftRevision",
|
|
135
|
+
"resourceSequence",
|
|
136
|
+
"images"
|
|
137
|
+
]);
|
|
138
|
+
if (!revision(request.draftRevision) || !revision(request.resourceSequence) || request.resourceSequence === 0 || !Array.isArray(request.images) || !request.images.length || request.images.length > 64)
|
|
139
|
+
throw new ProtocolError("INVALID", "Invalid image refresh sequence or target count");
|
|
140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
141
|
+
for (const image of request.images) {
|
|
142
|
+
if (!record(image) || !string(image.documentId) || forbidden.has(image.documentId) || !Array.isArray(image.path) || !image.path.length || image.path.length > 32 || image.path.some((part) => !string(part) || forbidden.has(part)) || image.contentHash !== null && (typeof image.contentHash !== "string" || !/^[a-f0-9]{64}$/.test(image.contentHash)))
|
|
143
|
+
throw new ProtocolError("INVALID", "Invalid image refresh target");
|
|
144
|
+
exactKeys(image, ["documentId", "path", "contentHash"]);
|
|
145
|
+
const key = fingerprint([image.documentId, image.path]);
|
|
146
|
+
if (seen.has(key)) throw new ProtocolError("INVALID", "Duplicate image refresh target");
|
|
147
|
+
seen.add(key);
|
|
148
|
+
}
|
|
149
|
+
limit(value, 64 * 1024);
|
|
150
|
+
}
|
|
151
|
+
function assertImageResourceData(value) {
|
|
152
|
+
if (!record(value) || typeof value.mimeType !== "string" || typeof value.dataUrl !== "string")
|
|
153
|
+
throw new ProtocolError("INVALID", "Invalid image resource response");
|
|
154
|
+
const allowed = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif", "image/bmp"];
|
|
155
|
+
if (!allowed.includes(value.mimeType))
|
|
156
|
+
throw new ProtocolError("INVALID", "Unsupported image resource type");
|
|
157
|
+
const prefix = `data:${value.mimeType};base64,`;
|
|
158
|
+
const data = value.dataUrl.startsWith(prefix) ? value.dataUrl.slice(prefix.length) : "";
|
|
159
|
+
if (!data || data.length % 4 !== 0 || data.length > Math.ceil(MAX_IMAGE_RESOURCE_BYTES / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data))
|
|
160
|
+
throw new ProtocolError("LIMIT", "Invalid or oversized image resource data");
|
|
161
|
+
const bytes = data.length / 4 * 3 - (data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
|
|
162
|
+
if (bytes > MAX_IMAGE_RESOURCE_BYTES)
|
|
163
|
+
throw new ProtocolError("LIMIT", "Image resource exceeds the byte limit");
|
|
164
|
+
if (value.contentHash !== void 0 && !/^[a-f0-9]{64}$/.test(String(value.contentHash)))
|
|
165
|
+
throw new ProtocolError("INVALID", "Invalid image content fingerprint");
|
|
166
|
+
}
|
|
167
|
+
function assertSnapshot(value) {
|
|
168
|
+
assertIdentity(value);
|
|
169
|
+
exactKeys(value, [
|
|
170
|
+
...identityKeys,
|
|
171
|
+
"draftRevision",
|
|
172
|
+
"documents"
|
|
173
|
+
]);
|
|
174
|
+
if (!revision(value.draftRevision))
|
|
175
|
+
throw new ProtocolError("INVALID", "Invalid snapshot revision");
|
|
176
|
+
assertDocuments(value.documents);
|
|
177
|
+
limit(value, 512 * 1024);
|
|
178
|
+
}
|
|
179
|
+
function assertPatch(value) {
|
|
180
|
+
assertIdentity(value);
|
|
181
|
+
const v = value;
|
|
182
|
+
exactKeys(v, [
|
|
183
|
+
...identityKeys,
|
|
184
|
+
"baseDraftRevision",
|
|
185
|
+
"draftRevision",
|
|
186
|
+
"phase",
|
|
187
|
+
"operations"
|
|
188
|
+
]);
|
|
189
|
+
if (!revision(v.baseDraftRevision) || !revision(v.draftRevision) || v.draftRevision !== v.baseDraftRevision + 1 || !["preview", "commit"].includes(v.phase) || !Array.isArray(v.operations) || v.operations.length > 256)
|
|
190
|
+
throw new ProtocolError("INVALID", "Invalid patch sequence or operation count");
|
|
191
|
+
for (const op of v.operations) {
|
|
192
|
+
if (!record(op) || !["set", "remove"].includes(op.op) || !string(op.documentId) || forbidden.has(op.documentId) || !Array.isArray(op.path) || !op.path.length || op.path.length > 32 || op.path.some((p) => !string(p) || forbidden.has(p)))
|
|
193
|
+
throw new ProtocolError("INVALID", "Unsafe or invalid operation path");
|
|
194
|
+
exactKeys(
|
|
195
|
+
op,
|
|
196
|
+
op.op === "set" ? ["op", "documentId", "path", "value"] : ["op", "documentId", "path"]
|
|
197
|
+
);
|
|
198
|
+
if (op.op === "set") assertJson(op.value);
|
|
199
|
+
}
|
|
200
|
+
limit(v, 64 * 1024);
|
|
201
|
+
}
|
|
202
|
+
function limit(value, bytes) {
|
|
203
|
+
if (new TextEncoder().encode(JSON.stringify(value)).length > bytes)
|
|
204
|
+
throw new ProtocolError("LIMIT", "Runtime message exceeds byte limit");
|
|
205
|
+
}
|
|
206
|
+
function applyOperations(documents, operations) {
|
|
207
|
+
const next = clone(documents);
|
|
208
|
+
for (const op of operations) {
|
|
209
|
+
if (op.op === "remove" && readPath(next[op.documentId], op.path) === void 0) continue;
|
|
210
|
+
let target = next[op.documentId] ??= {};
|
|
211
|
+
for (const part of op.path.slice(0, -1)) {
|
|
212
|
+
if (!record(target[part])) target[part] = {};
|
|
213
|
+
target = target[part];
|
|
214
|
+
}
|
|
215
|
+
const key = op.path[op.path.length - 1];
|
|
216
|
+
if (op.op === "set") target[key] = clone(op.value);
|
|
217
|
+
else delete target[key];
|
|
218
|
+
}
|
|
219
|
+
return next;
|
|
220
|
+
}
|
|
221
|
+
function readPath(value, path) {
|
|
222
|
+
for (const key of path) {
|
|
223
|
+
if (!record(value) && !Array.isArray(value)) return void 0;
|
|
224
|
+
value = value[key];
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
|
|
1
229
|
// ../iframe-bridge/package.json
|
|
2
230
|
var package_default = {
|
|
3
231
|
name: "@playcraft/iframe-bridge",
|
|
@@ -48,7 +276,7 @@ var package_default = {
|
|
|
48
276
|
};
|
|
49
277
|
|
|
50
278
|
// ../iframe-bridge/src/configs/constants.ts
|
|
51
|
-
var
|
|
279
|
+
var PROTOCOL_VERSION2 = package_default.version;
|
|
52
280
|
var DEFAULT_REQUEST_TIMEOUT_MS = 15e4;
|
|
53
281
|
var BridgeErrorCodes = {
|
|
54
282
|
ABORTED: "ABORTED",
|
|
@@ -110,11 +338,11 @@ function isPlaycraftEnvelopeShape(data) {
|
|
|
110
338
|
function getPlaycraftEnvelopeVersionMismatch(data) {
|
|
111
339
|
if (!isPlaycraftEnvelopeShape(data)) return void 0;
|
|
112
340
|
const peer = data.__playcraft__;
|
|
113
|
-
if (peer ===
|
|
114
|
-
return { local:
|
|
341
|
+
if (peer === PROTOCOL_VERSION2) return void 0;
|
|
342
|
+
return { local: PROTOCOL_VERSION2, peer };
|
|
115
343
|
}
|
|
116
344
|
function createEnvelope(kind, id, name, data) {
|
|
117
|
-
return { __playcraft__:
|
|
345
|
+
return { __playcraft__: PROTOCOL_VERSION2, kind, id, name, data };
|
|
118
346
|
}
|
|
119
347
|
function createSuccessResponse(id, name, data) {
|
|
120
348
|
return { ...createEnvelope("res", id, name), ret: RetCodes.OK, data };
|
|
@@ -202,6 +430,12 @@ var BridgeMethods = {
|
|
|
202
430
|
capabilities: "runtime.parameters.capabilities",
|
|
203
431
|
result: "runtime.parameters.result",
|
|
204
432
|
resyncRequired: "runtime.parameters.resync-required"
|
|
433
|
+
},
|
|
434
|
+
levelEditor: {
|
|
435
|
+
hello: "runtime.level-editor.hello",
|
|
436
|
+
snapshot: "runtime.level-editor.snapshot",
|
|
437
|
+
cancel: "runtime.level-editor.cancel",
|
|
438
|
+
dispose: "runtime.level-editor.dispose"
|
|
205
439
|
}
|
|
206
440
|
};
|
|
207
441
|
var BRIDGE_EVENT_NAMES = [
|
|
@@ -224,7 +458,11 @@ var BRIDGE_RPC_NAMES = [
|
|
|
224
458
|
BridgeMethods.runtimeParameters.status,
|
|
225
459
|
BridgeMethods.runtimeParameters.dispose,
|
|
226
460
|
BridgeMethods.runtimeParameters.asset,
|
|
227
|
-
BridgeMethods.runtimeParameters.refreshImages
|
|
461
|
+
BridgeMethods.runtimeParameters.refreshImages,
|
|
462
|
+
BridgeMethods.levelEditor.hello,
|
|
463
|
+
BridgeMethods.levelEditor.snapshot,
|
|
464
|
+
BridgeMethods.levelEditor.cancel,
|
|
465
|
+
BridgeMethods.levelEditor.dispose
|
|
228
466
|
];
|
|
229
467
|
|
|
230
468
|
// ../iframe-bridge/src/configs/perspective.ts
|
|
@@ -479,264 +717,471 @@ var Bridge = class {
|
|
|
479
717
|
addEventListener(type, listener) {
|
|
480
718
|
this.assertAlive();
|
|
481
719
|
this.listeners.add(listener);
|
|
482
|
-
return () => this.listeners.delete(listener);
|
|
483
|
-
}
|
|
484
|
-
destroy() {
|
|
485
|
-
if (this.destroyed) return;
|
|
486
|
-
this.destroyed = true;
|
|
487
|
-
this.messageTarget.removeEventListener("message", this.onWindowMessage);
|
|
488
|
-
this.listeners.clear();
|
|
489
|
-
}
|
|
490
|
-
assertAlive() {
|
|
491
|
-
if (this.destroyed) {
|
|
492
|
-
throw new PlaycraftBridgeError(BridgeErrorCodes.DESTROYED, "[playcraft] bridge destroyed");
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
log(event, msg) {
|
|
496
|
-
const { debug, perspective } = this.options;
|
|
497
|
-
if (!debug) return;
|
|
498
|
-
if (typeof debug === "function") debug(event, msg);
|
|
499
|
-
else logBridgeDebug(perspective, event, msg);
|
|
500
|
-
}
|
|
501
|
-
warnVersionMismatch(localVersion, peerVersion) {
|
|
502
|
-
if (this.warnedPeerVersions.has(peerVersion)) return;
|
|
503
|
-
this.warnedPeerVersions.add(peerVersion);
|
|
504
|
-
console.warn(
|
|
505
|
-
`[playcraft] protocol version mismatch: local ${localVersion}, peer ${peerVersion}. Messages are still processed; align @playcraft/iframe-bridge versions when possible.`
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
onWindowMessage = (e) => {
|
|
509
|
-
const msgEvent = e;
|
|
510
|
-
if (this.destroyed || !this.options.allowedOrigins.includes(msgEvent.origin)) return;
|
|
511
|
-
if (this.options.strictSource) {
|
|
512
|
-
const peer = this.options.peer();
|
|
513
|
-
if (!peer || msgEvent.source !== peer) return;
|
|
514
|
-
}
|
|
515
|
-
if (!isPlaycraftEnvelope(msgEvent.data)) return;
|
|
516
|
-
const mismatch = getPlaycraftEnvelopeVersionMismatch(msgEvent.data);
|
|
517
|
-
if (mismatch) {
|
|
518
|
-
this.warnVersionMismatch(mismatch.local, mismatch.peer);
|
|
519
|
-
}
|
|
520
|
-
this.log("recv", msgEvent.data);
|
|
521
|
-
const event = {
|
|
522
|
-
data: msgEvent.data,
|
|
523
|
-
origin: msgEvent.origin,
|
|
524
|
-
source: msgEvent.source
|
|
525
|
-
};
|
|
526
|
-
this.listeners.forEach((fn) => fn(event));
|
|
527
|
-
};
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
// ../preview-runtime/src/protocol.ts
|
|
531
|
-
var MAX_IMAGE_RESOURCE_BYTES = 32 * 1024 * 1024;
|
|
532
|
-
var ResourceLoadError = class extends Error {
|
|
533
|
-
constructor(message) {
|
|
534
|
-
super(message);
|
|
535
|
-
this.name = "ResourceLoadError";
|
|
536
|
-
}
|
|
537
|
-
};
|
|
538
|
-
var SupersededResourceError = class extends Error {
|
|
539
|
-
constructor() {
|
|
540
|
-
super("A newer image value replaced this resource request");
|
|
541
|
-
this.name = "SupersededResourceError";
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
var ProtocolError = class extends Error {
|
|
545
|
-
constructor(code, message) {
|
|
546
|
-
super(message);
|
|
547
|
-
this.code = code;
|
|
548
|
-
this.name = "ProtocolError";
|
|
549
|
-
}
|
|
550
|
-
code;
|
|
551
|
-
};
|
|
552
|
-
var forbidden = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
553
|
-
var record = (value) => !!value && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
554
|
-
var exactKeys = (value, allowed) => {
|
|
555
|
-
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
556
|
-
throw new ProtocolError("INVALID", "Unexpected message field");
|
|
557
|
-
};
|
|
558
|
-
var string = (v) => typeof v === "string" && v.length > 0 && v.length <= 1024;
|
|
559
|
-
var revision = (v) => Number.isSafeInteger(v) && v >= 0;
|
|
560
|
-
function assertJson(value, depth = 0) {
|
|
561
|
-
if (depth > 64) throw new ProtocolError("LIMIT", "Configuration nesting exceeds limit");
|
|
562
|
-
if (value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value))
|
|
563
|
-
return;
|
|
564
|
-
if (Array.isArray(value)) {
|
|
565
|
-
for (const child of value) assertJson(child, depth + 1);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
if (record(value)) {
|
|
569
|
-
for (const [key, child] of Object.entries(value)) {
|
|
570
|
-
if (forbidden.has(key)) throw new ProtocolError("INVALID", "Unsafe configuration key");
|
|
571
|
-
assertJson(child, depth + 1);
|
|
572
|
-
}
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
throw new ProtocolError("INVALID", "Configuration must contain finite JSON data");
|
|
576
|
-
}
|
|
577
|
-
function clone(value) {
|
|
578
|
-
return structuredClone(value);
|
|
579
|
-
}
|
|
580
|
-
function freeze(value) {
|
|
581
|
-
if (value && typeof value === "object") {
|
|
582
|
-
Object.freeze(value);
|
|
583
|
-
for (const child of Object.values(value)) freeze(child);
|
|
584
|
-
}
|
|
585
|
-
return value;
|
|
586
|
-
}
|
|
587
|
-
function fingerprint(value) {
|
|
588
|
-
if (Array.isArray(value)) return "[" + value.map(fingerprint).join(",") + "]";
|
|
589
|
-
if (record(value))
|
|
590
|
-
return "{" + Object.keys(value).sort().map((k) => JSON.stringify(k) + ":" + fingerprint(value[k])).join(",") + "}";
|
|
591
|
-
return JSON.stringify(value);
|
|
592
|
-
}
|
|
593
|
-
function assertHello(value) {
|
|
594
|
-
if (!record(value) || value.protocolVersion !== 1 || !string(value.sessionId) || !revision(value.frameEpoch) || !string(value.schemaVersion) || !string(value.configContractVersion) || !string(value.locale) || !record(value.scope))
|
|
595
|
-
throw new ProtocolError("IDENTITY", "Invalid runtime session binding");
|
|
596
|
-
if (!["projectId", "branchName", "themeId"].every(
|
|
597
|
-
(k) => string(value.scope[k])
|
|
598
|
-
))
|
|
599
|
-
throw new ProtocolError("IDENTITY", "Invalid configuration scope");
|
|
600
|
-
exactKeys(value.scope, ["projectId", "branchName", "themeId"]);
|
|
601
|
-
}
|
|
602
|
-
function assertIdentity(value) {
|
|
603
|
-
assertHello(value);
|
|
604
|
-
if (!string(value.runtimeInstanceId))
|
|
605
|
-
throw new ProtocolError("IDENTITY", "Missing runtime instance");
|
|
606
|
-
}
|
|
607
|
-
function sameIdentity(a, b) {
|
|
608
|
-
try {
|
|
609
|
-
assertIdentity(b);
|
|
610
|
-
} catch {
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
return [
|
|
614
|
-
"protocolVersion",
|
|
615
|
-
"sessionId",
|
|
616
|
-
"frameEpoch",
|
|
617
|
-
"runtimeInstanceId",
|
|
618
|
-
"schemaVersion",
|
|
619
|
-
"configContractVersion",
|
|
620
|
-
"locale"
|
|
621
|
-
].every((k) => a[k] === b[k]) && fingerprint(a.scope) === fingerprint(b.scope);
|
|
622
|
-
}
|
|
623
|
-
function assertBinding(expected, actual) {
|
|
624
|
-
if (fingerprint(expected.scope) !== fingerprint(actual.scope) || expected.locale !== actual.locale || expected.schemaVersion !== actual.schemaVersion || expected.configContractVersion !== actual.configContractVersion)
|
|
625
|
-
throw new ProtocolError("CONTRACT", "Scope, locale, schema or configuration contract mismatch");
|
|
626
|
-
}
|
|
627
|
-
function assertDocuments(value) {
|
|
628
|
-
assertJson(value);
|
|
629
|
-
if (!record(value) || Object.values(value).some((v) => !record(v)))
|
|
630
|
-
throw new ProtocolError("INVALID", "Expected raw document objects");
|
|
631
|
-
}
|
|
632
|
-
var identityKeys = [
|
|
633
|
-
"protocolVersion",
|
|
634
|
-
"sessionId",
|
|
635
|
-
"frameEpoch",
|
|
636
|
-
"runtimeInstanceId",
|
|
637
|
-
"scope",
|
|
638
|
-
"schemaVersion",
|
|
639
|
-
"configContractVersion",
|
|
640
|
-
"locale"
|
|
641
|
-
];
|
|
642
|
-
function assertImageRefresh(value) {
|
|
643
|
-
assertIdentity(value);
|
|
644
|
-
const request = value;
|
|
645
|
-
exactKeys(value, [
|
|
646
|
-
...identityKeys,
|
|
647
|
-
"draftRevision",
|
|
648
|
-
"resourceSequence",
|
|
649
|
-
"images"
|
|
650
|
-
]);
|
|
651
|
-
if (!revision(request.draftRevision) || !revision(request.resourceSequence) || request.resourceSequence === 0 || !Array.isArray(request.images) || !request.images.length || request.images.length > 64)
|
|
652
|
-
throw new ProtocolError("INVALID", "Invalid image refresh sequence or target count");
|
|
653
|
-
const seen = /* @__PURE__ */ new Set();
|
|
654
|
-
for (const image of request.images) {
|
|
655
|
-
if (!record(image) || !string(image.documentId) || forbidden.has(image.documentId) || !Array.isArray(image.path) || !image.path.length || image.path.length > 32 || image.path.some((part) => !string(part) || forbidden.has(part)) || image.contentHash !== null && (typeof image.contentHash !== "string" || !/^[a-f0-9]{64}$/.test(image.contentHash)))
|
|
656
|
-
throw new ProtocolError("INVALID", "Invalid image refresh target");
|
|
657
|
-
exactKeys(image, ["documentId", "path", "contentHash"]);
|
|
658
|
-
const key = fingerprint([image.documentId, image.path]);
|
|
659
|
-
if (seen.has(key)) throw new ProtocolError("INVALID", "Duplicate image refresh target");
|
|
660
|
-
seen.add(key);
|
|
661
|
-
}
|
|
662
|
-
limit(value, 64 * 1024);
|
|
663
|
-
}
|
|
664
|
-
function assertImageResourceData(value) {
|
|
665
|
-
if (!record(value) || typeof value.mimeType !== "string" || typeof value.dataUrl !== "string")
|
|
666
|
-
throw new ProtocolError("INVALID", "Invalid image resource response");
|
|
667
|
-
const allowed = ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif", "image/bmp"];
|
|
668
|
-
if (!allowed.includes(value.mimeType))
|
|
669
|
-
throw new ProtocolError("INVALID", "Unsupported image resource type");
|
|
670
|
-
const prefix = `data:${value.mimeType};base64,`;
|
|
671
|
-
const data = value.dataUrl.startsWith(prefix) ? value.dataUrl.slice(prefix.length) : "";
|
|
672
|
-
if (!data || data.length % 4 !== 0 || data.length > Math.ceil(MAX_IMAGE_RESOURCE_BYTES / 3) * 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data))
|
|
673
|
-
throw new ProtocolError("LIMIT", "Invalid or oversized image resource data");
|
|
674
|
-
const bytes = data.length / 4 * 3 - (data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
|
|
675
|
-
if (bytes > MAX_IMAGE_RESOURCE_BYTES)
|
|
676
|
-
throw new ProtocolError("LIMIT", "Image resource exceeds the byte limit");
|
|
677
|
-
if (value.contentHash !== void 0 && !/^[a-f0-9]{64}$/.test(String(value.contentHash)))
|
|
678
|
-
throw new ProtocolError("INVALID", "Invalid image content fingerprint");
|
|
679
|
-
}
|
|
680
|
-
function assertSnapshot(value) {
|
|
681
|
-
assertIdentity(value);
|
|
682
|
-
exactKeys(value, [
|
|
683
|
-
...identityKeys,
|
|
684
|
-
"draftRevision",
|
|
685
|
-
"documents"
|
|
686
|
-
]);
|
|
687
|
-
if (!revision(value.draftRevision))
|
|
688
|
-
throw new ProtocolError("INVALID", "Invalid snapshot revision");
|
|
689
|
-
assertDocuments(value.documents);
|
|
690
|
-
limit(value, 512 * 1024);
|
|
691
|
-
}
|
|
692
|
-
function assertPatch(value) {
|
|
693
|
-
assertIdentity(value);
|
|
694
|
-
const v = value;
|
|
695
|
-
exactKeys(v, [
|
|
696
|
-
...identityKeys,
|
|
697
|
-
"baseDraftRevision",
|
|
698
|
-
"draftRevision",
|
|
699
|
-
"phase",
|
|
700
|
-
"operations"
|
|
701
|
-
]);
|
|
702
|
-
if (!revision(v.baseDraftRevision) || !revision(v.draftRevision) || v.draftRevision !== v.baseDraftRevision + 1 || !["preview", "commit"].includes(v.phase) || !Array.isArray(v.operations) || v.operations.length > 256)
|
|
703
|
-
throw new ProtocolError("INVALID", "Invalid patch sequence or operation count");
|
|
704
|
-
for (const op of v.operations) {
|
|
705
|
-
if (!record(op) || !["set", "remove"].includes(op.op) || !string(op.documentId) || forbidden.has(op.documentId) || !Array.isArray(op.path) || !op.path.length || op.path.length > 32 || op.path.some((p) => !string(p) || forbidden.has(p)))
|
|
706
|
-
throw new ProtocolError("INVALID", "Unsafe or invalid operation path");
|
|
707
|
-
exactKeys(
|
|
708
|
-
op,
|
|
709
|
-
op.op === "set" ? ["op", "documentId", "path", "value"] : ["op", "documentId", "path"]
|
|
720
|
+
return () => this.listeners.delete(listener);
|
|
721
|
+
}
|
|
722
|
+
destroy() {
|
|
723
|
+
if (this.destroyed) return;
|
|
724
|
+
this.destroyed = true;
|
|
725
|
+
this.messageTarget.removeEventListener("message", this.onWindowMessage);
|
|
726
|
+
this.listeners.clear();
|
|
727
|
+
}
|
|
728
|
+
assertAlive() {
|
|
729
|
+
if (this.destroyed) {
|
|
730
|
+
throw new PlaycraftBridgeError(BridgeErrorCodes.DESTROYED, "[playcraft] bridge destroyed");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
log(event, msg) {
|
|
734
|
+
const { debug, perspective } = this.options;
|
|
735
|
+
if (!debug) return;
|
|
736
|
+
if (typeof debug === "function") debug(event, msg);
|
|
737
|
+
else logBridgeDebug(perspective, event, msg);
|
|
738
|
+
}
|
|
739
|
+
warnVersionMismatch(localVersion, peerVersion) {
|
|
740
|
+
if (this.warnedPeerVersions.has(peerVersion)) return;
|
|
741
|
+
this.warnedPeerVersions.add(peerVersion);
|
|
742
|
+
console.warn(
|
|
743
|
+
`[playcraft] protocol version mismatch: local ${localVersion}, peer ${peerVersion}. Messages are still processed; align @playcraft/iframe-bridge versions when possible.`
|
|
710
744
|
);
|
|
711
|
-
if (op.op === "set") assertJson(op.value);
|
|
712
745
|
}
|
|
713
|
-
|
|
746
|
+
onWindowMessage = (e) => {
|
|
747
|
+
const msgEvent = e;
|
|
748
|
+
if (this.destroyed || !this.options.allowedOrigins.includes(msgEvent.origin)) return;
|
|
749
|
+
if (this.options.strictSource) {
|
|
750
|
+
const peer = this.options.peer();
|
|
751
|
+
if (!peer || msgEvent.source !== peer) return;
|
|
752
|
+
}
|
|
753
|
+
if (!isPlaycraftEnvelope(msgEvent.data)) return;
|
|
754
|
+
const mismatch = getPlaycraftEnvelopeVersionMismatch(msgEvent.data);
|
|
755
|
+
if (mismatch) {
|
|
756
|
+
this.warnVersionMismatch(mismatch.local, mismatch.peer);
|
|
757
|
+
}
|
|
758
|
+
this.log("recv", msgEvent.data);
|
|
759
|
+
const event = {
|
|
760
|
+
data: msgEvent.data,
|
|
761
|
+
origin: msgEvent.origin,
|
|
762
|
+
source: msgEvent.source
|
|
763
|
+
};
|
|
764
|
+
this.listeners.forEach((fn) => fn(event));
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// ../preview-runtime/src/host.ts
|
|
769
|
+
function validateCapabilities(value, binding) {
|
|
770
|
+
assertIdentity(value);
|
|
771
|
+
assertBinding(binding, value);
|
|
772
|
+
const cap = value;
|
|
773
|
+
if (!["bootstrap-ready", "game-ready"].includes(cap.stage) || cap.canAcceptSnapshot !== true || cap.canRefreshImages !== void 0 && cap.canRefreshImages !== true || cap.canReportMissingImages !== void 0 && (cap.canReportMissingImages !== true || cap.canRefreshImages !== true) || !Array.isArray(cap.fields) || cap.fields.some(
|
|
774
|
+
(field) => !field || typeof field.documentId !== "string" || !Array.isArray(field.path) || !field.path.every((p) => typeof p === "string") || !Array.isArray(field.effectivePath) || !field.effectivePath.every((p) => typeof p === "string") || typeof field.groupId !== "string" || field.resource !== void 0 && field.resource !== "image" || !["live", "restart-scene", "reload"].includes(field.mode)
|
|
775
|
+
))
|
|
776
|
+
throw new ProtocolError("INVALID", "Invalid runtime capabilities");
|
|
777
|
+
return clone(cap);
|
|
714
778
|
}
|
|
715
|
-
function
|
|
716
|
-
if (
|
|
717
|
-
|
|
779
|
+
function validateResult(value, identity) {
|
|
780
|
+
if (!sameIdentity(identity, value)) throw new ProtocolError("STALE", "Stale result identity");
|
|
781
|
+
const result = value;
|
|
782
|
+
if (!revision(result.resultSequence) || ![result.receivedRevision, result.installedRevision, result.appliedRevision].every(
|
|
783
|
+
(v) => Number.isSafeInteger(v) && v >= -1
|
|
784
|
+
) || result.appliedRevision > result.receivedRevision || result.installedRevision > result.receivedRevision || !["idle", "applying", "applied", "pending-commit", "requires-reload", "error"].includes(
|
|
785
|
+
result.status
|
|
786
|
+
) || result.recoverable !== void 0 && result.recoverable !== true || result.resourceErrors !== void 0 && (!Array.isArray(result.resourceErrors) || result.resourceErrors.some(
|
|
787
|
+
(item) => !item || typeof item.documentId !== "string" || !Array.isArray(item.path) || !item.path.every((part) => typeof part === "string") || typeof item.error !== "string"
|
|
788
|
+
)) || !Array.isArray(result.groups) || result.groups.some(
|
|
789
|
+
(g) => !g || typeof g.groupId !== "string" || !Number.isSafeInteger(g.appliedRevision) || !["applied", "pending-commit", "requires-reload", "unknown"].includes(g.status)
|
|
790
|
+
))
|
|
791
|
+
throw new ProtocolError("INVALID", "Invalid application result");
|
|
792
|
+
return clone(result);
|
|
718
793
|
}
|
|
719
|
-
function
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
794
|
+
function createParameterHost(options) {
|
|
795
|
+
const origin = new URL(options.targetOrigin).origin;
|
|
796
|
+
if (origin === "null" || origin !== options.targetOrigin)
|
|
797
|
+
throw new ProtocolError("ORIGIN", "An exact preview origin is required");
|
|
798
|
+
const opaque = options.transport === "opaque-sandbox";
|
|
799
|
+
if (options.transport !== void 0 && options.transport !== "origin" && !opaque)
|
|
800
|
+
throw new ProtocolError("ORIGIN", "Unknown preview transport");
|
|
801
|
+
function assertFrameOrigin() {
|
|
802
|
+
if (!opaque) return;
|
|
803
|
+
let frameUrl;
|
|
804
|
+
try {
|
|
805
|
+
frameUrl = new URL(options.iframe.src);
|
|
806
|
+
} catch {
|
|
807
|
+
throw new ProtocolError("ORIGIN", "An absolute preview address is required");
|
|
727
808
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
else delete target[key];
|
|
809
|
+
if (!["http:", "https:"].includes(frameUrl.protocol) || frameUrl.origin !== origin)
|
|
810
|
+
throw new ProtocolError("ORIGIN", "The preview address does not match its expected origin");
|
|
731
811
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
812
|
+
assertFrameOrigin();
|
|
813
|
+
assertDocuments(options.documents);
|
|
814
|
+
const binding = freeze(clone(options.binding));
|
|
815
|
+
const sessionId = crypto.randomUUID();
|
|
816
|
+
let frameEpoch = 0, identity;
|
|
817
|
+
let bridge;
|
|
818
|
+
let abort = new AbortController();
|
|
819
|
+
let serial = Promise.resolve();
|
|
820
|
+
let state = {
|
|
821
|
+
connection: "idle",
|
|
822
|
+
draftRevision: 0,
|
|
823
|
+
documents: freeze(clone(options.documents))
|
|
824
|
+
};
|
|
825
|
+
let disposed = false;
|
|
826
|
+
const documentHistory = /* @__PURE__ */ new Map([[0, state.documents]]);
|
|
827
|
+
const imageRequests = /* @__PURE__ */ new Map();
|
|
828
|
+
const refreshTargets = /* @__PURE__ */ new Map();
|
|
829
|
+
let resourceSequence = 0;
|
|
830
|
+
let imageReads = 0;
|
|
831
|
+
const imageWaiters = /* @__PURE__ */ new Set();
|
|
832
|
+
async function imageSlot(signal) {
|
|
833
|
+
while (imageReads >= 2) {
|
|
834
|
+
await new Promise((resolve, reject) => {
|
|
835
|
+
const wake = () => {
|
|
836
|
+
imageWaiters.delete(wake);
|
|
837
|
+
signal.removeEventListener("abort", cancelled);
|
|
838
|
+
resolve();
|
|
839
|
+
};
|
|
840
|
+
const cancelled = () => {
|
|
841
|
+
imageWaiters.delete(wake);
|
|
842
|
+
reject(signal.reason);
|
|
843
|
+
};
|
|
844
|
+
signal.throwIfAborted();
|
|
845
|
+
signal.addEventListener("abort", cancelled, { once: true });
|
|
846
|
+
imageWaiters.add(wake);
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
signal.throwIfAborted();
|
|
850
|
+
imageReads++;
|
|
851
|
+
return () => {
|
|
852
|
+
imageReads--;
|
|
853
|
+
[...imageWaiters].forEach((wake) => wake());
|
|
854
|
+
};
|
|
738
855
|
}
|
|
739
|
-
|
|
856
|
+
const names = BridgeMethods.runtimeParameters;
|
|
857
|
+
const getState = () => freeze(clone(state));
|
|
858
|
+
const publish = () => options.onState?.(getState());
|
|
859
|
+
function receiveResult(value, currentIdentity) {
|
|
860
|
+
const result = validateResult(value, currentIdentity);
|
|
861
|
+
if (state.result && result.resultSequence < state.result.resultSequence) return;
|
|
862
|
+
state = { ...state, result };
|
|
863
|
+
publish();
|
|
864
|
+
}
|
|
865
|
+
function failed(error, epoch) {
|
|
866
|
+
if (epoch !== frameEpoch || disposed) return;
|
|
867
|
+
state = {
|
|
868
|
+
...state,
|
|
869
|
+
connection: "error",
|
|
870
|
+
error: error instanceof Error ? error.message : String(error)
|
|
871
|
+
};
|
|
872
|
+
publish();
|
|
873
|
+
}
|
|
874
|
+
function receipt(value, expected) {
|
|
875
|
+
const r = value;
|
|
876
|
+
if (!r || r.accepted !== true || r.receivedRevision !== expected)
|
|
877
|
+
throw new ProtocolError("INVALID", "Invalid acceptance receipt");
|
|
878
|
+
return r;
|
|
879
|
+
}
|
|
880
|
+
const host = {
|
|
881
|
+
getState,
|
|
882
|
+
async connect() {
|
|
883
|
+
if (disposed) throw new ProtocolError("DISPOSED", "Preview host is disposed");
|
|
884
|
+
const epoch = ++frameEpoch;
|
|
885
|
+
abort.abort();
|
|
886
|
+
abort = new AbortController();
|
|
887
|
+
const signal = abort.signal;
|
|
888
|
+
bridge?.destroy();
|
|
889
|
+
bridge = void 0;
|
|
890
|
+
identity = void 0;
|
|
891
|
+
serial = Promise.resolve();
|
|
892
|
+
refreshTargets.clear();
|
|
893
|
+
resourceSequence = 0;
|
|
894
|
+
state = {
|
|
895
|
+
...state,
|
|
896
|
+
connection: "connecting",
|
|
897
|
+
capabilities: void 0,
|
|
898
|
+
result: void 0,
|
|
899
|
+
error: void 0
|
|
900
|
+
};
|
|
901
|
+
publish();
|
|
902
|
+
try {
|
|
903
|
+
assertFrameOrigin();
|
|
904
|
+
} catch (error) {
|
|
905
|
+
failed(error, epoch);
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
const current = new Bridge({
|
|
909
|
+
peer: () => options.iframe.contentWindow,
|
|
910
|
+
targetOrigin: opaque ? "*" : origin,
|
|
911
|
+
allowedOrigins: [opaque ? "null" : origin],
|
|
912
|
+
strictSource: true
|
|
913
|
+
});
|
|
914
|
+
bridge = current;
|
|
915
|
+
const hello = { ...binding, protocolVersion: PROTOCOL_VERSION, sessionId, frameEpoch: epoch };
|
|
916
|
+
assertHello(hello);
|
|
917
|
+
current.on(names.capabilities, (value) => {
|
|
918
|
+
if (!identity || signal.aborted || !sameIdentity(identity, value)) return;
|
|
919
|
+
try {
|
|
920
|
+
state = { ...state, capabilities: validateCapabilities(value, binding) };
|
|
921
|
+
publish();
|
|
922
|
+
} catch (error) {
|
|
923
|
+
failed(error, epoch);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
current.on(names.result, (value) => {
|
|
927
|
+
if (!identity || signal.aborted || !sameIdentity(identity, value)) return;
|
|
928
|
+
try {
|
|
929
|
+
receiveResult(value, identity);
|
|
930
|
+
} catch (error) {
|
|
931
|
+
failed(error, epoch);
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
current.on(names.resyncRequired, (value) => {
|
|
935
|
+
if (!identity || signal.aborted || !sameIdentity(identity, value)) return;
|
|
936
|
+
failed(
|
|
937
|
+
new ProtocolError(
|
|
938
|
+
"RESYNC",
|
|
939
|
+
"Runtime requested a complete snapshot; reconnect to recover"
|
|
940
|
+
),
|
|
941
|
+
epoch
|
|
942
|
+
);
|
|
943
|
+
});
|
|
944
|
+
current.on(names.asset, async (value) => {
|
|
945
|
+
assertImageResourceRequest(value);
|
|
946
|
+
if (!identity || signal.aborted || !sameIdentity(identity, value))
|
|
947
|
+
throw new ProtocolError("STALE", "Stale image resource identity");
|
|
948
|
+
assertFrameOrigin();
|
|
949
|
+
const frameAddress = options.iframe.src;
|
|
950
|
+
if (!state.capabilities?.fields.some(
|
|
951
|
+
(field) => field.resource === "image" && field.documentId === value.documentId && fingerprint(field.path) === fingerprint(value.path)
|
|
952
|
+
))
|
|
953
|
+
throw new ProtocolError("UNSUPPORTED", "Unregistered image resource field");
|
|
954
|
+
if (!options.resolveImage)
|
|
955
|
+
throw new ProtocolError("UNSUPPORTED", "Image resource reader is unavailable");
|
|
956
|
+
const key = fingerprint([value.documentId, value.path]);
|
|
957
|
+
const refresh = value.resourceSequence === void 0 ? void 0 : refreshTargets.get(key);
|
|
958
|
+
if (value.resourceSequence !== void 0 && (!refresh || refresh.sequence !== value.resourceSequence || (refresh.contentHash === null ? value.expectedContentHash !== void 0 : refresh.contentHash !== value.expectedContentHash) || value.draftRevision < refresh.draftRevision || value.draftRevision > state.draftRevision))
|
|
959
|
+
return { ...value, status: "stale" };
|
|
960
|
+
const version = documentHistory.get(value.draftRevision);
|
|
961
|
+
if (!version && !refresh)
|
|
962
|
+
throw new ProtocolError("STALE", "Expired image resource revision");
|
|
963
|
+
const raw = version ? readPath(version[value.documentId], value.path) : refresh.raw;
|
|
964
|
+
if (raw !== void 0 && raw !== value.value)
|
|
965
|
+
throw new ProtocolError("INVALID", "Image resource value does not match its parameter");
|
|
966
|
+
const stale = () => fingerprint(readPath(state.documents[value.documentId], value.path)) !== fingerprint(raw) || refresh !== void 0 && refreshTargets.get(key) !== refresh;
|
|
967
|
+
if (stale()) return { ...value, status: "stale" };
|
|
968
|
+
if (imageRequests.size >= 64)
|
|
969
|
+
throw new ProtocolError("LIMIT", "Too many pending image resource reads");
|
|
970
|
+
const read = new AbortController();
|
|
971
|
+
const cancel = () => read.abort(signal.reason);
|
|
972
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
973
|
+
imageRequests.set(read, { request: clone(value), raw });
|
|
974
|
+
const timer = setTimeout(
|
|
975
|
+
() => read.abort(new ResourceLoadError("Image resource read timed out")),
|
|
976
|
+
15e3
|
|
977
|
+
);
|
|
978
|
+
let release;
|
|
979
|
+
let offAbort = () => {
|
|
980
|
+
};
|
|
981
|
+
try {
|
|
982
|
+
release = await imageSlot(read.signal);
|
|
983
|
+
read.signal.throwIfAborted();
|
|
984
|
+
const cancelled = new Promise((_, reject) => {
|
|
985
|
+
const onAbort = () => reject(read.signal.reason);
|
|
986
|
+
read.signal.addEventListener("abort", onAbort, { once: true });
|
|
987
|
+
offAbort = () => read.signal.removeEventListener("abort", onAbort);
|
|
988
|
+
});
|
|
989
|
+
const data = await Promise.race([
|
|
990
|
+
options.resolveImage(
|
|
991
|
+
{ documentId: value.documentId, path: clone(value.path), value: value.value },
|
|
992
|
+
read.signal
|
|
993
|
+
),
|
|
994
|
+
cancelled
|
|
995
|
+
]);
|
|
996
|
+
read.signal.throwIfAborted();
|
|
997
|
+
assertFrameOrigin();
|
|
998
|
+
if (options.iframe.src !== frameAddress)
|
|
999
|
+
throw new ProtocolError("ORIGIN", "The preview address changed during an image read");
|
|
1000
|
+
if (!identity || !sameIdentity(identity, value) || stale())
|
|
1001
|
+
return { ...value, status: "stale" };
|
|
1002
|
+
assertImageResourceData(data);
|
|
1003
|
+
if (value.expectedContentHash !== void 0 && data.contentHash !== value.expectedContentHash)
|
|
1004
|
+
throw new ResourceLoadError("Image resource content changed during refresh");
|
|
1005
|
+
return { ...value, ...data, status: "ready" };
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
if (error instanceof SupersededResourceError || stale())
|
|
1008
|
+
return { ...value, status: "stale" };
|
|
1009
|
+
if (signal.aborted || !identity || !sameIdentity(identity, value))
|
|
1010
|
+
throw new ProtocolError("STALE", "Image resource session expired");
|
|
1011
|
+
if (error instanceof ProtocolError) throw error;
|
|
1012
|
+
return {
|
|
1013
|
+
...value,
|
|
1014
|
+
status: "failed",
|
|
1015
|
+
error: error instanceof Error ? error.message : "Image resource read failed"
|
|
1016
|
+
};
|
|
1017
|
+
} finally {
|
|
1018
|
+
clearTimeout(timer);
|
|
1019
|
+
offAbort();
|
|
1020
|
+
signal.removeEventListener("abort", cancel);
|
|
1021
|
+
imageRequests.delete(read);
|
|
1022
|
+
release?.();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
try {
|
|
1026
|
+
let cap;
|
|
1027
|
+
const deadline = Date.now() + 1e4;
|
|
1028
|
+
while (!cap) {
|
|
1029
|
+
signal.throwIfAborted();
|
|
1030
|
+
try {
|
|
1031
|
+
cap = validateCapabilities(
|
|
1032
|
+
await current.emit(names.hello, hello, { timeout: 800, signal }),
|
|
1033
|
+
binding
|
|
1034
|
+
);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (signal.aborted || Date.now() >= deadline || error instanceof ProtocolError)
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (cap.sessionId !== sessionId || cap.frameEpoch !== epoch)
|
|
1041
|
+
throw new ProtocolError("STALE", "Handshake returned a different session");
|
|
1042
|
+
signal.throwIfAborted();
|
|
1043
|
+
identity = { ...hello, runtimeInstanceId: cap.runtimeInstanceId };
|
|
1044
|
+
state = { ...state, capabilities: cap };
|
|
1045
|
+
publish();
|
|
1046
|
+
const draftRevision = state.draftRevision;
|
|
1047
|
+
const request = { ...identity, draftRevision, documents: clone(state.documents) };
|
|
1048
|
+
receipt(
|
|
1049
|
+
await current.emit(names.snapshot, request, { timeout: 5e3, signal }),
|
|
1050
|
+
draftRevision
|
|
1051
|
+
);
|
|
1052
|
+
signal.throwIfAborted();
|
|
1053
|
+
state = { ...state, connection: "ready" };
|
|
1054
|
+
publish();
|
|
1055
|
+
const result = await current.emit(names.status, identity, { timeout: 2e3, signal });
|
|
1056
|
+
signal.throwIfAborted();
|
|
1057
|
+
receiveResult(result, identity);
|
|
1058
|
+
return state.capabilities;
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
failed(error, epoch);
|
|
1061
|
+
throw error;
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
patch(operations, phase = "preview") {
|
|
1065
|
+
if (!identity || !bridge || disposed || state.connection !== "ready")
|
|
1066
|
+
return Promise.reject(new ProtocolError("SESSION", "Preview is not connected"));
|
|
1067
|
+
const epoch = frameEpoch, signal = abort.signal, current = bridge;
|
|
1068
|
+
const request = {
|
|
1069
|
+
...identity,
|
|
1070
|
+
baseDraftRevision: state.draftRevision,
|
|
1071
|
+
draftRevision: state.draftRevision + 1,
|
|
1072
|
+
operations: clone(operations),
|
|
1073
|
+
phase
|
|
1074
|
+
};
|
|
1075
|
+
assertPatch(request);
|
|
1076
|
+
state = {
|
|
1077
|
+
...state,
|
|
1078
|
+
draftRevision: request.draftRevision,
|
|
1079
|
+
documents: freeze(applyOperations(state.documents, request.operations))
|
|
1080
|
+
};
|
|
1081
|
+
for (const [key, refresh] of refreshTargets) {
|
|
1082
|
+
const field = state.capabilities?.fields.find(
|
|
1083
|
+
(field2) => fingerprint([field2.documentId, field2.path]) === key
|
|
1084
|
+
);
|
|
1085
|
+
if (field && fingerprint(readPath(state.documents[field.documentId], field.path)) !== fingerprint(refresh.raw))
|
|
1086
|
+
refreshTargets.delete(key);
|
|
1087
|
+
}
|
|
1088
|
+
documentHistory.set(request.draftRevision, state.documents);
|
|
1089
|
+
if (documentHistory.size > 64) documentHistory.delete(documentHistory.keys().next().value);
|
|
1090
|
+
for (const [controller, image] of imageRequests)
|
|
1091
|
+
if (fingerprint(readPath(state.documents[image.request.documentId], image.request.path)) !== fingerprint(image.raw))
|
|
1092
|
+
controller.abort(new SupersededResourceError());
|
|
1093
|
+
publish();
|
|
1094
|
+
const work = serial.then(async () => {
|
|
1095
|
+
signal.throwIfAborted();
|
|
1096
|
+
if (state.connection === "error")
|
|
1097
|
+
throw new ProtocolError("RECOVERY", "Reconnect to replay the retained draft");
|
|
1098
|
+
return receipt(
|
|
1099
|
+
await current.emit(names.patch, request, { timeout: 3e3, signal }),
|
|
1100
|
+
request.draftRevision
|
|
1101
|
+
);
|
|
1102
|
+
}).catch((error) => {
|
|
1103
|
+
failed(error, epoch);
|
|
1104
|
+
throw error;
|
|
1105
|
+
});
|
|
1106
|
+
serial = work.catch(() => {
|
|
1107
|
+
});
|
|
1108
|
+
return work;
|
|
1109
|
+
},
|
|
1110
|
+
refreshImages(images) {
|
|
1111
|
+
if (!identity || !bridge || disposed || state.connection !== "ready")
|
|
1112
|
+
return Promise.reject(new ProtocolError("SESSION", "Preview is not connected"));
|
|
1113
|
+
if (state.capabilities?.canRefreshImages !== true || state.capabilities.stage !== "game-ready")
|
|
1114
|
+
return Promise.reject(
|
|
1115
|
+
new ProtocolError("UNSUPPORTED", "This preview cannot refresh images")
|
|
1116
|
+
);
|
|
1117
|
+
const request = {
|
|
1118
|
+
...identity,
|
|
1119
|
+
draftRevision: state.draftRevision,
|
|
1120
|
+
resourceSequence: resourceSequence + 1,
|
|
1121
|
+
images: clone(images)
|
|
1122
|
+
};
|
|
1123
|
+
assertImageRefresh(request);
|
|
1124
|
+
for (const image of request.images) {
|
|
1125
|
+
if (image.contentHash === null && state.capabilities.canReportMissingImages !== true)
|
|
1126
|
+
return Promise.reject(
|
|
1127
|
+
new ProtocolError("UNSUPPORTED", "This preview cannot report missing images")
|
|
1128
|
+
);
|
|
1129
|
+
if (!state.capabilities.fields.some(
|
|
1130
|
+
(field) => field.resource === "image" && field.mode === "live" && field.documentId === image.documentId && fingerprint(field.path) === fingerprint(image.path)
|
|
1131
|
+
))
|
|
1132
|
+
return Promise.reject(new ProtocolError("UNSUPPORTED", "Unregistered live image field"));
|
|
1133
|
+
if (readPath(state.documents[image.documentId], image.path) === "")
|
|
1134
|
+
return Promise.reject(
|
|
1135
|
+
new ProtocolError("UNSUPPORTED", "Image refresh requires a nonempty image")
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
resourceSequence = request.resourceSequence;
|
|
1139
|
+
for (const image of request.images) {
|
|
1140
|
+
const key = fingerprint([image.documentId, image.path]);
|
|
1141
|
+
refreshTargets.set(key, {
|
|
1142
|
+
sequence: resourceSequence,
|
|
1143
|
+
contentHash: image.contentHash,
|
|
1144
|
+
draftRevision: request.draftRevision,
|
|
1145
|
+
raw: clone(readPath(state.documents[image.documentId], image.path))
|
|
1146
|
+
});
|
|
1147
|
+
for (const [controller, pending] of imageRequests)
|
|
1148
|
+
if (fingerprint([pending.request.documentId, pending.request.path]) === key)
|
|
1149
|
+
controller.abort(new SupersededResourceError());
|
|
1150
|
+
}
|
|
1151
|
+
const epoch = frameEpoch, signal = abort.signal, current = bridge;
|
|
1152
|
+
const work = serial.then(async () => {
|
|
1153
|
+
signal.throwIfAborted();
|
|
1154
|
+
if (state.connection === "error")
|
|
1155
|
+
throw new ProtocolError("RECOVERY", "Reconnect to recover the preview");
|
|
1156
|
+
const value = await current.emit(names.refreshImages, request, { timeout: 3e3, signal });
|
|
1157
|
+
receipt(value, request.draftRevision);
|
|
1158
|
+
const response = value;
|
|
1159
|
+
if (response.resourceSequence !== request.resourceSequence)
|
|
1160
|
+
throw new ProtocolError("INVALID", "Invalid image refresh receipt");
|
|
1161
|
+
return response;
|
|
1162
|
+
}).catch((error) => {
|
|
1163
|
+
if (!(error instanceof ProtocolError && ["STALE", "UNSUPPORTED"].includes(error.code)))
|
|
1164
|
+
failed(error, epoch);
|
|
1165
|
+
throw error;
|
|
1166
|
+
});
|
|
1167
|
+
serial = work.catch(() => {
|
|
1168
|
+
});
|
|
1169
|
+
return work;
|
|
1170
|
+
},
|
|
1171
|
+
dispose(options2 = {}) {
|
|
1172
|
+
if (disposed) return;
|
|
1173
|
+
disposed = true;
|
|
1174
|
+
if (options2.notifyRuntime !== false && bridge && identity)
|
|
1175
|
+
void bridge.emit(names.dispose, identity, { timeout: 1e3 }).catch(() => {
|
|
1176
|
+
});
|
|
1177
|
+
abort.abort();
|
|
1178
|
+
bridge?.destroy();
|
|
1179
|
+
bridge = void 0;
|
|
1180
|
+
state = { ...state, connection: "disposed" };
|
|
1181
|
+
publish();
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
return host;
|
|
740
1185
|
}
|
|
741
1186
|
|
|
742
1187
|
// ../preview-runtime/src/runtime.ts
|
|
@@ -1651,8 +2096,448 @@ function connectPreviewRuntime(options) {
|
|
|
1651
2096
|
}
|
|
1652
2097
|
};
|
|
1653
2098
|
}
|
|
2099
|
+
|
|
2100
|
+
// ../preview-runtime/src/level-editor.ts
|
|
2101
|
+
var frameResetPrefix = "level_editor_frame_reset_required:";
|
|
2102
|
+
var LevelEditorFrameResetError = class extends Error {
|
|
2103
|
+
constructor(reason, cause) {
|
|
2104
|
+
super(
|
|
2105
|
+
`${frameResetPrefix}${reason}${cause instanceof Error ? `:${cause.message.slice(0, 512)}` : ""}`
|
|
2106
|
+
);
|
|
2107
|
+
this.reason = reason;
|
|
2108
|
+
this.name = "LevelEditorFrameResetError";
|
|
2109
|
+
}
|
|
2110
|
+
reason;
|
|
2111
|
+
code = "LEVEL_EDITOR_FRAME_RESET_REQUIRED";
|
|
2112
|
+
requiresNewFrame = true;
|
|
2113
|
+
};
|
|
2114
|
+
function isLevelEditorFrameResetError(value) {
|
|
2115
|
+
if (typeof value === "string") return value.startsWith(frameResetPrefix);
|
|
2116
|
+
return !!value && typeof value === "object" && "message" in value && typeof value.message === "string" && value.message.startsWith(frameResetPrefix);
|
|
2117
|
+
}
|
|
2118
|
+
function boundedInteger(value, fallback, maximum) {
|
|
2119
|
+
const result = value ?? fallback;
|
|
2120
|
+
if (!Number.isSafeInteger(result) || result < 1 || result > maximum)
|
|
2121
|
+
throw new Error("level_editor_invalid_deadline");
|
|
2122
|
+
return result;
|
|
2123
|
+
}
|
|
2124
|
+
var bindingKeys = [
|
|
2125
|
+
"workspaceId",
|
|
2126
|
+
"directoryName",
|
|
2127
|
+
"editorId",
|
|
2128
|
+
"dataContractVersion",
|
|
2129
|
+
"executionContractHash"
|
|
2130
|
+
];
|
|
2131
|
+
var identityKeys2 = [
|
|
2132
|
+
...bindingKeys,
|
|
2133
|
+
"protocolVersion",
|
|
2134
|
+
"hostSessionId",
|
|
2135
|
+
"frameEpoch",
|
|
2136
|
+
"runtimeInstanceId"
|
|
2137
|
+
];
|
|
2138
|
+
var object = (v) => {
|
|
2139
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
2140
|
+
throw new Error("level_editor_invalid_message");
|
|
2141
|
+
return v;
|
|
2142
|
+
};
|
|
2143
|
+
function assertBinding2(value) {
|
|
2144
|
+
const v = object(value);
|
|
2145
|
+
if (["workspaceId", "directoryName", "editorId"].some(
|
|
2146
|
+
(k) => typeof v[k] !== "string" || !v[k].length
|
|
2147
|
+
) || !Number.isSafeInteger(v.dataContractVersion) || Number(v.dataContractVersion) < 1 || typeof v.executionContractHash !== "string" || !/^[a-f0-9]{64}$/.test(v.executionContractHash))
|
|
2148
|
+
throw new Error("level_editor_invalid_binding");
|
|
2149
|
+
}
|
|
2150
|
+
function match(value, expected, keys) {
|
|
2151
|
+
const v = object(value);
|
|
2152
|
+
if (keys.some((k) => v[k] !== expected[k])) throw new Error("level_editor_stale_runtime");
|
|
2153
|
+
return v;
|
|
2154
|
+
}
|
|
2155
|
+
function cloneDocuments(value) {
|
|
2156
|
+
const v = object(value);
|
|
2157
|
+
const walk = (item, depth = 0) => {
|
|
2158
|
+
if (depth > 64) throw new Error("level_editor_snapshot_limit");
|
|
2159
|
+
if (item === null || typeof item === "string" || typeof item === "boolean") return;
|
|
2160
|
+
if (typeof item === "number" && Number.isFinite(item)) return;
|
|
2161
|
+
if (!item || typeof item !== "object") throw new Error("level_editor_invalid_snapshot");
|
|
2162
|
+
if (Array.isArray(item)) {
|
|
2163
|
+
for (const child of item) walk(child, depth + 1);
|
|
2164
|
+
} else
|
|
2165
|
+
for (const [key, child] of Object.entries(item)) {
|
|
2166
|
+
if (["__proto__", "constructor", "prototype"].includes(key))
|
|
2167
|
+
throw new Error("level_editor_unsafe_key");
|
|
2168
|
+
walk(child, depth + 1);
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
walk(v);
|
|
2172
|
+
if (JSON.stringify(v).length > 16 * 1024 * 1024) throw new Error("level_editor_snapshot_limit");
|
|
2173
|
+
return structuredClone(v);
|
|
2174
|
+
}
|
|
2175
|
+
function createLevelEditorRuntime(options) {
|
|
2176
|
+
assertBinding2(options.binding);
|
|
2177
|
+
if (!options.commands.length || options.commands.length > 256 || new Set(options.commands).size !== options.commands.length || options.commands.some((k) => typeof k !== "string" || !k || k.length > 128))
|
|
2178
|
+
throw new Error("level_editor_invalid_capabilities");
|
|
2179
|
+
const binding = structuredClone(options.binding);
|
|
2180
|
+
const lifecycle = {
|
|
2181
|
+
version: 1,
|
|
2182
|
+
canCancel: true,
|
|
2183
|
+
requiresNewFrameOnFailure: true,
|
|
2184
|
+
applicationTimeoutMs: boundedInteger(options.applicationTimeoutMs, 25e3, 25e3),
|
|
2185
|
+
disposeTimeoutMs: boundedInteger(options.disposeTimeoutMs, 2e3, 5e3),
|
|
2186
|
+
maxPendingSnapshots: boundedInteger(options.maxPendingSnapshots, 8, 64)
|
|
2187
|
+
};
|
|
2188
|
+
let identity;
|
|
2189
|
+
const retiredHosts = /* @__PURE__ */ new Set();
|
|
2190
|
+
let revision2 = -1, documentsKey = "", disposed = false;
|
|
2191
|
+
let queue = Promise.resolve();
|
|
2192
|
+
let abort = new AbortController();
|
|
2193
|
+
let retired;
|
|
2194
|
+
let cleanup;
|
|
2195
|
+
const pending = /* @__PURE__ */ new Set();
|
|
2196
|
+
const assertAlive = () => {
|
|
2197
|
+
if (retired) throw retired;
|
|
2198
|
+
if (disposed) throw new Error("level_editor_disposed");
|
|
2199
|
+
};
|
|
2200
|
+
function finish(job, error, result) {
|
|
2201
|
+
if (!pending.delete(job)) return;
|
|
2202
|
+
clearTimeout(job.timer);
|
|
2203
|
+
if (error !== void 0) job.reject(error);
|
|
2204
|
+
else job.resolve(result);
|
|
2205
|
+
}
|
|
2206
|
+
function startCleanup(reason) {
|
|
2207
|
+
if (cleanup) return cleanup;
|
|
2208
|
+
const controller = new AbortController();
|
|
2209
|
+
cleanup = new Promise((resolve, reject) => {
|
|
2210
|
+
const timer = setTimeout(() => {
|
|
2211
|
+
const error = new LevelEditorFrameResetError("dispose_timeout");
|
|
2212
|
+
controller.abort(error);
|
|
2213
|
+
reject(error);
|
|
2214
|
+
}, lifecycle.disposeTimeoutMs);
|
|
2215
|
+
Promise.resolve().then(() => options.adapter.dispose({ signal: controller.signal, reason })).then(
|
|
2216
|
+
() => {
|
|
2217
|
+
clearTimeout(timer);
|
|
2218
|
+
resolve();
|
|
2219
|
+
},
|
|
2220
|
+
(error) => {
|
|
2221
|
+
clearTimeout(timer);
|
|
2222
|
+
const failure = new LevelEditorFrameResetError("dispose_failed", error);
|
|
2223
|
+
controller.abort(failure);
|
|
2224
|
+
reject(failure);
|
|
2225
|
+
}
|
|
2226
|
+
);
|
|
2227
|
+
});
|
|
2228
|
+
void cleanup.catch(() => {
|
|
2229
|
+
});
|
|
2230
|
+
return cleanup;
|
|
2231
|
+
}
|
|
2232
|
+
function retire(reason, cause) {
|
|
2233
|
+
if (retired) return retired;
|
|
2234
|
+
retired = new LevelEditorFrameResetError(reason, cause);
|
|
2235
|
+
abort.abort(retired);
|
|
2236
|
+
for (const job of pending) finish(job, retired);
|
|
2237
|
+
void startCleanup(reason);
|
|
2238
|
+
return retired;
|
|
2239
|
+
}
|
|
2240
|
+
return {
|
|
2241
|
+
hello(value) {
|
|
2242
|
+
assertAlive();
|
|
2243
|
+
const v = match(value, binding, bindingKeys);
|
|
2244
|
+
if (v.protocolVersion !== 1 || typeof v.hostSessionId !== "string" || !v.hostSessionId || !Number.isSafeInteger(v.frameEpoch) || Number(v.frameEpoch) < 0)
|
|
2245
|
+
throw new Error("level_editor_invalid_hello");
|
|
2246
|
+
if (retiredHosts.has(v.hostSessionId) || identity && identity.hostSessionId === v.hostSessionId && Number(v.frameEpoch) < identity.frameEpoch)
|
|
2247
|
+
throw new Error("level_editor_stale_runtime");
|
|
2248
|
+
if (identity && (identity.hostSessionId !== v.hostSessionId || identity.frameEpoch !== v.frameEpoch)) {
|
|
2249
|
+
if (pending.size) throw retire("host_replaced_during_application");
|
|
2250
|
+
if (identity.hostSessionId !== v.hostSessionId) retiredHosts.add(identity.hostSessionId);
|
|
2251
|
+
abort.abort();
|
|
2252
|
+
abort = new AbortController();
|
|
2253
|
+
revision2 = -1;
|
|
2254
|
+
documentsKey = "";
|
|
2255
|
+
identity = void 0;
|
|
2256
|
+
}
|
|
2257
|
+
identity ??= {
|
|
2258
|
+
...binding,
|
|
2259
|
+
protocolVersion: 1,
|
|
2260
|
+
hostSessionId: v.hostSessionId,
|
|
2261
|
+
frameEpoch: Number(v.frameEpoch),
|
|
2262
|
+
runtimeInstanceId: crypto.randomUUID()
|
|
2263
|
+
};
|
|
2264
|
+
return { ...identity, commands: [...options.commands], lifecycle: { ...lifecycle } };
|
|
2265
|
+
},
|
|
2266
|
+
snapshot(value) {
|
|
2267
|
+
if (retired) return Promise.reject(retired);
|
|
2268
|
+
if (!identity || disposed) return Promise.reject(new Error("level_editor_not_connected"));
|
|
2269
|
+
const v = match(value, identity, identityKeys2);
|
|
2270
|
+
if (!Number.isSafeInteger(v.draftRevision) || Number(v.draftRevision) < 0 || !["edit", "play"].includes(String(v.mode)))
|
|
2271
|
+
return Promise.reject(new Error("level_editor_invalid_snapshot"));
|
|
2272
|
+
const documents = cloneDocuments(v.documents), selection = object(v.selection);
|
|
2273
|
+
if (Object.entries(selection).some(
|
|
2274
|
+
([key2, item]) => ["__proto__", "constructor", "prototype"].includes(key2) || !(typeof item === "string" || typeof item === "number" && Number.isFinite(item))
|
|
2275
|
+
))
|
|
2276
|
+
return Promise.reject(new Error("level_editor_invalid_selection"));
|
|
2277
|
+
const incoming = Number(v.draftRevision), key = JSON.stringify(documents);
|
|
2278
|
+
const requestIdentity = { ...identity };
|
|
2279
|
+
const context = {
|
|
2280
|
+
documents,
|
|
2281
|
+
selection: structuredClone(selection),
|
|
2282
|
+
draftRevision: incoming,
|
|
2283
|
+
mode: v.mode,
|
|
2284
|
+
signal: abort.signal
|
|
2285
|
+
};
|
|
2286
|
+
if (pending.size >= lifecycle.maxPendingSnapshots)
|
|
2287
|
+
return Promise.reject(retire("pending_limit"));
|
|
2288
|
+
return new Promise((resolve, reject) => {
|
|
2289
|
+
const job = {
|
|
2290
|
+
revision: incoming,
|
|
2291
|
+
resolve,
|
|
2292
|
+
reject,
|
|
2293
|
+
timer: setTimeout(() => retire("application_timeout"), lifecycle.applicationTimeoutMs)
|
|
2294
|
+
};
|
|
2295
|
+
pending.add(job);
|
|
2296
|
+
queue = queue.catch(() => {
|
|
2297
|
+
}).then(async () => {
|
|
2298
|
+
if (!pending.has(job)) return;
|
|
2299
|
+
try {
|
|
2300
|
+
assertAlive();
|
|
2301
|
+
if (incoming < revision2 || incoming === revision2 && key !== documentsKey) {
|
|
2302
|
+
finish(job, new Error("level_editor_stale_snapshot"));
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
if (context.mode === "edit") await options.adapter.enterEdit(context);
|
|
2306
|
+
else await options.adapter.loadLevel(context);
|
|
2307
|
+
assertAlive();
|
|
2308
|
+
context.signal.throwIfAborted();
|
|
2309
|
+
await options.adapter.afterRender(context);
|
|
2310
|
+
assertAlive();
|
|
2311
|
+
context.signal.throwIfAborted();
|
|
2312
|
+
match(identity, requestIdentity, identityKeys2);
|
|
2313
|
+
revision2 = incoming;
|
|
2314
|
+
documentsKey = key;
|
|
2315
|
+
finish(job, void 0, {
|
|
2316
|
+
...requestIdentity,
|
|
2317
|
+
applied: true,
|
|
2318
|
+
draftRevision: incoming,
|
|
2319
|
+
mode: context.mode
|
|
2320
|
+
});
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
retire("adapter_failed", error);
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
});
|
|
2326
|
+
},
|
|
2327
|
+
cancel(value) {
|
|
2328
|
+
assertAlive();
|
|
2329
|
+
if (!identity) throw new Error("level_editor_not_connected");
|
|
2330
|
+
const v = match(value, identity, identityKeys2);
|
|
2331
|
+
if (!Number.isSafeInteger(v.draftRevision) || Number(v.draftRevision) < 0)
|
|
2332
|
+
throw new Error("level_editor_invalid_snapshot");
|
|
2333
|
+
const cancelled = [...pending].some((job) => job.revision === v.draftRevision);
|
|
2334
|
+
if (cancelled) retire("application_cancelled");
|
|
2335
|
+
return { ...identity, cancelled, requiresNewFrame: cancelled };
|
|
2336
|
+
},
|
|
2337
|
+
dispose(value) {
|
|
2338
|
+
if (value !== void 0) {
|
|
2339
|
+
if (!identity) throw new Error("level_editor_not_connected");
|
|
2340
|
+
match(value, identity, identityKeys2);
|
|
2341
|
+
}
|
|
2342
|
+
disposed = true;
|
|
2343
|
+
retire("disposed");
|
|
2344
|
+
return startCleanup("disposed");
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
function readLevelEditorPreviewContext() {
|
|
2349
|
+
const profiles = document.querySelectorAll(
|
|
2350
|
+
'script#playcraft-level-editor-profile[type="application/json"]'
|
|
2351
|
+
);
|
|
2352
|
+
if (profiles.length !== 1) throw new Error("level_editor_build_profile_missing");
|
|
2353
|
+
const profile = object(JSON.parse(profiles[0].textContent ?? ""));
|
|
2354
|
+
if (profile.protocolVersion !== 1) throw new Error("level_editor_build_profile_invalid");
|
|
2355
|
+
assertBinding2(profile.binding);
|
|
2356
|
+
const mark = "playcraft-preview-storage:";
|
|
2357
|
+
if (!window.name.startsWith(mark)) throw new Error("level_editor_parent_missing");
|
|
2358
|
+
const context = object(
|
|
2359
|
+
object(JSON.parse(window.name.slice(mark.length))).__playcraft_level_editor_context__
|
|
2360
|
+
);
|
|
2361
|
+
const parentOrigin = String(context.parentOrigin);
|
|
2362
|
+
if (new URL(parentOrigin).origin !== parentOrigin || !/^https?:/.test(parentOrigin))
|
|
2363
|
+
throw new Error("level_editor_parent_invalid");
|
|
2364
|
+
return { binding: structuredClone(profile.binding), parentOrigin };
|
|
2365
|
+
}
|
|
2366
|
+
function connectLevelEditorRuntime(options) {
|
|
2367
|
+
if (new URL(options.parentOrigin).origin !== options.parentOrigin || !/^https?:/.test(options.parentOrigin))
|
|
2368
|
+
throw new Error("level_editor_parent_invalid");
|
|
2369
|
+
const runtime = createLevelEditorRuntime(options);
|
|
2370
|
+
const bridge = new Bridge({
|
|
2371
|
+
peer: () => window.parent,
|
|
2372
|
+
targetOrigin: options.parentOrigin,
|
|
2373
|
+
allowedOrigins: [options.parentOrigin],
|
|
2374
|
+
strictSource: true
|
|
2375
|
+
});
|
|
2376
|
+
const off = [
|
|
2377
|
+
bridge.on(BridgeMethods.levelEditor.hello, (value) => runtime.hello(value)),
|
|
2378
|
+
bridge.on(BridgeMethods.levelEditor.snapshot, (value) => runtime.snapshot(value)),
|
|
2379
|
+
bridge.on(BridgeMethods.levelEditor.cancel, (value) => runtime.cancel(value)),
|
|
2380
|
+
bridge.on(BridgeMethods.levelEditor.dispose, async (value) => {
|
|
2381
|
+
await runtime.dispose(value);
|
|
2382
|
+
return { disposed: true };
|
|
2383
|
+
})
|
|
2384
|
+
];
|
|
2385
|
+
return {
|
|
2386
|
+
runtime,
|
|
2387
|
+
async dispose() {
|
|
2388
|
+
off.forEach((fn) => fn());
|
|
2389
|
+
bridge.destroy();
|
|
2390
|
+
await runtime.dispose();
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
function createLevelEditorHost(options) {
|
|
2395
|
+
assertBinding2(options.binding);
|
|
2396
|
+
const requestTimeoutMs = boundedInteger(options.requestTimeoutMs, 3e4, 3e4);
|
|
2397
|
+
const connectTimeoutMs = boundedInteger(options.connectTimeoutMs, 8e3, 8e3);
|
|
2398
|
+
const hello = {
|
|
2399
|
+
...structuredClone(options.binding),
|
|
2400
|
+
protocolVersion: 1,
|
|
2401
|
+
hostSessionId: crypto.randomUUID(),
|
|
2402
|
+
frameEpoch: options.frameEpoch ?? 0
|
|
2403
|
+
};
|
|
2404
|
+
const bridge = new Bridge({
|
|
2405
|
+
peer: () => options.iframe.contentWindow,
|
|
2406
|
+
targetOrigin: options.opaque ? "*" : options.targetOrigin,
|
|
2407
|
+
allowedOrigins: options.opaque ? ["null"] : [options.targetOrigin],
|
|
2408
|
+
strictSource: true
|
|
2409
|
+
});
|
|
2410
|
+
let identity;
|
|
2411
|
+
let disposed = false;
|
|
2412
|
+
let retired;
|
|
2413
|
+
const connecting = /* @__PURE__ */ new Set();
|
|
2414
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2415
|
+
function cancelRemote(revision2) {
|
|
2416
|
+
if (!identity?.lifecycle?.canCancel) return;
|
|
2417
|
+
const controller = new AbortController();
|
|
2418
|
+
void bridge.emit(
|
|
2419
|
+
BridgeMethods.levelEditor.cancel,
|
|
2420
|
+
{ ...identity, draftRevision: revision2 },
|
|
2421
|
+
{ timeout: 1e3, signal: controller.signal }
|
|
2422
|
+
).catch(() => {
|
|
2423
|
+
});
|
|
2424
|
+
controller.abort();
|
|
2425
|
+
}
|
|
2426
|
+
function retire(reason, cause) {
|
|
2427
|
+
if (retired) return retired;
|
|
2428
|
+
retired = new LevelEditorFrameResetError(reason, cause);
|
|
2429
|
+
for (const controller of connecting) controller.abort(retired);
|
|
2430
|
+
for (const [controller, revision2] of pending) {
|
|
2431
|
+
cancelRemote(revision2);
|
|
2432
|
+
controller.abort(retired);
|
|
2433
|
+
}
|
|
2434
|
+
return retired;
|
|
2435
|
+
}
|
|
2436
|
+
return {
|
|
2437
|
+
async connect() {
|
|
2438
|
+
if (retired) throw retired;
|
|
2439
|
+
if (disposed) throw new Error("level_editor_host_disposed");
|
|
2440
|
+
const controller = new AbortController();
|
|
2441
|
+
connecting.add(controller);
|
|
2442
|
+
try {
|
|
2443
|
+
const value = object(
|
|
2444
|
+
await bridge.emit(BridgeMethods.levelEditor.hello, hello, {
|
|
2445
|
+
timeout: connectTimeoutMs,
|
|
2446
|
+
signal: controller.signal
|
|
2447
|
+
})
|
|
2448
|
+
);
|
|
2449
|
+
if (retired) throw retired;
|
|
2450
|
+
match(value, hello, [...bindingKeys, "protocolVersion", "hostSessionId", "frameEpoch"]);
|
|
2451
|
+
if (disposed || typeof value.runtimeInstanceId !== "string" || !value.runtimeInstanceId || !Array.isArray(value.commands) || !value.commands.length || value.commands.length > 256 || value.commands.some((k) => typeof k !== "string" || !k))
|
|
2452
|
+
throw new Error("level_editor_invalid_capabilities");
|
|
2453
|
+
if (value.lifecycle !== void 0) {
|
|
2454
|
+
const policy = object(value.lifecycle);
|
|
2455
|
+
if (policy.version !== 1 || policy.canCancel !== true || policy.requiresNewFrameOnFailure !== true)
|
|
2456
|
+
throw new Error("level_editor_invalid_capabilities");
|
|
2457
|
+
if (typeof policy.applicationTimeoutMs !== "number" || typeof policy.disposeTimeoutMs !== "number" || typeof policy.maxPendingSnapshots !== "number")
|
|
2458
|
+
throw new Error("level_editor_invalid_capabilities");
|
|
2459
|
+
boundedInteger(policy.applicationTimeoutMs, 25e3, 25e3);
|
|
2460
|
+
boundedInteger(policy.disposeTimeoutMs, 2e3, 5e3);
|
|
2461
|
+
boundedInteger(policy.maxPendingSnapshots, 8, 64);
|
|
2462
|
+
}
|
|
2463
|
+
identity = value;
|
|
2464
|
+
return identity;
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
if (retired) throw retired;
|
|
2467
|
+
if (isLevelEditorFrameResetError(error)) throw retire("runtime_retired", error);
|
|
2468
|
+
throw error;
|
|
2469
|
+
} finally {
|
|
2470
|
+
connecting.delete(controller);
|
|
2471
|
+
}
|
|
2472
|
+
},
|
|
2473
|
+
async snapshot(input, request = {}) {
|
|
2474
|
+
if (retired) throw retired;
|
|
2475
|
+
if (!identity || disposed) throw new Error("level_editor_not_connected");
|
|
2476
|
+
if (request.signal?.aborted) throw new Error("level_editor_request_aborted");
|
|
2477
|
+
const controller = new AbortController();
|
|
2478
|
+
pending.set(controller, input.draftRevision);
|
|
2479
|
+
const cancelled = () => retire("application_cancelled");
|
|
2480
|
+
request.signal?.addEventListener("abort", cancelled, { once: true });
|
|
2481
|
+
try {
|
|
2482
|
+
const value = object(
|
|
2483
|
+
await bridge.emit(
|
|
2484
|
+
BridgeMethods.levelEditor.snapshot,
|
|
2485
|
+
{ ...identity, ...input },
|
|
2486
|
+
{ timeout: requestTimeoutMs, signal: controller.signal }
|
|
2487
|
+
)
|
|
2488
|
+
);
|
|
2489
|
+
if (retired) throw retired;
|
|
2490
|
+
match(value, identity, identityKeys2);
|
|
2491
|
+
if (disposed || value.applied !== true || value.draftRevision !== input.draftRevision || value.mode !== input.mode)
|
|
2492
|
+
throw new Error("level_editor_application_mismatch");
|
|
2493
|
+
return value;
|
|
2494
|
+
} catch (error) {
|
|
2495
|
+
throw retire("application_failed", error);
|
|
2496
|
+
} finally {
|
|
2497
|
+
pending.delete(controller);
|
|
2498
|
+
request.signal?.removeEventListener("abort", cancelled);
|
|
2499
|
+
}
|
|
2500
|
+
},
|
|
2501
|
+
dispose() {
|
|
2502
|
+
if (disposed) return;
|
|
2503
|
+
if (pending.size) retire("host_disposed_during_application");
|
|
2504
|
+
disposed = true;
|
|
2505
|
+
for (const controller of connecting) controller.abort();
|
|
2506
|
+
bridge.destroy();
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
1654
2510
|
export {
|
|
2511
|
+
LevelEditorFrameResetError,
|
|
2512
|
+
MAX_IMAGE_RESOURCE_BYTES,
|
|
2513
|
+
PROTOCOL_VERSION,
|
|
2514
|
+
ProtocolError,
|
|
1655
2515
|
ResourceLoadError,
|
|
2516
|
+
SupersededResourceError,
|
|
2517
|
+
applyOperations,
|
|
2518
|
+
assertBinding,
|
|
2519
|
+
assertDocuments,
|
|
2520
|
+
assertHello,
|
|
2521
|
+
assertIdentity,
|
|
2522
|
+
assertImageRefresh,
|
|
2523
|
+
assertImageResourceData,
|
|
2524
|
+
assertImageResourceRequest,
|
|
2525
|
+
assertJson,
|
|
2526
|
+
assertPatch,
|
|
2527
|
+
assertSnapshot,
|
|
2528
|
+
clone,
|
|
2529
|
+
connectLevelEditorRuntime,
|
|
1656
2530
|
connectPreviewRuntime,
|
|
1657
|
-
|
|
2531
|
+
createLevelEditorHost,
|
|
2532
|
+
createLevelEditorRuntime,
|
|
2533
|
+
createParameterHost,
|
|
2534
|
+
createParameterRuntime,
|
|
2535
|
+
fingerprint,
|
|
2536
|
+
freeze,
|
|
2537
|
+
isLevelEditorFrameResetError,
|
|
2538
|
+
readLevelEditorPreviewContext,
|
|
2539
|
+
readPath,
|
|
2540
|
+
readWorkspacePreviewContext,
|
|
2541
|
+
revision,
|
|
2542
|
+
sameIdentity
|
|
1658
2543
|
};
|