diff --git a/compiler/backend/emit.ts b/compiler/backend/emit.ts index a701302..c13c09f 100644 --- a/compiler/backend/emit.ts +++ b/compiler/backend/emit.ts @@ -24,7 +24,7 @@ import type { Range } from "../ranges"; import { PlayerGroup, type ActionRecord, type ConditionRecord } from "../../vendor/triggers"; import { actionDef } from "../../vendor/triggerDefs"; import { OVERFLOW_FN, errorLine } from "./errors"; -import { END, MOST_ACTS, MOST_CONDS, NOW, STACK, SWITCHES, TRIGGER_BYTES, TRIGGER_OFF, TRIGGER_ON, checkGraph, nextOf, type Act, type ActPart, type Compare, type Cond, type Dword, type FixedFn, type Graph, type Num, type Region, type Trigger } from "./graph"; +import { END, MOST_ACTS, MOST_CONDS, NOW, PASS_END, STACK, SWITCHES, TRIGGER_BYTES, TRIGGER_OFF, TRIGGER_ON, checkGraph, nextOf, type Act, type ActPart, type Compare, type Cond, type Dword, type FixedFn, type Graph, type Num, type Region, type Trigger } from "./graph"; /** The game's tick (frames since the game started), and the fields of the trigger a map of our own wakes by. */ export const GAME_TICK = 0x57f23c; @@ -409,6 +409,10 @@ const CENTRE_LINE = 0x640b60 + 218 * 12, CENTRE_BYTES = 218, FIRST_FREE_UNIT = 0 export const TRIGGER_LISTS = 0x51a280; /** Where the game keeps the current player: whom Display Text shows to, and what "the current player" names. */ const CURRENT_PLAYER_ADDRESS = 0x6509b0; +/** The game's unit table: its places, how far apart, where the first one is (`watch`). */ +const UNIT_PLACES = 1700, UNIT_STRIDE = 336, UNIT_TABLE_ADDRESS = 0x59cca8, UNIT_TABLE_EPD = (UNIT_TABLE_ADDRESS - 0x58a364) / 4; +/** The events pass's variables the watch writes (`units.ts`'s): the record's EPD, the trigger's first condition's number, the boundary. */ +const EVENT_RECORD = "@ev.e", EVENT_FIELDS = "@ev.wf", EVENT_BOUNDARY = "@ev.be"; /** * The map's player settings, as a map of our own needs them (TrigScript 4, step 7): the slots a human or a computer * plays (0-based, P1 to P8), and each slot's force (0 to 3). Who a program runs for, who All Players and a force are, @@ -490,7 +494,7 @@ function gameConds(r: ConditionRecord, when: boolean): Cond[] | "true" | "false" } /** The kinds of statement, told apart from the expressions of the same name (`pop`, `call`). */ -export const STATEMENT_KINDS = new Set(["declare", "assignText", "storeText", "releaseText", "textLoop", "assign", "declareArray", "store", "push", "setLength", "assignBool", "assignUnit", "unitLoop", "unitWrite", "unitDo", "tableWrite", "if", "while", "do", "for", "unrolled", "switch", "break", "continue", "return", "sleep", "parallel", "start", "cancel", "action", "syncSend", "select", "centerLocation", "print", "buildText", "block", "try", "throw", "uncaught", "remark", "graph"]); +export const STATEMENT_KINDS = new Set(["declare", "assignText", "storeText", "releaseText", "textLoop", "assign", "declareArray", "store", "push", "setLength", "assignBool", "assignUnit", "unitLoop", "unitWrite", "unitDo", "tableWrite", "if", "while", "do", "for", "unrolled", "switch", "break", "continue", "return", "sleep", "parallel", "start", "cancel", "action", "syncSend", "select", "centerLocation", "print", "buildText", "block", "try", "throw", "uncaught", "remark", "graph", "watch"]); // ── what the emitter takes ── @@ -1111,12 +1115,13 @@ class Scope { return true; } case "return": return (this.native && this.returning.length > 0 && this.returned(s)) || this.island(s); - // (A map of our own: the slots `planTasks` made; listening — `on.*` — is part 3's.) + // (A map of our own: the slots `planTasks` made — a listener's, `on.*`'s, as any other's.) case "start": { - const slots = this.native && !s.listen ? this.slots.get(s) : undefined; + const slots = this.native ? this.slots.get(s) : undefined; return (!!slots && slots.every((slot) => this.taskCall(slot.call))) || this.island(s); } case "cancel": return (this.native && this.lowers(s.task) && (this.num(s.task), true)) || this.island(s); + case "watch": return (this.native && this.stmts(s.handler, false)) || this.island(s); case "parallel": return (this.native && s.branches.every((b) => this.stmts(b, false))) || this.island(s); // (Where a `throw` goes is the emitter's, `Emitter.handler`: the program keeps the error in flight.) case "try": return (this.native && !!this.errors && this.stmts(s.body, inLoop) && this.stmts(s.catch, inLoop)) || this.island(s); @@ -1199,7 +1204,7 @@ interface Store { array: ArrayDecl; index: Linear; reads: CellRead[]; value: Lin /** (`at` on a copy or a call: the statement it is for, when its trigger is another's.) */ /** (`label`: every place of `target` set to where `to` is — where a function that calls itself goes back to.) */ /** (`route`: a variable's cell into these dwords, and nothing else of its places.) */ -type Item = { act: Act } | { write: Write } | { fill: { temp: string; target: string; apart?: boolean; top?: number } } | { copy: string; at?: At } | { island: Stmt; writes: Set } | { bump: { target: string; add: number } } | { call: { fn: FixedFn; target: string }; at?: At } | { label: { target: string; to: string } } | { route: { from: string; into: Dword[]; add?: number } }; +type Item = { act: Act } | { write: Write } | { fill: { temp: string; target: string; apart?: boolean; top?: number } } | { copy: string; at?: At } | { island: Stmt; writes: Set } | { bump: { target: string; add: number } } | { call: { fn: FixedFn; target: string }; at?: At } | { label: { target: string; to: string } } | { pointer: { target: string; to: Dword } } | { route: { from: string; into: Dword[]; add?: number } }; /** * `keep`: an empty trigger that stays (a jump whose next pointer a place sets). `guarded`: an `if` in one trigger (its * conditions, its statements' actions), which may be followed by the copies of what it writes — a copy whether or not @@ -1355,6 +1360,8 @@ class Emitter { frames: { result?: { id: string; kind: string }; end: string }[] = []; /** Where an error thrown here goes: the `catch` of each `try` around, in the function being laid out, innermost last. */ handlers: string[] = []; + /** The variables whose cells nothing reads: a label or a pointer set for them is set at their other places only. */ + cellless = new Set(); /** * Inside statements only an error goes through — those that end in a `throw`, and a `catch` (a map of our own): what * they read of a variable is refreshed from its cell where it is read, as a routed variable's is, never a place that @@ -1892,6 +1899,7 @@ class Emitter { case "break": return this.goto(this.loops[this.loops.length - 1].exit); case "continue": return this.goto(this.loops[this.loops.length - 1].cont); case "start": return this.scope.islands.has(s) ? this.left(s) : this.start(s); + case "watch": return this.watch(s); case "cancel": return this.scope.islands.has(s) ? this.left(s) : this.cancel(s); case "parallel": return this.scope.islands.has(s) ? this.left(s) : this.parallel(s); case "try": { @@ -2673,6 +2681,63 @@ class Emitter { this.push({ label: resume, conds: [], items: [{ act: co.reset }], ...(then ? { next: then } : {}) }); } + /** + * The events pass's watch (`units.ts`): a trigger for each of the game's unit places, whose conditions read the + * place's record where it is against what the trigger saw there last — its own conditions' numbers, 0 for a place + * never handed out. They are walked from the place the game will hand out next (the entry's next pointer, moved down + * as places are first handed out) up to place 1 699 and then to place 0: the places the game has handed out, newest + * first. A place as it was is the one trigger: its conditions hold, and it puts the trigger before it back and goes + * on to the next. One that changed goes to its stub — the record's EPD into `@ev.e`, the trigger's first condition's + * number into `@ev.wf`, where to go on into `RET`, the place handed out after it into `NEXT` — and to the handler, + * laid out once, after which the boundary's check moves the entry down when the place was the one handed out next. + */ + watch(s: Extract): void { + const W = (k: number) => `${this.key}:w${k}`, S = (k: number) => `${this.key}:s${k}`; + const entry = this.label("ev"), handler = this.label("eh"), back = this.label("er"), exit = this.label("ex"), stays = this.label("eb"), move = this.label("emv"); + // (Variables whose places are next pointers and numbers, their cells read by nothing: what a stub sets is the + // places. Where the handler goes on to, and the place handed out after this one: the numbers of the two actions of + // `move` that point the walk's start and the handler's way on at it.) + const RET = `${this.key}.evRet`, NEXT = `${this.key}.evNext`; + for (const v of [RET, NEXT]) this.own.add(v); + for (const v of [RET, NEXT, EVENT_FIELDS]) this.cellless.add(v); + this.placesOf(RET).push({ dword: nextOf(back), scale: 1, add: 0 }); + this.placesOf(NEXT).push({ dword: { at: move, field: { act: 0, part: "value" } }, scale: 1, add: 0 }, { dword: { at: move, field: { act: 1, part: "value" } }, scale: 1, add: 0 }); + // (After place k in the walk: k + 1, then 0 after 1 699, the end after 0; before it, k − 1, and 1 699 before 0.) + const after = (k: number) => (k === 0 ? exit : k === UNIT_PLACES - 1 ? W(0) : W(k + 1)); + const before = (k: number) => (k === 0 ? UNIT_PLACES - 1 : k - 1); + // (The place handed out after it: 1 699 after 0, then down; none after 1.) + const handedNext = (k: number) => (k === 0 ? UNIT_PLACES - 1 : Math.max(1, k - 1)); + const putBack = (k: number): Item => ({ act: { dword: nextOf(W(k)), modifier: "set", value: { label: S(k) } } }); + // (Kept, though empty: their next pointers are written.) + this.push({ label: entry, conds: [], items: [], next: W(0), keep: true }); + for (let k = 0; k < UNIT_PLACES; k++) { + const record = UNIT_TABLE_ADDRESS + UNIT_STRIDE * k; + this.push({ label: W(k), conds: s.fields.map((f): Cond => ({ dword: { game: record + 4 * f.dword }, compare: f.compare, value: 0, ...(f.mask !== undefined ? { mask: f.mask } : {}) })), items: [putBack(before(k)), { act: { dword: nextOf(W(k)), modifier: "set", value: { label: after(k) } } }], next: S(k) }); + const items: Item[] = [putBack(before(k))]; + this.write(EVENT_RECORD, { c: UNIT_TABLE_EPD + (UNIT_STRIDE / 4) * k, terms: [] }, true, items); + items.push({ pointer: { target: EVENT_FIELDS, to: { at: W(k), field: { cond: 0 } } } }, { label: { target: RET, to: after(k) } }, { label: { target: NEXT, to: W(handedNext(k)) } }); + this.push({ label: S(k), conds: [], items, next: handler }); + } + this.land(handler); + this.stmts(s.handler); + // The place the game hands out next is the boundary (`EVENT_BOUNDARY` its distance from place 0, 0 to begin with): + // when it is handed out, the walk starts at the next one from now on, and goes on from there now — it was not + // walked in this frame. + const record: BoolExpr = { kind: "compare", op: "==", left: { kind: "var", id: EVENT_RECORD }, right: { kind: "binary", op: "+", left: { kind: "var", id: EVENT_BOUNDARY }, right: { kind: "const", value: UNIT_TABLE_EPD }, at: this.at!, label: "" }, at: this.at!, label: "" }; + this.jump(record, false, stays, this.at); + this.push({ label: move, conds: [], items: [{ act: { dword: nextOf(entry), modifier: "set", value: 0 } }, { act: { dword: nextOf(back), modifier: "set", value: 0 } }] }); + const first = this.label("e0"), moved = this.label("em"); + this.jump({ kind: "compare", op: "==", left: { kind: "var", id: EVENT_RECORD }, right: { kind: "const", value: UNIT_TABLE_EPD }, at: this.at!, label: "" }, true, first, this.at); + this.write(EVENT_BOUNDARY, { c: u(-UNIT_TABLE_EPD - UNIT_STRIDE / 4), terms: [{ id: EVENT_RECORD, k: 1 }] }); + this.goto(moved); + this.land(first); + this.write(EVENT_BOUNDARY, { c: (UNIT_STRIDE / 4) * (UNIT_PLACES - 1), terms: [] }); + this.land(moved); + this.land(stays); + this.push({ label: back, conds: [], items: [], next: W(0), keep: true }); + this.push({ label: exit, conds: [], items: [putBack(0)] }); + } + /** A trigger of a slot's or a branch's that says whether nothing runs in it (done) or something does. */ private flags(check: string, on: boolean): Item { return { act: { dword: { at: check, field: "flags" }, modifier: "set", value: on ? TRIGGER_ON : TRIGGER_OFF } }; @@ -4418,11 +4483,18 @@ class Emitter { else if ("fill" in it) pieces.push({ fill: it.fill }); else if ("route" in it) pieces.push({ route: it.route }); else if ("label" in it) { - for (const pl of this.placesOf(it.label.target)) { + for (const pl of this.placesOf(it.label.target).slice(this.cellless.has(it.label.target) ? 1 : 0)) { if (!pl.dword || pl.scale !== 1 || pl.add !== 0) throw new Error(`emitter: a place of ${it.label.target} that is no dword of its own`); acts().push({ dword: pl.dword, modifier: "set", value: { label: to(it.label.to) } }); } } + else if ("pointer" in it) { + // (Every place of a variable set to a dword's EPD: the events pass's, `watch`.) + for (const pl of this.placesOf(it.pointer.target).slice(this.cellless.has(it.pointer.target) ? 1 : 0)) { + if (!pl.dword || pl.scale !== 1 || pl.add !== 0) throw new Error(`emitter: a place of ${it.pointer.target} that is no dword of its own`); + acts().push({ dword: pl.dword, modifier: "set", value: { epd: it.pointer.to } }); + } + } else if ("copy" in it) { // (A variable never below zero and at most its `top`: only the bits it can have.) const top = this.scope.decls.get(it.copy)?.top; @@ -6009,6 +6081,7 @@ class Rewriter { return [...pre, { ...s, call: made.call }]; } case "parallel": return [{ ...s, branches: s.branches.map((b) => this.stmts(b)) }]; + case "watch": return [{ ...s, handler: this.stmts(s.handler) }]; case "switch": { const pre: Stmt[] = []; // (Tested against each case: one variable plus a constant — bits under a mask computed first.) @@ -6093,7 +6166,7 @@ export const emitterCounts = { hoisted: 0, reciprocals: 0, jumps: 0, products: 0 * call of a function of the Python side's, and nothing read that the body did not write first (there is no one to * copy it in; a copy within the body is made of triggers by the layout). Returns the graph, or why it cannot be. */ -export function emitBody(body: Stmt[], decls: Map, key: string, functions = new Map(), arrays = new Map(), stackDepth = STACK_DEPTH, owner?: number, players: MapPlayers = ANY_PLAYERS, errors?: ProgramErrors): Graph | string { +export function emitBody(body: Stmt[], decls: Map, key: string, functions = new Map(), arrays = new Map(), stackDepth = STACK_DEPTH, owner?: number, players: MapPlayers = ANY_PLAYERS, errors?: ProgramErrors, pass?: Stmt[]): Graph | string { // (Every statement must be the emitter's here, so a rewriting it would not take is refused whichever way it goes: // no trial is needed.) const take = () => true; @@ -6141,10 +6214,14 @@ export function emitBody(body: Stmt[], decls: Map, key: string, const planned = planTasks(block.length === 1 ? block[0] : ({ kind: "block", body: block, at: body[0]?.at } as Stmt), key, known, arrays); if (typeof planned === "string") return planned; const s = planned.root; + // (The map's events pass, its statements as the program's: laid out apart, before the run's `@wake`.) + const passed = pass?.length ? rw.stmt({ kind: "block", body: pass, at: pass[0].at } as Stmt) : []; + const passStmt: Stmt | undefined = passed.length ? { kind: "block", body: passed, at: pass![0].at } : undefined; const scope = new Scope(known); rules(scope); scope.slots = planned.slots; scope.once = planned.once; + if (passStmt && !scope.stmt(passStmt, false)) return scope.why || "the events pass"; // (The map's own variables, which no program declares and no run's start sets: 0 as the map starts, one cell each.) for (const d of known.values()) if (d.global) scope.declared.add(d.id); if (!scope.stmt(s, false)) return scope.why || "?"; @@ -6188,6 +6265,13 @@ export function emitBody(body: Stmt[], decls: Map, key: string, if (!e.pending.length && !e.pendingTasks.length) break; } const tasks = e.turnOfTasks(); + // The events pass: in a trigger of its own, kept, which the layout sends the walk to before the run's `@wake`. + const passAt = `${key}:pass`; + if (passStmt) { + e.push({ label: passAt, conds: [], items: [], keep: true }); + e.stmt(passStmt); + e.goto(PASS_END); + } e.settleThrown(); e.emitDigits(); if (start) { @@ -6204,6 +6288,7 @@ export function emitBody(body: Stmt[], decls: Map, key: string, } if (typeof g !== "string" && e.templates.length) g.templates = e.templates; if (typeof g !== "string" && tasks) g.tasks = tasks; + if (typeof g !== "string" && passStmt) g.pass = passAt; if (typeof g !== "string" && e.regions.size) g.cells = [...e.regions.values(), ...(g.cells ?? [])]; const cells = typeof g === "string" ? 0 : (g.cells ?? []).reduce((n, r) => n + r.cells, 0); if (cells > STACK_CELLS_MOST) return { e, g: `arrays and stacks of ${cells} cells, past the ${STACK_CELLS_MOST} a map of our own lays out (72 bytes a cell)` }; diff --git a/compiler/backend/graph.ts b/compiler/backend/graph.ts index 3ee285a..239075f 100644 --- a/compiler/backend/graph.ts +++ b/compiler/backend/graph.ts @@ -199,7 +199,13 @@ export interface Entry { from: string; into: Dword; add: Num; scale?: number } * `tasks`: where a run's tasks have their turn (a map of our own): the walk goes there after the program's turn, and when * the program's wake does not hold; it ends at `END`. */ -export interface Graph { entries: Entry[]; triggers: Trigger[]; arrays?: { id: string; stride: number }[]; cells?: Region[]; templates?: { id: string; bytes: number[]; at?: number; data?: boolean }[]; rows?: string[][]; tasks?: string } +/** + * `pass`: where the map's events pass starts (a map of our own), which the walk goes through before the run's `@wake`; + * it ends at `PASS_END`. + */ +export interface Graph { entries: Entry[]; triggers: Trigger[]; arrays?: { id: string; stride: number }[]; cells?: Region[]; templates?: { id: string; bytes: number[]; at?: number; data?: boolean }[]; rows?: string[][]; tasks?: string; pass?: string } +/** Where the events pass goes when it is done: its run's `@wake`. */ +export const PASS_END = "@passed"; export interface Region { id: string; cells: number; values?: number[] } /** @@ -231,7 +237,7 @@ export const actValue = (label: string, k: number): Dword => ({ at: label, field export function checkGraph(g: Graph): string[] { const labels = new Set(g.triggers.map((t) => t.label)); const out: string[] = []; - const known = (l: string) => l === END || labels.has(l); + const known = (l: string) => l === END || l === PASS_END || labels.has(l); const dword = (d: Dword, where: string) => { if ("at" in d && !labels.has(d.at)) out.push(`${where}: no trigger ${d.at}`); }; diff --git a/compiler/backend/heap.ts b/compiler/backend/heap.ts index b824b7e..17ddac1 100644 --- a/compiler/backend/heap.ts +++ b/compiler/backend/heap.ts @@ -840,7 +840,7 @@ export function growingArrays(program: Program, cells = HEAP_CELLS): Program { /** The statements whose bodies run again and again. */ const LOOPS = new Set(["while", "do", "for", "unitLoop", "textLoop", "unrolled"]); /** The kinds of statement (a `call` and a `pop` with a label: an expression of either has none). */ -const STMT_KINDS = new Set(["action", "assign", "assignBool", "assignText", "assignUnit", "block", "break", "buildText", "call", "cancel", "centerLocation", "continue", "declare", "declareArray", "do", "for", "graph", "if", "parallel", "pop", "print", "push", "releaseText", "remark", "return", "select", "setLength", "sleep", "start", "store", "storeText", "switch", "syncSend", "tableWrite", "textLoop", "throw", "try", "uncaught", "unitDo", "unitLoop", "unitWrite", "unrolled", "while"]); +const STMT_KINDS = new Set(["action", "assign", "assignBool", "assignText", "assignUnit", "block", "break", "buildText", "call", "cancel", "centerLocation", "continue", "declare", "declareArray", "do", "for", "graph", "if", "parallel", "pop", "print", "push", "releaseText", "remark", "return", "select", "setLength", "sleep", "start", "store", "storeText", "switch", "syncSend", "tableWrite", "textLoop", "throw", "try", "uncaught", "unitDo", "unitLoop", "unitWrite", "unrolled", "watch", "while"]); /** * The variables the closures count (`VarDecl.held`: a reference to an environment, a row of the front end's arrays — the diff --git a/compiler/backend/layout.ts b/compiler/backend/layout.ts index 2cd8269..cd6e8d4 100644 --- a/compiler/backend/layout.ts +++ b/compiler/backend/layout.ts @@ -30,7 +30,7 @@ * went on — a trigger of a few conditions and actions in a little over 200 bytes, not 2,408. A row a doubling trigger * walks (`Graph.rows`) is stacked as one, its triggers 2,408 bytes apart. */ -import { END, NOW, TRIGGER_BYTES, TRIGGER_OFF, TRIGGER_ON, type Act, type Cond, type Dword, type Graph, type Value } from "./graph"; +import { END, NOW, PASS_END, TRIGGER_BYTES, TRIGGER_OFF, TRIGGER_ON, type Act, type Cond, type Dword, type Graph, type Value } from "./graph"; import { TRIGGER_LISTS, WAKE_AT, WAKE_NEXT, WAKE_TICK } from "./emit"; import type { ScriptString } from "../runtime"; @@ -281,11 +281,13 @@ export function layout(input: LayoutInput): OwnMap { // past the program once woken. const wake = (i: number) => (i ? `@wake#${i}` : "@wake"), init = (i: number) => (i ? `@init#${i}` : "@init"); const stop = "@stop", run = "@run", entry = (p: number) => `@entry${p}`, gate = (p: number) => `@gate${p}`; - const after = (i: number) => (i + 1 < runs.length ? wake(i + 1) : stop); + // (A run whose graph holds the map's events pass goes through it first.) + const first = (i: number) => runs[i].graph.pass ?? wake(i); + const after = (i: number) => (i + 1 < runs.length ? first(i + 1) : stop); const list = (p: number) => TRIGGER_LISTS + 12 * p; const listEnd = (p: number) => u(~(list(p) + 4)); // (Where each run's graph ends: the next run.) - const ends = new Map(runs.map((_, i) => [i ? `${END}#${i}` : END, after(i)])); + const ends = new Map(runs.flatMap((_, i): [string, string][] => [[i ? `${END}#${i}` : END, after(i)], [i ? `${PASS_END}#${i}` : PASS_END, wake(i)]])); const endOf = (l: string) => ends.get(l) ?? l; type T = { label: string; conds: Cond[]; acts: Act[]; next?: string | number; prev?: number; off?: boolean }; const triggers: T[] = runs.flatMap(({ graph, owner }, i) => [ @@ -323,7 +325,7 @@ export function layout(input: LayoutInput): OwnMap { next: listEnd(p), })), // Every entry and gate back to ending at its list's end, the count of frames one more, then the first run. - { label: run, conds: [], acts: [...players.flatMap((p): Act[] => [{ dword: { at: entry(p), field: "next" }, modifier: "set", value: listEnd(p) }, { dword: { at: gate(p), field: "next" }, modifier: "set", value: listEnd(p) }]), { dword: { var: NOW }, modifier: "add", value: 1 }], next: wake(0) }, + { label: run, conds: [], acts: [...players.flatMap((p): Act[] => [{ dword: { at: entry(p), field: "next" }, modifier: "set", value: listEnd(p) }, { dword: { at: gate(p), field: "next" }, modifier: "set", value: listEnd(p) }]), { dword: { var: NOW }, modifier: "add", value: 1 }], next: first(0) }, ); for (const t of runs.flatMap((r) => r.graph.triggers)) if (t.copy || t.island || t.call) throw new Error(`${t.label}: a copy, an island or a call has no place in a map of our own`); // The triggers stacked, a row as one. diff --git a/compiler/backend/own.ts b/compiler/backend/own.ts index 17fd69c..46fe1c6 100644 --- a/compiler/backend/own.ts +++ b/compiler/backend/own.ts @@ -15,7 +15,7 @@ import { ANY_PLAYERS, emitBody, groupSlots, type MapPlayers } from "./emit"; import { NOW, type Act, type Cond, type Dword, type Graph, type Num, type Trigger, type Value } from "./graph"; import { TextPool, knownTexts } from "./texts"; import { HEAP_GLOBALS, growingArrays, heldVariables, isHeapGlobal, loopScoped } from "./heap"; -import { UNIT_GLOBALS, unitValues } from "./units"; +import { EVENT_GLOBALS, UNIT_GLOBALS, eventSources, isEventGlobal, unitValues } from "./units"; import { ownErrors } from "./errors"; export interface OwnRun { @@ -66,7 +66,10 @@ export function emitOwn(r: Pick, p const texts = pool.finish(); // Units as two numbers each (`units.ts`), then arrays that grow on the heap (`heap.ts`), once the texts' numbers are // in: those passes copy what they rewrite. - const programs = known.map((p) => loopScoped(growingArrays(heldVariables(unitValues(p))))); + // (The map's events pass is one, laid out by the first program that has events, for what all of them loop over.) + const sources = eventSources(known); + const passer = known.findIndex((p) => eventSources(p).size); + const programs = known.map((p, k) => loopScoped(growingArrays(heldVariables(unitValues(p, { sources, pass: k === passer }))))); for (const [k, program] of programs.entries()) { const slots = ownerSlots(program, players); if (typeof slots === "string") return slots; @@ -81,19 +84,20 @@ export function emitOwn(r: Pick, p }; collect(program); // (The heap's own variables, which no program declares, and the count of frames: one cell each for the map.) - for (const d of [...HEAP_GLOBALS, ...UNIT_GLOBALS, NOW_DECL]) decls.set(d.id, d); + for (const d of [...HEAP_GLOBALS, ...UNIT_GLOBALS, ...EVENT_GLOBALS, NOW_DECL]) decls.set(d.id, d); const functions = new Map((program.functions ?? []).map((f) => [f.id, f])); const arrays = new Map((program.arrays ?? []).map((a) => [a.id, a])); // The variables and arrays every run of the program has in common: one for every player of a per-player program. const shared = new Set(program.perPlayer ? [...[...decls.values()].filter((d) => d.shared).map((d) => d.id), ...[...arrays.values()].filter((a) => a.shared).map((a) => a.id)] : []); // A program for one player runs as the first of its owners (the Python side's `slots[0]`), one for each player // as each of them, in the order of the slots. + const firstRun = runs.length; for (const player of program.perPlayer ? [...slots].sort((a, b) => a - b) : slots.slice(0, 1)) { const n = runs.length; // (The first run keeps the emitter's names; each other one's are its own, but for what its program shares.) - const graph = emitBody(program.body, decls, n ? `own${n}` : "own", functions, arrays, undefined, player, players, program.errors); + const graph = emitBody(program.body, decls, n ? `own${n}` : "own", functions, arrays, undefined, player, players, program.errors, runs.length === firstRun ? program.pass : undefined); if (typeof graph === "string") return graph; - const name = (id: string) => (!n || isHeapGlobal(id) || id === NOW || UNIT_GLOBALS.some((d) => d.id === id) ? id : shared.has(id) ? (k ? `${id}#p${k}` : id) : `${id}#${n}`); + const name = (id: string) => (!n || isHeapGlobal(id) || id === NOW || isEventGlobal(id) || UNIT_GLOBALS.some((d) => d.id === id) ? id : shared.has(id) ? (k ? `${id}#p${k}` : id) : `${id}#${n}`); const g = n ? renamed(graph, `#${n}`, name) : graph; // An action of the game's names its text and its sound file by the script's strings: given here, for the layout. const of = (i: number) => (i ? r.strings[i - 1] : undefined); @@ -143,6 +147,7 @@ function renamed(g: Graph, suffix: string, name: (id: string) => string): Graph ...(g.templates ? { templates: g.templates.map((t) => ({ ...t, id: t.id + suffix })) } : {}), ...(g.rows ? { rows: g.rows.map((r) => r.map(label)) } : {}), ...(g.tasks ? { tasks: label(g.tasks) } : {}), + ...(g.pass ? { pass: label(g.pass) } : {}), }; } diff --git a/compiler/backend/texts.ts b/compiler/backend/texts.ts index 9797531..dd84646 100644 --- a/compiler/backend/texts.ts +++ b/compiler/backend/texts.ts @@ -1470,7 +1470,7 @@ const TEXT_BYTES = 1023; const DISPLAY_TEXT = 9; // (`call` and `pop` are numbers too: a list of numbers that starts with one is walked as statements, which does to // each what walking it as a number would.) -const STMT_KINDS = new Set(["action", "assign", "assignBool", "assignText", "assignUnit", "block", "break", "buildText", "call", "cancel", "centerLocation", "continue", "declare", "declareArray", "do", "for", "graph", "if", "parallel", "pop", "print", "push", "releaseText", "remark", "return", "select", "setLength", "sleep", "start", "store", "storeText", "switch", "syncSend", "tableWrite", "textLoop", "throw", "try", "uncaught", "unitDo", "unitLoop", "unitWrite", "unrolled", "while"]); +const STMT_KINDS = new Set(["action", "assign", "assignBool", "assignText", "assignUnit", "block", "break", "buildText", "call", "cancel", "centerLocation", "continue", "declare", "declareArray", "do", "for", "graph", "if", "parallel", "pop", "print", "push", "releaseText", "remark", "return", "select", "setLength", "sleep", "start", "store", "storeText", "switch", "syncSend", "tableWrite", "textLoop", "throw", "try", "uncaught", "unitDo", "unitLoop", "unitWrite", "unrolled", "watch", "while"]); function isStmt(o: unknown): boolean { return !!o && typeof o === "object" && STMT_KINDS.has((o as { kind?: string }).kind ?? ""); } diff --git a/compiler/backend/units.ts b/compiler/backend/units.ts index 09702f8..c32fd05 100644 --- a/compiler/backend/units.ts +++ b/compiler/backend/units.ts @@ -17,7 +17,7 @@ * What it does not take — a unit an event gives (step 11), a pick near the mouse and the selection read (step 12) — * it leaves as it was, for the emitter to refuse in a script's words. */ -import type { At, BoolExpr, Call, CompareOp, FuncDecl, NumExpr, Program, Stmt, UnitExpr, UnitFilter, UnitFlag, UnitNumField, VarDecl } from "../ir"; +import type { ArrayDecl, At, BoolExpr, Call, CompareOp, FuncDecl, NumExpr, Program, Stmt, UnitExpr, UnitFilter, UnitFlag, UnitNumField, VarDecl } from "../ir"; import type { ActionRecord, ConditionRecord } from "../../vendor/triggers"; import { STATEMENT_KINDS } from "./emit"; @@ -82,6 +82,37 @@ const ME_ID = "@u.me"; * whole map, as the heap's are, read and moved down by every walk (`scan`). */ export const UNIT_END = "@unit.end"; + +/** + * The frame's events pass (TrigScript 4, step 11, part 3), the map's one, before the programs' turns: a watch of every + * unit place (`emit.ts`'s `watch`), whose handler lists this frame's units, each an EPD — what came on, what died, what + * took up an order or turned into another type (with what it was) — and how many of each. + */ +const MADE = "@ev.made", GONE = "@ev.gone", TURNED = "@ev.turned", TURNED_WAS = "@ev.turnedWas", CHANGED = "@ev.changed", CHANGED_WAS = "@ev.changedWas"; +const MADE_N = "@ev.madeN", GONE_N = "@ev.goneN", TURNED_N = "@ev.turnedN", CHANGED_N = "@ev.changedN"; +/** + * What the watch hands its handler: the record's EPD, and the number of the watch trigger's first condition (its next + * ones five dwords on); the distance from place 0 of the place the game hands out next. + */ +const EV_RECORD = "@ev.e", EV_FIELDS = "@ev.wf", EV_BOUNDARY = "@ev.be"; +const AT0 = { file: "", line: 0, column: 0 }; +/** The pass's variables, one each for the map, which no program declares; and its lists, the map's (a program's events read them). */ +export const EVENT_GLOBALS: readonly VarDecl[] = [MADE_N, GONE_N, TURNED_N, CHANGED_N, EV_RECORD, EV_BOUNDARY, EV_FIELDS].map((id) => ({ id, name: "(events)", kind: "number", shared: true, ...(id === EV_FIELDS ? {} : { routed: true }), global: true, at: AT0 })); +export const isEventGlobal = (id: string): boolean => id.startsWith("@ev."); +/** A unit's hit points (256ths, 0 when it died in a fight) and the last player whose attack hurt it. */ +const HP_DWORD = 0x08 >> 2, LAST_ATTACKER = word(0x90, 1); +/** The most units of each kind a pass hands on in a frame, as the Python side's. */ +const EVENT_CAP = 512; +type EventSource = "created" | "died" | "killed" | "ordered" | "morphed"; +/** The sources a piece of the IR loops over the events pass's units of. */ +export function eventSources(x: unknown, out = new Set()): Set { + if (Array.isArray(x)) { x.forEach((y) => eventSources(y, out)); return out; } + if (!x || typeof x !== "object") return out; + const o = x as { kind?: string; source?: EventSource }; + if (o.kind === "unitLoop" && o.source) out.add(o.source); + for (const v of Object.values(o)) if (v && typeof v === "object") eventSources(v, out); + return out; +} export const UNIT_GLOBALS: readonly VarDecl[] = [{ id: UNIT_END, name: "(units)", kind: "number", shared: true, routed: true, global: true, at: { file: "", line: 0, column: 0 } }]; /** * Each player's list of units: its first unit (a pointer a player, 0 for none), and a unit's place in its owner's list — @@ -146,7 +177,11 @@ function isStmtList(x: unknown): x is Stmt[] { * The program with its units as numbers: `unitValues` (see the file's comment). `owner`: the player the program runs * as, when it is one player known when built (the current player of a filter); otherwise read where the game keeps it. */ -export function unitValues(program: Program): Program { +/** + * `events`: the sources the map's programs' events loop over, and whether this program lays out the map's one events + * pass (`Program.pass`) — the first program that has any. + */ +export function unitValues(program: Program, events?: { sources: Set; pass: boolean }): Program { if (!hasUnits(program)) return program; let here: At = program.at; let made = 0; @@ -374,6 +409,117 @@ export function unitValues(program: Program): Program { ]; }; + // ── the events pass's units ── + const back = (): Stmt => poke(k(CURRENT), ME); + const store = (array: string, index: NumExpr, value: NumExpr): Stmt => ({ kind: "store", array, index, value, ...at() }); + const element = (array: string, index: NumExpr): NumExpr => ({ kind: "element", array, index, ...at() } as NumExpr); + /** + * A loop over the units this frame's events pass found (`on.*`'s), in the order it found them, the filter asked of + * each: one that came on, took up an order or turned into another type and is gone since is passed over; one that died + * is its record as it died (the game gives its place to no other unit before the next frame's dying pass). + */ + const eventLoop = (s: Extract): Stmt[] => { + const source = s.source as EventSource; + const list = { created: MADE, died: GONE, killed: GONE, ordered: TURNED, morphed: CHANGED }[source]; + const count = { created: MADE_N, died: GONE_N, killed: GONE_N, ordered: TURNED_N, morphed: CHANGED_N }[source]; + const was = source === "ordered" ? TURNED_WAS : source === "morphed" ? CHANGED_WAS : undefined; + const type = s.filter.type; + if (type !== undefined && type >= 228 && GROUP_BITS[type] === undefined) throw new Leave(); + const pre: Stmt[] = []; + const I = fresh("ei"), E = fresh("ee"); + const e = V(E); + const who = s.filter.owner !== undefined ? playerOf(s.filter.owner, pre) : undefined; + const box = s.filter.at ? sides(s.filter.at, pre) : undefined; + const by = s.event?.by !== undefined ? playerOf(s.event.by, pre) : undefined; + const asks: BoolExpr[] = []; + if (source !== "died" && source !== "killed") asks.push(cmp(">=", peek(of(e, SPRITE)), k(1)), cmp(">=", bits(e, ORDER), k(0x100))); + // (Died in a fight: no hit points, the last attack one of the eight players'.) + if (source === "killed") asks.push(cmp("==", peek(of(e, HP_DWORD)), k(0)), cmp("<=", read(e, LAST_ATTACKER), k(7))); + if (by) asks.push(cmp("==", read(e, LAST_ATTACKER), by)); + if (s.event?.order !== undefined) asks.push(cmp("==", bits(e, ORDER), k(s.event.order * 256))); + if (who) asks.push(cmp("==", read(e, OWNER), who)); + if (type !== undefined && type < 228) asks.push(cmp("==", read(e, TYPE), k(type))); + if (box) { + const [l, t, r, b] = box; + const xy = (mask: number) => peek(of(e, POSITION), mask); + asks.push(cmp(">=", xy(0xffff), l), cmp("<=", xy(0xffff), r), cmp(">=", xy(0xffff0000), ranged(bin("*", t, k(0x10000)), 0, 0xffff0000)), cmp("<=", xy(0xffff0000), ranged(bin("*", b, k(0x10000)), 0, 0xffff0000))); + } + let inner: Stmt[]; + turns.set(s.decl.id, { q: e, killed: mayKill(s.body) }); + try { + inner = stmts(s.body); + } finally { + turns.delete(s.decl.id); + } + if (s.was) inner = [{ kind: "declare", decl: s.was, init: source === "killed" ? k(0) : element(was!, V(I)), ...at() }, ...(source === "killed" ? readInto(s.was.id, e, LAST_ATTACKER) : []), ...inner]; + if (type !== undefined && GROUP_BITS[type] !== undefined) { + // (A trigger's class: the type's group byte, read where units.dat keeps it.) + const ty = fresh("ty"), qt = fresh("tq"), bit = GROUP_BITS[type], tyv = ranged(V(ty), 0, 0xffff); + inner = [dec(ty, k(0), 0xffff), ...readInto(ty, e, TYPE), dec(qt, bin(">>>", tyv, k(2)), 0x3fff), + ...when(any([0, 1, 2, 3].map((r) => all([cmp("==", bin("&", tyv, k(3)), k(r)), cmp(">=", peek(bin("+", k(GROUPS), V(qt)), (bit * 2 ** (8 * r)) >>> 0), k(1))]))), inner)]; + } + return [...pre, dec(I, k(0)), { kind: "for", cond: cmp("<", V(I), V(count)), update: [set(I, bin("+", V(I), k(1)))], body: [dec(E, element(list, V(I))), ...when(all(asks), inner)], ...at() } as Stmt]; + }; + + /** + * The frame's events pass: the counts back to 0, and the watch of every unit place (`emit.ts`), whose conditions read + * the place's record where it is against what it saw there last — its uniqueness byte, and as the program's events + * need them its order (whether it is alive, or which) and its type. The handler, for a place + * that changed: what the watch saw, read from its conditions' numbers through the current player pointed at the + * first, and the record as it is now; a unit of another uniqueness byte that is alive came on, the one seen + * alive and alive no more died, one that took up another order or turned into another type did; and what it sees now + * written back into the conditions' numbers. As the simulator, newest first: the places walked so. + */ + const pass = (sources: Set): Stmt[] => { + here = AT0; + const died = sources.has("died") || sources.has("killed"), ordered = sources.has("ordered"), morphed = sources.has("morphed"); + type Field = { name: string; dword: number; mask?: number; compare: "atLeast" | "exactly"; read: number }; + // (The uniqueness byte first: the watch of a place whose unit is not alive ends there. The game hands a place's + // first unit a byte above 0, and every unit dies through the order Die, its sprite still there.) + const fields: Field[] = [{ name: "uid", dword: UID.dword, mask: 0xff00, compare: "exactly", read: 0xff00 }]; + if (ordered) fields.push({ name: "order", dword: ORDER.dword, mask: 0xff00, compare: "exactly", read: 0xff00 }); + else if (died) fields.push({ name: "alive", dword: ORDER.dword, mask: 0xff00, compare: "atLeast", read: 0x100 }); + if (morphed) fields.push({ name: "type", dword: TYPE.dword, mask: 0xffff, compare: "exactly", read: 0xffff }); + const old = (name: string) => `@ev.was.${name}`, now = (name: string) => `@ev.now.${name}`; + const e = V(EV_RECORD); + const step = (by: number): Stmt => ({ kind: "action", record: { location: 0, text: 0, wav: 0, time: 0, player: 0, target: by, unitId: 0, type: 45, modifier: 8, flags: 0, padding: 0, mask: 0 }, variables: [{ field: "player", bits: 32, name: "at", expr: k(CURRENT) }], ...at() } as Stmt); + const out: Stmt[] = [set(MADE_N, k(0))]; + if (died) out.push(set(GONE_N, k(0))); + if (ordered) out.push(set(TURNED_N, k(0))); + if (morphed) out.push(set(CHANGED_N, k(0))); + const h: Stmt[] = []; + // What the watch saw, its conditions' numbers five dwords apart. + h.push(poke(k(CURRENT), V(EV_FIELDS))); + fields.forEach((f, i) => { if (i) h.push(step(5)); h.push(dec(old(f.name), peek(k(CURRENT_PLAYER), f.read))); }); + h.push(back()); + // What the place holds now. + // (Read into a variable only what is compared or written back; whether it is alive asked of the record.) + h.push(dec(now("uid"), peek(of(e, UID.dword), 0xff00))); + if (ordered) h.push(dec(now("order"), peek(of(e, ORDER.dword), 0xff00))); + if (morphed) h.push(dec(now("type"), peek(of(e, TYPE.dword), 0xffff))); + const ALIVE = "@ev.alive"; + h.push(dec(ALIVE, k(0), 1), ...when(all([cmp(">=", peek(of(e, SPRITE)), k(1)), cmp(">=", ordered ? V(now("order")) : peek(of(e, ORDER.dword), 0xff00), k(0x100))]), [set(ALIVE, k(1))])); + const listed = (list: string, n: string, extra: Stmt[] = []): Stmt[] => when(cmp("<", V(n), k(EVENT_CAP)), [store(list, V(n), e), ...extra, set(n, bin("+", V(n), k(1)))]); + const same: Stmt[] = []; + if (died) same.push(...when(all([cmp(">=", V(old(ordered ? "order" : "alive")), k(0x100)), cmp("==", V(ALIVE), k(0))]), listed(GONE, GONE_N))); + if (ordered) same.push(...when(all([cmp("==", V(ALIVE), k(1)), cmp("!=", V(now("order")), V(old("order")))]), listed(TURNED, TURNED_N, [store(TURNED_WAS, V(TURNED_N), bin(">>>", V(old("order")), k(8)))]))); + if (morphed) same.push(...when(all([cmp("==", V(ALIVE), k(1)), cmp("!=", V(now("type")), V(old("type")))]), listed(CHANGED, CHANGED_N, [store(CHANGED_WAS, V(CHANGED_N), V(old("type")))]))); + h.push(...when(cmp("!=", V(now("uid")), V(old("uid"))), when(cmp("==", V(ALIVE), k(1)), listed(MADE, MADE_N)), same)); + // What it sees now, into the watch's numbers: whether it is alive, or its order. For a unit not alive, only another + // in its place is news: the second condition's kind (the top byte of its dword after the number) is 0 then, and + // the game reads no further. + const seen: Record = { uid: V(now("uid")), order: ordered ? V(now("order")) : k(0), alive: bin("*", V(ALIVE), k(0x100)), type: morphed ? V(now("type")) : k(0) }; + h.push(poke(k(CURRENT), V(EV_FIELDS))); + fields.forEach((f, i) => { + if (i) h.push(step(i === 2 ? 4 : 5)); + h.push(poke(k(CURRENT_PLAYER), seen[f.name])); + if (i === 1) h.push(step(1), poke(k(CURRENT_PLAYER), bin("*", V(ALIVE), k(0x0f000000)), 0xff000000)); + }); + h.push(back()); + out.push({ kind: "watch", fields: fields.map(({ dword, mask, compare }) => ({ dword, ...(mask !== undefined ? { mask } : {}), compare })), handler: h, ...at() } as Stmt); + return out; + }; + /** A pick: the first unit the filter matches, the nearest to a location's centre, or one drawn at random. */ const pick = (p: Extract, pre: Stmt[]): Unit => { if (p.mouse !== undefined) throw new Leave(); @@ -801,7 +947,7 @@ export function unitValues(program: Program): Program { return [...pre, { kind: "assign", target: p.e, value: u.e, at: s.at, label: s.label }, { kind: "assign", target: p.k, value: kv, at: s.at, label: s.label }]; } case "unitLoop": { - if (s.source) throw new Leave(); + if (s.source) return [...pre, ...eventLoop(s)]; const killed = mayKill(s.body); const out = scan(s.filter, pre, (q) => { turns.set(s.decl.id, { q, killed }); @@ -813,6 +959,12 @@ export function unitValues(program: Program): Program { }); return [...pre, ...out]; } + // (A task's call as an inlined call's: a unit among its parameters two numbers, its body rewritten.) + case "start": { + callOf(s.call, pre); + const made = pre.pop() as Extract; + return [...pre, { ...s, call: made.call }]; + } case "unitWrite": write(s, pre); return pre; case "unitDo": verb(s, pre); return pre; case "select": select(s, pre); return pre; @@ -869,6 +1021,11 @@ export function unitValues(program: Program): Program { }); here = program.at; const body = stmts(program.body); + // The map's events pass, and its lists, in the program that lays it out; the lists in every program that reads them. + const sources = events?.sources ?? new Set(); + const passed = events?.pass && sources.size ? { pass: pass(sources) } : {}; + const lists = sources.size ? [MADE, ...(sources.has("died") || sources.has("killed") ? [GONE] : []), ...(sources.has("ordered") ? [TURNED, TURNED_WAS] : []), ...(sources.has("morphed") ? [CHANGED, CHANGED_WAS] : [])] : []; + const arrays = [...(program.arrays ?? []), ...lists.map((id): ArrayDecl => ({ id, name: "(events)", kind: "number", length: EVENT_CAP, shared: true, at: AT0 }))]; if (usesMe) body.unshift(dec(ME_ID, peek(k(CURRENT)))); - return { ...program, body, ...(program.functions ? { functions: rewritten } : {}) }; + return { ...program, body, ...(program.functions ? { functions: rewritten } : {}), ...passed, ...(lists.length ? { arrays } : {}) }; } diff --git a/compiler/ir.ts b/compiler/ir.ts index c99c000..ec1f0b5 100644 --- a/compiler/ir.ts +++ b/compiler/ir.ts @@ -695,6 +695,13 @@ export type Stmt = | { kind: "textBlock"; decl: VarDecl; block: string; bytes: number; text?: string; data?: boolean; at: At } /** A map of our own only: Display Text of block `block` as it is now, to `to` as a print's is. */ | { kind: "showText"; block: string; bytes: number; to: number; at: At } + /** + * A map of our own only (`compiler/backend/units.ts`): the events pass's watch of the game's unit places — a trigger + * for each, whose conditions read `fields` of the unit record where it is (`dword` into it, `mask`) against what the + * trigger saw there last, its own numbers. A place as it was goes on to the next; one that changed runs `handler`, + * with `@ev.e` the record's EPD and the current player at the trigger's first condition's number (`emit.ts`). + */ + | { kind: "watch"; fields: { dword: number; mask?: number; compare: "atLeast" | "exactly" }[]; handler: Stmt[]; at: At; label: string } /** A loop the emitter took (version 26): `graph` is made as it is; `original` is the loop, run where nothing is made (the simulator). */ | { kind: "graph"; graph: Graph; original: Stmt[]; at: At; label: string }; @@ -710,6 +717,11 @@ export interface Program { functions?: FuncDecl[]; /** Present when the program throws or catches anything (B4): where an error in flight is kept. */ errors?: ProgramErrors; + /** + * A map of our own only (`compiler/backend/units.ts`): the frame's events pass — what came onto the map, died, took up + * an order or turned into another type since the pass before — run before the programs' turns, once for the map. + */ + pass?: Stmt[]; body: Stmt[]; at: At; } diff --git a/compiler/simulateIr.ts b/compiler/simulateIr.ts index fe015b4..24eff3d 100644 --- a/compiler/simulateIr.ts +++ b/compiler/simulateIr.ts @@ -1308,7 +1308,7 @@ class ProgramRun { } case "remark": return "next"; // (A map of our own's own statements, which only its backend makes and never hands the simulator.) - case "textBlock": case "showText": throw new Error(`the simulator does not run a ${s.kind}`); + case "textBlock": case "showText": case "watch": throw new Error(`the simulator does not run a ${s.kind}`); } } diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 666daac..5d9b27e 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -3568,8 +3568,8 @@ found what they left out; where a step moved, its old number is given. queries scheduled (ideas 6, 7) still to come. 11. **Tasks, `async` and errors** (was 6; errors new). Part 1 ✓: `try`, `catch`, `finally`, `throw`, `uncaught`, a program's `errors`, the error classes — "Errors on our own backend". Part 2 ✓: `start`, `cancel`, `running`, - `Promise.all`/`race`/`any`, `every`, `after`, a task's error awaited — "Tasks on our own backend". Part 3: the - `on.*` handlers and the pass that finds what they are handed. + `Promise.all`/`race`/`any`, `every`, `after`, a task's error awaited — "Tasks on our own backend". Part 3 ✓: the + `on.*` handlers and the pass that finds what they are handed — "Unit events on our own backend". 12. **Input** (was 7; idea 13). Keys, clicks, the mouse, typed lines, sync channels, the `local.*` reads and the user's player id. The test harness types and presses through chatEvent's and MSQC's addresses (`GameSim`): it moves with them, and the map stops needing what MSQC takes (a location, the Valkyrie's entry, Player 12's units). @@ -3927,7 +3927,7 @@ lists what no script reaches. A step is done when its scripts say so. | 8 Texts as values | 48 of 48 | | 9 Arrays that grow, the heap | 27 of 27 | | 10 Units | 99 of 99 | -| 11 Tasks, `async` and errors | 35 of 50 | +| 11 Tasks, `async` and errors | 50 of 50 | | 12 Input | 0 of 26 | | 13 The map's own triggers | 0 of 13 | @@ -4023,6 +4023,16 @@ the ledger is the list it can be written again from at 4.0. is a condition that fails each frame (13 ns), one with nothing in it a trigger passed by as done: a timer wheel (idea 2) would make a sleeper cost nothing until its frame. "did not start" is said in red by the game, as the Python side's build says it, where the simulator notes a fault (both builds). +- **Unit events on a map of our own (step 11, part 3), what is left:** the watch costs every place the game has handed + out 48–60 ns a frame, so a frame where nothing comes on and nothing is dying grows with the map's units, where + eudplib's build without `on.order` looks at its lists' heads alone: 9.3 µs against 5.2 with 128 places, and at the + full table about 90 against 5. The way out is a gate that passes the watch by when nothing can have changed: the head + of the game's list of free places (it moves when a unit is handed a place or one is freed) and a count that moves + when a unit starts to die (a player's losses), a dozen conditions — to be read in both games first, since Remastered + links its lists otherwise. Without `on.order` or `on.morphed` only. + The handler reads a changed place's values a bit at a time, about 4 µs for a unit made: through the current player, + pointed at the record once, instead of a copy of the record's EPD for every read. The 1,700 places are two triggers + each (1.84 MB against eudplib's 1.27 for the probe without `on.order`): a stub of fewer actions packs tighter. - A variable set from a constant less itself (`j = 1 - j`) is copied a bit at a time — 32 triggers, about 0.5 µs, where eudplib's is two actions: the game's subtract stops at 0, so the emitter copies a variable read times −1 (found weighing the errors probe, 2026-10-22). @@ -5033,6 +5043,63 @@ Played in Remastered (2026-10-22, recording 20.57.34): all 9 lines word for word each group at the lifted game's frame (2, 14, 22, 29, 40, 43, 47, 75), the frames where 32 tasks start among them; no hitch. +### Unit events on our own backend (2026-10-22) + +Step 11, part 3 (`compiler/backend/units.ts`: the events pass's handler and the loops over what it found; the emitter's +`watch`; the pass before the run's `@wake` in the layout). The ledger's 15 rows of `on.*` are taken: step 11 is done, +50 of 50. + +- **The map's events pass**, once for the map before every program's turn (laid out by the first program with events, + for what all of them loop over), is a watch of the game's unit places: a trigger for each, whose conditions read the + place's record where it is against what the trigger saw there last, its own conditions' numbers — the uniqueness + byte, and as the events need them whether the unit is alive or its order, and its type. A place as it was is that + one trigger: its conditions hold, and its two actions put the trigger before it back and go on to the next. One that + changed goes to its stub (the record's EPD, where the trigger's numbers are, where to go on) and to the handler, laid + out once: a unit of another uniqueness byte that is alive came on, the one seen alive and alive no more died, one + that took up another order or turned into another type did; and what it sees now goes back into the trigger's + numbers. +- **A place whose unit is dead or gone is watched by its uniqueness byte alone:** the handler writes 0 into the kind of + the trigger's second condition, and the game reads no further (48 ns a place against 60 for one alive). Every unit + dies through the order Die, its sprite still there, and the game hands a place's first unit a uniqueness byte above + 0, so the sprite is not watched. +- **The places are walked newest first**, as the simulator hands units on: from the place the game will hand out next + (the boundary) up to place 1 699, then place 0. The game hands places out in that order and never-used ones last, so + the boundary only moves down: when the handler sees it handed out, the walk starts at the next one from then on, and + goes on from there in the same frame. +- **A listener** is a task that loops over what the pass found, as the front end writes it; a handler that waits starts + a task for each unit it is handed. What was on the map before the first pass is not handed on: a listener sleeps a + frame before it first looks (eudplib's build is the same). +- **Built first, the eudplib build's way:** walking the game's lists by their pointers. A record cost about 3.2 µs a + frame against eudplib's 1.9 (the pointer read a bit at a time and copied into the places that read it): 46 units + dying cost 146 µs a frame against 89, and `on.order` 221 against 164. It was replaced before the merge. +- **Found on the way:** an action whose number is a place is found by its index in its trigger, counted over the + trigger's actions — a write before it in the same trigger moved it. A unit's place in a task's call (`start`) was + rewritten as an expression, not as statements. An empty trigger whose next pointer is written must be kept. + +| Lifted game, Remastered's weights, µs a frame (`probes/own-events.ts`; without `on.order` beside it) | Ours | eudplib | +| --- | ---: | ---: | +| nothing coming or going, `on.order` among the events | **7.2** | 164 | +| forty-six units dying, `on.order` among the events | **9.0** | 224 | +| forty units made / killed in a frame, `on.order` among the events | **227 / 228** | 506 / 528 | +| without `on.order`: forty-six units dying | **8.7** | 89 | +| without `on.order`: forty units made / killed in a frame | **188 / 180** | 248 / 392 | +| without `on.order`: nothing coming or going, about 80 places handed out | 6.7 | **5.1** | +| without `on.order`: the same, 128 places handed out | 9.3 | **5.2** | +| the first frame (the listeners started) | **249** | 1,272 | +| the map, `on.order` among the events / without | 1.96 / 1.84 MB | **1.78 / 1.27 MB** | + +eudplib's build without `on.order` looks at its lists' heads alone when nothing comes on and nothing is dying: on such +a frame it is ahead, by 60 ns for each place the game has handed out (Open, below). Every other frame is ours. + +Checked: `tests/emitter.test.ts` ("a map of our own, unit events": a unit table laid out as the game keeps it, changed +between frames — a unit handed out, an order taken up, a type changed, a unit dying in a fight, its place handed to +another — the payload walked from its bytes frame after frame, each line at its frame; a handler that waits, a task +each); `probes/own-events.ts` and the same without `on.order`, both ways in the lifted game, every line the same at +the same frame and "ok". + +Played in Remastered (2026-10-22, recording 21.59.29, the lists' build): all 7 lines word for word and in order, each at +the lifted game's frame (2, 4, 6, 12, 15, 29); the Marines made and killed as the script says; no hitch. + ### Triggers stacked (2026-10-20) Brought forward from step 14 (`compiler/backend/layout.ts`). The game reads no more of a trigger's 2,408 bytes than diff --git a/ledger/input/on-chat.ts b/ledger/input/on-chat.ts index ca8b257..865057d 100644 --- a/ledger/input/on-chat.ts +++ b/ledger/input/on-chat.ts @@ -1,4 +1,4 @@ -// ledger: step 12, refused: an event handled (on.chat) +// ledger: step 12, refused: the players' input (chat) // A function run each time a player types a line that fits a pattern: `on.chat("-give {n}", (p, { n }) => …)`. program(async () => { on.chat("-give {n}", (p, { n }) => { diff --git a/ledger/input/on-key-from.ts b/ledger/input/on-key-from.ts index 7eeab33..6aae70d 100644 --- a/ledger/input/on-key-from.ts +++ b/ledger/input/on-key-from.ts @@ -1,4 +1,4 @@ -// ledger: step 12, refused: an event handled (on.key) +// ledger: step 12, refused: the players' input (key) // A function run each time one player presses a key: `on.key("Space", (p) => …, { from: P1 })`. program(async () => { let n = 0; diff --git a/ledger/input/on-key.ts b/ledger/input/on-key.ts index 12e1943..90ed392 100644 --- a/ledger/input/on-key.ts +++ b/ledger/input/on-key.ts @@ -1,4 +1,4 @@ -// ledger: step 12, refused: an event handled (on.key) +// ledger: step 12, refused: the players' input (key) // A function run each time a player presses a key: `on.key("F7", (p) => …)`. program(async () => { on.key("F7", (p) => { diff --git a/ledger/programs/listener-at-the-top.ts b/ledger/programs/listener-at-the-top.ts index fbcf4c9..4eac16a 100644 --- a/ledger/programs/listener-at-the-top.ts +++ b/ledger/programs/listener-at-the-top.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.created) +// ledger: step 11, takes // A listener at the top of a script, beside a program: a program of its own that listens from the start of the game. on.created((u) => { u.energy = 50; diff --git a/ledger/units/on-created-at-the-top.ts b/ledger/units/on-created-at-the-top.ts index 1a1d740..21053f1 100644 --- a/ledger/units/on-created-at-the-top.ts +++ b/ledger/units/on-created-at-the-top.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.created) +// ledger: step 11, takes // A listener written at the top of a script, not in a program: it listens from the start of the game. on.created(units.ZergZergling, (u) => { u.energy = 50; diff --git a/ledger/units/on-created-filter.ts b/ledger/units/on-created-filter.ts index a4c80fa..f61a98f 100644 --- a/ledger/units/on-created-filter.ts +++ b/ledger/units/on-created-filter.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.created) +// ledger: step 11, takes // The units that come onto the map that a filter matches: `on.created({ owner: P2 }, (u) => …)`. program(async () => { on.created({ owner: P2 }, (u) => { diff --git a/ledger/units/on-created-type.ts b/ledger/units/on-created-type.ts index 414cbc3..cba962a 100644 --- a/ledger/units/on-created-type.ts +++ b/ledger/units/on-created-type.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.created) +// ledger: step 11, takes // The units of one type that come onto the map: `on.created(units.ZergZergling, (u) => …)`. program(async () => { on.created(units.ZergZergling, (u) => { diff --git a/ledger/units/on-created.ts b/ledger/units/on-created.ts index f9c6914..49c4aad 100644 --- a/ledger/units/on-created.ts +++ b/ledger/units/on-created.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.created) +// ledger: step 11, takes // Every unit that comes onto the map, handed to a function in the next frame: `on.created((u) => …)`. program(async () => { on.created((u) => { diff --git a/ledger/units/on-died-filter.ts b/ledger/units/on-died-filter.ts index 45f7d29..acadfcc 100644 --- a/ledger/units/on-died-filter.ts +++ b/ledger/units/on-died-filter.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.died) +// ledger: step 11, takes // The units that die that a filter matches, as they died: `on.died({ owner: P8 }, (u) => …)`. program(async () => { on.died({ owner: P8 }, (u) => { diff --git a/ledger/units/on-died.ts b/ledger/units/on-died.ts index ee01743..4932c0f 100644 --- a/ledger/units/on-died.ts +++ b/ledger/units/on-died.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.died) +// ledger: step 11, takes // Every unit that dies or is removed, handed to a function as it was: `on.died((u) => …)`. program(async () => { let n = 0; diff --git a/ledger/units/on-event-async.ts b/ledger/units/on-event-async.ts index 200f59b..312640d 100644 --- a/ledger/units/on-event-async.ts +++ b/ledger/units/on-event-async.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.died) +// ledger: step 11, takes // An event's function that waits, started as a task each time: `on.died(…, async (u) => { await sleep(…); … })`. program(async () => { on.died({ owner: P8 }, async (u) => { diff --git a/ledger/units/on-event-stopped.ts b/ledger/units/on-event-stopped.ts index f2d145b..8dd8b37 100644 --- a/ledger/units/on-event-stopped.ts +++ b/ledger/units/on-event-stopped.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.created) +// ledger: step 11, takes // An event's listening stopped by the task it gives back: `const t = on.created(…); t.cancel()`. program(async () => { const t = on.created((u) => { diff --git a/ledger/units/on-killed-by.ts b/ledger/units/on-killed-by.ts index f1c1e9b..e1a1952 100644 --- a/ledger/units/on-killed-by.ts +++ b/ledger/units/on-killed-by.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.killed) +// ledger: step 11, takes // The units one player kills of another's: `on.killed({ owner: P8, by: P1 }, (u, by) => …)`. program(async () => { on.killed({ owner: P8, by: P1 }, (u, by) => { diff --git a/ledger/units/on-killed.ts b/ledger/units/on-killed.ts index 79d445b..a211ce4 100644 --- a/ledger/units/on-killed.ts +++ b/ledger/units/on-killed.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.killed) +// ledger: step 11, takes // Every unit killed in a fight, with the player whose attack killed it: `on.killed((u, by) => …)`. program(async () => { on.killed((u, by) => { diff --git a/ledger/units/on-morphed-type.ts b/ledger/units/on-morphed-type.ts index e21b9a5..831a21f 100644 --- a/ledger/units/on-morphed-type.ts +++ b/ledger/units/on-morphed-type.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.morphed) +// ledger: step 11, takes // The units that turn into one type: `on.morphed(units.TerranSiegeTankSiegeMode, (u) => …)`. program(async () => { on.morphed(units.TerranSiegeTankSiegeMode, (u) => { diff --git a/ledger/units/on-morphed.ts b/ledger/units/on-morphed.ts index 2edc4fa..d6b05b7 100644 --- a/ledger/units/on-morphed.ts +++ b/ledger/units/on-morphed.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.morphed) +// ledger: step 11, takes // A unit that turns into another type and stays the same unit, with the type it was: `on.morphed((u, was) => …)`. program(async () => { on.morphed((u, was) => { diff --git a/ledger/units/on-order-of-one.ts b/ledger/units/on-order-of-one.ts index 77ff87f..52e0ed0 100644 --- a/ledger/units/on-order-of-one.ts +++ b/ledger/units/on-order-of-one.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.order) +// ledger: step 11, takes // The units of a type that take up one order: `on.order(units.TerranMarine, orders.Move, (u) => …)`. program(async () => { on.order(units.TerranMarine, orders.Move, (u) => { diff --git a/ledger/units/on-order.ts b/ledger/units/on-order.ts index a474f71..55539e6 100644 --- a/ledger/units/on-order.ts +++ b/ledger/units/on-order.ts @@ -1,4 +1,4 @@ -// ledger: step 11, refused: an event handled (on.order) +// ledger: step 11, takes // Every unit that takes up a new order, with the order and the one it had: `on.order((u, order, was) => …)`. program(async () => { on.order((u, order, was) => { diff --git a/probes/own-events.ts b/probes/own-events.ts new file mode 100644 index 0000000..b91af90 --- /dev/null +++ b/probes/own-events.ts @@ -0,0 +1,72 @@ +// TrigScript 4, the backend: unit events in a map of our own (scripts/build-own.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 +// a task that loops over what the pass found. +// 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-eud.scx +import { program, print, sleep, frames } from "trigscript"; + +program(async () => { + print("Own events: the payload runs."); + let made = 0, marines = 0, theirs = 0, died = 0, killed = 0, moved = 0, late = 0; + on.created((u) => { + made += 1; + if (u.type == units.TerranMarine) marines += 1; + }); + on.created({ owner: P2 }, (u) => { + theirs += 1; + }); + on.died((u) => { + died += 1; + }); + on.killed((u, by) => { + if (by == 0) killed += 1; + }); + on.order(units.TerranMarine, orders.Move, (u) => { + moved += 1; + }); + // A handler that waits: a task each time. + on.died(units.ZergZergling, async (u) => { + await sleep(frames(3)); + late += 1; + }); + await sleep(frames(2)); + + // A: what was on the map before the first pass is not handed on: a listener sleeps a frame before it looks. + const atStart = made; + if (made == 0 && died == 0) print("A: nothing at the start ok"); else print(`A: WRONG ${made} ${died}`); + + // B: units made by the program come on in the next frame. + createUnit(P1, units.TerranMarine, 2, locations.Anywhere); + createUnit(P2, units.ZergZergling, 3, locations.Anywhere); + await sleep(frames(2)); + if (made == 5 && marines == 2 && theirs == 3) print("B: made ok"); else print(`B: made WRONG ${made} ${marines} ${theirs}`); + + // C: units killed by the program die, not in a fight; the handler that waits counts a frame or three later. + killUnit(P2, units.ZergZergling); + await sleep(frames(2)); + const lings = died; + await sleep(frames(4)); + if (lings >= 7 && killed == 0 && late == lings) print(`C: died ok (${lings})`); else print(`C: died WRONG ${lings} ${killed} ${late}`); + + // D: an order taken up. + const m = first({ owner: P1, type: units.TerranMarine }); + m?.order("move", locations.Anywhere); + await sleep(frames(3)); + if (moved == 1) print("D: an order ok"); else print(`D: an order WRONG ${moved}`); + + print("Weighed:"); + // W1: nothing coming or going, a frame of the pass and the listeners alone. + await sleep(frames(4)); + // W2: forty units made in one frame, each handed to on.created the next. + createUnit(P1, units.TerranMarine, 40, locations.Anywhere); + await sleep(frames(4)); + // W3: forty killed in one frame. + killUnit(P1, units.TerranMarine); + await sleep(frames(6)); + if (made == atStart + 5 + 40 && died == lings + 46) print("W: weighed ok"); else print(`W: weighed WRONG ${made - atStart} ${died - lings}`); +}); diff --git a/tests/emitter.test.ts b/tests/emitter.test.ts index 5570dcc..846dfde 100644 --- a/tests/emitter.test.ts +++ b/tests/emitter.test.ts @@ -1071,7 +1071,8 @@ const laidOut = new WeakMap; own: ReturnT * `frames`: walked that many times, as the game walks the triggers once a frame — from `@run` (the map's count of frames * one more) when more than one, the memory kept from one to the next; each text shown noted with its frame. */ -function runOwn(g: Graph, memory?: Map, arrays = new Map(), runs?: { graph: Graph; owner: number }[], frames = 1): Env { +/** `between`: what the game does before each frame — memory written, as units come and go. */ +function runOwn(g: Graph, memory?: Map, arrays = new Map(), runs?: { graph: Graph; owner: number }[], frames = 1, between?: (frame: number, write: (addr: number, value: number) => void) => void): Env { // (Laid out once a graph: a test may walk one many times.) let done = laidOut.get(g); if (!done || (done.arrays !== arrays && (done.arrays.size || arrays.size))) { @@ -1097,10 +1098,11 @@ function runOwn(g: Graph, memory?: Map, arrays = new Map u(DEATHS + 4 * (epd === 13 ? rd(0x6509b0) : epd)); PRINTED.frames = []; for (let frame = 1; frame <= frames; frame++) { + between?.(frame, wr); let pc = frames > 1 ? own.triggers.get("@run")! : own.address; const shownBefore = PRINTED.got.length; for (let steps = 0; !(pc & 0x80000000); steps++) { - if (steps > 200_000_000) throw new Error("the payload does not end"); + if (steps > Number(process.env.OWN_STEPS ?? 200_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++) { @@ -2116,3 +2118,65 @@ describe("a map of our own, tasks", () => { ].join("\n")); }); }); + +describe("a map of our own, unit events", () => { + /** + * The game's unit table as the game keeps it, two units to start with in the places it hands out first (0, then + * 1 699); each frame the world may change (`then`), and the payload is walked from its bytes. + */ + const TABLE = 0x59cca8, place = (k: number) => TABLE + 336 * k; + const run = (script: string, frames: number, then: (frame: number, w: (addr: number, value: number) => void) => void): string[] => { + const r = compileScript(ts, { "main.ts": script }, defaultScriptNames(), { lib: defaultLib(), emitter: false }); + expect(r.diagnostics).toEqual([]); + const o = emitOwn(r); + if (typeof o === "string") throw new Error(o); + const memory = new Map(); + const unit = (k: number, type: number, uid: number, order = 3) => { + memory.set(place(k) + 0x08, 40 * 256); + memory.set(place(k) + 0x0c, 0x700000 + k); + memory.set(place(k) + 0x4c, order * 256); + memory.set(place(k) + 0x64, type); + memory.set(place(k) + 0xa4, uid * 256); + }; + unit(0, 0, 9); + unit(1699, 37, 8); + PRINTED.got = []; + runOwn(o.runs[0].graph, memory, o.values, undefined, frames, then); + return PRINTED.got.map((t, i) => `${PRINTED.frames[i]} ${Buffer.from(t, "hex").toString()}`); + }; + + it("hands on what came on, died, was killed, took up an order and turned into another type, in the frame it did", () => { + const got = run([ + "program(async () => {", + " on.created((u) => { print(`made ${u.type}`); });", + " on.died((u) => { print(`died ${u.type}`); });", + " on.killed((u, by) => { print(`killed ${u.type} by ${by}`); });", + " on.order((u, order, was) => { print(`order ${u.type} ${order} ${was}`); });", + " on.morphed((u, was) => { print(`morphed ${u.type} from ${was}`); });", + "});", + ].join("\n"), 14, (f, w) => { + // Frame 3: a Marine at place 1698, the next the game hands out. Frame 5: the Marine at place 0 takes up order 6. + // Frame 7: the Zergling turns into type 38. Frame 9: the new Marine dies in a fight with Player 1's: no hit + // points, the order Die. Frame 11: it is gone, and a Firebat (type 32) is handed its place, the uniqueness byte + // one on. + if (f === 3) { w(place(1698) + 0x08, 40 * 256); w(place(1698) + 0x0c, 0x700001); w(place(1698) + 0x4c, 0x0300); w(place(1698) + 0xa4, 7 * 256); } + if (f === 5) w(place(0) + 0x4c, 0x0600); + if (f === 7) w(place(1699) + 0x64, 38); + if (f === 9) { w(place(1698) + 0x08, 0); w(place(1698) + 0x4c, 0x0000); w(place(1698) + 0x90, 0); } + if (f === 10) w(place(1698) + 0x0c, 0); + if (f === 11) { w(place(1698) + 0x08, 50 * 256); w(place(1698) + 0x0c, 0x700002); w(place(1698) + 0x4c, 0x0300); w(place(1698) + 0x64, 32); w(place(1698) + 0xa4, 8 * 256); } + }); + expect(got).toEqual(["3 made 0", "5 order 0 6 3", "7 morphed 38 from 37", "9 died 0", "9 killed 0 by 0", "11 made 32"]); + }); + + it("starts a task for each unit a handler that waits is handed", () => { + const got = run([ + "program(async () => {", + " on.created(async (u) => { await sleep(frames(2)); print(`late ${u.type}`); });", + "});", + ].join("\n"), 8, (f, w) => { + if (f === 3) for (const [kk, uid] of [[1698, 7], [1697, 6]]) { w(place(kk) + 0x0c, 0x700001); w(place(kk) + 0x4c, 0x0300); w(place(kk) + 0x64, 5); w(place(kk) + 0xa4, uid * 256); } + }); + expect(got).toEqual(["5 late 5", "5 late 5"]); + }); +}); diff --git a/tests/ledger.test.ts b/tests/ledger.test.ts index 3bb9fd0..25f95e1 100644 --- a/tests/ledger.test.ts +++ b/tests/ledger.test.ts @@ -14,6 +14,7 @@ const NOT_FROM_A_SCRIPT: Record = { graph: "the emitter's own statement, made only when a map is built through eudplib with the emitter on", textBlock: "a map of our own's statement, which compiler/backend/texts.ts makes for a made text's block", showText: "a map of our own's statement, which compiler/backend/texts.ts makes to print a made text's block", + watch: "a map of our own's statement, which compiler/backend/units.ts makes for the events pass", cast: "`u32()`, `i32()` and `fixed()` settle a value's type; `compiler/numbers.ts` takes every cast out once types are known", "intrinsic floor": "`compiler/numbers.ts` makes it the number itself, or a shift of a fixed one", "intrinsic ceil": "`compiler/numbers.ts` makes it the number itself, or a shift of a fixed one",