Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export const gametestUrl = (dist: string = DEFAULT_DIST) =>
export const allowlistUrl = (dist: string = DEFAULT_DIST) =>
dist === DEFAULT_DIST ? `https://cdn.jsdelivr.net/gh/scm-js/plugin-trigscript@v${VERSION}/dist/allowlist.json` : `${dist.replace(/\/+$/, "")}/allowlist.json`;

/** The Ogg Vorbis encoder (`compiler/vorbis.ts`, `dist/vorbis.wasm`), at this release's own tag: fetched only by a build that has a long clip to put in the map. */
export const vorbisUrl = (dist: string = DEFAULT_DIST) =>
dist === DEFAULT_DIST ? `https://cdn.jsdelivr.net/gh/scm-js/plugin-trigscript@v${VERSION}/dist/vorbis.wasm` : `${dist.replace(/\/+$/, "")}/vorbis.wasm`;

/**
* What the worker is told to import. The editor's loader turns a fetched plugin into
* `blob:` modules, and the compiler's own blob URL is the compiler the main thread would
Expand All @@ -62,18 +66,23 @@ export const allowlistUrl = (dist: string = DEFAULT_DIST) =>
export const workerModuleUrl = (dist: string = DEFAULT_DIST) => (ENTRY_URL.startsWith("blob:") && !blobRefused ? ENTRY_URL : compilerUrl(dist));

export const COMPILE_TIMEOUT_MS = 15_000;
/** A build's compile makes the script's sounds too — a long song mixed and encoded — and gets longer. */
export const BUILD_TIMEOUT_MS = 60_000;

export interface CompileInput {
files: ScriptFiles;
names: ScriptNames;
/** Run the script's tests after the compile: the world they start from, and which of them. */
tests?: TestRunOptions;
/** Encode the sounds the map is to get (a build): a check leaves an Ogg Vorbis one as its name (`compiler/vorbis.ts`). */
sounds?: boolean;
}

interface CompileRequest extends CompileInput {
id: number;
moduleUrl: string;
libUrl: string;
vorbisUrl?: string;
}

