From bbbe7fc9ec7fe0f964d1c9511f06d2f920156190 Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 23 Oct 2026 15:11:04 -0600 Subject: [PATCH] Step 15: the tools on maps of our own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trigger map and the variable map come out of the layout (compiler/backend/maps.ts), in the Python build's own lines, so every tool that reads a built map in the lifted game — sim.program(), the cost by line, the desync detector, Find an EUD Error, the debugger — reads a map of our own unchanged; the editor logs them under withVariableMap. build-fixture builds through the own build by default (EUDPLIB=1 for the Python side), so test-game, game-check, debug-game and bench-lowering run on maps of our own: the program-variables probe 3 of 3 in the game, the debugger's record held, the bench's baseline anew (-53%). tests/ownWalk.ts walks the built file as the game does, against the simulator, with the variables read back through the map: end to end in CI. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01En4SHqafb1QFJ4xgbUkAe5 --- README.md | 6 +- compiler/backend/build.ts | 16 +- compiler/backend/layout.ts | 15 +- compiler/backend/maps.ts | 136 +++++++++++++++ compiler/backend/own.ts | 4 +- compiler/gameVariables.ts | 16 +- docs/v4-plan.md | 51 +++++- editor.ts | 2 +- ko.ts | 2 +- probes/lowering-bench.baseline.json | 246 ++++++++++++++-------------- probes/own-arrays.ts | 4 +- probes/own-bits.ts | 4 +- probes/own-calls.ts | 4 +- probes/own-colours-check.ts | 2 +- probes/own-errors.ts | 4 +- probes/own-events.ts | 4 +- probes/own-fields.ts | 4 +- probes/own-fixed.ts | 4 +- probes/own-game.ts | 4 +- probes/own-growing-loops.ts | 2 +- probes/own-growing.ts | 4 +- probes/own-heap-values.ts | 4 +- probes/own-input.ts | 4 +- probes/own-leaver.ts | 4 +- probes/own-loader.ts | 4 +- probes/own-locations.ts | 4 +- probes/own-made-texts.ts | 4 +- probes/own-math.ts | 4 +- probes/own-memory.ts | 4 +- probes/own-numbers.ts | 4 +- probes/own-print.ts | 4 +- probes/own-programs.ts | 4 +- probes/own-random.ts | 4 +- probes/own-reads.ts | 4 +- probes/own-recursion.ts | 4 +- probes/own-step5.ts | 2 +- probes/own-tasks.ts | 4 +- probes/own-texts.ts | 4 +- probes/own-triggers.ts | 4 +- probes/own-units.ts | 4 +- scripts/build-fixture.mts | 48 ++++-- scripts/build-own.mts | 79 --------- service.ts | 4 +- tests/own-build.test.ts | 56 +++++++ tests/ownWalk.ts | 124 ++++++++++++++ tests/service.test.ts | 7 + 46 files changed, 641 insertions(+), 285 deletions(-) create mode 100644 compiler/backend/maps.ts delete mode 100644 scripts/build-own.mts create mode 100644 tests/ownWalk.ts diff --git a/README.md b/README.md index c19d6c3..248fe58 100644 --- a/README.md +++ b/README.md @@ -1689,12 +1689,12 @@ The layout: | `bundle/`, `dist/` | `npm run bundle` builds Monaco with esbuild — the editor core, its features and the TypeScript language alone, styles injected by the module and the codicon font inlined, plus the two workers — and writes `lib.d.ts`, the standard library the compile worker checks scripts against (`bundle/lib.mjs`), into `dist/`, which is committed. A CDN's on-the-fly bundler turns Monaco's lazy language chunks into standalone bundles carrying a second editor core, which is why the plugin carries its own. After a Monaco bump: rebuild, commit, tag `monaco--`, move `DIST_TAG`. | | `compile.ts` | Compiling in a worker: a blob worker `importScripts` TypeScript from the CDN, fetches `lib.d.ts` once, and imports this plugin's own compiler module by the `blob:` URL the editor's loader gave it; a request the script does not answer in fifteen seconds (an endless loop outside `program()`) terminates the worker. A worker that cannot import the blob is replaced by one that imports the release's `dist/compiler.js`; when that fails too the compile rejects with `CompilerUnavailable` and a later one starts over. The script is never run on the main thread, where an endless loop could not be stopped. | | `script.ts` | The files, the block and its manifest: hashing (the block, and every record on its own), finding the block by content, staleness and what a stale block can still be taken apart into, planning a build. Pure over a trigger list and a map of the members. | -| `compiler/` | The language. `names.ts` and `declarations.ts` generate the `.d.ts`; `runtime.ts` is the library the script calls; `compiler.ts` checks the files as one `ts.createProgram`, collects the map references, emits them through `hoist.ts`'s transformer, links and runs them (`link.ts`), and turns each `program()` — and the `game()` functions it calls — through `structured.ts` (with `scope.ts`, what a name is bound to, and `tables.ts`, the game's tables) into the IR (`ir.ts`, `docs/ir.md`); `input.ts` sets up what the programs read of keys, mouse and chat, `numbers.ts` writes into every operation whether it reads its 32 bits as a `number` or a `u32`, `recursion.ts` finds the functions that call themselves and what each call has to keep, and `eud.ts` checks the result (a loop that never sleeps, a constant divisor, a constant that does not fit its width) and serialises it with every text written out; `backend/` is the build of a map of our own — `emit.ts` lowers a program's whole body to a graph of the game's triggers, `own.ts` every program of a script (and says what it does not take), `layout.ts` lays the graph out as the game's trigger nodes at the addresses Remastered keeps the strings at, `build.ts` does it to a scenario's bytes (`chk.ts` reads and writes the sections it touches, `fixes.ts` does what eudplib does to a map before writing it); `python/trigscript.py` is the other way, the euddraft plugin that lowers the IR to eudplib for what the own build does not take yet, embedded by `npm run embed`; `simulate.ts` is the trigger-cycle interpreter and `simulateIr.ts` the program interpreter, which computes every number the way the Python does, the two over the one world of `world.ts` (the unit table, the players and forces, what is done to units and what the unit conditions count); `testing.ts` is the script's own `test()`s — the registry the script fills as it runs, `sim` and `expect`, and the runner, whose report is plain data because it crosses from the compile worker; `lower.ts` is what the raw level still needs (condition negation, hyper triggers); `print.ts` is the inverse for records; `api.ts` and `record.ts` are the shared vocabulary. Nothing in here touches the DOM or the editor. | -| `python/trigscript.py` | The other lowering: the euddraft plugin that turns the IR into eudplib code, handed to the eudplib plugin with a build of what TrigScript's own build does not take yet. `npm run embed` writes it into `compiler/generated/trigscriptPy.ts`; `tests/python.test.ts` fails when the two drift. `scripts/build-fixture.mts` builds a script into a playable map under Node through a plugin-eudplib checkout; `probes/` holds a probe script for each part of the language (`arrays.ts`, `recursion.ts`, `strings.ts`, `classes.ts`, `map.ts`, …): every line prints what it expects, a test plays it in the simulator against those lines, and the built map — in the ignored `fixtures/` folder, since it sits on a Blizzard map — is played in the game before the part ships, which is the only place the Python really runs. | +| `compiler/` | The language. `names.ts` and `declarations.ts` generate the `.d.ts`; `runtime.ts` is the library the script calls; `compiler.ts` checks the files as one `ts.createProgram`, collects the map references, emits them through `hoist.ts`'s transformer, links and runs them (`link.ts`), and turns each `program()` — and the `game()` functions it calls — through `structured.ts` (with `scope.ts`, what a name is bound to, and `tables.ts`, the game's tables) into the IR (`ir.ts`, `docs/ir.md`); `input.ts` sets up what the programs read of keys, mouse and chat, `numbers.ts` writes into every operation whether it reads its 32 bits as a `number` or a `u32`, `recursion.ts` finds the functions that call themselves and what each call has to keep, and `eud.ts` checks the result (a loop that never sleeps, a constant divisor, a constant that does not fit its width) and serialises it with every text written out; `backend/` is the build of a map of our own — `emit.ts` lowers a program's whole body to a graph of the game's triggers, `own.ts` every program of a script (and says what it does not take), `layout.ts` lays the graph out as the game's trigger nodes at the addresses Remastered keeps the strings at, `build.ts` does it to a scenario's bytes (`chk.ts` reads and writes the sections it touches, `fixes.ts` does what eudplib does to a map before writing it, `maps.ts` says where each trigger and each variable is for the tools that read a built map in the lifted game); `python/trigscript.py` is the other way, the euddraft plugin that lowers the IR to eudplib for what the own build does not take yet, embedded by `npm run embed`; `simulate.ts` is the trigger-cycle interpreter and `simulateIr.ts` the program interpreter, which computes every number the way the Python does, the two over the one world of `world.ts` (the unit table, the players and forces, what is done to units and what the unit conditions count); `testing.ts` is the script's own `test()`s — the registry the script fills as it runs, `sim` and `expect`, and the runner, whose report is plain data because it crosses from the compile worker; `lower.ts` is what the raw level still needs (condition negation, hyper triggers); `print.ts` is the inverse for records; `api.ts` and `record.ts` are the shared vocabulary. Nothing in here touches the DOM or the editor. | +| `python/trigscript.py` | The other lowering: the euddraft plugin that turns the IR into eudplib code, handed to the eudplib plugin with a build of what TrigScript's own build does not take yet. `npm run embed` writes it into `compiler/generated/trigscriptPy.ts`; `tests/python.test.ts` fails when the two drift. `scripts/build-fixture.mts` builds a script into a playable map under Node — TrigScript's own build, or through a plugin-eudplib checkout with `EUDPLIB=1` — with the maps of where its triggers and variables are beside it for the lifted game's scripts (`test-game`, `game-check`, `debug-game`, `bench-lowering`); `probes/` holds a probe script for each part of the language (`arrays.ts`, `recursion.ts`, `strings.ts`, `classes.ts`, `map.ts`, …): every line prints what it expects, a test plays it in the simulator against those lines, and the built map — in the ignored `fixtures/` folder, since it sits on a Blizzard map — is played in the game before the part ships, which is the only place the Python really runs. | | `vendor/` | The tables the compiler reads, copied from the editor: the trigger record layout and its codec, the condition and action definitions, the unit names, the flag names, the text format's printer and parser. The editor is the source of truth; copy them again when it changes. | | `dist/plugin.js`, `dist/compiler.js` | The bundle the editor loads, and the compiler alone (`compiler/entry.ts`) for the compile worker of a copy compiled into the editor, which has no `blob:` module to hand it; `npm run build` writes both — commit both before tagging (CI commits and checks `plugin.js` on its own). | | `dist/testing.js`, `dist/testing.d.ts` | Not loaded by the plugin. The trigger text parser (`vendor/text.ts`), the trigger interpreter and its world, and the record constants, bundled from `compiler/testingEntry.ts` so that another repository's tests can run trigger text instead of reading it, and compile a script (`compileScript`, `defaultScriptNames`, `defaultLib`) to check that the TrigScript it shows a model still compiles: a devDependency on the tarball of one of this repository's tags (`https://github.com/scm-js/plugin-trigscript/archive/refs/tags/v3.10.4.tar.gz`), then `import { parseTriggers, Simulation } from "scmjs-plugin-trigscript/testing"`. `npm run build` writes both and CI holds them to the same rule as `compiler.js`. | -| `tests/` | vitest. `script.test.ts` pins the names, the declarations, the runtime's argument handling, files and imports, the printer and the block logic; `script-structured.test.ts` compiles programs and asserts the simulation, and a file a part of the language does the same for it (`arrays`, `grids`, `callbacks`, `destructuring`, `functions`, `recursion`, `classes`, `strings`, `hash`, `leftovers`, `units`, `input`) — `hash` and `leftovers` also run each body in JavaScript itself and compare what was printed; `readme.test.ts` compiles every example of this file, and checks what an example says it prints; `python.test.ts` holds the Python to the compiler's IR version; `simulate-ir.test.ts` pins the program interpreter's contract with the game (frames, and how a number comes out); `service.test.ts` the apply and the part in saving — the own build step, and the contribution to eudplib's — against a stand-in host and library; `own-build.test.ts` the build of a map of our own from a scenario's bytes (the payload after the strings, the start-up, eudplib's fixes, the map's triggers, the input's properties slot, a refusal said); `eud-build.test.ts` builds golden maps through a plugin-eudplib checkout beside this repository when there is one; `refs.test.ts` the references and renames. Copies of Blizzard's own maps in `fixtures/maps/` (gitignored) make every trigger eject to script and run back to the same record. | +| `tests/` | vitest. `script.test.ts` pins the names, the declarations, the runtime's argument handling, files and imports, the printer and the block logic; `script-structured.test.ts` compiles programs and asserts the simulation, and a file a part of the language does the same for it (`arrays`, `grids`, `callbacks`, `destructuring`, `functions`, `recursion`, `classes`, `strings`, `hash`, `leftovers`, `units`, `input`) — `hash` and `leftovers` also run each body in JavaScript itself and compare what was printed; `readme.test.ts` compiles every example of this file, and checks what an example says it prints; `python.test.ts` holds the Python to the compiler's IR version; `simulate-ir.test.ts` pins the program interpreter's contract with the game (frames, and how a number comes out); `service.test.ts` the apply and the part in saving — the own build step, and the contribution to eudplib's — against a stand-in host and library; `own-build.test.ts` the build of a map of our own from a scenario's bytes (the payload after the strings, the start-up, eudplib's fixes, the map's triggers, the input's properties slot, a refusal said, the maps of triggers and variables) and the built file walked as the game walks it (`ownWalk.ts`), its lines against the simulator's and its variables read through the map; `eud-build.test.ts` builds golden maps through a plugin-eudplib checkout beside this repository when there is one; `refs.test.ts` the references and renames. Copies of Blizzard's own maps in `fixtures/maps/` (gitignored) make every trigger eject to script and run back to the same record. | ### How the compiler is built diff --git a/compiler/backend/build.ts b/compiler/backend/build.ts index c55fe66..58f3d49 100644 --- a/compiler/backend/build.ts +++ b/compiler/backend/build.ts @@ -21,6 +21,7 @@ import { Sections, addString, freeCuwpSlot, intern, markInTransit, readFacts, re import { fixMap } from "./fixes"; import { STRINGS, layout } from "./layout"; import { emitOwn, type OwnOptions } from "./own"; +import { ownMaps, type OwnMaps } from "./maps"; import type { MapPlayers } from "./emit"; import type { Graph } from "./graph"; @@ -42,6 +43,8 @@ export interface OwnBuildInput { dropMapTriggers?: boolean; /** A dword written into every free cell of the payload (a probe's marker). */ free?: number; + /** Say where each trigger and each variable is (`maps.ts`): for the tools that read a built map in the lifted game. */ + maps?: boolean; } export interface OwnBuildStats { @@ -60,6 +63,11 @@ export interface OwnBuildStats { /** How many strings the map had before, and has now. */ stringsBefore: number; stringsAfter: number; + /** The players whose lists the payload is linked into, and where each one's entry is: what a walk of the payload starts from. */ + players: number[]; + entries: number[]; + /** The string section's length before the payload: the payload's offsets count from there. */ + stringsBytes: number; } export interface OwnBuild { @@ -67,6 +75,8 @@ export interface OwnBuild { /** What was done, for the Output panel: the fixes, the input, the counts. */ lines: string[]; stats: OwnBuildStats; + /** With `maps`: where the triggers and the variables are, and the same as lines of a build's log. */ + maps?: OwnMaps; } export type OwnBuildOutcome = OwnBuild | { refused: string }; @@ -158,10 +168,14 @@ export function buildOwnMap(input: OwnBuildInput): OwnBuildOutcome { input: lowered.input ? { keys: lowered.input.keys.length, mouse: lowered.input.mouse, syncs: lowered.input.syncs, carrier, ring: lowered.input.ring, rings: lowered.input.rings, humans: lowered.input.humans, cuwp, bits: lowered.input.bits } : null, stringsBefore, stringsAfter: table.entries.length, + players: players.slots, + entries: players.slots.map((p) => own.triggers.get(`@entry${p}`)!), + stringsBytes: strings.length, }; + const maps = input.maps ? ownMaps(lowered, own, input.compiled.ir, strings.length, textBase) : undefined; if (stats.input) lines.push(`input: ${stats.input.keys} keys, ${stats.input.mouse ? "the mouse, " : ""}${stats.input.syncs} sync slots; carriers of type ${carrier}, ${stats.input.ring} a ring, ${stats.input.rings} rings, for players ${stats.input.humans.map((h) => h + 1).join(", ")}; unit-properties slot ${cuwp}; ${stats.input.bits} bits a value`); lines.push(`${stats.runs} run${stats.runs === 1 ? "" : "s"} (${stats.runNames.join(", ")}), ${stats.graphTriggers} triggers of the programs, payload ${stats.payloadBytes} bytes at 0x${stats.address.toString(16)}, ${stats.mapTriggers} trigger${stats.mapTriggers === 1 ? "" : "s"} of the map's own (${stats.mapTriggerCopies} copies${stats.hyperTriggersLeftOut ? `, ${stats.hyperTriggersLeftOut} hyper triggers left out` : ""}), start-up ${stats.startUpTriggers} triggers, ${stats.stringsAfter - stats.stringsBefore} strings added`); - return { chk: s.bytes(), lines, stats }; + return { chk: s.bytes(), lines, stats, ...(maps ? { maps } : {}) }; } export const isRefused = (o: OwnBuildOutcome): o is { refused: string } => "refused" in o; diff --git a/compiler/backend/layout.ts b/compiler/backend/layout.ts index 4de0387..404786a 100644 --- a/compiler/backend/layout.ts +++ b/compiler/backend/layout.ts @@ -61,8 +61,16 @@ export interface OwnMap { templates: Map; /** Where each region of cell triggers (an array, a stack) starts: its first cell's trigger. */ regions: Map; + /** Where each list the graph reads where it is (an array known when built) starts, and its cells' stride in dwords. */ + lists: Map; /** Where each trigger is, by label. */ triggers: Map; + /** How many cell triggers hold routed variables, before the regions: the regions' cells follow them. */ + routeCells: number; + /** The payload's cell triggers in all (routed variables, then the regions' cells), 72 bytes apart from `regions`' first or `vars`' first route. */ + cellCount: number; + /** Where the cell triggers start. */ + cellBase: number; } /** @@ -570,7 +578,12 @@ export function layout(input: LayoutInput): OwnMap { } const vars = new Map([...cells].filter(([k]) => k.startsWith("var:")).map(([k, a]) => [k.slice(4), a])); routes.forEach((id, k) => vars.set(id, cellBase + 72 * k + 348)); - return { payload: out, trig: startUp(players, (p) => at.get(entry(p))!), address: base, vars, templates, regions, triggers: new Map(triggers.map((t) => [t.label, at.get(t.label)!])) }; + return { + payload: out, trig: startUp(players, (p) => at.get(entry(p))!), address: base, vars, templates, regions, + lists: new Map([...lists].map(([id, l]) => [id, { at: l, stride: listData.find((d) => d.at === l)!.stride }])), + triggers: new Map(triggers.map((t) => [t.label, at.get(t.label)!])), + routeCells: routes.length, cellCount, cellBase, + }; } /** diff --git a/compiler/backend/maps.ts b/compiler/backend/maps.ts new file mode 100644 index 0000000..3003621 --- /dev/null +++ b/compiler/backend/maps.ts @@ -0,0 +1,136 @@ +/** + * The tools' maps of a map of our own (TrigScript 4, step 15): where each trigger of the payload is and the line it + * came from (`TriggerMap`), and where each program's variables and arrays are (`VariableMap`) — the rows the Python + * build wrote into its log for the lifted game, Find an EUD Error, the cost by line, the desync detector, the debugger + * and `sim.program()` to read (`compiler/gameVariables.ts`), made here from the layout instead, and said in the same + * lines (`TRIGSCRIPT-MAP`, `-ROWS`, `-VARS`, `-PROGRAM`) so that every reader of them is unchanged. + * + * - A trigger's origin is the statement its graph trigger was made for ("file:line:column"); a trigger holding several + * statements' work says each action's. A cell trigger (a routed variable, an array's or a stack's cell, the heap's) + * and what no statement made (a run's wake, the gate, a route) have no origin. + * - A unit is two numbers of the program's: the EPD of its record and its uniqueness byte (`units.ts`); a made text is + * its block's EPD and its length (`texts.ts`): said with `epd` set, and the reader makes the address. + * - A per-player program runs once a player, each run with variables of its own (`own.ts`): one row of the map a run, + * its owner that player, its ids the program's own (the run's renaming undone). + */ +import { programDeclarations, type ArrayDecl, type At, type Program, type VarDecl } from "../ir"; +import type { GameArray, GameProgramPlaces, GameVariable, TriggerMap, VariableMap } from "../gameVariables"; +import { STRINGS, type OwnMap } from "./layout"; +import { HEAP } from "./heap"; +import type { Own } from "./own"; + +/** A cell trigger's value: its action's number field. */ +const CELL_VALUE = 348; +const CELL_BYTES = 72; + +export interface OwnMaps { + triggers: TriggerMap; + variables: VariableMap; + /** The maps as lines of a build's log, for the readers of the Python build's. */ + lines: string[]; +} + +/** + * `stringsBefore`: the string section's length before the payload (the payload's offsets count from its end); + * `textBase`: the map's string the pool's first text is (a text variable holds its number in the pool). + */ +export function ownMaps(own: Own, laid: OwnMap, ir: readonly Program[], stringsBefore: number, textBase: number): OwnMaps { + const payload = laid.payload; + const dv = new DataView(payload.buffer, payload.byteOffset, payload.byteLength); + const payloadStart = STRINGS + stringsBefore; + const offsetOf = (address: number) => address - payloadStart; + const kindByte = (address: number, at: number) => { const o = offsetOf(address) + at; return o >= 0 && o < payload.length ? dv.getUint8(o) : 0; }; + /** How many conditions and actions the game reads of a node: up to the first of kind 0. */ + const counts = (address: number): [number, number] => { + let conds = 0; + while (conds < 16 && kindByte(address, 8 + 20 * conds + 15) !== 0) conds++; + let acts = 0; + while (acts < 64 && kindByte(address, 328 + 32 * acts + 26) !== 0) acts++; + return [conds, acts]; + }; + const origin = (at?: At): string => (at ? `${at.file}:${at.line}:${at.column}` : ""); + const rows: TriggerMap["triggers"] = []; + const seen = new Set(); + for (const run of own.runs) { + for (const t of run.graph.triggers) { + const address = laid.triggers.get(t.label); + if (address === undefined || seen.has(address)) continue; + seen.add(address); + const [conds, acts] = counts(address); + const row: TriggerMap["triggers"][number] = [offsetOf(address), conds, acts, origin(t.at)]; + // (A trigger of the emitter's holding several lines' work: each action's line, where any differs from the trigger's.) + if (t.acts.some((a) => a.at && origin(a.at) !== origin(t.at))) row.push(t.acts.slice(0, acts).map((a) => origin(a.at ?? t.at))); + rows.push(row); + } + } + for (const t of own.triggers) for (const slot of t.slots) { + const address = laid.triggers.get(`@trig${own.triggers.indexOf(t)}.p${slot}`); + if (address === undefined || seen.has(address)) continue; + seen.add(address); + const [conds, acts] = counts(address); + rows.push([offsetOf(address), conds, acts, ""]); + } + for (let k = 0; k < laid.cellCount; k++) { + const address = laid.cellBase + CELL_BYTES * k; + if (seen.has(address)) continue; + seen.add(address); + const [conds, acts] = counts(address); + rows.push([offsetOf(address), conds, acts, ""]); + } + rows.sort((a, b) => a[0] - b[0]); + const triggers: TriggerMap = { payload: payload.length, triggers: rows }; + + const heapRegion = laid.regions.get(HEAP); + const programs: GameProgramPlaces[] = own.runs.map((run) => { + const program = ir[run.program]; + const cellOf = (id: string): number | null => laid.vars.get(run.rename(id)) ?? null; + const vars: GameVariable[] = []; + const seenIds = new Set(); + for (const d of programDeclarations(program)) { + if (seenIds.has(d.id) || d.temp || d.task !== undefined) continue; + seenIds.add(d.id); + vars.push(variableRow(d, cellOf, textBase)); + } + const arrays: GameArray[] = []; + for (const a of program.arrays ?? []) { + if (a.task !== undefined) continue; + arrays.push(arrayRow(a, run.rename, laid, cellOf)); + } + return { index: run.program, name: program.name ?? null, perPlayer: !!program.perPlayer, owners: [run.player], vars, arrays }; + }); + const variables: VariableMap = { heap: null, cells: heapRegion === undefined ? null : heapRegion + CELL_VALUE, strings: STRINGS, programs }; + + const lines = [ + "TRIGSCRIPT-MAP " + JSON.stringify({ payload: triggers.payload, count: rows.length }), + ...chunks(rows, 400).map((part) => "TRIGSCRIPT-ROWS " + JSON.stringify(part)), + "TRIGSCRIPT-VARS " + JSON.stringify({ programs: programs.length, heap: variables.heap, cells: variables.cells, strings: variables.strings }), + ...programs.map((p) => "TRIGSCRIPT-PROGRAM " + JSON.stringify(p)), + ]; + return { triggers, variables, lines }; +} + +function variableRow(d: VarDecl, cellOf: (id: string) => number | null, textBase: number): GameVariable { + const base = { id: d.id, kind: d.kind, text: d.kind === "text" ? d.text ?? "id" : null, bits: d.bits ?? null, unsigned: !!d.unsigned, rowed: false, stride: 4 }; + if (d.kind === "unit") return { ...base, cells: { ptr: cellOf(`${d.id}@e`), uid: cellOf(`${d.id}@k`) }, epd: true }; + if (d.kind === "text" && d.text === "made") return { ...base, cells: { addr: cellOf(`${d.id}@e`), len: cellOf(`${d.id}@n`) }, epd: true }; + if (d.kind === "text") return { ...base, cells: { value: cellOf(d.id) }, textBase }; + return { ...base, cells: { value: cellOf(d.id) } }; +} + +function arrayRow(a: ArrayDecl, rename: (id: string) => string, laid: OwnMap, cellOf: (id: string) => number | null): GameArray { + const base = { id: a.id, kind: a.kind, bits: a.bits ?? null, unsigned: !!a.unsigned, length: a.length, rowed: false }; + // One that grows: its handle's cells, its block among the heap's (`heap.ts`). + const ptr = cellOf(`${a.id}@p`), len = cellOf(`${a.id}@n`); + if (ptr !== null || len !== null) return { ...base, dynamic: true, ptr, len, stride: 4 }; + const region = laid.regions.get(rename(a.id)); + if (region !== undefined) return { ...base, at: region + CELL_VALUE, stride: CELL_BYTES }; + const list = laid.lists.get(rename(a.id)); + if (list) return { ...base, at: list.at, stride: 4 * list.stride }; + return { ...base, at: null, stride: 4 }; +} + +function chunks(xs: T[], n: number): T[][] { + const out: T[][] = []; + for (let i = 0; i < xs.length; i += n) out.push(xs.slice(i, i + n)); + return out.length ? out : [[]]; +} diff --git a/compiler/backend/own.ts b/compiler/backend/own.ts index 4559ad2..f933e48 100644 --- a/compiler/backend/own.ts +++ b/compiler/backend/own.ts @@ -27,6 +27,8 @@ export interface OwnRun { /** The player it runs as, 0-based: the current player while it runs. */ player: number; graph: Graph; + /** A variable's or an array's id as this run's graph names it (the first run keeps the program's; each other has its own, but for what is shared). */ + rename: (id: string) => string; } export interface Own { @@ -175,7 +177,7 @@ export function emitOwn(r: Pick & const of = (i: number) => (i ? r.strings[i - 1] : undefined); for (const t of g.triggers) for (const a of t.acts) if (a.record && (a.record.text || a.record.wav)) a.strings = { text: of(a.record.text), wav: of(a.record.wav) }; for (const a of program.arrays ?? []) if (a.values) values.set(name(a.id), a.values); - runs.push({ program: k, player, graph: g }); + runs.push({ program: k, player, graph: g, rename: name }); } } if (!runs.length) return "a script with no program"; diff --git a/compiler/gameVariables.ts b/compiler/gameVariables.ts index b85876b..cc8c8fe 100644 --- a/compiler/gameVariables.ts +++ b/compiler/gameVariables.ts @@ -23,8 +23,15 @@ export interface GameVariable { cells: Record; /** Bytes from a row's cell to the next (4; 72 where the rows are an EUDVArray, a variable each); 4 when not said. */ stride?: number; + /** A map of our own keeps a unit's `ptr` and a made text's `addr` as EPDs (dwords from the deaths table), not addresses. */ + epd?: boolean; + /** A map of our own keeps a text of the table as its number in the pool of texts known when built: the table's string `textBase` + it. */ + textBase?: number; } +/** The deaths table: what an EPD counts dwords from. */ +const DEATHS = 0x58a364; + export interface GameArray { id: string; kind: "number" | "boolean"; @@ -110,13 +117,16 @@ export function programVariables(game: VariableReader, map: VariableMap, name: s const raw = (at: number | null | undefined, rowed: boolean, stride = 4): number => (at == null ? 0 : m.getUint32(rowed ? at + stride * who : at, true)); const num = (v: number, t: { kind: string; bits?: number | null; unsigned: boolean }): number | boolean => (t.kind === "boolean" ? v !== 0 : t.bits || t.unsigned ? v >>> 0 : v | 0); const cString = (at: number): string => { let end = at; while (end < u8.length && end - at < MOST_TEXT && u8[end] !== 0) end++; return utf8.decode(u8.subarray(at, end)); }; + // (A map of our own keeps where a unit or a made text is as an EPD: the address is the deaths table's plus four times it.) + const where = (v: GameVariable, cell: number | null | undefined): number => { const x = raw(cell, v.rowed, v.stride); return x && v.epd ? (DEATHS + 4 * x) >>> 0 : x; }; const text = (v: GameVariable): string => { - if (v.text === "made") { const at = raw(v.cells.addr, v.rowed, v.stride); return at ? cString(at) : ""; } - const id = raw(v.cells.value, v.rowed, v.stride); + if (v.text === "made") { const at = where(v, v.cells.addr); return at ? cString(at) : ""; } + const id = raw(v.cells.value, v.rowed, v.stride) + (v.textBase ?? 0); return id ? cString(map.strings + m.getUint32(map.strings + 4 * id, true)) : ""; }; const value = (v: GameVariable): unknown => { - if (v.kind === "unit") { const ptr = raw(v.cells.ptr, v.rowed, v.stride); return ptr ? game.unitAt(ptr, raw(v.cells.uid, v.rowed, v.stride)) : null; } + // (A map of our own keeps the uniqueness byte itself; eudplib's build the word it sits in, the byte above.) + if (v.kind === "unit") { const ptr = where(v, v.cells.ptr); const uid = raw(v.cells.uid, v.rowed, v.stride); return ptr ? game.unitAt(ptr, v.epd ? uid << 8 : uid) : null; } if (v.kind === "text") return text(v); return num(raw(v.cells.value, v.rowed, v.stride), v); }; diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 8bb90d9..0118990 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -3584,7 +3584,7 @@ found what they left out; where a step moved, its old number is given. (stack depth, heap) passed; a refusal shown as a Problem while the script is typed, naming what it refuses; the changeover (below); Magenta and TrigScript in one map; the payload's size (triggers stacked as eudplib stacks its objects: done early, below; what is left is the routing, a trigger a place). -15. **The tools on maps of our own** (new). The trigger map and the variable map from the layout, in the rows scm-sim +15. **The tools on maps of our own** ✓ (new; "The tools on maps of our own (2026-10-23)" below). The trigger map and the variable map from the layout, in the rows scm-sim reads — A1 to A5, Find an EUD Error, cost by line, the desync detector and `sim.program()` name lines with them; `build-fixture` moved onto the own build (the `trigger()` records, Home/Away/Pixel, PEOPLE, the settings); the allowlist's evidence traced again; the lowering bench's baseline anew; a built map tested end to end in CI. @@ -3976,6 +3976,13 @@ the ledger is the list it can be written again from at 4.0. (`tests/events.test.ts`). A raw number is a second signature, the action's own. *Open:* +- Step 15's leftovers (2026-10-23). *Two bench lines slower* than the Python side's: REF_FIELD_WRITE 43 → 51 ns, + DIV_CONST_R 261 → 293. *The allowlist* is not made anew from own-build traces (`scripts/allowlist.mts`); the + probes' traces say nothing outside it is touched, which is what the list is for. *`game-check`'s line checks* + hold only a line that says in brackets what it expects ("A: 7 30 (expect 7 30)", the lifted game's rule): the own + probes say "ok" instead, so "0 as expected" — their answer sheets are what holds them. + *A probe through eudplib* (`EUDPLIB=1`) still needs the plugin-eudplib checkout and the Python: step 16's gate is + the last use of it. - Step 14's leftovers (2026-10-23). *Magenta.* A map with Magenta's member goes through eudplib with TrigScript's programs in its build; Magenta on the own layout, or two payloads behind each other, is for later. *Protection.* The own build scrambles nothing (eudplib's MRGN, shuffle and fake ISOM): the user's call. *The fixes' lines.* @@ -5469,6 +5476,48 @@ worker), registered with the host's `document.buildSteps.add` beside the `before and the other way round, the manifest's refusal, Magenta's member, the events, no second compile), the suite 1,362 green, the ledger unchanged. +### The tools on maps of our own (2026-10-23) + +Step 15: everything that read a built map through the Python build's log reads a map of our own the same way, and +every probe is built our way by default. + +- **The two maps from the layout** (`compiler/backend/maps.ts`, `buildOwnMap`'s `maps`): the trigger map — every + trigger of the payload by rising offset from the string section's end, its conditions and actions counted off the + bytes as the game counts them, the statement it was made for as "file:line:column", each action's own where a + trigger of the emitter's holds several lines' work, nothing for a cell trigger, a route, a wake or the gate — and the + variable map, a row a run (a per-player program's runs each with their owner, the run's renaming undone), each + variable's cell out of the layout's `vars`: a unit its EPD and uniqueness byte (`epd`, the reader makes the address + and the word), a made text its block's EPD and length, a text of the table its number in the pool (`textBase`, the + reader adds it), an array that grows its handle's cells and the heap's region as `cells` (72 bytes a cell), a fixed + array its region's value field, a list known when built its data. Said in the Python's own lines + (`TRIGSCRIPT-MAP`/`-ROWS`/`-VARS`/`-PROGRAM`), so `readTriggerMap`, `readVariableMap`, `programVariables`, the cost + by line, the desync detector, Find an EUD Error and the debugger are unchanged; the editor's build logs them under + `withVariableMap`. +- **`build-fixture` on the own build.** The default now: the prepared map (players, a base each, Home/Away/Pixel, the + script's records in TRIG, MAP_NAME, KEEP_TRIGGERS, STACK_DEPTH/HEAP_CELLS, OWN_FREE) through `buildOwnMap`, the maps + beside it with TRIGGER_MAP/VARIABLE_MAP; `EUDPLIB=1` builds through plugin-eudplib as before (the second build a step + is held against). The compile is without the emitter's graphs for the own path (they are the Python side's). So + `test-game`, `game-check`, `debug-game`, `lifted-weigh` and `bench-lowering` run on maps of our own with nothing + changed in them; `build-own.mts` is gone. +- **Held in the lifted game:** `probes/game-program.ts` (sim.program(): numbers, booleans, texts of the table, a + record, arrays fixed and growing, a unit held, a per-player program by player) 3 of 3 in the game, after two faults + the first run found — a text of the table read as its pool number (the reader needed `textBase`), a unit's + uniqueness byte kept as the byte where eudplib keeps the word (`epd` shifts it); `probes/game-debug.ts` through + `debug-game` ("every check held": the record against pictures of frames, variables agreeing); `game-check` on + `own-triggers` and `game-emitter` with lines named by the map and the allowlist: 0 ending the game, 0 never seen — + the evidence holds for our builds without the list being made anew. +- **The lowering bench's baseline anew** (`probes/lowering-bench.baseline.json`): all lines 42,936 ns against the + Python side's 90,559 (−53 %); UNITS −56 %, LIST_WRITE −88 %, LIST_LOOP_WRITE −78 %, ARRAY_LOOP_WRITE −72 %, + DIV_VAR_THRESHOLDS −70 %, LIST_READ −79 %. Slower: REF_FIELD_WRITE +20 % (43 → 51 ns), DIV_CONST_R +12 % + (261 → 293): for the Open list. +- **End to end in CI** (`tests/ownWalk.ts`): the built file's STRx payload walked as the game walks the lists — + each player's entry every frame, the current player theirs, Deaths and Set Deaths masked and not, Always, Never, + Switch, Display Text read from the file's own string table — against the IR simulator's lines frame by frame, and the + variables read back through the map with `programVariables` over the walked memory (a number, a growing array, a + fixed array, a text of the table reassigned, a per-player program's by player). No lifted game needed: CI runs it. +- **Tests:** `tests/own-build.test.ts` +2 (the maps' rows and the lines read back; the walk), `tests/service.test.ts` + (the maps logged under `withVariableMap`), `tests/game-variables.test.ts` as before. + ## Part C — the game, pushed Each is a library over Part B, offered only after its probe. "Known" means a played probe diff --git a/editor.ts b/editor.ts index 474380c..318137e 100644 --- a/editor.ts +++ b/editor.ts @@ -1044,7 +1044,7 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa const got = await prepareInGame({}, "trace"); if (!got || cancelled) return; const { engine, a, names, variables, triggers, module, bytes, built } = got; - if (!triggers) { setStatus("error", t("Not looked up: the build did not say where its triggers are. Update the eudplib plugin.")); return; } + if (!triggers) { setStatus("error", t("Not looked up: the build did not say where its triggers are. Update the eudplib plugin, or save once more.")); return; } setStatus("busy", t("Playing the map in the game, writing down what each line reads and writes…")); allowlist ??= fetch(allowlistUrl(svc.dist())).then((r) => { if (!r.ok) throw new Error(`allowlist.json: ${r.status}`); return r.json() as Promise; }); const [report, list] = await Promise.all([ diff --git a/ko.ts b/ko.ts index 38b6f83..e9f512f 100644 --- a/ko.ts +++ b/ko.ts @@ -388,7 +388,7 @@ export const KO: Record = { "In the game, this line {did} the same field of another unit ({times, plural, one {# time} other {# times}}): a unit sits elsewhere in the table from one game to the next. {said}": "게임에서 이 줄은 다른 유닛의 같은 필드를 {did} ({times}번). 유닛이 테이블에서 놓이는 자리는 게임마다 다릅니다. {said}", "The code Remastered showed when the game ended (EUD ERROR: 0x…):": "게임이 끝날 때 리마스터가 보여 준 코드 (EUD ERROR: 0x…):", "“{text}” is not an EUD error code: Remastered shows one as EUD ERROR: 0x and eight digits.": "“{text}”은(는) EUD 오류 코드가 아닙니다. 리마스터는 EUD ERROR: 0x 뒤에 여덟 자리로 보여 줍니다.", - "Not looked up: the build did not say where its triggers are. Update the eudplib plugin.": "찾지 않음: 빌드가 트리거의 위치를 알려 주지 않았습니다. eudplib 플러그인을 업데이트하세요.", + "Not looked up: the build did not say where its triggers are. Update the eudplib plugin, or save once more.": "찾지 않음: 빌드가 트리거의 위치를 알려 주지 않았습니다. eudplib 플러그인을 업데이트하세요. 또는 한 번 더 저장하세요.", "Playing the map in the game, writing down what each line reads and writes…": "게임에서 맵을 플레이하며 각 줄이 읽고 쓰는 것을 기록하는 중…", "the engine's page sent back no trace": "엔진 페이지가 기록을 돌려주지 않았습니다", "EUD ERROR {code}: {place}, at 0x{address}.": "EUD ERROR {code}: {place}, 주소 0x{address}.", diff --git a/probes/lowering-bench.baseline.json b/probes/lowering-bench.baseline.json index 4c94464..7e1f309 100644 --- a/probes/lowering-bench.baseline.json +++ b/probes/lowering-bench.baseline.json @@ -1,8 +1,8 @@ { "LOOP": { - "records": 6.9394871794871795, - "triggers": 5.77, - "ns": 103.25602564099502 + "records": 1.04, + "triggers": 1.03, + "ns": 13.947999999999881 }, "ADD_CONST": { "records": 1, @@ -16,23 +16,23 @@ }, "LESS_VARS": { "records": 3, - "triggers": 3, - "ns": 50.00000000001299 + "triggers": 1, + "ns": 27.900000000003576 }, "LESS_CONST": { - "records": 3, - "triggers": 3, - "ns": 50.00000000001299 + "records": 2, + "triggers": 1, + "ns": 23.10000000000084 }, "EQUALS_VARS": { - "records": 3, + "records": 1, "triggers": 1, - "ns": 27.900000000003576 + "ns": 13.399999999999846 }, "ARRAY_READ": { - "records": 15, - "triggers": 10, - "ns": 238.5999999996402 + "records": 5, + "triggers": 3, + "ns": 69.40000000002478 }, "ARRAY_WRITE": { "records": 2, @@ -40,19 +40,19 @@ "ns": 23.10000000000084 }, "ARRAY_READ_CONST": { - "records": 15, - "triggers": 10, - "ns": 238.5999999996402 + "records": 9, + "triggers": 4, + "ns": 116.80000000002381 }, "ARRAY_WRITE_CONST": { "records": 4, - "triggers": 1, - "ns": 42.50000000000474 + "triggers": 2, + "ns": 51.1000000000124 }, "FIELD_WRITE": { - "records": 2, + "records": 4, "triggers": 1, - "ns": 27.999999999997943 + "ns": 47.40000000001112 }, "FIELD_READ": { "records": 1, @@ -65,14 +65,14 @@ "ns": 9.700000000000056 }, "MULTIPLY": { - "records": 39, - "triggers": 19, - "ns": 555.0999999999548 + "records": 38, + "triggers": 25, + "ns": 519.9000000005228 }, "MASK": { - "records": 19.99897435897436, - "triggers": 16, - "ns": 301.39005128188893 + "records": 41.99589743589743, + "triggers": 12, + "ns": 493.96020512753074 }, "MUL_CONST": { "records": 1, @@ -85,29 +85,29 @@ "ns": 9.700000000000056 }, "SHIFT_RIGHT": { - "records": 22, + "records": 24, "triggers": 14, - "ns": 284.7999999998142 + "ns": 304.19999999985873 }, "DIV_CONST": { - "records": 73, - "triggers": 53, - "ns": 1042.300000003804 + "records": 20, + "triggers": 15, + "ns": 269.09999999979334 }, "MOD_CONST": { - "records": 73, - "triggers": 53, - "ns": 1042.300000003804 + "records": 21, + "triggers": 16, + "ns": 287.3999999998711 }, "FIXED_MUL": { - "records": 217, - "triggers": 131, - "ns": 2929.1999999658906 + "records": 180, + "triggers": 105, + "ns": 2246.399999984045 }, "FIXED_DIV": { - "records": 395, - "triggers": 199, - "ns": 5017.89999994021 + "records": 242, + "triggers": 133, + "ns": 3149.299999955716 }, "FIXED_MUL_INT": { "records": 1, @@ -115,44 +115,44 @@ "ns": 18.299999999998192 }, "FIXED_MUL_CONST": { - "records": 52, - "triggers": 47, - "ns": 731.4000000018799 + "records": 33, + "triggers": 29, + "ns": 447.00000000130115 }, "FIXED_DIV_CONST": { - "records": 99, - "triggers": 77, - "ns": 1293.3999999956652 + "records": 80, + "triggers": 58, + "ns": 1005.3000000016303 }, "FIXED_RECIP": { - "records": 116, - "triggers": 68, - "ns": 1513.199999999318 + "records": 110, + "triggers": 64, + "ns": 1499.800000002949 }, "FIXED_SQRT": { - "records": 682, - "triggers": 361, - "ns": 10117.30000026812 + "records": 175, + "triggers": 98, + "ns": 2305.0999999892856 }, "FIXED_LESS": { - "records": 3, - "triggers": 2, - "ns": 41.40000000000661 + "records": 1, + "triggers": 1, + "ns": 13.399999999999846 }, "FIXED_SIGN": { - "records": 2, + "records": 1, "triggers": 1, - "ns": 23.10000000000084 + "ns": 13.399999999999846 }, "FIXED_FLOOR": { - "records": 21, + "records": 18, "triggers": 13, - "ns": 266.4999999997683 + "ns": 237.39999999982922 }, "ELEM_FIELD_READ": { - "records": 11, - "triggers": 9, - "ns": 191.1999999998047 + "records": 5, + "triggers": 3, + "ns": 69.40000000002478 }, "ELEM_FIELD_WRITE": { "records": 2, @@ -160,44 +160,44 @@ "ns": 23.10000000000084 }, "REF_FIELD_READ": { - "records": 15, - "triggers": 10, - "ns": 238.5999999996402 + "records": 9, + "triggers": 4, + "ns": 116.80000000002381 }, "REF_FIELD_WRITE": { "records": 4, - "triggers": 1, - "ns": 42.50000000000474 + "triggers": 2, + "ns": 51.1000000000124 }, "DIV_THRESHOLDS": { - "records": 30, - "triggers": 23, - "ns": 479.6000000002924 + "records": 17, + "triggers": 12, + "ns": 228.89999999980276 }, "DIV_FLOOR": { - "records": 166, - "triggers": 112, - "ns": 2293.8999999840576 + "records": 147, + "triggers": 95, + "ns": 2046.899999995108 }, "DIV_VAR_THRESHOLDS": { - "records": 29, - "triggers": 23, - "ns": 468.8000000002339 + "records": 10, + "triggers": 9, + "ns": 140.09999999988474 }, "DIV_VAR": { - "records": 267, - "triggers": 119, - "ns": 3402.199999943714 + "records": 124, + "triggers": 61, + "ns": 1536.3000000010302 }, "LIST_READ": { - "records": 51.81205128205128, - "triggers": 31, - "ns": 729.7768974380244 + "records": 13, + "triggers": 4, + "ns": 155.5999999999626 }, "LIST_WRITE": { - "records": 41.81205128205128, - "triggers": 25, - "ns": 577.0768974367378 + "records": 6, + "triggers": 2, + "ns": 70.50000000002612 }, "MUL_CONST_R": { "records": 1, @@ -205,29 +205,29 @@ "ns": 18.299999999998192 }, "MULTIPLY_R": { - "records": 49, - "triggers": 25, - "ns": 699.9000000019383 + "records": 48, + "triggers": 24, + "ns": 613.2000000009405 }, "DIV_CONST_R": { - "records": 19, - "triggers": 13, - "ns": 260.99999999977064 + "records": 22, + "triggers": 15, + "ns": 293.3999999998479 }, "MOD_CONST_R": { - "records": 17, - "triggers": 11, - "ns": 234.199999999785 + "records": 13, + "triggers": 10, + "ns": 177.7999999998759 }, "DIV_VAR_R": { - "records": 110, - "triggers": 61, - "ns": 1637.0000000042105 + "records": 131, + "triggers": 63, + "ns": 1626.3000000010493 }, "DIV_VARS_R": { - "records": 107, - "triggers": 62, - "ns": 1619.5000000039145 + "records": 90, + "triggers": 44, + "ns": 1138.700000005464 }, "SHIFT_RIGHT_R": { "records": 19, @@ -240,14 +240,14 @@ "ns": 9.700000000000056 }, "LESS_VARS_R": { - "records": 3, - "triggers": 2, - "ns": 41.40000000000661 + "records": 1, + "triggers": 1, + "ns": 13.399999999999846 }, "LESS_CONST_R": { "records": 2, - "triggers": 2, - "ns": 31.70000000000438 + "triggers": 1, + "ns": 23.10000000000084 }, "TAIL": { "records": 2, @@ -255,33 +255,33 @@ "ns": 27.999999999997943 }, "UNITS": { - "records": 1615, - "triggers": 1111, - "ns": 28117.399999973615 + "records": 877.7179487179487, + "triggers": 276.15384615384613, + "ns": 12311.943589742878 }, "LIST_LOOP": { - "records": 251, - "triggers": 168, - "ns": 4051.2000000005614 + "records": 210, + "triggers": 82, + "ns": 2658.899999999801 }, "ARRAY_LOOP": { - "records": 338, - "triggers": 238, - "ns": 5229.700000001467 + "records": 184, + "triggers": 83, + "ns": 2331.9999999998786 }, "ARRAY_LOOP_WRITE": { - "records": 265, - "triggers": 167, - "ns": 3962.7000000004914 + "records": 87, + "triggers": 51, + "ns": 1115.8999999999521 }, "LIST_LOOP_WRITE": { - "records": 653, - "triggers": 278, - "ns": 9211.900000002759 + "records": 168, + "triggers": 67, + "ns": 2039.1999999998616 }, "EMPTY_LOOP": { - "records": 41, - "triggers": 40, - "ns": 659.4999999999878 + "records": 38, + "triggers": 35, + "ns": 581.3999999999932 } } diff --git a/probes/own-arrays.ts b/probes/own-arrays.ts index 7bb5200..755df8f 100644 --- a/probes/own-arrays.ts +++ b/probes/own-arrays.ts @@ -1,8 +1,8 @@ -// TrigScript 4, the backend: arrays in a map of our own (scripts/build-own.mts) — each array a region of cell triggers +// TrigScript 4, the backend: arrays in a map of our own (scripts/build-fixture.mts) — each array a region of cell triggers // the layout makes, a read at an index a jump into its cell, a store one action whose destination follows the index. // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the // same frames; every line should end "ok", and a line ending "WRONG" names the check that failed. -// npx tsx scripts/build-own.mts probes/own-arrays.ts /tmp/own-arrays.scx +// npx tsx scripts/build-fixture.mts probes/own-arrays.ts /tmp/own-arrays.scx // npx tsx scripts/build-fixture.mts probes/own-arrays.ts /tmp/own-arrays-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-bits.ts b/probes/own-bits.ts index f6bbec6..f0b07b3 100644 --- a/probes/own-bits.ts +++ b/probes/own-bits.ts @@ -1,10 +1,10 @@ -// TrigScript 4, the backend: bits and shifts in a map of our own (scripts/build-own.mts) — step 4 of the way off +// TrigScript 4, the backend: bits and shifts in a map of our own (scripts/build-fixture.mts) — step 4 of the way off // eudplib, part 2. `&` `|` with a constant are a masked write of a copy; `^` with a constant and the three of two values // a trigger a bit; a shift left by a value doublings in a doubling trigger, a shift right Horner's way over the bits // that stay. Built both ways, the lifted game should print the same lines at the same frames; every line should end // "ok", and a line ending "WRONG" names the check that failed. Frames 2, 4 and 6 (the lifted game's 4, 6 and 8) are // each one kind of work, 200 times, to weigh. -// npx tsx scripts/build-own.mts probes/own-bits.ts /tmp/own-bits.scx +// npx tsx scripts/build-fixture.mts probes/own-bits.ts /tmp/own-bits.scx // npx tsx scripts/build-fixture.mts probes/own-bits.ts /tmp/own-bits-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-calls.ts b/probes/own-calls.ts index d85f3e5..b022b65 100644 --- a/probes/own-calls.ts +++ b/probes/own-calls.ts @@ -1,9 +1,9 @@ -// TrigScript 4, the backend: calls in a map of our own (scripts/build-own.mts) — functions laid out once and called +// TrigScript 4, the backend: calls in a map of our own (scripts/build-fixture.mts) — functions laid out once and called // from several places, arguments written into the function's places, back by a next pointer. Built both ways (through // eudplib, and as a map of our own), the lifted game should print the same lines at the same frames; every line // should end "ok", and a line ending "WRONG" names the check that failed. (Loops are `while` loops: a `for` with a // bound known when the script is built is unrolled by the front end.) -// npx tsx scripts/build-own.mts probes/own-calls.ts /tmp/own-calls.scx +// npx tsx scripts/build-fixture.mts probes/own-calls.ts /tmp/own-calls.scx // npx tsx scripts/build-fixture.mts probes/own-calls.ts /tmp/own-calls-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-colours-check.ts b/probes/own-colours-check.ts index 4053a57..2caa4f2 100644 --- a/probes/own-colours-check.ts +++ b/probes/own-colours-check.ts @@ -2,7 +2,7 @@ // the code `color(P)` comes out as is the one nearest the first shade the game keeps for the player (8 bytes a player // at 0x581D76), through eudplib's table. Built both ways (`build-own`, `build-fixture`), to tell a colour the game keeps // from one either build picks. -// npx tsx scripts/build-own.mts probes/own-colours-check.ts /tmp/own-colours-check.scx +// npx tsx scripts/build-fixture.mts probes/own-colours-check.ts /tmp/own-colours-check.scx // npx tsx scripts/build-fixture.mts probes/own-colours-check.ts /tmp/own-colours-check-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-errors.ts b/probes/own-errors.ts index d2b6b15..c7d2e81 100644 --- a/probes/own-errors.ts +++ b/probes/own-errors.ts @@ -1,4 +1,4 @@ -// TrigScript 4, the backend: errors in a map of our own (scripts/build-own.mts) — step 11 of the way off eudplib, part 1. +// TrigScript 4, the backend: errors in a map of our own (scripts/build-fixture.mts) — step 11 of the way off eudplib, part 1. // A `throw` goes to the `catch` of the nearest `try` around it, out of the functions it is in: from a trigger of the // function's own that each call points at where its error goes — nothing is looked at on the way back — or, for a // function that calls itself, from its end, the call looking at the error once its frame is taken back. A stack that @@ -6,7 +6,7 @@ // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the same // frames; every line should end "ok", and a line ending "WRONG" names the check that failed. The frames after "Weighed" // are to weigh. The last line is red — "TrigScript: uncaught RangeError: the end, line 214." — and nothing comes after. -// npx tsx scripts/build-own.mts probes/own-errors.ts /tmp/own-errors.scx +// npx tsx scripts/build-fixture.mts probes/own-errors.ts /tmp/own-errors.scx // npx tsx scripts/build-fixture.mts probes/own-errors.ts /tmp/own-errors-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-events.ts b/probes/own-events.ts index b91af90..65a3283 100644 --- a/probes/own-events.ts +++ b/probes/own-events.ts @@ -1,4 +1,4 @@ -// TrigScript 4, the backend: unit events in a map of our own (scripts/build-own.mts) — step 11 of the way off eudplib, +// TrigScript 4, the backend: unit events in a map of our own (scripts/build-fixture.mts) — step 11 of the way off eudplib, // part 3. The map's events pass, before the programs' turns each frame: a trigger for each unit place the game has // handed out, whose conditions hold what it saw there last (the uniqueness byte, whether the unit is alive or its // order); a place that changed goes to the handler, which lists what came on, died or took up an order. A listener is @@ -6,7 +6,7 @@ // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the same // frames; every line should end "ok", and a line ending "WRONG" names the check that failed. The frames after "Weighed" // are to weigh. -// npx tsx scripts/build-own.mts probes/own-events.ts /tmp/own-events.scx +// npx tsx scripts/build-fixture.mts probes/own-events.ts /tmp/own-events.scx // npx tsx scripts/build-fixture.mts probes/own-events.ts /tmp/own-events-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-fields.ts b/probes/own-fields.ts index d796cb5..25b71cb 100644 --- a/probes/own-fields.ts +++ b/probes/own-fields.ts @@ -1,10 +1,10 @@ -// TrigScript 4, the backend: the game's actions with fields from values in a map of our own (scripts/build-own.mts) — +// TrigScript 4, the backend: the game's actions with fields from values in a map of our own (scripts/build-fixture.mts) — // step 5 of the way off eudplib, part 2. A field from a value is a place of its variable, kept by the variable's // writes, so the action reads it where it is; a unit count is the action done 255 at a time while more are left, then // once with the rest, 0 or below doing nothing. Built both ways (through eudplib, and as a map of our own), the lifted // game should print the same lines at the same frames; every line should end "ok", and a line ending "WRONG" names // the check that failed; line I is a Transmission's own text. Frame 2 (the lifted game's 4) is 200 actions with an amount from a value, to weigh. -// npx tsx scripts/build-own.mts probes/own-fields.ts /tmp/own-fields.scx +// npx tsx scripts/build-fixture.mts probes/own-fields.ts /tmp/own-fields.scx // npx tsx scripts/build-fixture.mts probes/own-fields.ts /tmp/own-fields-eud.scx // The map build-own makes: Player 1 a person with a Command Center and 4 Marines, Player 2 a computer with a Hatchery // and 4 Zerglings. diff --git a/probes/own-fixed.ts b/probes/own-fixed.ts index 609726f..60c687e 100644 --- a/probes/own-fixed.ts +++ b/probes/own-fixed.ts @@ -1,10 +1,10 @@ -// TrigScript 4, the backend: numbers with a fraction (fixed, 16.16) in a map of our own (scripts/build-own.mts) — step 4 +// TrigScript 4, the backend: numbers with a fraction (fixed, 16.16) in a map of our own (scripts/build-fixture.mts) — step 4 // of the way off eudplib, part 3: a product and a quotient by a constant, a constant over a value, and a product and a // quotient of two values, and fixed numbers printed. Built both ways (through eudplib, and as a map of our own), the // lifted game should print the same lines at the same frames; every line should end "ok", and a line ending "WRONG" // names the check that failed. Line G should read "G: -2.5 0.75 30000 0 0 1 0.3333 -0.3333 -2500". // Frames 2 to 10 (the lifted game's 4 to 12) are each one kind of work, 200 times, to weigh. -// npx tsx scripts/build-own.mts probes/own-fixed.ts /tmp/own-fixed.scx +// npx tsx scripts/build-fixture.mts probes/own-fixed.ts /tmp/own-fixed.scx // npx tsx scripts/build-fixture.mts probes/own-fixed.ts /tmp/own-fixed-eud.scx import { program, print, sleep, frames, fixed, type fixed as Fixed } from "trigscript"; diff --git a/probes/own-game.ts b/probes/own-game.ts index 95e86a2..e9c8adb 100644 --- a/probes/own-game.ts +++ b/probes/own-game.ts @@ -1,11 +1,11 @@ -// TrigScript 4, the backend: the game's own conditions and actions in a map of our own (scripts/build-own.mts) — step +// TrigScript 4, the backend: the game's own conditions and actions in a map of our own (scripts/build-fixture.mts) — step // 5 of the way off eudplib, part 1. A condition or an action the script writes with every field known when the map is // built is laid out as the game's record, as it is; a condition turned around with `!` is one condition where one // says the opposite (at least n against at most n − 1, set against cleared), else a trigger that skips past. Built both // ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the same frames; // every line should end "ok", and a line ending "WRONG" names the check that failed. Frames 2 and 4 (the lifted game's // 4 and 6) are each one kind of work, 200 times, to weigh. -// npx tsx scripts/build-own.mts probes/own-game.ts /tmp/own-game.scx +// npx tsx scripts/build-fixture.mts probes/own-game.ts /tmp/own-game.scx // npx tsx scripts/build-fixture.mts probes/own-game.ts /tmp/own-game-eud.scx // The map build-own makes: Player 1 a person with a Command Center and 4 Marines, Player 2 a computer with a Hatchery // and 4 Zerglings. diff --git a/probes/own-growing-loops.ts b/probes/own-growing-loops.ts index b5a9aaf..2a58581 100644 --- a/probes/own-growing-loops.ts +++ b/probes/own-growing-loops.ts @@ -3,7 +3,7 @@ // filtered in place. Loops over an array test nothing at its counter and keep each cell's place beside it. // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the // same frames; every line should end "ok", and a line ending "WRONG" names the check that failed. -// npx tsx scripts/build-own.mts probes/own-growing-loops.ts /tmp/own-growing-loops.scx +// npx tsx scripts/build-fixture.mts probes/own-growing-loops.ts /tmp/own-growing-loops.scx // npx tsx scripts/build-fixture.mts probes/own-growing-loops.ts /tmp/own-growing-loops-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-growing.ts b/probes/own-growing.ts index 62bd2de..55932ce 100644 --- a/probes/own-growing.ts +++ b/probes/own-growing.ts @@ -1,9 +1,9 @@ -// TrigScript 4, the backend: arrays that grow in a map of our own (scripts/build-own.mts) — every growing array's cells +// TrigScript 4, the backend: arrays that grow in a map of our own (scripts/build-fixture.mts) — every growing array's cells // a block of one heap of cell triggers, the array four numbers beside it (where its block is, its length, its room, its // block's size), a push a store at the end and an addition, more room the run time's `grow`. // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the // same frames; every line should end "ok", and a line ending "WRONG" names the check that failed. -// npx tsx scripts/build-own.mts probes/own-growing.ts /tmp/own-growing.scx +// npx tsx scripts/build-fixture.mts probes/own-growing.ts /tmp/own-growing.scx // npx tsx scripts/build-fixture.mts probes/own-growing.ts /tmp/own-growing-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-heap-values.ts b/probes/own-heap-values.ts index f630804..32a543b 100644 --- a/probes/own-heap-values.ts +++ b/probes/own-heap-values.ts @@ -1,9 +1,9 @@ -// TrigScript 4, the backend: what stands on the heap in a map of our own (scripts/build-own.mts) — rows that grow inside +// TrigScript 4, the backend: what stands on the heap in a map of our own (scripts/build-fixture.mts) — rows that grow inside // an array, arrays of texts that grow (each cell's text a block of a pool), closures, an array in a function that calls // itself, signals, and two programs whose arrays share the one heap. // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the // same frames; every line should end "ok", and a line ending "WRONG" names the check that failed. -// npx tsx scripts/build-own.mts probes/own-heap-values.ts /tmp/own-heap-values.scx +// npx tsx scripts/build-fixture.mts probes/own-heap-values.ts /tmp/own-heap-values.scx // npx tsx scripts/build-fixture.mts probes/own-heap-values.ts /tmp/own-heap-values-eud.scx import { program, print, sleep, frames } from "trigscript"; import { Signal } from "trigscript/signals"; diff --git a/probes/own-input.ts b/probes/own-input.ts index 869e8f7..7a9a434 100644 --- a/probes/own-input.ts +++ b/probes/own-input.ts @@ -1,4 +1,4 @@ -// TrigScript 4, the backend: the players' input in a map of our own (scripts/build-own.mts) — step 12 of the way off +// TrigScript 4, the backend: the players' input in a map of our own (scripts/build-fixture.mts) — step 12 of the way off // eudplib. Keys, clicks, the mouse and sync() carried between the players' computers by the map itself: a ring of // hidden carrier units a player, a Select and a Move to a point each send, the point read back from the carrier's order // target on every computer. Nothing of chatEvent's or MSQC's. @@ -6,7 +6,7 @@ // keys, clicks and mouse; a line ending "WRONG" names the check that failed. The trip takes two frames in a map of our // own and about five through MSQC, so the frames differ. Typed lines (part 2) are read from the text lines on the // computer they were typed on and matched there; a pattern's number and its values make the trip. -// npx tsx scripts/build-own.mts probes/own-input.ts /tmp/own-input.scx +// npx tsx scripts/build-fixture.mts probes/own-input.ts /tmp/own-input.scx // npx tsx scripts/build-fixture.mts probes/own-input.ts /tmp/own-input-eud.scx import { program, print, keyPressed, keyHeld, clicked, mouse, sync, local, underMouse, chatted, nextFrame } from "trigscript"; diff --git a/probes/own-leaver.ts b/probes/own-leaver.ts index 3a0b5ed..a09d693 100644 --- a/probes/own-leaver.ts +++ b/probes/own-leaver.ts @@ -1,10 +1,10 @@ // probe-players: {"1":{"type":6}} -// TrigScript 4, the backend: a player who leaves, in a map of our own (scripts/build-own.mts) — step 7 of the way off +// TrigScript 4, the backend: a player who leaves, in a map of our own (scripts/build-fixture.mts) — step 7 of the way off // eudplib. Two people, P1 and P2; each frame a program for every player adds 1 to its player's Marine deaths, P1's own // program to P1's Firebat deaths, and P2's own program to P2's Firebat deaths and — once P1 has left — to P2's Ghost // deaths. When P1 leaves, P1's runs stop and P2's go on, the same in both builds (the lifted game's two-person game, // scratchpad leave.mts: the deaths read each frame, one person leaving between two turns). -// npx tsx scripts/build-own.mts probes/own-leaver.ts /tmp/own-leaver.scx +// npx tsx scripts/build-fixture.mts probes/own-leaver.ts /tmp/own-leaver.scx // npx tsx scripts/build-fixture.mts probes/own-leaver.ts /tmp/own-leaver-eud.scx import { program, sleep, frames } from "trigscript"; diff --git a/probes/own-loader.ts b/probes/own-loader.ts index 8622b3d..4df4e7d 100644 --- a/probes/own-loader.ts +++ b/probes/own-loader.ts @@ -1,8 +1,8 @@ -// TrigScript 4, the backend: a map of our own — no eudplib, no Python (scripts/build-own.mts). The program's whole +// TrigScript 4, the backend: a map of our own — no eudplib, no Python (scripts/build-fixture.mts). The program's whole // body is the emitter's: loops of numbers whose results it checks itself, each check showing one line of known text. // Every line should end "ok"; a line ending "WRONG" names the check that failed. (Loops are `while` loops: a `for` // with a bound known when the script is built is unrolled by the front end, a turn at a time in the map.) -// npx tsx scripts/build-own.mts probes/own-loader.ts /tmp/own-loader.scx +// npx tsx scripts/build-fixture.mts probes/own-loader.ts /tmp/own-loader.scx import { program, print, sleep, frames } from "trigscript"; program(() => { diff --git a/probes/own-locations.ts b/probes/own-locations.ts index b8cd38a..ad3ee4a 100644 --- a/probes/own-locations.ts +++ b/probes/own-locations.ts @@ -1,10 +1,10 @@ -// TrigScript 4, the backend: a location moved in a map of our own (scripts/build-own.mts) — step 5 of the way off +// TrigScript 4, the backend: a location moved in a map of our own (scripts/build-fixture.mts) — step 5 of the way off // eudplib, part 4. `centerLocation` writes the four sides where the game keeps them (MRGN), each a Set Deaths whose // amount is a place of the side, then throws Bring's counts away (the game keeps them for a location whose sides are // written); without a size, the sides are read first and the size kept. Built both ways (through eudplib, and as a map // of our own), the lifted game should print the same lines at the same frames; every line should end "ok", and a line // ending "WRONG" names the check that failed. Frame 2 (the lifted game's 4) is 200 moves, to weigh. -// npx tsx scripts/build-own.mts probes/own-locations.ts /tmp/own-locations.scx +// npx tsx scripts/build-fixture.mts probes/own-locations.ts /tmp/own-locations.scx // npx tsx scripts/build-fixture.mts probes/own-locations.ts /tmp/own-locations-eud.scx // Location 5 is one the map does not use (a location is its number where the map's names are not known). import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-made-texts.ts b/probes/own-made-texts.ts index 33d4d89..75c2f8e 100644 --- a/probes/own-made-texts.ts +++ b/probes/own-made-texts.ts @@ -1,9 +1,9 @@ -// TrigScript 4, the backend: texts made while the game runs, in a map of our own (scripts/build-own.mts) — step 8 of +// TrigScript 4, the backend: texts made while the game runs, in a map of our own (scripts/build-fixture.mts) — step 8 of // the way off eudplib, part 2. A made text is in a block of its own (a string of the map's: printed alone, it is shown // as it is), made at a cursor and read a byte at a time where it is. Built both ways (through eudplib, and as a map of // our own), the lifted game should print the same lines at the same frames; every line should end "ok", and the lines // that show a made text say what they should show. -// npx tsx scripts/build-own.mts probes/own-made-texts.ts /tmp/own-made-texts.scx +// npx tsx scripts/build-fixture.mts probes/own-made-texts.ts /tmp/own-made-texts.scx // npx tsx scripts/build-fixture.mts probes/own-made-texts.ts /tmp/own-made-texts-eud.scx import { program, print, sleep, frames } from "trigscript"; import { clock, thousands } from "trigscript/ui"; diff --git a/probes/own-math.ts b/probes/own-math.ts index 393dde6..b865850 100644 --- a/probes/own-math.ts +++ b/probes/own-math.ts @@ -1,9 +1,9 @@ -// TrigScript 4, the backend: square roots, sines, cosines and arc tangents in a map of our own (scripts/build-own.mts) +// TrigScript 4, the backend: square roots, sines, cosines and arc tangents in a map of our own (scripts/build-fixture.mts) // — step 4 of the way off eudplib, part 5. Built both ways (through eudplib, and as a map of our own), the lifted game // should print the same lines at the same frames; every line should end "ok", and a line ending "WRONG" names the check // that failed. Frames 2 to 10 (the lifted game's 4 to 12) are each one kind of work, 200 times, to weigh. // (`Math.sqrt(n)` of a whole number gives a fixed number: the root of n as it is, not of n / 65 536.) -// npx tsx scripts/build-own.mts probes/own-math.ts /tmp/own-math.scx +// npx tsx scripts/build-fixture.mts probes/own-math.ts /tmp/own-math.scx // npx tsx scripts/build-fixture.mts probes/own-math.ts /tmp/own-math-eud.scx import { program, print, sleep, frames, fixed, type fixed as Fixed } from "trigscript"; diff --git a/probes/own-memory.ts b/probes/own-memory.ts index 79c8cea..2d767ab 100644 --- a/probes/own-memory.ts +++ b/probes/own-memory.ts @@ -1,11 +1,11 @@ -// TrigScript 4, the backend: the game's tables read and written in a map of our own (scripts/build-own.mts) — step 6 +// TrigScript 4, the backend: the game's tables read and written in a map of our own (scripts/build-fixture.mts) — step 6 // of the way off eudplib. A cell of `stats()` or `memory.*` is at an address known when the map is built: read, its // bits of the dword it is in moved down; written, a Set Deaths into that dword — masked for a byte, a word or a bit — // its amount a place of the value. Built both ways (through eudplib, and as a map of our own), the lifted game should // print the same lines at the same frames; every line should end "ok", and a line ending "WRONG" names the check that // failed (line G prints what it read: both builds should agree). Frames 2 and 4 (the lifted game's 4 and 6) are each // 200 reads or writes, to weigh. -// npx tsx scripts/build-own.mts probes/own-memory.ts /tmp/own-memory.scx +// npx tsx scripts/build-fixture.mts probes/own-memory.ts /tmp/own-memory.scx // npx tsx scripts/build-fixture.mts probes/own-memory.ts /tmp/own-memory-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-numbers.ts b/probes/own-numbers.ts index 53d4f39..64e94d5 100644 --- a/probes/own-numbers.ts +++ b/probes/own-numbers.ts @@ -1,11 +1,11 @@ -// TrigScript 4, the backend: products, quotients and remainders in a map of our own (scripts/build-own.mts) — step 4 of +// TrigScript 4, the backend: products, quotients and remainders in a map of our own (scripts/build-fixture.mts) — step 4 of // the way off eudplib. A product tests the narrower side a bit at a time and adds the other side's multiples, which // are places of it; a quotient by a constant is a chain of triggers written where it is used; a quotient of two values // doubles the remainder in one action of a trigger of its own. Built both ways (through eudplib, and as a map of our // own), the lifted game should print the same lines at the same frames; every line should end "ok", and a line ending // "WRONG" names the check that failed. Frames 2, 4, 6, 8 and 10 (the lifted game's 4, 6, 8, 10 and 12) are each one kind of // work, 200 times, to weigh. -// npx tsx scripts/build-own.mts probes/own-numbers.ts /tmp/own-numbers.scx +// npx tsx scripts/build-fixture.mts probes/own-numbers.ts /tmp/own-numbers.scx // npx tsx scripts/build-fixture.mts probes/own-numbers.ts /tmp/own-numbers-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-print.ts b/probes/own-print.ts index 48d3014..49740a0 100644 --- a/probes/own-print.ts +++ b/probes/own-print.ts @@ -1,11 +1,11 @@ -// TrigScript 4, the backend: texts printed with numbers in them, in a map of our own (scripts/build-own.mts). Each text +// TrigScript 4, the backend: texts printed with numbers in them, in a map of our own (scripts/build-fixture.mts). Each text // is laid out when the map is built, a field of 12 bytes for each number padded with 0x0D — a carriage return, which // StarCraft draws as nothing — and a routine shared by every print writes the number's digits into it. Built both ways (through eudplib, // and as a map of our own), the lifted game should print the same lines at the same frames once the 0x0D are left out; // in the game, both should look the same: no gaps, boxes or marks where the padding is. The values are variables, so // nothing here is worked out when the map is built. (A byte from a value — a colour from JSX — is checked by the // emitter's random programs, not here.) -// npx tsx scripts/build-own.mts probes/own-print.ts /tmp/own-print.scx +// npx tsx scripts/build-fixture.mts probes/own-print.ts /tmp/own-print.scx // npx tsx scripts/build-fixture.mts probes/own-print.ts /tmp/own-print-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-programs.ts b/probes/own-programs.ts index fac645b..66bfcb9 100644 --- a/probes/own-programs.ts +++ b/probes/own-programs.ts @@ -1,10 +1,10 @@ -// TrigScript 4, the backend: programs, owners and players in a map of our own (scripts/build-own.mts) — step 7 of the +// TrigScript 4, the backend: programs, owners and players in a map of our own (scripts/build-fixture.mts) — step 7 of the // way off eudplib. Several programs in one map, each with variables of its own; a program for each of several players, // each run with its own variables and the `shared` ones in common, `CurrentPlayer` the player it runs as; a print to a // force; `hasLeft`. Built both ways (through eudplib, and as a map of our own), the lifted game should print the same // lines at the same frames; every line should end "ok", and a line ending "WRONG" names the check that failed. Player 1 // is the person, Player 2 a computer in a force of its own (B prints how many runs there were: both builds should agree). -// npx tsx scripts/build-own.mts probes/own-programs.ts /tmp/own-programs.scx +// npx tsx scripts/build-fixture.mts probes/own-programs.ts /tmp/own-programs.scx // npx tsx scripts/build-fixture.mts probes/own-programs.ts /tmp/own-programs-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-random.ts b/probes/own-random.ts index 4080da3..f685626 100644 --- a/probes/own-random.ts +++ b/probes/own-random.ts @@ -1,10 +1,10 @@ -// TrigScript 4, the backend: random numbers in a map of our own (scripts/build-own.mts) — step 4 of the way off +// TrigScript 4, the backend: random numbers in a map of our own (scripts/build-fixture.mts) — step 4 of the way off // eudplib, part 4. random(n) draws the bits of n − 1 from the top, each one Set Switch to a random state, the game's own // random numbers, and draws again when they make n or more; random() is one bit. Built both ways (through eudplib, and // as a map of our own), the lifted game should print lines ending "ok" either way — the numbers drawn differ, so the // checks are of ranges: a line ending "WRONG" names the check that failed. Frames 2, 4 and 6 (the lifted game's 4, 6 // and 8) are each one kind of work, 200 times, to weigh. -// npx tsx scripts/build-own.mts probes/own-random.ts /tmp/own-random.scx +// npx tsx scripts/build-fixture.mts probes/own-random.ts /tmp/own-random.scx // npx tsx scripts/build-fixture.mts probes/own-random.ts /tmp/own-random-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-reads.ts b/probes/own-reads.ts index 320ea22..e040a28 100644 --- a/probes/own-reads.ts +++ b/probes/own-reads.ts @@ -1,11 +1,11 @@ -// TrigScript 4, the backend: the game's numbers read as values in a map of our own (scripts/build-own.mts) — step 5 +// TrigScript 4, the backend: the game's numbers read as values in a map of our own (scripts/build-fixture.mts) — step 5 // of the way off eudplib, part 3. One player's deaths, kills, ore or gas are read where the game keeps them, a bit at a // time from the highest group of eight bits the number has; anything else — a sum over a group, units counted, a score, // the clocks — is the condition asked a bit at a time from the top. Built both ways (through eudplib, and as a map of // our own), the lifted game should print the same lines at the same frames; every line should end "ok", and a line // ending "WRONG" names the check that failed (line F prints what it read: the two builds should agree). Frames 2, 4 and 8 // (the lifted game's 4, 6 and 10) are each 200 reads or comparisons, to weigh. -// npx tsx scripts/build-own.mts probes/own-reads.ts /tmp/own-reads.scx +// npx tsx scripts/build-fixture.mts probes/own-reads.ts /tmp/own-reads.scx // npx tsx scripts/build-fixture.mts probes/own-reads.ts /tmp/own-reads-eud.scx // The map build-own makes: Player 1 a person with a Command Center and 4 Marines, Player 2 a computer with a Hatchery // and 4 Zerglings. diff --git a/probes/own-recursion.ts b/probes/own-recursion.ts index 2b4cb0c..5244a3e 100644 --- a/probes/own-recursion.ts +++ b/probes/own-recursion.ts @@ -1,9 +1,9 @@ -// TrigScript 4, the backend: functions that call themselves in a map of our own (scripts/build-own.mts) — each call that +// TrigScript 4, the backend: functions that call themselves in a map of our own (scripts/build-fixture.mts) — each call that // may come back keeps the function's variables and where it goes back to on a stack of its own, cell triggers the // layout makes. Built both ways (through eudplib, and as a map of our own), the lifted game should print the same // lines at the same frames; every line should end "ok", a line ending "WRONG" names the check that failed. The last // part goes past the stack's depth (1024 calls, the default setting): the program says so in red and stops for good. -// npx tsx scripts/build-own.mts probes/own-recursion.ts /tmp/own-recursion.scx +// npx tsx scripts/build-fixture.mts probes/own-recursion.ts /tmp/own-recursion.scx // npx tsx scripts/build-fixture.mts probes/own-recursion.ts /tmp/own-recursion-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-step5.ts b/probes/own-step5.ts index 746ee4f..610fb72 100644 --- a/probes/own-step5.ts +++ b/probes/own-step5.ts @@ -1,7 +1,7 @@ // TrigScript 4, the backend: step 5 of the way off eudplib in Remastered — the game's conditions and actions in a map // of our own, what the lifted game cannot show: a print at the centre of the screen, Transmissions in a loop, a print // to every player, counts read straight after units are made, a location moved and units made there. -// npx tsx scripts/build-own.mts probes/own-step5.ts own-step5.scx +// npx tsx scripts/build-fixture.mts probes/own-step5.ts own-step5.scx // Played with own-step5-answers.txt beside it. import { program, print, sleep, seconds } from "trigscript"; diff --git a/probes/own-tasks.ts b/probes/own-tasks.ts index 93ded22..e2e1007 100644 --- a/probes/own-tasks.ts +++ b/probes/own-tasks.ts @@ -1,11 +1,11 @@ -// TrigScript 4, the backend: tasks and Promise.all / race in a map of our own (scripts/build-own.mts) — step 11 of the +// TrigScript 4, the backend: tasks and Promise.all / race in a map of our own (scripts/build-fixture.mts) — step 11 of the // way off eudplib, part 2. The program, each task's slot and each branch of a Promise.all or race is a coroutine: a // trigger that wakes it once the map's count of frames reaches the frame its sleep wrote. The tasks have their turn // after the program's, a start's slots in order; a slot with nothing in it is passed by as done. // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the same // frames; every line should end "ok", and a line ending "WRONG" names the check that failed. The frames after "Weighed" // are to weigh. One line says in red that a task did not start, as a start whose tasks all run says it. -// npx tsx scripts/build-own.mts probes/own-tasks.ts /tmp/own-tasks.scx +// npx tsx scripts/build-fixture.mts probes/own-tasks.ts /tmp/own-tasks.scx // npx tsx scripts/build-fixture.mts probes/own-tasks.ts /tmp/own-tasks-eud.scx import { program, print, sleep, frames } from "trigscript"; import { every, after } from "trigscript/timers"; diff --git a/probes/own-texts.ts b/probes/own-texts.ts index 4df46e5..7bab445 100644 --- a/probes/own-texts.ts +++ b/probes/own-texts.ts @@ -1,9 +1,9 @@ // TrigScript 4, the backend: texts known when the map is built, chosen while the game runs, in a map of our own -// (scripts/build-own.mts) — step 8 of the way off eudplib, part 1. A text that is one of a few the script wrote is a +// (scripts/build-fixture.mts) — step 8 of the way off eudplib, part 1. A text that is one of a few the script wrote is a // number there: its place in the script's pool of texts, which the map's strings hold in order. Built both ways // (through eudplib, and as a map of our own), the lifted game should print the same lines at the same frames; every // line ending "ok" says so, and the lines that show a chosen text say what they should show. -// npx tsx scripts/build-own.mts probes/own-texts.ts /tmp/own-texts.scx +// npx tsx scripts/build-fixture.mts probes/own-texts.ts /tmp/own-texts.scx // npx tsx scripts/build-fixture.mts probes/own-texts.ts /tmp/own-texts-eud.scx import { program, print, sleep, frames } from "trigscript"; diff --git a/probes/own-triggers.ts b/probes/own-triggers.ts index c7ddace..91f3ab0 100644 --- a/probes/own-triggers.ts +++ b/probes/own-triggers.ts @@ -1,4 +1,4 @@ -// TrigScript 4, the backend: the map's own triggers in a map of our own (scripts/build-own.mts) — step 13 of the way off +// TrigScript 4, the backend: the map's own triggers in a map of our own (scripts/build-fixture.mts) — step 13 of the way off // eudplib. The script's trigger() and hyperTriggers() records are laid out as the game's own nodes in the payload, a // copy for each player whose list the game would copy them into, chained after the player's entry and before their // gate: each player's run in turn before the programs, as eudplib's build runs a map's triggers (RunTrigTrigger) before @@ -6,7 +6,7 @@ // slot, a disabled trigger, condition or action is passed over — all the game's own doing. // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the same // frames; every line should end "ok", and a line ending "WRONG" names the check that failed. -// npx tsx scripts/build-own.mts probes/own-triggers.ts /tmp/own-triggers.scx +// npx tsx scripts/build-fixture.mts probes/own-triggers.ts /tmp/own-triggers.scx // npx tsx scripts/build-fixture.mts probes/own-triggers.ts /tmp/own-triggers-eud.scx import { program, print, sleep, frames, trigger, hyperTriggers, always, never, disabled, deaths, setDeaths, displayText, wait, elapsedTime, preserve, units, P1, P2, P5, AllPlayers, CurrentPlayer } from "trigscript"; diff --git a/probes/own-units.ts b/probes/own-units.ts index 3ecfd43..7778586 100644 --- a/probes/own-units.ts +++ b/probes/own-units.ts @@ -1,10 +1,10 @@ -// TrigScript 4, the backend: units in a map of our own (scripts/build-own.mts) — step 10 of the way off eudplib. A unit +// TrigScript 4, the backend: units in a map of our own (scripts/build-fixture.mts) — step 10 of the way off eudplib. A unit // is two numbers, the EPD of its record and its place's uniqueness byte, checked before use when it was kept; a loop or // a pick walks the game's unit table, place 0 and then 1 699 down, until it has met the units in use. // Built both ways (through eudplib, and as a map of our own), the lifted game should print the same lines at the same // frames; every line should end "ok", and a line ending "WRONG" names the check that failed. The frames after "Weighed" // are walks of about 160 units, to weigh. In the game, at S two Marines are selected, and at T one walks right. -// npx tsx scripts/build-own.mts probes/own-units.ts /tmp/own-units.scx +// npx tsx scripts/build-fixture.mts probes/own-units.ts /tmp/own-units.scx // npx tsx scripts/build-fixture.mts probes/own-units.ts /tmp/own-units-eud.scx // The map build-own makes: Player 1 a person with a Command Center and 4 Marines, Player 2 a computer with a Hatchery // and 4 Zerglings. diff --git a/scripts/build-fixture.mts b/scripts/build-fixture.mts index 26594d2..d0e613b 100644 --- a/scripts/build-fixture.mts +++ b/scripts/build-fixture.mts @@ -1,16 +1,20 @@ /** - * Build a script into a map under Node, the way saving does in the editor: compile, write - * the `trigger()` records into the map, hand the IR, the map and `python/trigscript.py` to a - * plugin-eudplib checkout's `scripts/build-map.mts`, write the built map. For probe maps - * that get played. + * Build a script into a map under Node, the way saving does in the editor: compile, write the `trigger()` records + * into the map, and lay the programs out into the scenario as TrigScript's own build does (`compiler/backend/build.ts`, + * step 14) — or, with EUDPLIB=1, hand the IR, the map and `python/trigscript.py` to a plugin-eudplib checkout's + * `scripts/build-map.mts` (the second build every step of the way off eudplib is held against). For probe maps that + * get played. * - * [TRIGGER_MAP=1] [PEOPLE=0,1] [PREP_ONLY=1] npx tsx scripts/build-fixture.mts probes/spike.ts fixtures/eud/spike-eud.scx [map.scx] [../plugin-eudplib] + * [TRIGGER_MAP=1] [VARIABLE_MAP=1] [PEOPLE=0,1] [PREP_ONLY=1] [EUDPLIB=1] npx tsx scripts/build-fixture.mts probes/spike.ts fixtures/eud/spike.scx [map.scx] [../plugin-eudplib] * * The output lands in the ignored fixtures/ folder: it is built on a Blizzard map, which is never committed. * - * The map is prepared through the editor's own code (a scm-js checkout beside this - * repository, or SCMJS_DIR): players, a base each, and the script's `trigger()` records with - * their texts interned. A program's texts are in the IR and eudplib adds them. + * The map is prepared through the editor's own code (a scm-js checkout beside this repository, or SCMJS_DIR): players, + * a base each, and the script's `trigger()` records with their texts interned. TRIGGER_MAP=1 and VARIABLE_MAP=1 write + * where each trigger and each variable is beside the map (.triggers.json, .variables.json), for scm-sim's + * run in the lifted game and `sim.program()`. MAP_NAME names the scenario (what the lobby shows); KEEP_TRIGGERS=1 keeps + * the base map's own triggers before the script's; STACK_DEPTH and HEAP_CELLS are the script's settings; OWN_FREE a + * dword written into every free cell of the payload. */ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { spawnSync } from "node:child_process"; @@ -18,6 +22,7 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import ts from "typescript"; import { compileScript } from "../compiler/compiler"; +import { buildOwnMap, isRefused } from "../compiler/backend/build"; import { serializeIr } from "../compiler/eud"; import { buildPlugins } from "../compiler/input"; import { readTriggerMap, readVariableMap } from "../compiler/gameVariables"; @@ -32,19 +37,24 @@ if (!scriptPath || !outPath) { console.error("usage: build-fixture < const root = resolve(import.meta.dirname, ".."); const map = resolve(mapArg ?? join(root, "fixtures", "maps", "(2)Binary Burghs.scx")); const eudplib = resolve(eudplibArg ?? process.env.EUDPLIB_DIR ?? join(root, "..", "plugin-eudplib")); +// EUDPLIB=1: built through eudplib (python/trigscript.py), the second build a step is held against; else TrigScript's own. +const throughEudplib = process.env.EUDPLIB === "1"; if (!existsSync(map)) { console.error(`no map at ${map}`); process.exit(2); } -if (!existsSync(join(eudplib, "scripts", "build-map.mts"))) { console.error(`no plugin-eudplib checkout at ${eudplib}`); process.exit(2); } +if (throughEudplib && !existsSync(join(eudplib, "scripts", "build-map.mts"))) { console.error(`no plugin-eudplib checkout at ${eudplib}`); process.exit(2); } // RANGES=0: built without value ranges (compiler/ranges.ts), to weigh what they give. // EMITTER=0: no loop lowered by the emitter (compiler/backend/emit.ts), all by the Python side; EMITTER_REPORT=1 says which it took. -const r = compileScript(ts, scriptFiles(scriptPath), defaultScriptNames(), { lib: defaultLib(), ranges: process.env.RANGES !== "0", emitter: process.env.EMITTER !== "0", ...(process.env.EMITTER_REPORT ? { emitterReport: (line: string) => console.log(line) } : {}) }); +// (TrigScript's own build lowers every program whole itself: the emitter's graphs are for the Python side's lowering.) +const r = compileScript(ts, scriptFiles(scriptPath), defaultScriptNames(), { lib: defaultLib(), ranges: process.env.RANGES !== "0", emitter: throughEudplib && process.env.EMITTER !== "0", ...(process.env.EMITTER_REPORT ? { emitterReport: (line: string) => console.log(line) } : {}) }); if (r.diagnostics.length) { for (const d of r.diagnostics) console.error(`${d.file}:${d.line}:${d.column} ${d.message}`); process.exit(1); } const EDITOR = resolve(process.env.SCMJS_DIR ?? join(root, "..", "scm-js")); const { loadMap, saveMap, readExtras } = await import(join(EDITOR, "src", "formats", "mpq", "scm.ts")); -const { parseScenario, serializeScenario } = await import(join(EDITOR, "src", "formats", "chk", "scenario.ts")); +const { parseScenario, serializeScenario, setScenarioName } = await import(join(EDITOR, "src", "formats", "chk", "scenario.ts")); const { internString, patchPlayer } = await import(join(EDITOR, "src", "editor", "settings.ts")); const loaded = await loadMap(new Uint8Array(readFileSync(map))); const scn = parseScenario(loaded.chk); +// (MAP_NAME: the name the lobby shows, so that maps built on the same base can be told apart in a recording.) +if (process.env.MAP_NAME) setScenarioName(scn, process.env.MAP_NAME); // A probe map is played alone: Player 1 is the human (Terran), Player 2 a computer (Zerg) in a // force of its own, so a Use Map Settings game starts with one person. Fixed races on purpose: // with User Selectable the game hands out melee units and drops the placed ones. @@ -63,7 +73,7 @@ if (playersLine) for (const [slot, patch] of Object.entries(JSON.parse(playersLi const { markDirty } = await import(join(EDITOR, "src", "formats", "chk", "scenario.ts")); const { addUnits, applyUnitChanges, makeUnit, nextSerial } = await import(join(EDITOR, "src", "editor", "units.ts")); // … and the script's own trigger() records, as applying the script writes them. -scn.triggers = resolveStrings(r, (text: string) => internString(scn, text)); +scn.triggers = [...(process.env.KEEP_TRIGGERS ? scn.triggers : []), ...resolveStrings(r, (text: string) => internString(scn, text))]; const START_LOCATION = 214, COMMAND_CENTER = 106, HATCHERY = 131, MARINE = 0, ZERGLING = 37; const placed = []; let serial = nextSerial(scn); @@ -102,9 +112,21 @@ const extras = loaded.archive ? await readExtras(loaded.archive, loaded.files) : // here as a build in the editor encodes them (compiler/vorbis.ts). if (r.sounds.some((s) => s.vorbis)) finishSounds(r.sounds, vorbisEncoder(readFileSync(resolve(import.meta.dirname, "..", "node_modules", "wasm-media-encoders", "wasm", "ogg.wasm")))); for (const s of r.sounds) extras.set(s.path, s.bytes!); -const withStrings = await saveMap(serializeScenario(scn), { extras, compress: "pkware", listfile: true }); +const prepared: Uint8Array = serializeScenario(scn); +const withStrings = await saveMap(prepared, { extras, compress: "pkware", listfile: true }); // PREP_ONLY=1: the prepared map as it is, not built: what the editor opens in a run end to end, and builds itself. if (process.env.PREP_ONLY) { writeFileSync(resolve(outPath), withStrings); process.exit(0); } +if (!throughEudplib) { + // TrigScript's own build, as the editor's step runs it: the records are in the map's TRIG already. + const settings = { ...(process.env.STACK_DEPTH ? { stackDepth: Number(process.env.STACK_DEPTH) } : {}), ...(process.env.HEAP_CELLS ? { heapCells: Number(process.env.HEAP_CELLS) } : {}) }; + const built = buildOwnMap({ chk: prepared, compiled: { ...r, triggers: [] }, settings, maps: Boolean(process.env.TRIGGER_MAP || process.env.VARIABLE_MAP), ...(process.env.OWN_FREE ? { free: Number(process.env.OWN_FREE) } : {}) }); + if (isRefused(built)) { console.error(`TrigScript's own build does not take this script: ${built.refused}`); process.exit(1); } + writeFileSync(resolve(outPath), await saveMap(built.chk, { extras, compress: "pkware", listfile: true })); + if (process.env.TRIGGER_MAP) writeFileSync(resolve(outPath).replace(/\.scx$/, ".triggers.json"), JSON.stringify(built.maps!.triggers)); + if (process.env.VARIABLE_MAP) writeFileSync(resolve(outPath).replace(/\.scx$/, ".variables.json"), JSON.stringify(built.maps!.variables)); + for (const line of built.lines) console.log(line); + process.exit(0); +} const dir = mkdtempSync(join(tmpdir(), "trigscript-fixture-")); const irPath = join(dir, "trigscript.json"); const pluginsPath = join(dir, "plugins.json"); diff --git a/scripts/build-own.mts b/scripts/build-own.mts deleted file mode 100644 index 0888020..0000000 --- a/scripts/build-own.mts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * A map of our own: a script built with no eudplib and no Python (TrigScript 4, the backend: our own layout and - * start-up). Each program's whole body is lowered by the emitter (`compiler/backend/emit.ts`, `emitBody`) — once for - * each player a per-player program runs for — laid out at fixed addresses after the strings - * (`compiler/backend/layout.ts`), and handed to the game's trigger walk by a few triggers in the map's TRIG section. - * What the emitter cannot lower yet is refused, and said. The build itself is `compiler/backend/build.ts`, the one - * the plugin's build step runs in the editor (step 14); this script prepares a probe map for it and writes the file. - * - * npx tsx scripts/build-own.mts probes/own-loader.ts out.scx [map.scx] - * - * The map is prepared as build-fixture prepares it (a base each, Player 1 the person; PEOPLE and a `// probe-players:` - * line as there; MAP_NAME the name the lobby shows), and written through the editor's own code (a scm-js checkout - * beside this repository, or SCMJS_DIR). The base is a melee map, whose stock triggers would end a Use Map Settings - * game at once: they are left out unless OWN_KEEP_TRIGGERS says otherwise. OWN_FREE= fills the payload's free - * cells with a marker. - */ -import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { join, resolve } from "node:path"; -import ts from "typescript"; -import { compileScript } from "../compiler/compiler"; -import { buildOwnMap, isRefused } from "../compiler/backend/build"; -import { defaultScriptNames } from "../compiler/names"; -import { defaultLib } from "../bundle/lib.mjs"; -import { finishSounds, vorbisEncoder } from "../compiler/vorbis"; -import { scriptFiles } from "./script-files.mts"; - -const started = performance.now(); -const [scriptPath, outPath, mapArg] = process.argv.slice(2); -if (!scriptPath || !outPath) { console.error("usage: build-own [map.scx]"); process.exit(2); } -const root = resolve(import.meta.dirname, ".."); -const map = resolve(mapArg ?? join(root, "fixtures", "maps", "(2)Binary Burghs.scx")); -if (!existsSync(map)) { console.error(`no map at ${map}`); process.exit(2); } - -const r = compileScript(ts, scriptFiles(scriptPath), defaultScriptNames(), { lib: defaultLib(), emitter: false }); -if (r.diagnostics.length) { for (const d of r.diagnostics) console.error(`${d.file}:${d.line}:${d.column} ${d.message}`); process.exit(1); } -const compiled = performance.now(); - -const EDITOR = resolve(process.env.SCMJS_DIR ?? join(root, "..", "scm-js")); -const { loadMap, saveMap, readExtras } = await import(join(EDITOR, "src", "formats", "mpq", "scm.ts")); -const { parseScenario, serializeScenario, markDirty, setScenarioName } = await import(join(EDITOR, "src", "formats", "chk", "scenario.ts")); -const { patchPlayer } = await import(join(EDITOR, "src", "editor", "settings.ts")); -const { addUnits, applyUnitChanges, makeUnit, nextSerial } = await import(join(EDITOR, "src", "editor", "units.ts")); -const loaded = await loadMap(new Uint8Array(readFileSync(map))); -const scn = parseScenario(loaded.chk); -// (MAP_NAME: the name the lobby shows, so that maps built on the same base can be told apart in a recording.) -if (process.env.MAP_NAME) setScenarioName(scn, process.env.MAP_NAME); -patchPlayer(scn, 0, { type: 6, race: 1, force: 0 }); -patchPlayer(scn, 1, { type: 5, race: 0, force: 1 }); -for (const slot of (process.env.PEOPLE ?? "").split(",").filter(Boolean).map(Number)) patchPlayer(scn, slot, { type: 6, race: 1, force: slot === 0 ? 0 : 1 }); -const playersLine = /^\/\/ probe-players: (.+)$/m.exec(readFileSync(scriptPath, "utf8")); -if (playersLine) for (const [slot, patch] of Object.entries(JSON.parse(playersLine[1]) as Record)) patchPlayer(scn, Number(slot), patch); -// A base and a few troops each, as build-fixture places them. -const START_LOCATION = 214, COMMAND_CENTER = 106, HATCHERY = 131, MARINE = 0, ZERGLING = 37; -const placed = []; -let serial = nextSerial(scn); -for (const [owner, base, troop] of [[0, COMMAND_CENTER, MARINE], [1, HATCHERY, ZERGLING]] as const) { - const start = scn.units.find((u: { unitId: number; owner: number }) => u.unitId === START_LOCATION && u.owner === owner); - if (!start) continue; - placed.push(makeUnit(null, base, owner, start.x, start.y, serial++)); - for (let i = 0; i < 4; i++) placed.push(makeUnit(null, troop, owner, start.x - 48 + i * 32, start.y + 96, serial++)); -} -applyUnitChanges(scn, addUnits(scn, placed)); -markDirty(scn, "UNIT"); -const chk: Uint8Array = serializeScenario(scn); -const prepared = performance.now(); - -// The build as the editor runs it: the compile's records go in as the script's (the map's trigger list has none of -// them here, where the editor's apply would have written them into it). -const built = buildOwnMap({ chk, compiled: r, dropMapTriggers: !process.env.OWN_KEEP_TRIGGERS, ...(process.env.OWN_FREE ? { free: Number(process.env.OWN_FREE) } : {}) }); -if (isRefused(built)) { console.error(`the emitter cannot lower this program: ${built.refused}`); process.exit(1); } -const emitted = performance.now(); -const extras = loaded.archive ? await readExtras(loaded.archive, loaded.files) : new Map(); -// The clips the script plays, in the archive (an Ogg one encoded first), as build-fixture puts them. -if (r.sounds.some((s) => s.vorbis)) finishSounds(r.sounds, vorbisEncoder(readFileSync(join(root, "node_modules", "wasm-media-encoders", "wasm", "ogg.wasm")))); -for (const s of r.sounds) extras.set(s.path, s.bytes!); -writeFileSync(resolve(outPath), await saveMap(built.chk, { extras, compress: "pkware", listfile: true })); -const done = performance.now(); -for (const line of built.lines) console.log(line); -console.log(`${resolve(outPath)}: built in ${(done - started).toFixed(0)} ms (compile ${(compiled - started).toFixed(0)}, map ${(prepared - compiled).toFixed(0)}, build ${(emitted - prepared).toFixed(0)}, archive ${(done - emitted).toFixed(0)})`); diff --git a/service.ts b/service.ts index b21dcc3..b8956b4 100644 --- a/service.ts +++ b/service.ts @@ -306,9 +306,11 @@ export class ScriptService { const chk = await this.api.document.sections.chkOf(input.map); const { heapCells, stackDepth } = this.settings(); log(this.t("Built by TrigScript itself: {n, plural, one {# program} other {# programs}} laid out into the map, no eudplib", { n: artifact.compiled.ir.length })); - const out = await this.builder({ chk, compiled: { ir: artifact.compiled.ir, strings: artifact.compiled.strings, input: artifact.compiled.input, triggers: [] }, settings: { heapCells, stackDepth } }, this.dist()); + // (With the maps of where the triggers and the variables are, for a test run in the lifted game: `withVariableMap`.) + const out = await this.builder({ chk, compiled: { ir: artifact.compiled.ir, strings: artifact.compiled.strings, input: artifact.compiled.input, triggers: [] }, settings: { heapCells, stackDepth }, ...(this.variableMaps ? { maps: true } : {}) }, this.dist()); if (isRefused(out)) throw new Error(this.t("TrigScript's own build does not take the script: {reason}. Apply the script again to build it through eudplib.", { reason: out.refused })); for (const line of out.lines) log(line); + for (const line of out.maps?.lines ?? []) log(line); say({ kind: "done", purpose: input.purpose, contributors, log: lines.join("\n"), chkBytes: out.chk.length, ms: Date.now() - started }); return out.chk; } catch (err) { diff --git a/tests/own-build.test.ts b/tests/own-build.test.ts index 99256bb..267a4a1 100644 --- a/tests/own-build.test.ts +++ b/tests/own-build.test.ts @@ -11,6 +11,9 @@ import { buildOwnMap, isRefused, type OwnBuild } from "../compiler/backend/build import { Sections, freeCuwpSlot, intern, readFacts, readStrings, readTriggers, writeChunks, writeStrings } from "../compiler/backend/chk"; import { STRINGS } from "../compiler/backend/layout"; import { ActionType, PlayerGroup, decodeTriggers, emptyAction, emptyTrigger, encodeTriggers, type TriggerRecord } from "../vendor/triggers"; +import { programVariables, readTriggerMap, readVariableMap } from "../compiler/gameVariables"; +import { simulatePrograms } from "../compiler/simulateIr"; +import { walkBuiltMap } from "./ownWalk"; import { defaultLib } from "../bundle/lib.mjs"; let lib: ReturnType | undefined; @@ -197,6 +200,59 @@ describe("the build in the editor", () => { expect(!isRefused(whole) && !isRefused(ok) && ok.stats.payloadBytes < whole.stats.payloadBytes).toBe(true); }); + it("says where every trigger and every variable is, in the rows the tools read", () => { + const r = compile(`program(() => {\n let n = 0;\n let title = "Probe";\n let seen: number[] = [];\n let fixed = [1, 2, 3];\n while (true) {\n n++;\n seen.push(n);\n fixed[n % 3] = n;\n displayText(\`\${title} \${n} \${seen.length} \${fixed[0]}\`);\n sleep(seconds(1));\n }\n}, { name: "counting" });`); + const out = buildOwnMap({ chk: scenario(), compiled: r, maps: true }); + if (isRefused(out)) throw new Error(out.refused); + const { triggers, variables, lines } = out.maps!; + // Every trigger of the payload once, by rising offset, the programs' with the line they came from. + expect(triggers.payload).toBe(out.stats.payloadBytes); + expect(triggers.triggers.length).toBeGreaterThan(out.stats.graphTriggers); + expect(triggers.triggers.every(([o], i, xs) => i === 0 || o > xs[i - 1][0])).toBe(true); + expect(triggers.triggers.every(([o]) => o >= 0 && o < triggers.payload)).toBe(true); + const origins = new Set(triggers.triggers.map((t) => t[3])); + expect([...origins].some((o) => /^main\.ts:(7|8|9|10):\d+$/.test(o))).toBe(true); + expect(origins.has("")).toBe(true); + // The same rows read back from the log's lines, as the lifted game's scripts read them. + expect(readTriggerMap(lines)).toEqual(triggers); + expect(readVariableMap(lines)).toEqual(variables); + // The program's variables and arrays, each with a cell in the payload (an EPD's address); the heap's cells named. + expect(variables.strings).toBe(STRINGS); + expect(variables.cells).not.toBeNull(); + const [p] = variables.programs; + expect(p).toMatchObject({ index: 0, name: "counting", perPlayer: false, owners: [0] }); + const byName = (id: string) => p.vars.find((v) => v.id === id || v.id.startsWith(`${id}#`))!; + expect(byName("n").cells.value).toBeGreaterThanOrEqual(out.stats.address); + expect(byName("title")).toMatchObject({ kind: "text", text: "id" }); + const seen = p.arrays.find((a) => a.id.startsWith("seen"))!, fixed = p.arrays.find((a) => a.id.startsWith("fixed"))!; + expect(seen).toMatchObject({ dynamic: true }); + expect(seen.ptr).toBeGreaterThanOrEqual(out.stats.address); + expect(fixed).toMatchObject({ length: 3, stride: 72 }); + expect(fixed.at).toBeGreaterThanOrEqual(out.stats.address); + }); + + it("the built file walked as the game walks it prints what the simulator prints, and its variables read as the simulator's", () => { + const source = `program(() => {\n let n = 0;\n let seen: number[] = [];\n let fixed = [10, 20, 30];\n let title = "Probe";\n while (true) {\n n++;\n seen.push(n * 2);\n fixed[n % 3] += n;\n if (n === 2) title = "Second";\n displayText(\`\${title} \${n} \${seen.length} \${fixed[1]}\`);\n sleep(frames(1));\n }\n}, { name: "counting" });\nprogram(() => {\n let mine = 0;\n while (true) { mine += 3; displayText(\`mine \${mine}\`); sleep(frames(1)); }\n}, { owner: AllPlayers, name: "each" });`; + const r = compile(source); + const out = buildOwnMap({ chk: scenario(), compiled: r, maps: true }); + if (isRefused(out)) throw new Error(out.refused); + const frames = 4; + const walked = walkBuiltMap(out.chk, out.stats.players, out.stats.entries, frames); + const sim = simulatePrograms(r.ir, frames, { strings: r.strings }); + const want = sim.events.filter((e) => e.text !== undefined).map((e) => `${e.cycle}:${e.text}`); + // (A line every player's run showed in one frame is one line, as the lifted game and the simulator have it.) + const got = [...new Set(walked.printed.map((p) => `${p.frame - 1}:${p.text}`))]; + expect(got).toEqual(want); + // The variables through the map, as sim.program() reads them in the lifted game. + const v = (name: string | number, player?: number) => programVariables({ ...walked.memory, unitAt: () => null }, out.maps!.variables, name, player) as Record; + expect(v("counting").n).toBe(frames); + expect(v("counting").seen).toEqual([2, 4, 6, 8]); + expect(v("counting").fixed).toEqual([10 + 3, 20 + 1 + 4, 30 + 2]); + expect(v("counting").title).toBe("Second"); + expect(v("each", 0).mine).toBe(3 * frames); + expect(v("each", 1).mine).toBe(3 * frames); + }); + it("reads and writes the string table and the chunks faithfully", () => { expect(readStrings(new Uint8Array(0), false).entries).toEqual([]); const s = new Sections(scenario({ strings: ["a", "b", "a"] })); diff --git a/tests/ownWalk.ts b/tests/ownWalk.ts new file mode 100644 index 0000000..8946457 --- /dev/null +++ b/tests/ownWalk.ts @@ -0,0 +1,124 @@ +/** + * A built map of our own walked as the game walks its triggers, from the scenario's bytes alone (TrigScript 4, step + * 15: a built map tested end to end where no lifted game is, in CI). The payload is read out of the STRx section, each + * player's list walked from their entry every frame with the current player theirs, and what Display Text shows is + * read from the map's own string table — so what comes out is what the file holds, not what the layout meant. + * The game's conditions and actions a map of our own uses are modelled as `tests/emitter.test.ts`'s walker models + * them: Deaths and Set Deaths (masked too), Always, Never, Switch, Set Switch, Display Text, Preserve Trigger, + * Comment, Play WAV; Create Unit with Properties and Give Units are passed over. + */ +import { Sections } from "../compiler/backend/chk"; +import { STRINGS } from "../compiler/backend/layout"; +import { GAME_TICK } from "../compiler/backend/emit"; + +const DEATHS = 0x58a364; +const CURRENT_PLAYER = 0x6509b0; +const SWITCHES = 0x58dc40; +const U32 = 2 ** 32; +const u = (n: number) => ((n % U32) + U32) % U32; + +export interface Walked { + /** What was shown, in order, each with the frame it was shown in (frames from 1). */ + printed: { frame: number; text: string }[]; + /** The game's memory as the walk left it: a dword at an address — inside the payload or out of it. */ + read(address: number): number; + /** A byte of it. */ + byte(address: number): number; + /** `programVariables`' view of the memory. */ + memory: { m: DataView; u8: Uint8Array }; +} + +/** `entries`: where each player's entry is (`OwnBuildStats.entries`), `players` who they are; `memory`: the game's dwords to start from. */ +export function walkBuiltMap(chk: Uint8Array, players: number[], entries: number[], frames: number, memory?: Map): Walked { + const s = new Sections(chk); + const strx = s.get("STRx"); + if (!strx) throw new Error("the map has no STRx"); + // The whole section is at STRINGS: the strings, then the payload. + const section = strx.slice(); + const dv = new DataView(section.buffer); + const inside = (a: number) => a >= STRINGS && a + 4 <= STRINGS + section.length; + const game = new Map(memory); + const rd = (a: number) => (inside(a) ? dv.getUint32(a - STRINGS, true) : game.get(a) ?? 0); + const wr = (a: number, v: number) => (inside(a) ? dv.setUint32(a - STRINGS, u(v), true) : game.set(a, u(v))); + const byte = (a: number) => (inside(a) ? dv.getUint8(a - STRINGS) : (game.get(a & ~3) ?? 0) >>> (8 * (a & 3)) & 0xff); + const u16 = (a: number) => dv.getUint16(a - STRINGS, true); + const at = (epd: number) => u(DEATHS + 4 * (epd === 13 ? rd(CURRENT_PLAYER) : epd)); + const switchAt = (sw: number): [number, number] => [SWITCHES + 4 * (sw >> 5), 2 ** (sw & 31)]; + const stringCount = dv.getUint32(0, true); + const stringAt = (index: number): string => { + if (index < 1 || index > stringCount) return `string ${index}`; + const start = STRINGS + dv.getUint32(4 * index, true); + const out: number[] = []; + for (let a = start; inside(a) && byte(a) !== 0; a++) out.push(byte(a)); + return new TextDecoder().decode(new Uint8Array(out)).replace(/\r/g, ""); + }; + const printed: { frame: number; text: string }[] = []; + for (let frame = 1; frame <= frames; frame++) { + game.set(GAME_TICK, frame); + entries.forEach((start, w) => { + wr(CURRENT_PLAYER, players[w]); + let pc = start; + for (let steps = 0; !(pc & 0x80000000); steps++) { + if (steps > 50_000_000) throw new Error("the payload does not end"); + if (!inside(pc)) throw new Error(`the walk left the payload: 0x${pc.toString(16)}`); + let holds = (rd(pc + 2376) & 8) === 0; + for (let k = 0; holds && k < 16; k++) { + const o = pc + 8 + 20 * k, kind = byte(o + 15); + if (byte(o + 17) & 2) continue; + if (kind === 0) break; + if (kind === 22) continue; + if (kind === 23) { holds = false; continue; } + if (kind === 11) { + const [addr, bit] = switchAt(byte(o + 16)); + holds = byte(o + 14) === 2 ? (rd(addr) & bit) !== 0 : (rd(addr) & bit) === 0; + continue; + } + if (kind !== 15) throw new Error(`condition kind ${kind} at 0x${o.toString(16)}`); + const d = at(rd(o + 4)) + 48 * u16(o + 12); + const masked = u16(o + 18) === 0x4353; + const x = masked ? (rd(d) & rd(o)) >>> 0 : rd(d), y = rd(o + 8), compare = byte(o + 14); + holds = compare === 0 ? x >= y : compare === 1 ? x <= y : x === y; + } + for (let k = 0; holds && k < 64; k++) { + const o = pc + 328 + 32 * k, kind = byte(o + 26); + if (byte(o + 28) & 2) continue; + if (kind === 0) break; + if (kind === 9) { printed.push({ frame, text: stringAt(rd(o + 4)) }); continue; } + if (kind === 13) { + const [addr, bit] = switchAt(rd(o + 20)), modifier = byte(o + 27); + const on = modifier === 4 ? true : modifier === 5 ? false : modifier === 6 ? (rd(addr) & bit) === 0 : modifier === 11 ? Math.random() < 0.5 : null; + if (on === null) throw new Error(`Set Switch ${modifier}`); + wr(addr, on ? (rd(addr) | bit) >>> 0 : (rd(addr) & ~bit) >>> 0); + continue; + } + if (kind === 3) { wr(pc + 2376, rd(pc + 2376) | 4); continue; } + if (kind === 47 || kind === 8 || kind === 11 || kind === 48) continue; + if (kind !== 45) throw new Error(`action kind ${kind} at 0x${o.toString(16)}`); + const dest = at(rd(o + 16)) + 48 * u16(o + 24), value = rd(o + 20), modifier = byte(o + 27), old = rd(dest); + if (u16(o + 30) === 0x4353) { + const m = rd(o), f = (old & m) >>> 0, w = (value & m) >>> 0; + const next = modifier === 7 ? w : modifier === 8 ? u(f + w) : Math.max(0, f - w); + wr(dest, ((old & ~m) | (next & m)) >>> 0); + continue; + } + wr(dest, modifier === 7 ? value : modifier === 8 ? u(old + value) : Math.max(0, old - value)); + } + if (holds && !(rd(pc + 2376) & 4)) wr(pc + 2376, rd(pc + 2376) | 8); + pc = rd(pc + 4); + } + }); + } + // A memory for `programVariables`: the section where it is, everything else from the game's dwords. + const m = { + getUint32: (a: number) => rd(a), + getUint8: (a: number) => byte(a), + } as unknown as DataView; + const u8 = new Proxy(new Uint8Array(0), { + get: (_, key) => { + if (key === "length") return 2 ** 32; + if (key === "subarray") return (from: number, to: number) => Uint8Array.from({ length: Math.max(0, to - from) }, (_x, i) => byte(from + i)); + return typeof key === "string" && /^\d+$/.test(key) ? byte(Number(key)) : undefined; + }, + }) as Uint8Array; + return { printed, read: rd, byte, memory: { m, u8 } }; +} diff --git a/tests/service.test.ts b/tests/service.test.ts index 3761b6b..6940262 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -267,6 +267,13 @@ describe("service: saving applies the script and builds its programs", () => { expect(heard[0]).toMatchObject({ kind: "start", purpose: "save", contributors: ["trigscript"] }); expect(heard.at(-1)).toMatchObject({ kind: "done", purpose: "save", chkBytes: out.length }); expect((heard[1] as { line: string }).line).toMatch(/^Built by TrigScript itself: 1 program laid out into the map, no eudplib$/); + // A build asked for the maps of where the triggers and the variables are says them as the Python build did, in the log. + heard.length = 0; + await svc.withVariableMap(() => step.run({ map: chk(), fileName: "x.scx", purpose: "test", signal: new AbortController().signal })); + const logged = heard.flatMap((e) => (e.kind === "log" ? [e.line] : [])); + expect(logged.some((l) => l.startsWith("TRIGSCRIPT-MAP "))).toBe(true); + expect(logged.some((l) => l.startsWith("TRIGSCRIPT-VARS "))).toBe(true); + expect(logged.filter((l) => l.startsWith("TRIGSCRIPT-PROGRAM ")).length).toBe(1); // The step going with the service. svc.attach().dispose(); });