interface CompileResponse {
Expand All @@ -89,7 +98,7 @@ importScripts(${JSON.stringify(TS_URL)});
let loading = null;
let lib = null;
self.onmessage = async (e) => {
const { id, moduleUrl, libUrl, files, names, tests } = e.data;
const { id, moduleUrl, libUrl, vorbisUrl, files, names, tests } = e.data;
try {
if (!loading) loading = import(moduleUrl);
let mod;
Expand All @@ -101,7 +110,14 @@ self.onmessage = async (e) => {
if (!r.ok) throw new Error("Could not load the standard library from " + libUrl + " (" + r.status + ").");
lib = await r.text();
}
postMessage({ id, result: mod.compileScript(self.ts, files, names, { lib, tests }) });
// A check names the script's clips and makes none of them; a build makes them, the long ones encoded below.
const result = mod.compileScript(self.ts, files, names, { lib, tests, sounds: Boolean(vorbisUrl) });
// A build's long clips encoded as Ogg Vorbis; a check's left as names, their samples not sent.
if (typeof mod.finishSounds === "function") {
const encoder = vorbisUrl && result.sounds.some((s) => s.vorbis) ? await mod.loadVorbis(vorbisUrl) : null;
mod.finishSounds(result.sounds, encoder);
}
postMessage({ id, result });
} catch (err) {
postMessage({ id, error: String((err && err.message) || err) });
}
Expand Down Expand Up @@ -199,14 +215,14 @@ function breakWorker(reason: string) {
}

/** The script ran too long: drop this worker (the next request starts another) and say so. */
function timeOut(id: number) {
function timeOut(id: number, ms: number) {
const p = pending.get(id);
if (!p) return;
pending.delete(id);
worker?.terminate();
worker = null;
rejectAll("The compile was stopped.");
p.reject(new Error(`The script did not finish in ${COMPILE_TIMEOUT_MS / 1000} seconds. Is there an endless loop outside program()?`));
p.reject(new Error(`The script did not finish in ${ms / 1000} seconds. Is there an endless loop outside program()?`));
}

/** The worker, started if there is none. Throws `CompilerUnavailable` when one cannot be made, or could not a moment ago. */
Expand Down Expand Up @@ -258,8 +274,9 @@ export function compileInBackground(input: CompileInput, dist: string = DEFAULT_
p.reject(new CompileSuperseded());
}
return new Promise<CompileResult>((resolve, reject) => {
pending.set(id, { resolve, reject, timer: setTimeout(() => timeOut(id), COMPILE_TIMEOUT_MS) });
const req: CompileRequest = { id, moduleUrl: workerModuleUrl(dist), libUrl: libUrl(dist), files: input.files, names: input.names, ...(input.tests ? { tests: input.tests } : {}) };
const ms = input.sounds ? BUILD_TIMEOUT_MS : COMPILE_TIMEOUT_MS;
pending.set(id, { resolve, reject, timer: setTimeout(() => timeOut(id, ms), ms) });
const req: CompileRequest = { id, moduleUrl: workerModuleUrl(dist), libUrl: libUrl(dist), files: input.files, names: input.names, ...(input.tests ? { tests: input.tests } : {}), ...(input.sounds ? { vorbisUrl: vorbisUrl(dist) } : {}) };
w.postMessage(req);
}).catch((err: Error) => {
if (err.message === RETRY) return compileInBackground(input, dist);
Expand Down
17 changes: 14 additions & 3 deletions compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { allTables, type ScriptNames } from "./names";
import { Collector, createRuntime, type GameFunctionValue, type ProgramDescriptor, type ScriptString } from "./runtime";
import { newBody, Structured, type Body } from "./structured";
import { english, type Translate } from "../i18n";
import type { ClipFile } from "./sound";

export type { ScriptString } from "./runtime";
export { DEATHS_TABLE_ADDRESS } from "./runtime";
Expand Down Expand Up @@ -145,8 +146,12 @@ export interface CompileResult {
sources: (TriggerSource | null)[];
/** Local string table: a record's `text` / `wav` field `k > 0` means `strings[k - 1]`. */
strings: ScriptString[];
/** The clips the script plays (`sound.ts`), as the WAVs the map's archive gets, by their names there: once each. */
sounds: { path: string; bytes: Uint8Array }[];
/**
* The clips the script plays (`sound.ts`), as the files the map's archive gets, by their names there: once each. An
* Ogg Vorbis one has its samples instead of bytes until `finishSounds` (`vorbis.ts`) encodes it; after a compile with
* `sounds: false` (a check) each is its name alone: the map is to get them from a build.
*/
sounds: ClipFile[];
diagnostics: ScriptDiagnostic[];
/** The programs' variables, in declaration order; a function's copies and results are left out. */
variables: VariableInfo[];
Expand Down Expand Up @@ -183,6 +188,11 @@ export interface CompileOptions {
emitter?: boolean;
/** Told which loops the emitter took and which it left, and why. */
emitterReport?: (line: string) => void;
/**
* false: the clips the script plays are named, not made — a check as the user types, whose sounds the map does not
* get (`sound.ts`'s clips work their samples out only when asked). A build's compile leaves it out.
*/
sounds?: boolean;
}

export { LowerError };
Expand Down Expand Up @@ -383,6 +393,7 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa

/* ── Run ── */
const collector = new Collector();
collector.soundContents = options.sounds !== false;
const registry = new TestRegistry();
const where = (err: unknown) => { const at = locate(err, linked); return at.file ? { file: at.file, line: at.line ?? 1, ...(at.column ? { column: at.column } : {}) } : null; };
registry.where = () => where(new Error()) ?? { file: ENTRY_FILE, line: 1 };
Expand Down Expand Up @@ -540,7 +551,7 @@ export function compileScript(ts: typeof TS, files: ScriptFiles, names: ScriptNa
? runTests(registry, { triggers, sources, ir, strings: collector.strings, locate: where }, options.tests)
: { list: listTests(registry).list, results: [], only: [], ms: 0 };
}
return result({ ir, input, triggers, sources, strings: collector.strings, sounds: [...collector.sounds].map(([path, bytes]) => ({ path, bytes })), variables, programs, buildTime, hints, tests, ...(registry.empty ? {} : { modules: [...linked] }) });
return result({ ir, input, triggers, sources, strings: collector.strings, sounds: [...collector.sounds.values()], variables, programs, buildTime, hints, tests, ...(registry.empty ? {} : { modules: [...linked] }) });
}

/**
Expand Down
Loading
Loading