From c5afe2cac04fe9bcf53a50ff67784e7da35566ab Mon Sep 17 00:00:00 2001 From: Jeany Date: Fri, 23 Oct 2026 16:27:14 -0600 Subject: [PATCH] The Open list's bug pass: the emitter's masked fold, a program's own return, and the false errors Between step 15 and step 16 of the way off eudplib, the Open list's bugs (docs/v4-plan.md, "The Open list's bug pass (2026-10-23)"): - The emitter folded a scratch cell worked by a masked write (`&`, `|`, `^` by a constant) into a dword that starts from a constant, turning the mask's `set 0` into a set of the constant: `(v & 0xffff) - 1025` came out as v's low half under -1025's high half. The fold is not made for a masked scratch cell. - `return` at the top of a program ends it (front end, emitter, Python side). - memory() and setMemory() of the deaths table's dwords 12 to 26 name the cell as the deaths of player e mod 12 of unit e div 12, so no build reads a player group. - disabled() in a program is refused, preserve() and a dropped `false` of displayText are remarks; the build warns of hyper triggers left out and of triggers in no list. - False errors taken: n++ as an index (and the index before the value), a comparison as a boolean argument, a switch on a known text, this in an arrow of a method, a pick's method through ?. (libraryName no longer takes an interface member for the library's), u?.hp ?? 0, sleep().then() as a value, new P(4).x, && of conditions in a const, rows[i].pop() ?? -1, a build-time throw inside a try. - Against JavaScript: ~, ** and Math.pow by a known power, Math.sign, Math.log2, fill(v, start, end). first() and randomUnit() take a set of units. - The cutscene module's header example compiles as written, and a test reads it. Thirteen ledger scripts, tests/open-list.test.ts, the README's language passages, and the plan: the Open list, the section, and two decisions (no map protection on the own build; Magenta's rows on our backend after step 17). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01En4SHqafb1QFJ4xgbUkAe5 --- README.md | 16 +- compiler/backend/build.ts | 13 +- compiler/backend/emit.ts | 12 +- compiler/declarations.ts | 6 +- compiler/eud.ts | 5 +- compiler/generated/stdlib.ts | 2 +- compiler/generated/trigscriptPy.ts | 2 +- compiler/hoist.ts | 6 + compiler/runtime.ts | 16 +- compiler/structured.ts | 191 +++++++++++++++++-- docs/v4-plan.md | 98 +++++++--- editor.ts | 14 +- ko.ts | 1 + ledger/arrays/fill-range.ts | 10 + ledger/arrays/pop-of-a-row.ts | 11 ++ ledger/language/comparison-as-an-argument.ts | 12 ++ ledger/language/condition-in-a-const.ts | 6 + ledger/language/return-at-the-top.ts | 8 + ledger/language/switch-on-a-known-text.ts | 12 ++ ledger/language/this-in-an-arrow.ts | 18 ++ ledger/language/throw-when-built-in-a-try.ts | 15 ++ ledger/memory/deaths-cells-12-to-26.ts | 6 + ledger/numbers/bitwise-not.ts | 8 + ledger/numbers/power.ts | 8 + ledger/tasks/then-as-a-value.ts | 7 + ledger/units/first-of-a-set.ts | 9 + python/trigscript.py | 9 +- service.ts | 10 +- stdlib/cutscene.ts | 2 +- tests/emitter.test.ts | 29 +++ tests/open-list.test.ts | 139 ++++++++++++++ tests/script-structured.test.ts | 2 +- tests/service.test.ts | 5 +- 33 files changed, 628 insertions(+), 80 deletions(-) create mode 100644 ledger/arrays/fill-range.ts create mode 100644 ledger/arrays/pop-of-a-row.ts create mode 100644 ledger/language/comparison-as-an-argument.ts create mode 100644 ledger/language/condition-in-a-const.ts create mode 100644 ledger/language/return-at-the-top.ts create mode 100644 ledger/language/switch-on-a-known-text.ts create mode 100644 ledger/language/this-in-an-arrow.ts create mode 100644 ledger/language/throw-when-built-in-a-try.ts create mode 100644 ledger/memory/deaths-cells-12-to-26.ts create mode 100644 ledger/numbers/bitwise-not.ts create mode 100644 ledger/numbers/power.ts create mode 100644 ledger/tasks/then-as-a-value.ts create mode 100644 ledger/units/first-of-a-set.ts create mode 100644 tests/open-list.test.ts diff --git a/README.md b/README.md index 248fe58..f8866d4 100644 --- a/README.md +++ b/README.md @@ -435,7 +435,7 @@ with a message, never passed over in silence. | `try` / `catch` / `throw` | Missing | The game has no exceptions. What is always a mistake — an index past the end, a full heap, a stack overflow — is said in the game in red, or listed by Simulate. | | `async` / `await`, promises, generators, timers | Missing | `sleep()` is how a program waits: it gives the frame back and carries on later, and may stand anywhere but in a function that calls itself or a loop over units. Something on its own clock is another `program()`. | | Modules | Same | `import` between the script's files and from `"trigscript"`; nothing from npm. A function another file's program calls is made with `game()`. | -| The standard library | A little | `Math.min`, `Math.max`, `Math.abs` (and the rounding ones, which change nothing), `String(n)`, `n.toString()`, `Array.from`, `new Array(n).fill(v)`. For `console.log` there is `print()`, for `Math.random()` there is `random(n)`. Inside a program `Math.random()`, `Date.now()` and `new Date()` are refused, since they would be worked out once when the map is built; game time is `elapsed()` or `memory.game.frame`. No `JSON`, `Date`, `RegExp`, `Object.*`, and no `Math.sqrt` or `**` on a variable. | +| The standard library | A little | `Math.min`, `Math.max`, `Math.abs` (and the rounding ones, which change nothing), `String(n)`, `n.toString()`, `Array.from`, `new Array(n).fill(v)`; `Math.sqrt`, `Math.sign`, `Math.log2` (of a whole number: rounded down), `~`, and `**` / `Math.pow` by a power known when the script is built. For `console.log` there is `print()`, for `Math.random()` there is `random(n)`. Inside a program `Math.random()`, `Date.now()` and `new Date()` are refused, since they would be worked out once when the map is built; game time is `elapsed()` or `memory.game.frame`. No `JSON`, `Date`, `RegExp`, `Object.*`, and no `**` by a variable. | | — | Added | What TypeScript has no word for: `sleep`, `rose` and `once`, reads of the game (`minerals(p)`), `Unit` objects and loops over units, `stats()`, `keyPressed` / `clicked` / `mouse` / `chatted`, `shared()`, per-player programs. | The sections below say each of these in full, and [What a program does not @@ -553,8 +553,11 @@ program(() => { Everything inside the arrow runs in the game. **A program is a coroutine the game resumes every frame.** Its body runs from where it left off until it reaches a `sleep()` or its end, all within that frame; the next frame it goes on from there. A body that ends -stops for good. `while (true) { …; sleep(frames(1)); }` is therefore a game loop running -once per frame, and `sleep(seconds(2))` at its end makes it one every two seconds. +stops for good, and a `return` at the top of the body ends it there (`if (!hero) return;`). +`while (true) { …; sleep(frames(1)); }` is therefore a game loop running once per frame, +and `sleep(seconds(2))` at its end makes it one every two seconds. A trigger's `preserve()` +does nothing in a program, which is said as a remark; `disabled(…)` is refused, since a +program tests its conditions and runs its actions rather than skipping them. #### Variables and records @@ -571,7 +574,7 @@ played and cost the map nothing: no death counters, no switches, no triggers in arrays; each kind has its paragraph below. `let hp = [10, 20, 30]`, `let xs = [a, a * 2, 0]`, `let lives: u8[] = new Array(12).fill(3)`; an index is a constant or a variable (`hp[i] += 7`, `hp[i + 1]++`), `.length`, `for (const x of xs)` with `break` and `continue` -(`x` is a copy of the cell, as in TypeScript), `fill(v)`, `includes(v)` and `indexOf(v)` +(`x` is a copy of the cell, as in TypeScript), `fill(v, start?, end?)`, `includes(v)` and `indexOf(v)` (−1 when there is none) all run within the frame. A `const` array the body stores into is an array of the program like a `let` one. Cells keep their type: a `u8[]` stops at both ends, a `number[]` goes below zero. An array handed to a function is the same array. In a program of @@ -1199,8 +1202,9 @@ if (target) target.order("move", locations.Exit); `unitsAt(location, filter?)`, `unitsOf(player, filter?)` and `allUnits(filter?)` are what a `for…of` runs over; `first(filter?)`, `nearest(type, location, filter?)` and -`randomUnit(filter?)` pick one unit or `null`, and TypeScript wants the `if (target)` before -the unit is used (or `target?.kill()`). A filter is `{ type, owner, at }`, every part known +`randomUnit(filter?)` pick one unit or `null` — `first` and `randomUnit` take a set too, +`first(unitsAt(locations.Base, { owner: P1 }))` — and TypeScript wants the `if (target)` +before the unit is used (or `target?.kill()`, `target?.hp ?? 0`). A filter is `{ type, owner, at }`, every part known when you build; `units.Men`, `units.Buildings` and `units.Factories` work as a type, and `CurrentPlayer` as an owner. Units come oldest first: the game's unit table in the order the game hands its places out (slot 0, then 1699 downwards), the walk stopping once it has met diff --git a/compiler/backend/build.ts b/compiler/backend/build.ts index 58f3d49..3a7154b 100644 --- a/compiler/backend/build.ts +++ b/compiler/backend/build.ts @@ -58,6 +58,8 @@ export interface OwnBuildStats { mapTriggers: number; mapTriggerCopies: number; hyperTriggersLeftOut: number; + /** The map's own triggers for a player group the game runs no list for (Players 9 to 12, Foes, Allies, Neutral, Non-Allied Victory Players): laid out in no list, as in the game. */ + mapTriggersUnlisted: number; startUpTriggers: number; input: { keys: number; mouse: boolean; syncs: number; carrier: number; ring: number; rings: number; humans: number[]; cuwp: number; bits: number } | null; /** How many strings the map had before, and has now. */ @@ -77,6 +79,8 @@ export interface OwnBuild { stats: OwnBuildStats; /** With `maps`: where the triggers and the variables are, and the same as lines of a build's log. */ maps?: OwnMaps; + /** What the map maker should hear of beyond the log: triggers of the map's own that are left out or never run. */ + warnings: string[]; } export type OwnBuildOutcome = OwnBuild | { refused: string }; @@ -164,6 +168,7 @@ export function buildOwnMap(input: OwnBuildInput): OwnBuildOutcome { mapTriggers: lowered.triggers.length, mapTriggerCopies: lowered.triggers.reduce((n, t) => n + t.slots.length, 0), hyperTriggersLeftOut: lowered.hyperTriggersLeftOut, + mapTriggersUnlisted: lowered.triggers.filter((t) => t.slots.length === 0).length, startUpTriggers: own.trig.length / 2400, 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, @@ -174,8 +179,12 @@ export function buildOwnMap(input: OwnBuildInput): OwnBuildOutcome { }; 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, ...(maps ? { maps } : {}) }; + 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` : ""}${stats.mapTriggersUnlisted ? `, ${stats.mapTriggersUnlisted} in no list` : ""}), start-up ${stats.startUpTriggers} triggers, ${stats.stringsAfter - stats.stringsBefore} strings added`); + // What the map maker should hear of, beyond the log: the editor shows each as a notice. + const warnings: string[] = []; + if (stats.hyperTriggersLeftOut) warnings.push(`${stats.hyperTriggersLeftOut} hyper trigger${stats.hyperTriggersLeftOut === 1 ? "" : "s"} of the map's own ${stats.hyperTriggersLeftOut === 1 ? "was" : "were"} left out: a map with a program in it goes through its triggers every frame as it is, and with hyper triggers in a list the game would hold it to every second frame.`); + if (stats.mapTriggersUnlisted) warnings.push(`${stats.mapTriggersUnlisted} trigger${stats.mapTriggersUnlisted === 1 ? "" : "s"} of the map's own ${stats.mapTriggersUnlisted === 1 ? "is" : "are"} for a player group the game runs no triggers for (Players 9 to 12, Foes, Allies, Neutral, Non-Allied Victory Players): ${stats.mapTriggersUnlisted === 1 ? "it" : "they"} never run, in this build as in the game.`); + return { chk: s.bytes(), lines, stats, warnings, ...(maps ? { maps } : {}) }; } export const isRefused = (o: OwnBuildOutcome): o is { refused: string } => "refused" in o; diff --git a/compiler/backend/emit.ts b/compiler/backend/emit.ts index 28b1f33..b95fb87 100644 --- a/compiler/backend/emit.ts +++ b/compiler/backend/emit.ts @@ -1125,7 +1125,8 @@ class Scope { } return true; } - case "return": return (this.native && this.returning.length > 0 && this.returned(s)) || this.island(s); + // (A `return` at the top of a program ends it: nothing to give back.) + case "return": return (this.native && (this.returning.length > 0 ? this.returned(s) : s.value === undefined)) || this.island(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 ? this.slots.get(s) : undefined; @@ -2680,6 +2681,8 @@ class Emitter { /** A `return`: the value into the result of the function or inlined call around it, and on to its end. */ ret(s: Extract): void { const frame = this.frames[this.frames.length - 1]; + // At the top of the program: its end, for good. + if (!frame) { this.goto(END); return; } const r = frame.result; if (r && r.kind === "boolean" && s.value !== undefined) this.setBool(r.id, s.value as BoolExpr); else if (r) this.write(r.id, s.value === undefined ? { c: 0, terms: [] } : linear(s.value as NumExpr)!); @@ -4757,8 +4760,11 @@ class Emitter { // product of the target — and stays one of its own.) // (Or a field that holds it plus a constant — a store of `xs[i] + 1` — when nothing tests the scratch cell or // routes into it: the scratch's 0 to start from is that constant.) - // (A map of our own only: a cell trigger of its own adds its number in, where the Python side's reads may set it.) - const fromConst = (p: Place) => this.scope.native && p.scale === 1 && !p.base && !tested.get(t.copy!.from.slice(1))?.length && !routedInto.get(t.copy!.from.slice(1))?.length; + // (A map of our own only: a cell trigger of its own adds its number in, where the Python side's reads may set it. + // Not when a masked write works the scratch cell — `&`, `|`, `^` by a constant: a `set 0` under a mask would + // become a set of the constant under it, and put the constant's bits there: `(v & 0xffff) − 1025` came out as + // v's low half under −1025's high half, step 12's Open list.) + const fromConst = (p: Place) => this.scope.native && p.scale === 1 && !p.base && !tested.get(t.copy!.from.slice(1))?.length && !routedInto.get(t.copy!.from.slice(1))?.length && !(scratch.get(t.copy!.from.slice(1)) ?? []).some((a) => a.mask !== undefined); const lonePlace = !apart && ps.length === 1 && ps[0].dword && plainPlace(ps[0]) ? ps[0] : !apart && ps.length === 2 && (this.own.has(target) || !!this.scope.decls.get(target)?.scoped) && (plainPlace(ps[1]) || fromConst(ps[1])) && ps[0].dword && !cellsRead.has(JSON.stringify(ps[0].dword)) ? ps[1] : undefined; diff --git a/compiler/declarations.ts b/compiler/declarations.ts index d875c42..e290c44 100644 --- a/compiler/declarations.ts +++ b/compiler/declarations.ts @@ -397,12 +397,12 @@ ${kw}function unitsAt(location: Location, filter?: Omit): Unit ${kw}function unitsOf(player: Player, filter?: Omit): UnitSet; /** Every unit on the map the filter matches. */ ${kw}function allUnits(filter?: UnitFilter): UnitSet; -/** The first unit the filter matches, or null. */ -${kw}function first(filter?: UnitFilter): Unit | null; +/** The first unit the filter matches, or null; a set of units — \`first(unitsAt(locations.Base, { owner: P1 }))\` — is its filter. */ +${kw}function first(filter?: UnitFilter | UnitSet): Unit | null; /** The unit of a type nearest to the centre of a location, or null; units.AnyUnit for any type. */ ${kw}function nearest(unit: UnitType, location: Location, filter?: Omit): Unit | null; /** One of the units the filter matches, picked by the game, or null. */ -${kw}function randomUnit(filter?: UnitFilter): Unit | null; +${kw}function randomUnit(filter?: UnitFilter | UnitSet): Unit | null; /** * The game's own tables, inside program() only: what a unit type costs, what a weapon does, a player's * upgrades. Read a field as a number, assign to it, += it: \`stats(units.TerranMarine).minerals = 25;\` diff --git a/compiler/eud.ts b/compiler/eud.ts index 8dd067a..6857f7a 100644 --- a/compiler/eud.ts +++ b/compiler/eud.ts @@ -267,8 +267,9 @@ function assigned(body: Stmt[], functions: Map, into = new Set const f = c.fn ? functions.get(c.fn) : undefined; if (f && !followed.has(f.id)) { followed.add(f.id); f.body.forEach(stmt); pops(f.body); } }; - // A pop() whose value is used — `const i = queue.pop()!` — is an expression, and shortens the array as the statement does. - const pops = (stmts: Stmt[]) => expressions(stmts, (e) => { if (e.kind === "pop") into.add(whole(e.array)); }); + // A pop() whose value is used — `const i = queue.pop()!` — is an expression, and shortens the array as the statement + // does; a call used as a value — `xs[n++] = 1`, whose index is the update's call — assigns what its body does. + const pops = (stmts: Stmt[]) => expressions(stmts, (e) => { if (e.kind === "pop") into.add(whole(e.array)); else if (e.kind === "call") call(e.call); }); body.forEach(stmt); pops(body); return into; diff --git a/compiler/generated/stdlib.ts b/compiler/generated/stdlib.ts index a0a8229..2c484a3 100644 --- a/compiler/generated/stdlib.ts +++ b/compiler/generated/stdlib.ts @@ -1,6 +1,6 @@ // Generated by scripts/embed-python.mts from stdlib/*.ts and *.tsx — do not edit. export const STDLIB_SOURCES: Readonly> = { - "cutscene": "/**\n * Scenes acted out in the game: the standard library's \"trigscript/cutscene\" (TrigScript 4).\n *\n * import { Camera, walk, line, play, SECOND } from \"trigscript/cutscene\";\n *\n * async function intro(raider: Unit) {\n * const cam = new Camera(raider.x, raider.y);\n * await line(units.JimRaynorMarine, \"Something's moving out there.\", 3 * SECOND);\n * const there = walk(raider, 1200, 640);\n * await cam.follow(raider, 2 * SECOND);\n * await there;\n * await cam.pan(1600, 400, 2 * SECOND);\n * await cam.shake(12);\n * }\n *\n * program(async () => {\n * const raider = first(unitsAt(locations.Base, { owner: P1, type: units.TerranMarine }));\n * if (!raider) return;\n * const skipped = await play(P1, start(intro(raider))); // Escape skips the rest of it\n * if (skipped) print(\"Skipped.\");\n * });\n *\n * Everything here is what a trigger can do, done a frame at a time: the camera is Center View on a location the library\n * keeps for itself, moved to where the camera should look; a unit walks by a Move order to another; a line is the\n * speaker's portrait talking and the words in the middle of the screen. Places are pixels (32 a tile), (0, 0) the map's\n * top left; time is frames of the game, 24 a second on Fastest (`SECOND`). The camera is the one of the player the\n * program runs as, as Center View's is.\n */\nimport { centerLocation, centerView, reserveLocation, talkingPortrait, print, sleep, frames, nextFrame, keyPressed, type Player, type Task, type Unit, type UnitType } from \"trigscript\";\nimport { easeInOutQuad, easeInQuad, easeOutQuad, easeOutBack } from \"trigscript/math\";\n\n/** Frames a second of game time on Fastest, the speed a map is played at. */\nexport const SECOND = 24;\n\nconst VIEW = reserveLocation(\"cutscene: where the camera looks\");\nconst GOAL = reserveLocation(\"cutscene: where a unit is sent\");\n\n/** How a glide speeds up and slows down: evenly, starting slow, ending slow, both, or ending past the mark and back. */\nexport type Ease = \"linear\" | \"in\" | \"out\" | \"inOut\" | \"back\";\n\n/** How far along a glide is at `t` (0 at its start, 1 at its end), as `ease` has it. */\nfunction eased(t: fixed, ease: Ease): fixed {\n if (ease === \"in\") return easeInQuad(t);\n if (ease === \"out\") return easeOutQuad(t);\n if (ease === \"inOut\") return easeInOutQuad(t);\n if (ease === \"back\") return easeOutBack(t);\n return t;\n}\n\n/**\n * The camera of the player the program runs as: where it looks, and how it gets somewhere else. It looks at its first\n * place as soon as it is made.\n */\nexport class Camera {\n /** The point the camera is centred on, in pixels. */\n x: number;\n y: number;\n\n constructor(x: number, y: number) {\n this.x = x;\n this.y = y;\n centerLocation(VIEW, x, y);\n centerView(VIEW);\n }\n\n /** Look at a point, now. */\n look(x: number, y: number) {\n this.x = x;\n this.y = y;\n centerLocation(VIEW, x, y);\n centerView(VIEW);\n }\n\n /**\n * Glide to a point over `time` frames, looking at each place on the way a frame at a time.\n * @param ease How it speeds up and slows down; \"inOut\" unless it says.\n */\n async pan(x: number, y: number, time: number, ease: Ease = \"inOut\") {\n const fromX = this.x;\n const fromY = this.y;\n let i = 1;\n while (i <= time) {\n const k = eased(fixed(i) / time, ease);\n this.look(fromX + Math.round((x - fromX) * k), fromY + Math.round((y - fromY) * k));\n await nextFrame();\n i++;\n }\n this.look(x, y);\n }\n\n /** Keep a unit in the middle of the screen for `time` frames, or until it is gone. */\n async follow(u: Unit, time: number) {\n let i = 0;\n while (i < time && u) {\n this.look(u.x, u.y);\n await nextFrame();\n i++;\n }\n }\n\n /** Shake for `time` frames, `size` pixels each way (8 unless it says), and settle where it was. */\n async shake(time: number, size: number = 8) {\n const x = this.x;\n const y = this.y;\n let i = 0;\n while (i < time) {\n const left = (i % 4) < 2;\n const up = (i % 2) === 0;\n centerLocation(VIEW, left ? x - size : x + size, up ? y - size : y + size);\n centerView(VIEW);\n await nextFrame();\n i++;\n }\n this.look(x, y);\n }\n}\n\n/**\n * Send a unit to a point by a Move order, and wait until it is there — within `near` pixels (48 unless it says) — or\n * `limit` frames have passed (30 seconds unless it says), or it is gone.\n * @returns Whether it got there.\n */\nexport async function walk(u: Unit, x: number, y: number, near: number = 48, limit: number = 30 * SECOND): Promise {\n return go(u, x, y, \"move\", near, limit);\n}\n\n/** The same with an Attack order: the unit fights what it meets on the way. */\nexport async function charge(u: Unit, x: number, y: number, near: number = 48, limit: number = 30 * SECOND): Promise {\n return go(u, x, y, \"attack\", near, limit);\n}\n\nasync function go(u: Unit, x: number, y: number, how: \"move\" | \"attack\", near: number, limit: number): Promise {\n centerLocation(GOAL, x, y);\n u.order(how, GOAL);\n let waited = 0;\n while (u && Math.abs(u.x - x) + Math.abs(u.y - y) > near && waited < limit) {\n await nextFrame();\n waited++;\n }\n return !!u && Math.abs(u.x - x) + Math.abs(u.y - y) <= near;\n}\n\n/**\n * Send every unit of a group to a point together, and wait until each is there, gone, or `limit` frames have passed.\n * @returns How many got there.\n */\nexport async function walkAll(group: Unit[], x: number, y: number, near: number = 64, limit: number = 30 * SECOND): Promise {\n centerLocation(GOAL, x, y);\n for (const u of group) if (u) u.order(\"move\", GOAL);\n let waited = 0;\n let there = 0;\n while (waited < limit) {\n there = 0;\n let moving = 0;\n for (const u of group) {\n if (!u) continue;\n if (Math.abs(u.x - x) + Math.abs(u.y - y) <= near) there++;\n else moving++;\n }\n if (moving === 0) break;\n await nextFrame();\n waited++;\n }\n return there;\n}\n\n/**\n * A line of the scene: the speaker's portrait talks and the words show in the middle of the screen, for `time` frames\n * (3 seconds unless it says), and then the scene goes on.\n */\nexport async function line(speaker: UnitType, text: string, time: number = 3 * SECOND) {\n talkingPortrait(speaker, Math.round((time * 1000) / SECOND));\n print(text, { position: \"center\" });\n await sleep(frames(time));\n}\n\n/**\n * Play a scene, started as a task (`start(intro())`), for a player who may skip it: when they press `key` (Escape unless\n * it says) the scene stops where it is. Put things where the scene would have left them when it gives true.\n * @returns Whether it was skipped.\n */\nexport async function play(player: Player, scene: Task, key: \"Escape\" | \"Space\" | \"Enter\" = \"Escape\"): Promise {\n while (scene.running) {\n if (keyPressed(player, key)) {\n scene.cancel();\n return true;\n }\n await nextFrame();\n }\n return false;\n}\n", + "cutscene": "/**\n * Scenes acted out in the game: the standard library's \"trigscript/cutscene\" (TrigScript 4).\n *\n * import { Camera, walk, line, play, SECOND } from \"trigscript/cutscene\";\n *\n * async function intro(raider: Unit) {\n * const cam = new Camera(raider.x, raider.y);\n * await line(units.JimRaynorMarine, \"Something's moving out there.\", 3 * SECOND);\n * const there = start(walk(raider, 1200, 640)); // the walk goes on while the camera follows\n * await cam.follow(raider, 2 * SECOND);\n * await there;\n * await cam.pan(1600, 400, 2 * SECOND);\n * await cam.shake(12);\n * }\n *\n * program(async () => {\n * const raider = first(unitsAt(locations.Base, { owner: P1, type: units.TerranMarine }));\n * if (!raider) return;\n * const skipped = await play(P1, start(intro(raider))); // Escape skips the rest of it\n * if (skipped) print(\"Skipped.\");\n * });\n *\n * Everything here is what a trigger can do, done a frame at a time: the camera is Center View on a location the library\n * keeps for itself, moved to where the camera should look; a unit walks by a Move order to another; a line is the\n * speaker's portrait talking and the words in the middle of the screen. Places are pixels (32 a tile), (0, 0) the map's\n * top left; time is frames of the game, 24 a second on Fastest (`SECOND`). The camera is the one of the player the\n * program runs as, as Center View's is.\n */\nimport { centerLocation, centerView, reserveLocation, talkingPortrait, print, sleep, frames, nextFrame, keyPressed, type Player, type Task, type Unit, type UnitType } from \"trigscript\";\nimport { easeInOutQuad, easeInQuad, easeOutQuad, easeOutBack } from \"trigscript/math\";\n\n/** Frames a second of game time on Fastest, the speed a map is played at. */\nexport const SECOND = 24;\n\nconst VIEW = reserveLocation(\"cutscene: where the camera looks\");\nconst GOAL = reserveLocation(\"cutscene: where a unit is sent\");\n\n/** How a glide speeds up and slows down: evenly, starting slow, ending slow, both, or ending past the mark and back. */\nexport type Ease = \"linear\" | \"in\" | \"out\" | \"inOut\" | \"back\";\n\n/** How far along a glide is at `t` (0 at its start, 1 at its end), as `ease` has it. */\nfunction eased(t: fixed, ease: Ease): fixed {\n if (ease === \"in\") return easeInQuad(t);\n if (ease === \"out\") return easeOutQuad(t);\n if (ease === \"inOut\") return easeInOutQuad(t);\n if (ease === \"back\") return easeOutBack(t);\n return t;\n}\n\n/**\n * The camera of the player the program runs as: where it looks, and how it gets somewhere else. It looks at its first\n * place as soon as it is made.\n */\nexport class Camera {\n /** The point the camera is centred on, in pixels. */\n x: number;\n y: number;\n\n constructor(x: number, y: number) {\n this.x = x;\n this.y = y;\n centerLocation(VIEW, x, y);\n centerView(VIEW);\n }\n\n /** Look at a point, now. */\n look(x: number, y: number) {\n this.x = x;\n this.y = y;\n centerLocation(VIEW, x, y);\n centerView(VIEW);\n }\n\n /**\n * Glide to a point over `time` frames, looking at each place on the way a frame at a time.\n * @param ease How it speeds up and slows down; \"inOut\" unless it says.\n */\n async pan(x: number, y: number, time: number, ease: Ease = \"inOut\") {\n const fromX = this.x;\n const fromY = this.y;\n let i = 1;\n while (i <= time) {\n const k = eased(fixed(i) / time, ease);\n this.look(fromX + Math.round((x - fromX) * k), fromY + Math.round((y - fromY) * k));\n await nextFrame();\n i++;\n }\n this.look(x, y);\n }\n\n /** Keep a unit in the middle of the screen for `time` frames, or until it is gone. */\n async follow(u: Unit, time: number) {\n let i = 0;\n while (i < time && u) {\n this.look(u.x, u.y);\n await nextFrame();\n i++;\n }\n }\n\n /** Shake for `time` frames, `size` pixels each way (8 unless it says), and settle where it was. */\n async shake(time: number, size: number = 8) {\n const x = this.x;\n const y = this.y;\n let i = 0;\n while (i < time) {\n const left = (i % 4) < 2;\n const up = (i % 2) === 0;\n centerLocation(VIEW, left ? x - size : x + size, up ? y - size : y + size);\n centerView(VIEW);\n await nextFrame();\n i++;\n }\n this.look(x, y);\n }\n}\n\n/**\n * Send a unit to a point by a Move order, and wait until it is there — within `near` pixels (48 unless it says) — or\n * `limit` frames have passed (30 seconds unless it says), or it is gone.\n * @returns Whether it got there.\n */\nexport async function walk(u: Unit, x: number, y: number, near: number = 48, limit: number = 30 * SECOND): Promise {\n return go(u, x, y, \"move\", near, limit);\n}\n\n/** The same with an Attack order: the unit fights what it meets on the way. */\nexport async function charge(u: Unit, x: number, y: number, near: number = 48, limit: number = 30 * SECOND): Promise {\n return go(u, x, y, \"attack\", near, limit);\n}\n\nasync function go(u: Unit, x: number, y: number, how: \"move\" | \"attack\", near: number, limit: number): Promise {\n centerLocation(GOAL, x, y);\n u.order(how, GOAL);\n let waited = 0;\n while (u && Math.abs(u.x - x) + Math.abs(u.y - y) > near && waited < limit) {\n await nextFrame();\n waited++;\n }\n return !!u && Math.abs(u.x - x) + Math.abs(u.y - y) <= near;\n}\n\n/**\n * Send every unit of a group to a point together, and wait until each is there, gone, or `limit` frames have passed.\n * @returns How many got there.\n */\nexport async function walkAll(group: Unit[], x: number, y: number, near: number = 64, limit: number = 30 * SECOND): Promise {\n centerLocation(GOAL, x, y);\n for (const u of group) if (u) u.order(\"move\", GOAL);\n let waited = 0;\n let there = 0;\n while (waited < limit) {\n there = 0;\n let moving = 0;\n for (const u of group) {\n if (!u) continue;\n if (Math.abs(u.x - x) + Math.abs(u.y - y) <= near) there++;\n else moving++;\n }\n if (moving === 0) break;\n await nextFrame();\n waited++;\n }\n return there;\n}\n\n/**\n * A line of the scene: the speaker's portrait talks and the words show in the middle of the screen, for `time` frames\n * (3 seconds unless it says), and then the scene goes on.\n */\nexport async function line(speaker: UnitType, text: string, time: number = 3 * SECOND) {\n talkingPortrait(speaker, Math.round((time * 1000) / SECOND));\n print(text, { position: \"center\" });\n await sleep(frames(time));\n}\n\n/**\n * Play a scene, started as a task (`start(intro())`), for a player who may skip it: when they press `key` (Escape unless\n * it says) the scene stops where it is. Put things where the scene would have left them when it gives true.\n * @returns Whether it was skipped.\n */\nexport async function play(player: Player, scene: Task, key: \"Escape\" | \"Space\" | \"Enter\" = \"Escape\"): Promise {\n while (scene.running) {\n if (keyPressed(player, key)) {\n scene.cancel();\n return true;\n }\n await nextFrame();\n }\n return false;\n}\n", "fog": "/**\n * Fog of war as a picture: the standard library's \"trigscript/fog\" (TrigScript 4).\n *\n * import { fog } from \"trigscript/fog\";\n *\n * const heart = fog.picture([\".##.##.\", \"#######\", \".#####.\", \"..###..\"]);\n *\n * program(async () => {\n * fog.reveal(P1); // the whole map lit once: from now on dark is fog, not black\n * while (true) {\n * fog.light(P1, 30, 20, 5); // a disc of size 5 around tile (30, 20), this frame only\n * fog.light(P1, 60, 40, heart); // a picture, its top left on tile (60, 40)\n * await nextFrame();\n * }\n * });\n *\n * The fog is a canvas that starts every frame dark: what a program lights this frame is what shows this frame, and\n * nothing else needs doing. There is no dark brush — the game's sight only ever reveals — so dark is whatever is not\n * lit; to draw dark shapes, light around them (a picture with `{ ink: \"dark\" }`). Dark is also the sharper ink: a lit\n * part is at least a brush wide (3 tiles across, 4 down), while a dark line one tile wide stays exactly that. Nothing\n * else needs doing — the library writes the game's vision countdown after every frame (memory.vision.countdown), so\n * the next frame starts from a wiped board. Watch it on the minimap. With Classic graphics (Options > Graphics) a tile\n * of the map is a pixel there, sharp; Remastered graphics blurs it a few tiles wide.\n *\n * What the board is made of decides how far apart lit and fog look. Remastered draws a tile on the minimap as the\n * average of its art and fog as 0.64 of lit, whatever the tile, so the brightest art is the best board there: plain\n * Ice snow (lit 130 against fog 82, measured 2026-10-19). Classic draws a tile as one colour of its own; Badlands\n * tile 23778 is the widest apart there, and dim and textured in Remastered. Snow played the better of the two.\n *\n * How it draws, played in StarCraft: Remastered (2026-10-17): a brush is a unit made on a tile and taken away in the\n * same trigger pass; the tiles its sight revealed stay lit through the frame. A brush's size is the unit's sight\n * range, so each size is a unit type of its own — the flying heroes below, which no ordinary map places — whose sight\n * is set every frame (Remastered takes a sight write a frame late, so one type cannot change size within a frame).\n * Coordinates are tiles, (0, 0) the map's top left. About 800 brushes a frame is the most the game makes: it keeps a\n * removed unit's place until the next frame.\n */\nimport { game, program, sleep, frames, stats, units, createUnit, removeUnit, centerLocation, reserveLocation, memory, cover, nextFrame, type Player, type UnitType, type Video } from \"trigscript\";\n\n/**\n * Each brush's shape, sizes 1 to 11: the tiles a flying unit of that sight range reveals, read out of the game.\n * \"@\" is the tile the brush is centred on. The game's air sight leaves the centre's row one short below, so every\n * shape reaches a row further up than down.\n */\nexport const SHAPES: readonly (readonly string[])[] = [\n // 1\n [\"###\", \"###\", \"#@#\", \"###\"],\n // 2\n [\".###.\", \"#####\", \"#####\", \"##@##\", \"#####\", \"#####\"],\n // 3\n [\"..###..\", \"#######\", \"#######\", \"#######\", \"###@###\", \"#######\", \"#######\", \"#######\"],\n // 4\n [\"..#####...\", \".#######..\", \"#########.\", \"##########\", \"##########\", \"####@#####\", \"##########\", \"##########\", \"#########.\", \".#######..\", \"......#...\"],\n // 5\n [\"...#####....\", \"..#######...\", \".#########..\", \"###########.\", \"############\", \"############\", \"#####@######\", \"############\", \"############\", \"###########.\", \".#########..\", \"..#######...\", \".......#....\"],\n // 6\n [\"....#####.....\", \"..#########...\", \".###########..\", \"#############.\", \"#############.\", \"##############\", \"##############\", \"######@#######\", \"##############\", \"##############\", \"#############.\", \"#############.\", \".###########..\", \"..#########...\", \"........#.....\"],\n // 7\n [\".....#####......\", \"...#########....\", \"..###########...\", \".#############..\", \"###############.\", \"###############.\", \"################\", \"################\", \"#######@########\", \"################\", \"################\", \"###############.\", \"###############.\", \".#############..\", \"..###########...\", \"...#########....\", \".........#......\"],\n // 8\n [\"......#####.......\", \"....#########.....\", \"..#############...\", \".###############..\", \".###############..\", \"#################.\", \"#################.\", \"##################\", \"##################\", \"########@#########\", \"##################\", \"##################\", \"#################.\", \"#################.\", \".###############..\", \".###############..\", \"..#############...\", \"....#########.....\", \"..........#.......\"],\n // 9\n [\"......#######.......\", \"....###########.....\", \"...#############....\", \"..###############...\", \".#################..\", \"###################.\", \"###################.\", \"####################\", \"####################\", \"####################\", \"#########@##########\", \"####################\", \"####################\", \"####################\", \"###################.\", \"###################.\", \".#################..\", \"..###############...\", \"...#############....\", \"....###########.....\", \"........#####.......\"],\n // 10\n [\".......#######........\", \".....###########......\", \"....#############.....\", \"..#################...\", \"..#################...\", \".###################..\", \"#####################.\", \"#####################.\", \"######################\", \"######################\", \"######################\", \"##########@###########\", \"######################\", \"######################\", \"######################\", \"#####################.\", \"#####################.\", \".###################..\", \"..#################...\", \"..#################...\", \"....#############.....\", \".....###########......\", \".........#####........\"],\n // 11\n [\"........#######.........\", \"......###########.......\", \"....###############.....\", \"...#################....\", \"..###################...\", \".#####################..\", \".#####################..\", \"#######################.\", \"#######################.\", \"########################\", \"########################\", \"########################\", \"###########@############\", \"########################\", \"########################\", \"########################\", \"#######################.\", \"#######################.\", \".#####################..\", \".#####################..\", \"..###################...\", \"...#################....\", \"....###############.....\", \"......###########.......\", \"..........#####.........\"],\n];\n\n/** Each size's unit, size 1 first: flying heroes without a sight upgrade. A map that places one gets odd sight from it. */\nconst BRUSHES: readonly UnitType[] = [\n units.KukulzaMutalisk, units.KukulzaGuardian, units.YggdrasillOverlord, units.MagellanScienceVessel,\n units.GantrithorCarrier, units.HyperionBattlecruiser, units.NoradIIBattlecruiser, units.TomKazanskyWraith,\n units.DanimothArbiter, units.MojoScout, units.ArtanisScout,\n];\n\n/** Where each brush is made: a location the map does not use, taken when the script is built. */\nconst SPOT = reserveLocation(\"fog: where each brush is made\");\n\n/**\n * One disc: the tiles a brush of `size` covers (SHAPES), centred on tile (x, y). What light() does with a size. A brush\n * is a unit made where SPOT is and taken away again at once; each branch makes its own size's, so that the unit type\n * of the action is known when the script is built (one given by the program costs writing it into the action).\n */\nconst disc = game((player: Player, x: number, y: number, size: number) => {\n discPx(player, x * 32 + 16, y * 32 + 16, size);\n});\n\n/**\n * disc() in pixels, (px, py) the centre of the tile: what a caller with many brushes uses, its products by 32 worked\n * out once and the brushes' offsets added (a product by 32 of a number the game holds is a test a bit, about 0.4 µs).\n */\nconst discPx = game((player: Player, px: number, py: number, size: number) => {\n centerLocation(SPOT, px, py, 32, 32);\n switch (size) {\n case 0: case 1: createUnit(player, BRUSHES[0], 1, SPOT); removeUnit(player, BRUSHES[0]); break;\n case 2: createUnit(player, BRUSHES[1], 1, SPOT); removeUnit(player, BRUSHES[1]); break;\n case 3: createUnit(player, BRUSHES[2], 1, SPOT); removeUnit(player, BRUSHES[2]); break;\n case 4: createUnit(player, BRUSHES[3], 1, SPOT); removeUnit(player, BRUSHES[3]); break;\n case 5: createUnit(player, BRUSHES[4], 1, SPOT); removeUnit(player, BRUSHES[4]); break;\n case 6: createUnit(player, BRUSHES[5], 1, SPOT); removeUnit(player, BRUSHES[5]); break;\n case 7: createUnit(player, BRUSHES[6], 1, SPOT); removeUnit(player, BRUSHES[6]); break;\n case 8: createUnit(player, BRUSHES[7], 1, SPOT); removeUnit(player, BRUSHES[7]); break;\n case 9: createUnit(player, BRUSHES[8], 1, SPOT); removeUnit(player, BRUSHES[8]); break;\n case 10: createUnit(player, BRUSHES[9], 1, SPOT); removeUnit(player, BRUSHES[9]); break;\n default: createUnit(player, BRUSHES[10], 1, SPOT); removeUnit(player, BRUSHES[10]);\n }\n});\n\n/**\n * Light the whole map once, so that from now on a tile left dark shows as fog (grey) rather than as never seen\n * (black) — every picture then has the same two tones. Takes one frame.\n * @param player Whose fog: P1 … P8, known when the script is built.\n */\nexport const reveal = game((player: Player) => {\n const width = memory.map.width;\n const height = memory.map.height;\n // Size 11 is 23 tiles wide: a brush every 14 tiles leaves no gap, and one more past the end, kept inside, reaches the edge.\n for (let y = 2; y < height + 12; y += 14) {\n for (let x = 2; x < width + 12; x += 14) disc(player, Math.min(x, width - 3), Math.min(y, height - 3), 11);\n }\n});\n\n/**\n * Light a rectangle of tiles for this frame, worked out while the game runs: its top left on tile (x, y), `width`\n * tiles across and `height` down. Every one of the five may change from frame to frame — the walls of a raycaster, a\n * bar of a chart. It is exact from 3 × 4 up (the smallest brush); a thinner one is lit a brush wide, centred.\n *\n * fog.rect(P1, 10, 20, 3, wall); // a column 3 tiles wide, `wall` tall\n *\n * @param player Whose fog: P1 … P8, known when the script is built.\n * @param x The column of its left edge.\n * @param y The row of its top edge.\n * @param width How many tiles across.\n * @param height How many tiles down.\n */\nexport const rect = game((player: Player, x: number, y: number, width: number, height: number) => {\n if (width < 1 || height < 1) return;\n // Size 1 is 3 tiles across and 4 down, its centre the middle column and the third row: a grid of them, the last\n // column and row moved back to end on the edge (overlapping the one before), covers the rectangle exactly.\n const right = Math.max(x + width - 2, x + 1);\n const bottom = Math.max(y + height - 2, y + 2);\n // (The brush made here, not through disc(): its unit type is then known when the script is built, which makes the\n // action a constant — a size from the program costs a branch and a written field a brush.)\n let cy = y + 2;\n while (true) {\n let cx = x + 1;\n while (true) {\n centerLocation(SPOT, cx * 32 + 16, cy * 32 + 16, 32, 32);\n createUnit(player, BRUSHES[0], 1, SPOT);\n removeUnit(player, BRUSHES[0]);\n if (cx >= right) break;\n cx = Math.min(cx + 3, right);\n }\n if (cy >= bottom) break;\n cy = Math.min(cy + 4, bottom);\n }\n});\n\n/** A picture for the fog, made when the script is built: its size in tiles and the brushes that draw it. */\nexport interface Picture {\n readonly width: number;\n readonly height: number;\n readonly brushes: readonly { readonly x: number; readonly y: number; readonly size: number }[];\n}\n\n/**\n * A picture from rows of text, worked out when the script is built: `#` is the ink, anything else is not.\n *\n * fog.picture([\"..##..\", \".####.\", \"######\"]) // light ink: the # tiles are lit\n * fog.picture([\"..##..\", \".####.\", \"######\"], { ink: \"dark\" }) // dark ink: the # tiles stay dark, the rest is lit\n *\n * With light ink a lit part thinner than a brush (3 tiles across, 4 down) is drawn a tile or two thicker. An image's\n * halftone (`fog.picture(photo.halftone(6))`) is drawn with the brushes it was made of, one a cell. With dark\n * ink every tile of the picture's rectangle that is not `#` is lit and the `#` tiles keep their shape exactly,\n * however thin — the ink for line art and silhouettes.\n */\nexport function picture(rows: readonly string[], options: { readonly ink?: \"light\" | \"dark\" } = {}): Picture {\n const height = rows.length;\n const width = Math.max(0, ...rows.map((r) => r.length));\n // An image's halftone (image.halftone(6)) comes with the brushes that drew it: a brush a cell, as they are.\n const drawn = (rows as { readonly brushes?: readonly { readonly x: number; readonly y: number; readonly shape: number }[] }).brushes;\n if (drawn && options.ink !== \"dark\") return Object.freeze({ width, height, brushes: Object.freeze(drawn.map((b) => ({ x: b.x, y: b.y, size: b.shape + 1 }))) });\n // Dark ink: the picture's rectangle lit but for the # tiles, so what is covered is the rest.\n const ink = options.ink === \"dark\" ? Array.from({ length: height }, (_, y) => Array.from({ length: width }, (_, x) => (rows[y][x] === \"#\" ? \".\" : \"#\")).join(\"\")) : rows;\n const brushes = cover(ink, SHAPES).map((b) => ({ x: b.x, y: b.y, size: b.shape + 1 }));\n return Object.freeze({ width, height, brushes: Object.freeze(brushes) });\n}\n\n/**\n * A video's frames as brushes, worked out when the script is built: a word a brush (x | y << 8 | size << 16), the\n * frames one after another, and where each one starts — the tables the game reads a frame from. Once a build.\n */\ninterface Film { readonly words: readonly number[]; readonly starts: readonly number[] }\nconst films = new Map();\nfunction film(video: Video): Film {\n const had = films.get(video);\n if (had) return had;\n const words: number[] = [], starts: number[] = [0];\n for (const brushes of cover(video, SHAPES)) {\n for (const b of brushes) words.push(b.x | (b.y << 8) | ((b.shape + 1) << 16));\n starts.push(words.length);\n }\n const made = { words, starts };\n films.set(video, made);\n return made;\n}\n\n/**\n * Light the fog for this frame: a disc, a picture or a frame of a video. Tiles count from the map's top left, (0, 0).\n *\n * fog.light(P1, 30, 20, 5); // a disc of size 5 centred on tile (30, 20)\n * fog.light(P1, 60, 40, heart); // a picture (fog.picture), its top left on tile (60, 40)\n * fog.light(P1, 0, 0, badApple, f); // frame f of a video the script imports, its top left on tile (0, 0)\n *\n * @param player Whose fog: P1 … P8, known when the script is built.\n * @param x The tile's column: a disc's centre, a picture's left edge.\n * @param y The tile's row: a disc's centre, a picture's top edge.\n * @param shape A size, 1 (3 tiles wide) to 11 (23 wide) — it may change while the game runs — a picture, or a video.\n * @param frame Of a video, which frame, from 0: it may change while the game runs. Past the end, nothing is lit.\n */\nexport const light = game((player: Player, x: number, y: number, shape: number | Picture | Video, frame: number = 0) => {\n if (typeof shape === \"number\") disc(player, x, y, shape);\n else {\n // The picture's corner in pixels once; each brush is its offset from there (a constant of a picture).\n const px = x * 32 + 16, py = y * 32 + 16;\n if (\"brushes\" in shape) for (const b of shape.brushes) discPx(player, px + b.x * 32, py + b.y * 32, b.size);\n else {\n const { words, starts } = film(shape);\n const end = starts[frame + 1];\n for (let i = starts[frame]; i < end; i++) {\n const w = words[i];\n // (A row's offset in pixels straight from its byte: the same as ((w >> 8) & 255) * 32, and the shift only has\n // the byte's eight bits to look at.)\n discPx(player, px + (w & 255) * 32, py + ((w & 0xff00) >> 3), w >> 16);\n }\n }\n }\n});\n\n/**\n * Play a video in the fog, a frame of it every frame of the game, from its first to its last; resolves when it ends.\n * Inside an async program: `await fog.play(P1, badApple)`. On Fastest a game frame is 42 ms, which is what Import\n * Video makes a video's frames by default; the game runs a little slower when a frame is heavy, and so does the video.\n *\n * @param player Whose fog: P1 … P8, known when the script is built.\n * @param video A video the script imports.\n * @param x The column of its left edge, 0 by default.\n * @param y The row of its top edge, 0 by default.\n */\nexport const play = game(async (player: Player, video: Video, x: number = 0, y: number = 0) => {\n // (A while: a for over a length known when the script is built would be unrolled, a copy of the body a frame.)\n let f = 0;\n while (f < video.length) {\n light(player, x, y, video, f);\n await nextFrame();\n f++;\n }\n});\n\n/**\n * How fog.columns() lays its columns out, known when the script is built: `count` columns side by side, each `width`\n * tiles across (3, a brush's width, by default) and up to `height` tiles tall, centred on the middle row. `ink`:\n * \"light\" lights each column (the walls of a raycaster bright, on fog), \"dark\" lights all but the columns (dark walls\n * between a lit ceiling and floor); dark is cheaper when columns fill most of the board, light when they are short.\n */\nexport interface ColumnLayout {\n readonly count: number;\n readonly height: number;\n readonly width?: number;\n readonly ink?: \"light\" | \"dark\";\n /**\n * How the walls are told apart, with fog.columns()'s `faces`. \"outline\", the default: every wall drawn as its edges —\n * its top and bottom, a brush deep, and a column lit whole wherever one face meets another (on the nearer of the\n * two) — so that blocks read as boxes, the look of the raycasters of monochrome screens; with dark ink, the edges are\n * lit lines through dark walls. A few brushes a column. Chosen over the rest when played (2026-10-17).\n *\n * The others shade the faces whose number is odd (a raycaster's other side) and leave the rest whole: \"hollow\" —\n * dark, lit only along its top and bottom edges; \"ribbed\" — hollow and lit across the middle of each half; \"panelled\"\n * — hollow and lit across its quarters; \"line\" / \"lines\" — dark lines a tile thick across it at its middle or its\n * quarters (the finest, and two dozen brushes a column near the eye: for a view with room to spare).\n */\n readonly shading?: \"outline\" | \"lines\" | \"line\" | \"hollow\" | \"ribbed\" | \"panelled\";\n}\n\n/**\n * What fog.columns() reads, worked out once a layout when the script is built: for each level of the drawing that is\n * wider than a column (all the columns, a group of them, the last group) and each depth from 0 to half the height, the\n * brushes that cover a rectangle that wide and that deep exactly — the encoder's covers (cover()), a few large brushes\n * in the middle and small ones at the edges. A brush is a word, dx | dy << 8 | size << 16 from the rectangle's top\n * left; `starts` says where each (level, depth) begins.\n */\ninterface ColumnPlan { readonly words: readonly number[]; readonly starts: readonly number[]; readonly group: number; readonly half: number }\nconst columnPlans = new Map();\nconst COLUMN_GROUP = 6;\nfunction columnPlan(layout: ColumnLayout): ColumnPlan {\n const width = layout.width ?? 3, half = layout.height >> 1, group = Math.min(COLUMN_GROUP, layout.count);\n const key = `${layout.count}:${width}:${half}`;\n const had = columnPlans.get(key);\n if (had) return had;\n const words: number[] = [], starts: number[] = [];\n for (const columns of [layout.count, group, layout.count % group || group]) {\n for (let depth = 0; depth <= half; depth++) {\n starts.push(words.length);\n if (depth === 0) continue;\n // A rectangle shallower than a brush is drawn a brush deep, the rest of it on what the level before lit.\n const rows = Array.from({ length: Math.max(depth, 4) }, () => \"#\".repeat(columns * width));\n for (const b of cover(rows, SHAPES)) words.push(b.x | (b.y << 8) | ((b.shape + 1) << 16));\n }\n }\n starts.push(words.length);\n const made = { words, starts, group, half };\n columnPlans.set(key, made);\n return made;\n}\n\n/**\n * One rectangle of fog.columns() wider than a column: the cover at `at` (a level and a depth) drawn twice, the half\n * above the middle and the half below, its top rows `top` and `bottom`, from column `x`.\n */\nconst columnRect = game((player: Player, words: readonly number[], starts: readonly number[], at: number, x: number, top: number, bottom: number) => {\n let i = starts[at];\n const end = starts[at + 1];\n // In pixels once; each brush is its offset (a few bits, a few tests) from there.\n const px = x * 32 + 16, pt = top * 32 + 16, pb = bottom * 32 + 16;\n while (i < end) {\n const w = words[i];\n const dx = (w & 255) * 32, dy = (w & 0xff00) >> 3, size = w >> 16;\n discPx(player, px + dx, pt + dy, size);\n discPx(player, px + dx, pb + dy, size);\n i++;\n }\n});\n\n/**\n * One column's part of fog.columns(), in pixels: brushes (3 × 4) a column of 4 rows apart and the last moved back to\n * end on the edge, twice — from `pt` and from `pb`, the centres of the first brush of each — down a column `k32`\n * pixels deep (32 a row; under 4 rows, a brush deep), centred on `px`. What the smallest brush covers exactly, worked out\n * here rather than read: it is the most common rectangle by far. In pixels because the caller has them, stepped: a\n * product by 32 of a number the game holds is a test a bit, about 0.4 µs, where a step is an add.\n */\nconst columnBarPx = game((player: Player, px: number, pt: number, pb: number, k32: number) => {\n // How far down the last brush is from the first.\n const end = Math.max(k32, 128) - 128;\n let r = 0;\n while (true) {\n centerLocation(SPOT, px, pt, 32, 32);\n createUnit(player, BRUSHES[0], 1, SPOT);\n removeUnit(player, BRUSHES[0]);\n centerLocation(SPOT, px, pb, 32, 32);\n createUnit(player, BRUSHES[0], 1, SPOT);\n removeUnit(player, BRUSHES[0]);\n if (r >= end) break;\n const step = Math.min(end - r, 128);\n r += step;\n pt += step;\n pb += step;\n }\n});\n\n/** columnBarPx() in tiles: `k` rows from row `top` down and from row `bottom` down, centred on column `x`. */\nconst columnBar = game((player: Player, x: number, top: number, bottom: number, k: number) => {\n columnBarPx(player, x * 32 + 16, top * 32 + 80, bottom * 32 + 80, k * 32);\n});\n\n/**\n * A shaded column of fog.columns() (ColumnLayout's `shading`): `a` is half its height, `lines` how many lines across\n * each half — 0 hollow, 1 at its middle, 3 at its quarters. A line is a dark row through a lit column (light ink), or\n * a lit band a brush deep through a dark one; a column too short for its lines is drawn plain.\n */\nconst columnShaded = game((player: Player, cx: number, mid: number, a: number, dark: boolean, lines: number) => {\n if (lines <= 0) {\n if (dark) { if (a >= 6) columnBar(player, cx, mid - 4, mid, 4); }\n else if (a < 8) columnBar(player, cx, mid - a, mid, a);\n else {\n // Hollow: the edges. Ribbed (-1) and panelled (-3): bands across each half too, centred on its middle or\n // quarters, where there is room for them between the edges.\n columnBar(player, cx, mid - a, mid + a - 4, 4);\n const ribs = -lines;\n if (ribs > 0 && a >= 6 * (ribs + 1)) {\n let k = 1;\n while (k <= ribs) {\n const at = (a * k) / (ribs + 1);\n columnBar(player, cx, mid - at - 2, mid + at - 2, 4);\n k++;\n }\n }\n }\n } else if (a < 5 * (lines + 1)) {\n if (!dark) columnBar(player, cx, mid - a, mid, a);\n } else {\n // From the middle out: lit up to each line, the line's row dark (light ink); or a lit band on each line (dark ink).\n let from = 0, k = 1;\n while (k <= lines + 1) {\n const to = k > lines ? a : (a * k) / (lines + 1);\n if (dark) { if (k <= lines) columnBar(player, cx, mid - to - 2, mid + to - 2, 4); }\n else columnBar(player, cx, mid - to, mid + from, to - from);\n from = to + 1;\n k++;\n }\n }\n});\n\n/**\n * Light columns for this frame — the walls of a raycaster, a bar chart — each as tall as the program says, centred on\n * the layout's middle row. The covers are worked out when the script is built (see ColumnLayout), so a frame costs a\n * brush for each part of the picture that differs from its neighbours, not one for every few tiles: a wall that fills\n * the board is a few large brushes.\n *\n * const VIEW = { count: 42, height: 96, ink: \"dark\" } as const;\n * fog.columns(P1, 1, 0, VIEW, heights); // heights: one a column, in tiles, the program's\n * fog.columns(P1, 1, 0, VIEW, heights, faces); // faces: which face of which wall each column shows\n *\n * @param player Whose fog: P1 … P8, known when the script is built.\n * @param x The column of the layout's left edge.\n * @param y The row of its top edge.\n * @param layout How the columns are laid out: known when the script is built.\n * @param heights Each column's height in tiles (0 to the layout's height), an array of the program's.\n * @param faces Which face of which wall each column shows, a number the same for every column of one face — in a\n * raycaster, \\`(mapY * width + mapX) * 2 + side\\` — an array of the program's as long as `heights`: what the\n * layout's `shading` tells walls apart by. Optional: without it, every column is lit whole (a bar chart).\n */\nexport const columns = game((player: Player, x: number, y: number, layout: ColumnLayout, heights: readonly number[], faces?: readonly number[]) => {\n const { words, starts, half, group } = columnPlan(layout);\n const count = layout.count, width = layout.width ?? 3, dark = layout.ink === \"dark\";\n // (Dark lines through a lit column count up; lit bands through a dark one, ribbed and panelled, down.)\n // (Without faces there is nothing to tell apart, and every column is whole.)\n const outline = faces !== undefined && (layout.shading === undefined || layout.shading === \"outline\");\n const lines = layout.shading === \"lines\" ? 3 : layout.shading === \"line\" ? 1 : layout.shading === \"ribbed\" ? -1 : layout.shading === \"hollow\" ? 0 : -3;\n const span = half + 1;\n // Each column's depth from where its lit part starts: the middle row for light ink, the top and bottom for dark.\n // A shaded column of light ink is drawn on its own, after the rest: to the rest it is as if it were not there — and\n // so is every column of a light outline, which is all edges.\n const depth: number[] = new Array(count).fill(0);\n // Half of each column's height, at most half the layout's: worked out once. (Clamped before the shift, the same\n // number: the shift then has the few bits the clamp leaves, where a height from the program could have any.)\n const halves: number[] = new Array(count).fill(0);\n // And in pixels (32 a row), for the bars, which place their brushes in pixels: a small number times 32 is a few tests.\n const halves32: number[] = new Array(count).fill(0);\n let least = half, most = 0;\n // (A light outline is all edges: every depth is 0, nothing to work out.)\n const flat = !dark && outline;\n if (flat) least = 0;\n let i = 0;\n while (i < count) {\n const a = Math.min(Math.max(heights[i], 0), 2 * half + 1) >> 1;\n halves[i] = a;\n halves32[i] = a * 32;\n if (!flat) {\n let d = dark ? half - a : a;\n if (faces !== undefined && !dark && !outline && (faces[i] & 1) != 0) d = 0;\n depth[i] = d;\n if (d < least) least = d;\n if (d > most) most = d;\n }\n i++;\n }\n // A rectangle from depth p, k deep, starts on the rows above and below the middle (light) or below the top and above\n // the bottom (dark); one shallower than 4 is drawn 4 deep, reaching back over what is lit already.\n const mid = y + half;\n // Every column, from depth 0 to the least.\n if (least > 0) {\n const k = Math.max(least, 4);\n if (dark) columnRect(player, words, starts, least, x, y + least - k, y + 2 * half - least);\n else columnRect(player, words, starts, least, x, mid - least, mid + least - k);\n }\n // Each group of columns, from the least to the group's least; then each column, from there to its own. (Nothing,\n // when every column's depth is the least: a light outline's always.)\n let g = most > least ? 0 : count;\n while (g < count) {\n const last = Math.min(g + group, count);\n let gl = half;\n let j = g;\n while (j < last) { if (depth[j] < gl) gl = depth[j]; j++; }\n const level = last - g === group ? 1 : 2;\n if (gl > least) {\n const k = gl - least, kk = Math.max(k, 4);\n if (dark) columnRect(player, words, starts, level * span + k, x + g * width, y + gl - kk, y + 2 * half - gl);\n else columnRect(player, words, starts, level * span + k, x + g * width, mid - gl, mid + gl - kk);\n }\n j = g;\n while (j < last) {\n const k = depth[j] - gl;\n if (k > 0) {\n const kk = Math.max(k, 4), cx = x + j * width + 1;\n if (dark) columnBar(player, cx, y + depth[j] - kk, y + 2 * half - depth[j], k);\n else columnBar(player, cx, mid - depth[j], mid + depth[j] - kk, k);\n }\n j++;\n }\n g = last;\n }\n if (outline) {\n // Where one face meets another, the nearer (taller) of the two columns is an edge.\n const edge: number[] = new Array(count).fill(0);\n let face = faces[0], tall = heights[0];\n i = 1;\n while (i < count) {\n const f = faces[i], h = heights[i];\n if (f != face) { if (h >= tall) edge[i] = 1; else edge[i - 1] = 1; }\n face = f;\n tall = h;\n i++;\n }\n // The bars in pixels: a column's centre stepped across, the middle row's worked out once.\n const mid80 = mid * 32 + 80;\n let px = (x + 1) * 32 + 16;\n i = 0;\n while (i < count) {\n const a = halves[i], a32 = halves32[i];\n if (dark) { if (edge[i] != 0 && a >= 2) columnBarPx(player, px, mid80 - a32, mid80, a32); }\n else if (a > 0) {\n if (edge[i] != 0 || a < 8) columnBarPx(player, px, mid80 - a32, mid80, a32);\n else columnBarPx(player, px, mid80 - a32, mid80 + a32 - 128, 128);\n }\n px += width * 32;\n i++;\n }\n } else if (faces !== undefined) {\n i = 0;\n while (i < count) {\n if ((faces[i] & 1) != 0) columnShaded(player, x + i * width + 1, mid, halves[i], dark, lines);\n i++;\n }\n }\n});\n\n/**\n * Keep what was lit this frame on the board for `n` frames, and wait them out: \\`await fog.show(2)\\` in place of\n * \\`await nextFrame()\\` draws every other frame at half the cost. StarCraft: Remastered redraws the fog on the minimap\n * every other frame (measured in two played recordings, 2026-10-17), so a picture drawn every other frame looks the\n * same there as one drawn every frame. The board is wiped at the start of the frame after them, ready for the next.\n *\n * @param n How many frames, 1 to 24, known when the script is built.\n */\nexport const show = game(async (n: number) => {\n memory.vision.countdown = n;\n await sleep(frames(n));\n});\n\n/** Everything above as one name: import { fog } from \"trigscript/fog\"; fog.light(P1, x, y, 3). */\nexport const fog = { light, rect, columns, show, play, reveal, picture, SHAPES };\n\n/** The longest fog.show() keeps a picture: the countdown the game sets itself after a wipe is longer than this. */\nconst SHOW_MOST = 24;\n\n// The canvas: every brush's sight set, every frame, and the board wiped on the next — for as long as the game runs —\n// unless a program asked for the picture to stay (fog.show), when the countdown is that program's to run down.\nprogram(() => {\n while (true) {\n for (let s = 1; s <= 11; s++) stats(BRUSHES[s - 1]).sight = s;\n const left = memory.vision.countdown;\n if (left == 0 || left > SHOW_MOST) memory.vision.countdown = 1;\n sleep(frames(1));\n }\n});\n", "math": "/**\n * Numbers for motion and play: the standard library's \"trigscript/math\" (TrigScript 4).\n *\n * import { Vec, lerp, easeOutQuad, distance, angleTo, approach, wrap } from \"trigscript/math\";\n *\n * const t = fixed(frame) / 48; // 0 to 1 over two seconds\n * const x = lerp(fromX, toX, easeOutQuad(t)); // eased from one place to another\n * const v = Vec.fromAngle(angleTo(x, y, tx, ty), fixed(4));\n * pos = pos.add(v);\n *\n * Fractions are the language's fixed numbers (16.16: four decimal places or so, from about −32 768 to 32 767): a\n * number becomes one when a fraction is stored into it, or written `fixed(…)`. Angles are radians, as JavaScript's are.\n * Whole numbers stay whole where they can — `distanceSq`, `manhattan`, `wrap`, `sign` — and cost less.\n */\n\n/** π, and a whole turn. */\nexport const PI = Math.PI;\nexport const TAU = Math.PI * 2;\n\n/** From a to b by t: a at 0, b at 1, between them in between (t outside 0 … 1 goes past). */\nexport function lerp(a: fixed, b: fixed, t: fixed): fixed {\n return a + (b - a) * t;\n}\n\n/** Where v is from a to b, as lerp's t: 0 at a, 1 at b. */\nexport function inverseLerp(a: fixed, b: fixed, v: fixed): fixed {\n return b === a ? fixed(0) : (v - a) / (b - a);\n}\n\n/** v from the range a1 … b1 into a2 … b2. */\nexport function remap(v: fixed, a1: fixed, b1: fixed, a2: fixed, b2: fixed): fixed {\n return lerp(a2, b2, inverseLerp(a1, b1, v));\n}\n\n/** t kept inside 0 … 1. */\nexport function clamp01(t: fixed): fixed {\n return t < 0 ? fixed(0) : t > 1 ? fixed(1) : t;\n}\n\n/** From `current` towards `target` by at most `step`, never past it: a value that follows another at a speed. */\nexport function approach(current: number, target: number, step: number): number {\n if (current < target) return current + step > target ? target : current + step;\n return current - step < target ? target : current - step;\n}\n\n/** n into 0 … size − 1, going round: wrap(-1, 10) is 9, wrap(12, 10) is 2. */\nexport function wrap(n: number, size: number): number {\n return ((n % size) + size) % size;\n}\n\n/** −1, 0 or 1, by whether n is below, at or above zero. */\nexport function sign(n: number): number {\n return n > 0 ? 1 : n < 0 ? -1 : 0;\n}\n\n/** The distance from one point to another, as the crow flies. */\nexport function distance(x1: number, y1: number, x2: number, y2: number): fixed {\n return Math.hypot(x2 - x1, y2 - y1);\n}\n\n/** The distance squared: whole, and cheaper — compare it with a range squared. */\nexport function distanceSq(x1: number, y1: number, x2: number, y2: number): number {\n return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);\n}\n\n/** The distance along the axes, |dx| + |dy|: what the game's own \"nearest\" goes by. */\nexport function manhattan(x1: number, y1: number, x2: number, y2: number): number {\n return Math.abs(x2 - x1) + Math.abs(y2 - y1);\n}\n\n/** The angle from one point to another, in radians from −π to π: 0 is to the right (east), π / 2 down (south, y grows down). */\nexport function angleTo(x1: number, y1: number, x2: number, y2: number): fixed {\n return Math.atan2(y2 - y1, x2 - x1);\n}\n\n/** Degrees as radians. */\nexport function radians(degrees: fixed): fixed {\n return (degrees * PI) / 180;\n}\n\n/** Radians as degrees. */\nexport function degrees(radians: fixed): fixed {\n return (radians * 180) / PI;\n}\n\n/* Easing: t from 0 to 1, how far along a motion is at that point of its time — slow then fast, fast then slow, … */\n\n/** Slow at the start. */\nexport function easeInQuad(t: fixed): fixed {\n return t * t;\n}\n\n/** Slow at the end. */\nexport function easeOutQuad(t: fixed): fixed {\n return t * (2 - t);\n}\n\n/** Slow at both ends. */\nexport function easeInOutQuad(t: fixed): fixed {\n return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;\n}\n\n/** Slower at the start than easeInQuad. */\nexport function easeInCubic(t: fixed): fixed {\n return t * t * t;\n}\n\n/** Slower at the end than easeOutQuad. */\nexport function easeOutCubic(t: fixed): fixed {\n const u = t - 1;\n return u * u * u + 1;\n}\n\n/** Goes a little past the end and comes back. */\nexport function easeOutBack(t: fixed): fixed {\n const u = t - 1;\n return 1 + 2.70158 * u * u * u + 1.70158 * u * u;\n}\n\n/** Slow at both ends, smoothly (Hermite: 3t² − 2t³). */\nexport function smoothstep(t: fixed): fixed {\n return t * t * (3 - 2 * t);\n}\n\n/**\n * A point, or a direction and a length: two fixed numbers. Each operation gives a new Vec.\n *\n * let pos = new Vec(fixed(x), fixed(y));\n * const toward = new Vec(fixed(tx), fixed(ty)).sub(pos).normalized().scale(fixed(3));\n * pos = pos.add(toward);\n */\nexport class Vec {\n x: fixed;\n y: fixed;\n\n constructor(x: fixed, y: fixed) {\n this.x = x;\n this.y = y;\n }\n\n /** A direction `length` long at `angle` radians (0 to the right, π / 2 down). */\n static fromAngle(angle: fixed, length: fixed): Vec {\n return new Vec(Math.cos(angle) * length, Math.sin(angle) * length);\n }\n\n add(o: Vec): Vec {\n return new Vec(this.x + o.x, this.y + o.y);\n }\n\n sub(o: Vec): Vec {\n return new Vec(this.x - o.x, this.y - o.y);\n }\n\n scale(k: fixed): Vec {\n return new Vec(this.x * k, this.y * k);\n }\n\n dot(o: Vec): fixed {\n return this.x * o.x + this.y * o.y;\n }\n\n /** How long it is. */\n length(): fixed {\n return Math.sqrt(this.x * this.x + this.y * this.y);\n }\n\n /** The same direction, 1 long (or none, for none). */\n normalized(): Vec {\n const l = this.length();\n const k: fixed = l === 0 ? fixed(0) : 1 / l;\n return new Vec(this.x * k, this.y * k);\n }\n\n /** Its angle, in radians from −π to π. */\n angle(): fixed {\n return Math.atan2(this.y, this.x);\n }\n\n /** Turned by `angle` radians (clockwise on the map, where y grows down). */\n rotate(angle: fixed): Vec {\n const c = Math.cos(angle);\n const s = Math.sin(angle);\n return new Vec(this.x * c - this.y * s, this.x * s + this.y * c);\n }\n\n /** How far it is from another point. */\n distanceTo(o: Vec): fixed {\n return this.sub(o).length();\n }\n\n /** From this towards another by t (0 here, 1 there). */\n lerp(o: Vec, t: fixed): Vec {\n return new Vec(this.x + (o.x - this.x) * t, this.y + (o.y - this.y) * t);\n }\n}\n", "music": "/**\n * Music in a program: the standard library's \"trigscript/music\" (TrigScript 4).\n *\n * import { music } from \"trigscript/music\";\n * import pluck from \"./pluck.sound\";\n *\n * const lead = music.synth({ wave: \"square\", volume: 0.2 });\n * const bass = music.sampler(pluck, \"A3\");\n * const drums = music.kit({ k: kick, s: snare, h: hat });\n *\n * const song = music.song({ bpm: 120 },\n * music.track(lead, \"C5 - E5 - G5 - - . | A5 - G5 - E5 - - .\"),\n * music.track(bass, \"C3 - - - - - - - | F3 - - - - - - -\"),\n * music.track(drums, \"kh h sh h kh h sh h | kh h sh h kh kh sh h\"),\n * );\n *\n * program(async () => {\n * start(music.loop(song)); // the song again and again, beside the program\n * playWav(lead.note(\"C6\", 0.1)); // an instrument's note on its own, for a sound effect\n * });\n *\n * A pattern is steps, a word each, a quarter of a beat each by default: a note's name starts it (\"C4\", \"F#5\", \"Bb2\"),\n * several with + sound together (\"C4+E4+G4\"), \"-\" holds what sounds a step longer, \".\" is a rest, \"|\" is only for the\n * eye; \"@\" after a step's notes says how loud (\"C4@0.5\"). A kit's step is its sounds' letters (\"kh\": kick and hat).\n * Everything is worked out when the script is built: each note an instrument plays becomes a clip once (`trigscript`'s\n * Clip, put in the map once however often it plays), and the song a table of which clip starts on which frame, which\n * `play` reads while the game runs.\n *\n * A longer song is written in sections — parts named once, each section saying what they play in it, a pattern played\n * again until the section is full — and chords by name, their notes drawn by a template:\n *\n * const parts = { keys: music.part(music.presets.keys()), bass: music.part(music.presets.bass()), drums: music.part(music.drums()) };\n * const verse = music.section(4, {\n * keys: music.chords(\"Am F C G\", \"1+2+3 - . 1+2+3 . . 1+2+3 -\", { octave: 4, lead: true }),\n * bass: music.chords(\"Am F C G\", \"1 - . 1 . 1 3 -\", { octave: 2 }),\n * drums: music.beat({ k: [0, 3, 4], s: [2, 6], h: [0, 1, 2, 3, 4, 5, 6, 7] }, 8),\n * });\n * const song = music.arrange({ bpm: 120, steps: 2 }, parts, intro, verse, verse.with({ keys: null }), outro);\n *\n * Music that follows the game is a song of sections cut into bars and layers, a clip each, which the program steers\n * bar by bar — which section, which layers, a stinger on the next beat:\n *\n * const cut = music.stems(song, { layers: { drums: [\"drums\"], rest: [\"keys\", \"bass\"] } });\n * program(async () => {\n * const band = new Band(); // import { music, Band } from \"trigscript/music\"\n * start(music.conduct(cut, band));\n * if (danger) band.go(cut.section(\"chorus\")); // at the next bar line\n * if (cleared) band.sting(cut.stinger(\"crash\")); // on the next beat\n * });\n *\n * What the game does with it (the sound probe, 2026-10-19): a bar is a whole number of frames (42 ms on Fastest), so\n * the tempo is the nearest that fits — `song.bpm` says which; a sound starts within 21 ms of its frame; 32 sound at\n * once, and a song that would need more is refused; notes that overlap add, with no limiter, so keep the volumes down.\n * `render` mixes a song into one clip when the script is built instead: every note exactly in time, one sound playing,\n * nothing to change while it plays. A program plays to its own player; a program for each player plays to each.\n */\nimport { game, sleep, frames, playWav, tone, noise, fm, pluck, mixdown, together, memory, type Clip, type Notes } from \"trigscript\";\n\n/** Seconds a frame lasts on Fastest, the speed a map is played at. */\nconst FRAME = 0.042;\n/** Sounds the game plays at once. */\nconst VOICES = 32;\n\n/** What plays a pattern's notes: each note a clip, made when the script is built. */\nexport interface Instrument {\n readonly kind: \"notes\";\n /**\n * Note `name` (\"C4\", \"F#5\") lasting `seconds`, release and all: a clip to play on its own, too. `rate`: samples a\n * second, for an instrument that makes its notes (render asks for 44 100).\n */\n note(name: string, seconds: number, rate?: number): Clip;\n}\n\n/** What plays a pattern's letters: a clip a letter. */\nexport interface Kit {\n readonly kind: \"kit\";\n readonly hits: Readonly>;\n}\n\nconst SEMITONE: Readonly> = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };\n\n/** A note's number, 69 for A4: \"C4\" is 60, \"C#4\" 61, \"Db4\" 61. */\nfunction noteNumber(name: string): number {\n const m = /^([A-Ga-g])([#b]?)(-?\\d)$/.exec(name);\n if (!m) throw new Error(`A note is a name with its octave — \"C4\", \"F#5\", \"Bb2\" — not ${JSON.stringify(name)}.`);\n return 12 * (Number(m[3]) + 1) + SEMITONE[m[1].toLowerCase()] + (m[2] === \"#\" ? 1 : m[2] === \"b\" ? -1 : 0);\n}\n\n/** Notes kept as they are made, so a note played many times is the same clip (and the same file in the map). */\nfunction remembering(make: (name: string, seconds: number, rate?: number) => Clip): (name: string, seconds: number, rate?: number) => Clip {\n const made = new Map();\n return (name, seconds, rate) => {\n const key = `${name} ${seconds.toFixed(4)} ${rate ?? \"\"}`;\n let c = made.get(key);\n if (!c) { c = make(name, seconds, rate); made.set(key, c); }\n return c;\n };\n}\n\n/**\n * An instrument from a recording of one note, `root` (\"A3\"): every other note is it played faster or slower, as a\n * sampler does, cut to the note's length with a short release. Where the recording is shorter than a note, the note\n * is as long as the recording.\n */\nexport function sampler(clip: Clip, root: string = \"C4\", options: { readonly release?: number; readonly volume?: number } = {}): Instrument {\n const base = noteNumber(root), release = options.release ?? 0.05, volume = options.volume ?? 1;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds) => {\n const pitched = clip.pitch(noteNumber(name) - base).gain(volume);\n return pitched.trim(0, Math.min(pitched.seconds, seconds)).fade(0, Math.min(release, seconds / 2));\n }),\n });\n}\n\n/** An attack, a decay to the sustain level, and a release after the note's length, in seconds (sustain 0 … 1). */\nexport interface Shape { readonly attack?: number; readonly decay?: number; readonly sustain?: number; readonly release?: number }\n\n/** A shape's points for a note held `held` seconds, as levels 0 … 1: the attack from 0, the release to 0. */\nfunction shapePoints(shape: Required, held: number): [number, number][] {\n const { attack, decay, sustain, release } = shape;\n const h = Math.max(attack + decay, held);\n return [[0, 0], [attack, 1], [attack + decay, sustain], [h, sustain], [h + release, 0]];\n}\nconst full = (s: Shape | undefined, d: Required): Required => ({ attack: s?.attack ?? d.attack, decay: s?.decay ?? d.decay, sustain: s?.sustain ?? d.sustain, release: s?.release ?? d.release });\n\n/** A synth's filter: its type, the cutoff it rests at, its resonance, and how many octaves above it a note's envelope opens it. */\nexport interface SynthFilter {\n readonly type?: \"lowpass\" | \"highpass\" | \"bandpass\";\n readonly cutoff: number;\n readonly resonance?: number;\n /** Octaves above the cutoff at the envelope's top (0: the filter does not move). */\n readonly amount?: number;\n /** The filter's own attack, decay, sustain and release, as a note's loudness has. */\n readonly envelope?: Shape;\n}\n\nexport interface SynthOptions extends Shape {\n readonly wave?: \"sine\" | \"square\" | \"triangle\" | \"saw\";\n readonly volume?: number;\n /** Copies of each note (1 to 16), `detune` cents apart from lowest to highest, `width` 0 … 1 across left and right. */\n readonly voices?: number;\n readonly detune?: number;\n readonly width?: number;\n readonly filter?: SynthFilter;\n /** Saturation after the filter, dB of push (`Clip.drive`). */\n readonly drive?: number;\n /** Semitones each note slides up from (a negative number: from below) over `glideTime` seconds. */\n readonly glide?: number;\n readonly glideTime?: number;\n}\n\n/**\n * An instrument made of tones: `wave` (\"sine\", \"square\", \"triangle\", \"saw\") shaped by an attack, a decay to the\n * sustain level, and a release after the note's length — a stack of detuned copies with `voices`, through a filter\n * that opens and closes with each note with `filter`, saturated with `drive`.\n */\nexport function synth(options: SynthOptions = {}): Instrument {\n const wave = options.wave ?? \"square\", volume = options.volume ?? 0.2;\n const amp = full(options, { attack: 0.005, decay: 0.08, sustain: 0.7, release: 0.06 });\n const f = options.filter, fshape = full(f?.envelope, { attack: 0.005, decay: 0.25, sustain: 0.2, release: 0.2 });\n const voices = options.voices ?? 1, detune = options.detune ?? (voices > 1 ? 20 : 0), width = options.width ?? 0;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const held = Math.max(amp.attack + amp.decay, seconds - amp.release), length = held + amp.release;\n const r = rate === undefined ? {} : { rate };\n const pitch = options.glide ? [[0, 440 * 2 ** ((noteNumber(name) + options.glide - 69) / 12)], [options.glideTime ?? 0.06, name]] as [number, number | string][] : name;\n let c = tone(pitch, length, { wave, volume, voices, detune, width, ...r });\n if (f) {\n const amount = f.amount ?? 0, base = f.cutoff;\n const cutoff = amount === 0 ? base : shapePoints(fshape, held).map(([t, level]) => [t, base * 2 ** (amount * level)] as [number, number]);\n c = c.filter(f.type ?? \"lowpass\", cutoff, { resonance: f.resonance ?? 0.1 });\n }\n if (options.drive) c = c.drive(options.drive);\n return c.envelope(...shapePoints(amp, held));\n }),\n });\n}\n\n/**\n * An FM instrument (`fm()`): `ratio` the modulator's pitch to the note's, `index` how bright a note starts, falling to\n * `index × brightness` over `bright` seconds — an electric piano (ratio 1), a bell (3.5), a bass (0.5).\n */\nexport function fmSynth(options: Shape & { readonly ratio?: number; readonly index?: number; readonly brightness?: number; readonly bright?: number; readonly feedback?: number; readonly volume?: number } = {}): Instrument {\n const amp = full(options, { attack: 0.002, decay: 1.2, sustain: 0.35, release: 0.3 });\n const ratio = options.ratio ?? 1, index = options.index ?? 3, after = index * (options.brightness ?? 0.2), bright = options.bright ?? 0.8, volume = options.volume ?? 0.25;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const held = Math.max(amp.attack + amp.decay, seconds - amp.release), length = held + amp.release;\n return fm(name, length, { ratio, index: [[0, index], [bright, after]], feedback: options.feedback ?? 0, volume, ...(rate === undefined ? {} : { rate }) }).envelope(...shapePoints(amp, held));\n }),\n });\n}\n\n/** A plucked-string instrument (`pluck()`): `brightness` 0 … 1, `decay` seconds; a note rings out its decay unless `cut`. */\nexport function strings(options: { readonly brightness?: number; readonly decay?: number; readonly volume?: number; readonly cut?: boolean } = {}): Instrument {\n const brightness = options.brightness ?? 0.55, decay = options.decay ?? 1.5, volume = options.volume ?? 0.3;\n return Object.freeze({\n kind: \"notes\" as const,\n note: remembering((name, seconds, rate) => {\n const length = options.cut ? seconds + 0.05 : Math.max(seconds + 0.05, Math.min(decay, 4));\n return pluck(name, length, { brightness, decay, volume, ...(rate === undefined ? {} : { rate }) }).fade(0, 0.05);\n }),\n });\n}\n\n/**\n * Instruments to start from, each a function that takes the same options as the instrument it is made with, over its\n * own: `music.presets.supersaw({ volume: 0.1 })`.\n */\nexport const presets = {\n /** Seven detuned saws through a filter that opens on each note: a trance lead, big chords. */\n supersaw: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 7, detune: 28, width: 0.7, volume: 0.16, attack: 0.005, decay: 0.25, sustain: 0.8, release: 0.18, filter: { cutoff: 1800, amount: 2, resonance: 0.15, envelope: { decay: 0.35, sustain: 0.35 } }, ...o }),\n /** A slow, wide pad: five saws, the filter low and slowly opening. Give its track a chorus and the reverb. */\n pad: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 5, detune: 18, width: 1, volume: 0.1, attack: 0.35, decay: 0.6, sustain: 0.85, release: 0.8, filter: { cutoff: 700, amount: 1.3, resonance: 0.1, envelope: { attack: 0.6, decay: 1.2, sustain: 0.5, release: 0.8 } }, ...o }),\n /** A round, punchy bass: two saws, the filter snapping shut after each note, a little drive. */\n bass: (o: SynthOptions = {}) => synth({ wave: \"saw\", voices: 2, detune: 8, volume: 0.32, attack: 0.002, decay: 0.12, sustain: 0.9, release: 0.04, filter: { cutoff: 180, amount: 2.6, resonance: 0.25, envelope: { decay: 0.18, sustain: 0.15, release: 0.05 } }, drive: 6, ...o }),\n /** A squelchy, resonant line (a 303's): one saw, the filter wide open on each note and shutting fast. */\n acid: (o: SynthOptions = {}) => synth({ wave: \"saw\", volume: 0.22, attack: 0.002, decay: 0.1, sustain: 0.8, release: 0.05, filter: { cutoff: 260, amount: 3.6, resonance: 0.82, envelope: { decay: 0.16, sustain: 0 } }, drive: 10, ...o }),\n /** A warm lead: two square waves a little apart, softened by a filter. */\n lead: (o: SynthOptions = {}) => synth({ wave: \"square\", voices: 2, detune: 12, width: 0.3, volume: 0.15, attack: 0.004, decay: 0.15, sustain: 0.7, release: 0.12, filter: { cutoff: 2200, amount: 1.2, resonance: 0.2, envelope: { decay: 0.25, sustain: 0.4 } }, ...o }),\n /** The old consoles' lead: a bare square wave. */\n chip: (o: SynthOptions = {}) => synth({ wave: \"square\", volume: 0.14, attack: 0.002, decay: 0.05, sustain: 0.8, release: 0.03, ...o }),\n /** An electric piano: FM at ratio 1, bright on the strike and mellowing. */\n keys: (o: Parameters[0] = {}) => fmSynth({ ratio: 1, index: 2.6, brightness: 0.25, bright: 0.6, volume: 0.22, decay: 1.4, sustain: 0.3, release: 0.35, ...o }),\n /** A bell: FM at ratio 3.5, long and shimmering. */\n bell: (o: Parameters[0] = {}) => fmSynth({ ratio: 3.5, index: 5, brightness: 0.08, bright: 1.6, volume: 0.16, decay: 2.5, sustain: 0, release: 1.2, ...o }),\n /** A plucked string, a little bright. */\n pluck: (o: Parameters[0] = {}) => strings({ brightness: 0.6, decay: 1.4, ...o }),\n};\n\n/**\n * A drum kit made from nothing, a letter a drum: k kick, s snare, c clap, h closed hat, o open hat, t tom, r rim, x crash. At\n * 44 100 Hz (`rate`), so the hats keep their top; `volume` 0 … 1 over the whole kit (1).\n */\nexport function drums(options: { readonly rate?: number; readonly volume?: number } = {}): Kit {\n const rate = options.rate ?? 44100, v = options.volume ?? 1;\n const metal = (seconds: number) => [263, 400, 421, 474, 587, 845].map((hz) => tone(hz * 2.6, seconds, { wave: \"square\", volume: 0.16, rate })).reduce((a, b) => a.mix(b));\n const kick = tone([[0, 170], [0.025, 85], [0.3, 46]], 0.42, { volume: 0.95, rate }).envelope([0, 1], [0.08, 0.85], [0.42, 0])\n .mix(noise(0.006, { volume: 0.35, rate }).filter(\"highpass\", 3000)).drive(5);\n const snare = tone([[0, 260], [0.04, 185]], 0.18, { wave: \"triangle\", volume: 0.6, rate }).envelope([0, 1], [0.15, 0])\n .mix(noise(0.28, { volume: 0.6, rate }).filter(\"highpass\", 1400).filter(\"peak\", 5000, { gain: 4 }).envelope([0, 1], [0.03, 0.55], [0.26, 0])).drive(4);\n const burst = (at: number) => ({ clip: noise(0.012, { volume: 0.6, rate }).envelope([0, 1], [0.012, 0.2]), at });\n const clap = mixdown([burst(0), burst(0.011), burst(0.023), { clip: noise(0.22, { volume: 0.5, rate }).envelope([0, 1], [0.22, 0]), at: 0.03 }], undefined, rate)\n .filter(\"bandpass\", 1300, { resonance: 0.35 }).gain(1.6);\n const hat = (seconds: number) => metal(seconds).mix(noise(seconds, { volume: 0.4, rate })).filter(\"highpass\", 7200).filter(\"highshelf\", 10000, { gain: 3 }).envelope([0, 1], [seconds * 0.25, 0.4], [seconds, 0]);\n const tom = tone([[0, 190], [0.2, 120]], 0.4, { volume: 0.8, rate }).envelope([0, 1], [0.4, 0]).mix(noise(0.01, { volume: 0.2, rate }).filter(\"bandpass\", 2500));\n const rim = tone(1700, 0.025, { wave: \"triangle\", volume: 0.5, rate }).mix(noise(0.012, { volume: 0.3, rate }).filter(\"highpass\", 4000)).envelope([0, 1], [0.025, 0]);\n const crash = metal(2).mix(noise(2, { volume: 0.5, rate })).filter(\"highpass\", 4500).filter(\"peak\", 7500, { gain: 3 }).envelope([0, 1], [0.08, 0.5], [2, 0]);\n return kit({ k: kick.gain(v), s: snare.gain(v), c: clap.gain(v), h: hat(0.07).gain(0.55 * v), o: hat(0.4).gain(0.5 * v), t: tom.gain(v), r: rim.gain(v), x: crash.gain(0.45 * v) });\n}\n\n/** A kit: a clip a letter, played by a pattern's letters (\"kh\" plays k and h together). */\nexport function kit(hits: Readonly>): Kit {\n for (const k of Object.keys(hits)) if (k.length !== 1 || k === \".\" || k === \"-\" || k === \"|\") throw new Error(`A kit's sounds are named by one letter each, not ${JSON.stringify(k)}.`);\n return Object.freeze({ kind: \"kit\" as const, hits: Object.freeze({ ...hits }) });\n}\n\n/* ── Patterns: what is written, and what writes it ── */\n\n/** A pattern: steps a word each, as one text or a text a bar. */\nexport type Pattern = string | readonly string[];\n\n/** A pattern's steps, the \"|\" left out. */\nconst wordsOf = (p: Pattern): string[] => (typeof p === \"string\" ? p : p.join(\" \")).split(/\\s+/).filter((w) => w !== \"\" && w !== \"|\");\n\n/** A step's sounds and how loud: \"C4+E4@0.6\" is C4 and E4 at 0.6; \"kh\" k and h at 1. */\nfunction velocityOf(word: string, step: number): { body: string; velocity: number } {\n const at = word.indexOf(\"@\");\n if (at < 0) return { body: word, velocity: 1 };\n const body = word.slice(0, at), v = Number(word.slice(at + 1));\n if (body === \"\" || body === \"-\" || body === \".\" || !(v >= 0 && v <= 4)) throw new Error(`Step ${step + 1}: \"@\" follows a note, a chord or a kit's letters, with how loud they play, 0 to 4 (1 as written): \"C4@0.6\", \"kh@1.3\" — not ${JSON.stringify(word)}.`);\n return { body, velocity: v };\n}\n\nconst NAMES = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"];\n/** A note's name from its number: 60 is \"C4\". Sharps, not flats. */\nfunction nameOf(n: number): string {\n const octave = Math.floor(n / 12) - 1;\n if (octave < -1 || octave > 9) throw new Error(`A note goes from C-1 to B9: this one would be ${NAMES[((n % 12) + 12) % 12]}${octave}.`);\n return NAMES[((n % 12) + 12) % 12] + octave;\n}\n\n/** Each note of a step moved by a function of its number; holds, rests, kit letters and \"@\" kept. */\nconst eachNote = (p: Pattern, move: (n: number) => number): string[] => wordsOf(p).map((w) => w.replace(/[A-Ga-g][#b]?-?\\d/g, (name) => nameOf(move(noteNumber(name)))));\n\n/** A pattern `semitones` up (or down: a negative number): `transpose(tune, 12)` is the tune an octave up. A text a bar stays a text a bar. */\nexport function transpose(p: Pattern, semitones: number): Pattern {\n if (!Number.isInteger(semitones)) throw new Error(`transpose() moves by whole semitones, not ${semitones}.`);\n if (semitones === 0) return p;\n return typeof p === \"string\" ? eachNote(p, (n) => n + semitones).join(\" \") : p.map((bar) => eachNote(bar, (n) => n + semitones).join(\" \"));\n}\n\n/** A pattern with each step `by` steps long — eighths written as sixteenths with `stretch(p, 2)`. A text a bar stays a text a bar. */\nexport function stretch(p: Pattern, by: number): Pattern {\n if (!Number.isInteger(by) || by < 1) throw new Error(`stretch() makes each step a whole number of steps long, 1 or more — not ${by}.`);\n const one = (text: string) => wordsOf(text).map((w) => [w, ...Array.from({ length: by - 1 }, () => (w.startsWith(\".\") ? \".\" : \"-\"))].join(\" \")).join(\" \");\n return typeof p === \"string\" ? one(p) : p.map(one);\n}\n\n/** A pattern `n` times over, as one text. */\nexport function repeat(p: Pattern, n: number): string {\n if (!Number.isInteger(n) || n < 1) throw new Error(`repeat() plays a pattern a whole number of times, 1 or more — not ${n}.`);\n return Array.from({ length: n }, () => wordsOf(p).join(\" \")).join(\" \");\n}\n\n/** A kit's bar: the steps (from 0) each letter plays on, `steps` steps long — `beat({ k: [0, 8], s: [4, 12] })`. */\nexport function beat(hits: Readonly>, steps: number = 16): string {\n for (const [k, at] of Object.entries(hits)) for (const s of at) if (!Number.isInteger(s) || s < 0 || s >= steps) throw new Error(`beat(): ${k} on step ${s}, of steps 0 to ${steps - 1}.`);\n return Array.from({ length: steps }, (_, i) => Object.keys(hits).filter((k) => hits[k].includes(i)).join(\"\") || \".\").join(\" \");\n}\n\n/** A chord's notes above its root, by what follows the root's name. */\nconst QUALITIES: Readonly> = {\n \"\": [0, 4, 7], m: [0, 3, 7], dim: [0, 3, 6], aug: [0, 4, 8], \"+\": [0, 4, 8], sus2: [0, 2, 7], sus4: [0, 5, 7], sus: [0, 5, 7], \"5\": [0, 7],\n \"6\": [0, 4, 7, 9], m6: [0, 3, 7, 9], \"7\": [0, 4, 7, 10], maj7: [0, 4, 7, 11], M7: [0, 4, 7, 11], m7: [0, 3, 7, 10], dim7: [0, 3, 6, 9],\n m7b5: [0, 3, 6, 10], \"7sus4\": [0, 5, 7, 10], \"9\": [0, 4, 7, 10, 14], maj9: [0, 4, 7, 11, 14], m9: [0, 3, 7, 10, 14], add9: [0, 4, 7, 14], madd9: [0, 3, 7, 14],\n};\n\n/** A chord's notes as numbers, lowest first. */\nfunction chordNumbers(symbol: string, octave: number, inversion: number): number[] {\n const m = /^([A-G][#b]?)([^/]*)(?:\\/([A-G][#b]?))?$/.exec(symbol);\n const steps = m ? QUALITIES[m[2]] : undefined;\n if (!m || !steps) throw new Error(`A chord is a root and what kind — \"Am\", \"C\", \"G7\", \"Fmaj7\", \"Dm7\", \"Esus4\", \"Bdim\", \"C/E\" — not ${JSON.stringify(symbol)}. The kinds: ${Object.keys(QUALITIES).map((q) => `\"${q}\"`).join(\", \")}.`);\n if (!Number.isInteger(inversion) || inversion < 0 || inversion >= steps.length) throw new Error(`${symbol} has ${steps.length} notes: its inversion is 0 to ${steps.length - 1}, not ${inversion}.`);\n const root = noteNumber(m[1] + octave);\n const notes = steps.map((s) => root + s);\n for (let k = 0; k < inversion; k++) notes.push(notes.shift()! + 12);\n if (m[3]) {\n // Over another bass: that note, below the rest.\n let bass = noteNumber(m[3] + octave);\n while (bass >= notes[0]) bass -= 12;\n notes.unshift(bass);\n }\n return notes;\n}\n\n/**\n * A chord's notes, as a step plays them: `chord(\"Am\")` is \"A4+C5+E5\", its root in octave 4 (`octave`); `inversion`\n * moves its lowest notes up an octave, one each — `chord(\"E\", 3, 1)` is \"G#3+B3+E4\". \"C/E\" is C over an E below it.\n */\nexport function chord(symbol: string, octave: number = 4, inversion: number = 0): string {\n return chordNumbers(symbol, octave, inversion).map(nameOf).join(\"+\");\n}\n\n/** Each chord's notes as numbers, lowest first: the ones written, or the symbol's at `octave`. */\nconst notesOf = (c: string, octave: number): number[] => (/\\d/.test(c) ? c.split(\"+\").map(noteNumber).sort((a, b) => a - b) : chordNumbers(c, octave, 0));\n\n/**\n * A chord played as `template` says: its steps name the chord's notes by number, 1 the lowest — \"1 3 2 3\", \"1+2+3 - - -\"\n * — past the last, round again an octave up (in a three-note chord, 4 is 1 an octave up); each ^ after a number takes it\n * an octave higher, each _ an octave lower (\"1__\" the root two octaves down, for a bass). \"-\", \".\" and \"@\" as in any\n * pattern. `notes` is a chord's notes (\"A3+C4+E4\") or its name (\"Am\", with its root in octave `octave`).\n */\nexport function over(notes: string, template: Pattern, octave: number = 4): string {\n const n = notesOf(notes, octave);\n return wordsOf(template).map((w, s) => {\n const { body, velocity } = velocityOf(w, s);\n if (body === \"-\" || body === \".\") return w;\n const named = body.split(\"+\").map((d) => {\n const m = /^(\\d+)([\\^_]*)$/.exec(d);\n if (!m || Number(m[1]) < 1) throw new Error(`Step ${s + 1} of a chord's template: its notes are numbers from 1, the lowest, with ^ or _ for an octave up or down — \"1\", \"3^\", \"1__\" — not ${JSON.stringify(d)}.`);\n const k = Number(m[1]) - 1, up = Math.floor(k / n.length) + (m[2].match(/\\^/g)?.length ?? 0) - (m[2].match(/_/g)?.length ?? 0);\n return nameOf(n[k % n.length] + 12 * up);\n });\n return named.join(\"+\") + (velocity === 1 ? \"\" : `@${velocity}`);\n }).join(\" \");\n}\n\n/**\n * A progression played as `template` says, a template a chord: `chords(\"Am Am E Am Dm C E Am\", \"1_ 1 2 3\")` — as\n * `over` plays one chord. Each chord's root in octave `octave` (4); `lead: true` turns each chord after the first to\n * the inversion nearest the one before (its notes move least), as a player's hands do. A text a chord.\n */\nexport function chords(progression: string | readonly string[], template: Pattern, options: { readonly octave?: number; readonly lead?: boolean } = {}): string[] {\n const octave = options.octave ?? 4, list = wordsOf(progression);\n let before: number[] | null = null;\n return list.map((c) => {\n let n = notesOf(c, octave);\n if (options.lead && before && !/\\d/.test(c)) {\n // Each inversion, an octave either side: the one whose notes are nearest those before.\n const prev: number[] = before, mean = prev.reduce((a, b) => a + b, 0) / prev.length;\n let best = n, cost = Infinity;\n for (let inv = 0; inv < n.length; inv++) for (const shift of [-12, 0, 12]) {\n const cand = chordNumbers(c, octave, inv).map((x) => x + shift);\n const d = cand.length === prev.length ? cand.reduce((s, x, i) => s + Math.abs(x - prev[i]), 0) : Math.abs(cand.reduce((a, b) => a + b, 0) / cand.length - mean) * cand.length;\n if (d < cost) { cost = d; best = cand; }\n }\n n = best;\n }\n before = n;\n return over(n.map(nameOf).join(\"+\"), template);\n });\n}\n\n/** Seconds `n` bars last at `bpm` as a song plays it (its bar a whole number of frames): how long to make a riser. */\nexport function bars(bpm: number, n: number = 1, beats: number = 4): number {\n return n * Math.max(1, Math.round((60 / bpm) * beats / FRAME)) * FRAME;\n}\n\n/**\n * What makes a part breathe with the beat, as a mix does under a kick: down to `1 − depth` on each beat (`beat`\n * seconds apart, from the clip's start) and back over `release` of a beat — a part's fx, or a section's:\n * `{ fx: music.pump(music.bars(143) / 4) }`.\n */\nexport function pump(beat: number, depth: number = 0.45, release: number = 0.55): (clip: Clip) => Clip {\n if (!(beat > 0) || !(depth >= 0 && depth <= 1) || !(release > 0 && release <= 1)) throw new Error(\"pump() takes seconds a beat, how deep (0 … 1) and how long it takes to come back, as a part of a beat (0 … 1).\");\n return (c) => {\n const points: [number, number][] = [];\n for (let t = 0; t < c.seconds; t += beat) points.push([t, 1 - depth], [Math.min(c.seconds, t + release * beat), 1], [Math.min(c.seconds, t + beat - 0.004), 1]);\n points.push([c.seconds, 1]);\n return c.envelope(...points);\n };\n}\n\n/* ── Sounds between sections ── */\n\n/** Noise rising for `seconds` through a filter opening, loudest at the end: what leads into a drop. */\nexport function riser(seconds: number, options: { readonly volume?: number; readonly rate?: number } = {}): Clip {\n const rate = options.rate ?? 44100, v = options.volume ?? 0.5, s = seconds;\n const swept = noise(s, { volume: v, rate }).filter(\"bandpass\", [[0, 350], [s * 0.6, 1800], [s, 9000]], { resonance: 0.55 });\n const lift = tone([[0, 220], [s, 880]], s, { wave: \"saw\", voices: 3, detune: 25, width: 0.8, volume: v * 0.25, rate }).filter(\"lowpass\", [[0, 500], [s, 6000]]);\n return swept.mix(lift).envelope([0, 0], [s * 0.5, 0.25], [s * 0.9, 0.85], [s, 1]).fade(0, 0.01);\n}\n\n/** A boom to land on: a falling sine under a burst of noise, ringing `seconds` (1.5). */\nexport function impact(options: { readonly seconds?: number; readonly volume?: number; readonly rate?: number } = {}): Clip {\n const rate = options.rate ?? 44100, v = options.volume ?? 0.8, s = options.seconds ?? 1.5;\n return tone([[0, 90], [0.4, 38], [s, 30]], s, { volume: v, rate }).envelope([0, 1], [0.15, 0.7], [s, 0])\n .mix(noise(s, { volume: v * 0.5, rate }).filter(\"lowpass\", [[0, 6000], [s, 300]]).envelope([0, 1], [0.05, 0.4], [s, 0])).drive(3);\n}\n\n/** How a track or a part is heard. */\nexport interface PartOptions {\n /** Where it is heard, −1 left to 1 right; 0, the middle, when left out. */\n readonly pan?: number;\n /** How loud, 1 as the instrument plays. */\n readonly volume?: number;\n /** What it goes through: its whole part in `render`, each note on its own in `play`. */\n readonly fx?: (clip: Clip) => Clip;\n /** How much of it goes to the song's reverb in `render`, 0 … 1 (0). */\n readonly reverb?: number;\n}\n\n/** What an instrument plays, step by step. */\nexport interface Track {\n readonly instrument: Instrument | Kit;\n readonly steps: readonly string[];\n /** Where it is heard, −1 left to 1 right; 0, the middle, when left out. */\n readonly pan: number;\n /** How loud, 1 as the instrument plays. */\n readonly volume: number;\n /** What the track goes through: its whole part in `render`, each note on its own in `play`. */\n readonly fx?: (clip: Clip) => Clip;\n /** How much of it goes to the song's reverb in `render`, 0 … 1 (0). */\n readonly reverb: number;\n}\n\n/** A part's options checked, and its defaults filled in. */\nfunction heard(options: PartOptions, what: string): { pan: number; volume: number; reverb: number; fx?: (clip: Clip) => Clip } {\n const pan = options.pan ?? 0, volume = options.volume ?? 1, reverb = options.reverb ?? 0;\n if (!(pan >= -1 && pan <= 1)) throw new Error(`A ${what}'s pan is −1 (left) to 1 (right), not ${pan}.`);\n if (!(volume >= 0)) throw new Error(`A ${what}'s volume is 0 or more, not ${volume}.`);\n if (!(reverb >= 0 && reverb <= 1)) throw new Error(`A ${what}'s reverb is 0 to 1, not ${reverb}.`);\n return { pan, volume, reverb, ...(options.fx ? { fx: options.fx } : {}) };\n}\n\n/**\n * A pattern for an instrument: steps a word each — a note or notes joined by +, \"-\" to hold, \".\" to rest, \"|\" for the\n * eye; \"@\" after a step's notes says how loud, \"C4@0.6\". `pan`: where it is heard, −1 left to 1 right (a panned track's\n * notes have two channels); `volume`: 1 as the instrument plays; `fx`: what the track goes through —\n * `(c) => c.delay(0.21).chorus()` — its whole part when the song is rendered (as a mixer's insert), each note on its own\n * when it is played note by note; `reverb`: how much goes to the song's reverb when it is rendered, 0 … 1.\n */\nexport function track(instrument: Instrument | Kit, pattern: Pattern, options: PartOptions = {}): Track {\n return Object.freeze({ instrument, steps: Object.freeze(wordsOf(pattern)), ...heard(options, \"track\") });\n}\n\n/** An instrument and how it is heard, which a song's sections give its notes: `arrange`. */\nexport interface Part extends Omit { readonly kind: \"part\" }\n\n/** An instrument as a part of a song made of sections (`arrange`), heard as `track` says. */\nexport function part(instrument: Instrument | Kit, options: PartOptions = {}): Part {\n return Object.freeze({ kind: \"part\" as const, instrument, ...heard(options, \"part\") });\n}\n\n/** How long a section lasts, as its effects need it for their points: seconds, its bars, and seconds a bar. */\nexport interface SectionTime { readonly seconds: number; readonly bars: number; readonly bar: number }\n\n/** What a section is and does, besides its notes. */\nexport interface SectionOptions {\n /** What `song.sections` calls it. */\n readonly name?: string;\n /**\n * What a part goes through in this section only, given how long the section is — a filter opening over a build:\n * `{ pad: (c, t) => c.filter(\"lowpass\", [[0, 400], [t.seconds, 8000]]) }`. Its notes from this section, tails and\n * all, before the part's own `fx`. Mixed by `render` only: a song with one is not played note by note.\n */\n readonly fx?: Readonly Clip>>;\n /** How loud a part is in this section only, 1 as it is. */\n readonly volume?: Readonly>;\n}\n\n/** Bars of a song: what each part plays in them. */\nexport interface Section {\n readonly kind: \"section\";\n readonly name: string;\n readonly bars: number;\n readonly parts: Readonly>;\n readonly fx: Readonly Clip>>;\n readonly volume: Readonly>;\n /** This section with some parts written again — null: that part rests — and these options over its own. */\n with(parts: Readonly>, options?: SectionOptions): Section;\n}\n\n/**\n * `bars` bars of a song (`arrange`), and what each part plays in them, by the part's name: `{ lead: tune, drums: beat }`.\n * A pattern shorter than the section plays again until it is filled, so a bar of drums fills eight; a part not named\n * rests. `options`: its name, a part's effects or volume for this section only.\n */\nexport function section(bars: number, parts: Readonly>, options: SectionOptions = {}): Section {\n if (!Number.isInteger(bars) || bars < 1) throw new Error(`A section is a whole number of bars, 1 or more — not ${bars}.`);\n for (const [k, v] of Object.entries(options.volume ?? {})) if (!(v >= 0)) throw new Error(`A section's volume for ${k} is 0 or more, not ${v}.`);\n const made: Section = Object.freeze({\n kind: \"section\" as const, name: options.name ?? \"\", bars,\n parts: Object.freeze({ ...parts }), fx: Object.freeze({ ...(options.fx ?? {}) }), volume: Object.freeze({ ...(options.volume ?? {}) }),\n with(changes: Readonly>, more: SectionOptions = {}): Section {\n const next: Record = { ...parts };\n for (const [k, p] of Object.entries(changes)) { if (p === null) delete next[k]; else next[k] = p; }\n return section(bars, next, { name: more.name ?? made.name, fx: { ...made.fx, ...(more.fx ?? {}) }, volume: { ...made.volume, ...(more.volume ?? {}) } });\n },\n });\n return made;\n}\n\n/** A song's tempo and grid. */\nexport interface SongOptions {\n readonly bpm: number;\n /** Steps a beat (4: sixteenths). */\n readonly steps?: number;\n /** Beats a bar (4). */\n readonly beats?: number;\n /** How late every second step starts, as a part of a step: 0 straight (as it is when left out), 1/3 a triplet's lilt. */\n readonly swing?: number;\n}\n\n/** Where a song's section is: from bar `bar` (0 the first) for `bars`, from frame `frame`. */\nexport interface SongSection { readonly name: string; readonly bar: number; readonly bars: number; readonly frame: number }\n\n/** Steps of a track that belong to one section, and what that section does to them. */\ninterface Piece { readonly from: number; readonly to: number; readonly volume: number; readonly fx?: (clip: Clip, time: SectionTime) => Clip; readonly time: SectionTime }\nconst piecesOf = new WeakMap();\n/** A song of sections' parts, by name, and the track each became: what `stems` cuts. */\nconst partsOf = new WeakMap();\n\n/**\n * A song made of sections, one after another: `parts` names each instrument and how it is heard; each section says\n * what the parts play in it. A section can be written once and played again — `arrange(o, parts, intro, verse, verse,\n * chorus)` — or played again changed: `verse.with({ lead: transpose(tune, 12) })`.\n */\nexport function arrange(options: SongOptions, parts: Readonly>, ...sections: readonly Section[]): Song {\n const { perBar, barFrames } = grid(options);\n if (sections.length === 0) throw new Error(\"arrange() takes the song's sections, one at least.\");\n const names = Object.keys(parts);\n for (const s of sections) for (const k of [...Object.keys(s.parts), ...Object.keys(s.fx), ...Object.keys(s.volume)]) {\n if (!(k in parts)) throw new Error(`A section${s.name ? ` (${s.name})` : \"\"} names ${JSON.stringify(k)}, which is no part of the song: its parts are ${names.join(\", \")}.`);\n }\n const tracks = names.map((k) => {\n const steps: string[] = [], pieces: Piece[] = [];\n for (const s of sections) {\n const length = s.bars * perBar, from = steps.length, p = s.parts[k];\n if (p === undefined) for (let i = 0; i < length; i++) steps.push(\".\");\n else {\n const words = wordsOf(p);\n if (words.length === 0 || length % words.length !== 0) throw new Error(`${k}${s.name ? ` in ${s.name}` : \"\"}: a pattern of ${words.length} steps does not fill ${s.bars} bar${s.bars === 1 ? \"\" : \"s\"} of ${perBar} steps evenly.`);\n for (let i = 0; i < length; i++) steps.push(words[i % words.length]);\n }\n const fx = k in s.fx ? s.fx[k] : undefined, volume = s.volume[k] ?? 1;\n if (fx || volume !== 1) pieces.push({ from, to: from + length, volume, ...(fx ? { fx } : {}), time: { seconds: s.bars * barFrames * FRAME, bars: s.bars, bar: barFrames * FRAME } });\n }\n const t = track(parts[k].instrument, steps, parts[k]);\n if (pieces.length) piecesOf.set(t, pieces);\n return t;\n });\n let bar = 0;\n const placedSections = sections.map((s) => { const at = { name: s.name, bar, bars: s.bars, frame: bar * barFrames }; bar += s.bars; return Object.freeze(at); });\n const made = compose(options, tracks, placedSections);\n partsOf.set(made, names.map((k, i) => ({ name: k, track: tracks[i] })));\n return made;\n}\n\n/** A clip as a track plays it note by note: through its fx, at its volume and its pan, made once a clip a track. */\nfunction placed(t: Track, made: Map, c: Clip): Clip {\n if (t.pan === 0 && t.volume === 1 && !t.fx) return c;\n let p = made.get(c);\n if (!p) {\n p = t.fx ? t.fx(c) : c;\n if (t.volume !== 1) p = p.gain(t.volume);\n if (t.pan !== 0) p = p.pan(t.pan);\n made.set(c, p);\n }\n return p;\n}\n\n/** What a song's notes are, kept for render, which makes them again at its own rate; `piece`, the section whose effects it goes through. */\ninterface Score { readonly at: number; readonly track: Track; readonly made: Map; readonly velocity: number; readonly piece?: Piece; readonly name?: string; readonly seconds?: number; readonly hit?: Clip }\nconst scores = new WeakMap();\n\n/** A song, when the script is built: the tables `play` reads, and what it takes. */\nexport interface Song {\n /**\n * Frames a step lasts: a bar is a whole number of frames, a step need not be (2.5 for sixteenths at 143 beats a\n * minute). Played note by note, a note starts on the frame nearest its time — within the 21 ms the game's own\n * mixer starts a sound in anyway; rendered, exactly on it.\n */\n readonly stepFrames: number;\n /** Frames a bar lasts, a whole number. */\n readonly barFrames: number;\n /** Beats a bar. */\n readonly beats: number;\n /** The tempo it plays at: the nearest to the one asked for whose bar is a whole number of frames. */\n readonly bpm: number;\n /** Steps, the longest track's. */\n readonly steps: number;\n /** Its sections, made by `arrange`, in the order they play (none for a song of tracks). */\n readonly sections: readonly SongSection[];\n /** Frames it lasts, its last note's release included. */\n readonly frames: number;\n readonly seconds: number;\n /** Each clip the song plays, once. */\n readonly clips: readonly Clip[];\n /** Each note in the order they start: the frame it starts on, from 0, and which of `clips` it is. */\n readonly at: readonly number[];\n readonly which: readonly number[];\n /** The most notes sounding at once: `play` refuses a song of more than 32 (`render` has no limit). */\n readonly voices: number;\n}\n\n/** A song's grid: steps a bar, and frames a bar and a step. */\nfunction grid(options: SongOptions): { perBar: number; barFrames: number; stepFrames: number } {\n const perBeat = options.steps ?? 4, beats = options.beats ?? 4, swing = options.swing ?? 0;\n if (!(options.bpm > 0) || !(perBeat > 0) || !(beats > 0)) throw new Error(\"A song takes a tempo, { bpm: 120 }, steps a beat (4 by default) and beats a bar (4).\");\n if (!(swing >= 0 && swing < 1)) throw new Error(`A song's swing is 0 (straight) to under 1 (a step late), not ${swing}.`);\n const barFrames = Math.max(1, Math.round((60 / options.bpm) * beats / FRAME));\n return { perBar: perBeat * beats, barFrames, stepFrames: barFrames / (beats * perBeat) };\n}\n\n/**\n * Tracks played together from their first step: `bpm` beats a minute, `steps` steps a beat (4 by default), `swing`\n * every second step late. Played note by note (`play`), a song is refused when more than 32 notes would sound at once;\n * rendered, it has no limit. A song of sections: `arrange`.\n */\nexport function song(options: SongOptions, ...tracks: readonly Track[]): Song {\n return compose(options, tracks, []);\n}\n\nfunction compose(options: SongOptions, tracks: readonly Track[], sections: readonly SongSection[]): Song {\n const { barFrames, stepFrames } = grid(options), beats = options.beats ?? 4, swing = options.swing ?? 0;\n const stepSeconds = stepFrames * FRAME;\n // Where step s starts, in frames: every second step `swing` of a step late.\n const time = (s: number) => (s + (s % 2 === 1 ? swing : 0)) * stepFrames;\n const score: Score[] = [];\n let steps = 0, effects = false;\n for (const t of tracks) {\n const ws = t.steps, made = new Map(), pieces = piecesOf.get(t) ?? [];\n if (pieces.some((p) => p.fx)) effects = true;\n steps = Math.max(steps, ws.length);\n let k = 0;\n ws.forEach((w, s) => {\n while (k < pieces.length && pieces[k].to <= s) k++;\n const piece = k < pieces.length && pieces[k].from <= s ? pieces[k] : undefined;\n const { body, velocity: v } = velocityOf(w, s);\n if (body === \"-\" || body === \".\") return;\n const velocity = v * (piece?.volume ?? 1), into = piece ? { piece } : {};\n if (t.instrument.kind === \"kit\") {\n for (const letter of body) {\n const hit = t.instrument.hits[letter];\n if (!hit) throw new Error(`Step ${s + 1}: the kit has no ${JSON.stringify(letter)} (it has ${Object.keys(t.instrument.hits).join(\", \")}).`);\n score.push({ at: time(s), track: t, made, velocity, ...into, hit });\n }\n return;\n }\n // A note lasts its step and every \"-\" after it.\n let length = 1;\n while (s + length < ws.length && ws[s + length] === \"-\") length++;\n const seconds = swing === 0 ? length * stepSeconds : (time(s + length) - time(s)) * FRAME;\n for (const name of body.split(\"+\")) {\n noteNumber(name);\n score.push({ at: time(s), track: t, made, velocity, ...into, name, seconds });\n }\n });\n }\n score.sort((a, b) => a.at - b.at);\n const base = (n: Score) => n.hit ?? (n.track.instrument as Instrument).note(n.name!, n.seconds!);\n // The tables `play` reads, made when they are first read: a song only rendered never makes its notes for the game.\n let table: { clips: readonly Clip[]; at: readonly number[]; which: readonly number[]; voices: number; peakStep: number; frames: number } | null = null;\n const tables = () => {\n if (table) return table;\n const notes = score.map((n) => ({ at: Math.round(n.at), clip: placed(n.track, n.made, n.velocity === 1 ? base(n) : base(n).gain(n.velocity)) }));\n // How many sound at once: at each note's start, those started before it and not yet over.\n let voices = 0, peakStep = 0;\n notes.forEach((n, k) => {\n let sounding = 0;\n for (let j = 0; j <= k; j++) if (notes[j].at + notes[j].clip.seconds / FRAME > n.at) sounding++;\n if (sounding > voices) { voices = sounding; peakStep = Math.round(n.at / stepFrames) + 1; }\n });\n const clips: Clip[] = [];\n const which = notes.map((n) => { let k = clips.indexOf(n.clip); if (k < 0) { clips.push(n.clip); k = clips.length - 1; } return k; });\n const frames = Math.max(Math.round(steps * stepFrames), ...notes.map((n) => n.at + Math.ceil(n.clip.seconds / FRAME)));\n table = { clips: Object.freeze(clips), at: Object.freeze(notes.map((n) => n.at)), which: Object.freeze(which), voices, peakStep, frames };\n return table;\n };\n // What the game plays note by note: refused, when it is read, if more than 32 would sound at once (render has no limit).\n const playable = () => {\n if (effects) throw new Error(\"A section of this song puts a part through effects of its own, which only music.render mixes: render the song, or give the part those effects for the whole song.\");\n const t = tables();\n if (t.voices > VOICES) throw new Error(`At step ${t.peakStep}, ${t.voices} notes would sound at once; the game plays ${VOICES} note by note. Shorten notes or thin the chords — or music.render the song, one sound however many it mixes.`);\n return t;\n };\n const made: Song = Object.freeze({\n stepFrames, barFrames, beats, bpm: (60 * beats) / (barFrames * FRAME), steps, sections: Object.freeze([...sections]),\n get frames() { return tables().frames; },\n get seconds() { return tables().frames * FRAME; },\n get clips() { return playable().clips; },\n get at() { return playable().at; },\n get which() { return playable().which; },\n get voices() { return tables().voices; },\n });\n scores.set(made, score);\n return made;\n}\n\n/** The General MIDI drums, by number, as `drums()` has them: kicks k, snares s, the clap c, hats h and o, toms t, the rim r, crashes x. */\nconst GM_DRUMS: Readonly> = {\n 35: \"k\", 36: \"k\", 37: \"r\", 38: \"s\", 40: \"s\", 39: \"c\", 42: \"h\", 44: \"h\", 51: \"h\", 53: \"h\", 59: \"h\", 46: \"o\",\n 41: \"t\", 43: \"t\", 45: \"t\", 47: \"t\", 48: \"t\", 50: \"t\", 49: \"x\", 52: \"x\", 55: \"x\", 57: \"x\",\n};\n\n/** How imported notes are made a song: its grid, and which kit letter a drum's number plays. */\nexport interface MidiOptions extends Omit {\n /** Beats a minute: the file's own when left out. */\n readonly bpm?: number;\n /** A drum's MIDI number to a kit's letter, over General MIDI's (36 \"k\", 38 \"s\", 42 \"h\", 46 \"o\", 49 \"x\", …). */\n readonly drums?: Readonly>;\n}\n\n/**\n * A song of imported notes (`import tune from \"./tune.notes\"`): each track `parts` names — by its name, or its place\n * in `tune.tracks` from 0 — played by an instrument, a kit (the drums by their General MIDI numbers) or a `part`;\n * the tracks not named are left out. Each note starts on the step nearest its time and lasts the steps nearest its\n * length (`steps` a beat, 4: sixteenths); how hard it was struck is how loud it plays, 100 as written. A track whose\n * notes overlap becomes several, so the song can hold them. At the file's first tempo unless `bpm` says.\n */\nexport function midi(notes: Notes, parts: Readonly>, options: MidiOptions = {}): Song {\n const perBeat = options.steps ?? 4, map = { ...GM_DRUMS, ...(options.drums ?? {}) };\n const names = notes.tracks.map((t) => t.name);\n const tracks: Track[] = [];\n for (const [key, given] of Object.entries(parts)) {\n const index = names.indexOf(key) >= 0 ? names.indexOf(key) : /^\\d+$/.test(key) ? Number(key) : -1;\n const source = notes.tracks[index];\n if (!source) throw new Error(`The notes have no track ${JSON.stringify(key)}: their tracks are ${names.map((n) => JSON.stringify(n)).join(\", \")} (or 0 to ${names.length - 1}).`);\n const instrument = given.kind === \"part\" ? given.instrument : given, how = given.kind === \"part\" ? given : {};\n const kit = instrument.kind === \"kit\" ? instrument : null;\n // Voices: steps that each hold one chord at a time; a note joins a chord struck with it, or the first voice free.\n type Word = { sounds: string[]; velocity: number; length: number };\n const voices: { words: Map; end: number; last: number }[] = [];\n const placed = source.notes.map((n) => {\n const from = Math.round(n.at * perBeat);\n return { from, length: kit ? 1 : Math.max(1, Math.round((n.at + n.beats) * perBeat) - from), sound: kit ? map[n.pitch] : nameOf(n.pitch), velocity: Math.max(0.05, Math.round((n.velocity / 100) * 20) / 20) };\n }).filter((n) => n.sound !== undefined && (!kit || n.sound in kit.hits)).sort((a, b) => a.from - b.from || b.length - a.length);\n for (const n of placed) {\n let voice = voices.find((v) => { const w = v.words.get(v.last); return v.last === n.from && w !== undefined && w.length === n.length && w.velocity === n.velocity; });\n if (voice) { const w = voice.words.get(n.from)!; if (!w.sounds.includes(n.sound)) w.sounds.push(n.sound); continue; }\n voice = voices.find((v) => v.end <= n.from);\n if (!voice) { voice = { words: new Map(), end: 0, last: -1 }; voices.push(voice); }\n voice.words.set(n.from, { sounds: [n.sound], velocity: n.velocity, length: n.length });\n voice.end = n.from + n.length;\n voice.last = n.from;\n }\n for (const v of voices) {\n const steps: string[] = Array.from({ length: v.end }, () => \".\");\n for (const [at, w] of v.words) {\n steps[at] = (kit ? w.sounds.join(\"\") : w.sounds.join(\"+\")) + (w.velocity === 1 ? \"\" : `@${w.velocity}`);\n if (!kit) for (let k = 1; k < w.length; k++) steps[at + k] = \"-\";\n }\n tracks.push(track(instrument, steps, how));\n }\n }\n return compose({ ...options, bpm: options.bpm ?? notes.bpm, beats: options.beats ?? notes.beats }, tracks, []);\n}\n\n/** The song's reverb in `render`, as `Clip.reverb` takes it, the dry part left out. */\nexport interface ReverbOptions { readonly decay?: number; readonly damping?: number; readonly predelay?: number; readonly width?: number }\n\n/**\n * A song mixed into one clip when the script is built: every note exactly in time, one sound playing. Mixed as a\n * mixing desk does: each track's notes made again at `rate` (44 100 when left out: what the ear hears up to 20 kHz; a\n * note played in the game note by note is made at 22 050, to keep the map small) into its own part, through a\n * section's effects where it has them, then its `fx`, at its volume, to its side; what each track sends (its `reverb`)\n * into one reverb shared by them all (`reverb`, its room); the whole through `master` —\n * `(mix) => mix.compress({ threshold: -14, ratio: 2 })` — and last a limiter (`Clip.limit`) so it never goes over\n * −1.5 dB however the parts add, between the samples too: `limit: false` leaves it as mixed, `{ ceiling, release }` sets it. A long clip a\n * program plays goes in the map as Ogg Vorbis, two channels when it has two.\n */\nexport function render(s: Song, options: { readonly rate?: number; readonly limit?: boolean | { readonly ceiling?: number; readonly release?: number }; readonly reverb?: ReverbOptions; readonly master?: (mix: Clip) => Clip } = {}): Clip {\n const rate = options.rate ?? 44100, score = scores.get(s), least = s.steps * s.stepFrames * FRAME;\n let mixed: Clip;\n if (!score) {\n // (A song from elsewhere, without its score: its own clips, at their rate.)\n mixed = mixdown(s.at.map((at, k) => ({ clip: s.clips[s.which[k]], at: at * FRAME })), s.seconds, rate);\n } else {\n const parts: { clip: Clip; at: number }[] = [], sends: { clip: Clip; at: number }[] = [];\n const byTrack = new Map();\n for (const n of score) { const list = byTrack.get(n.track); if (list) list.push(n); else byTrack.set(n.track, [n]); }\n for (const [t, notes] of byTrack) {\n const clipOf = (n: Score) => { const c = n.hit ?? (t.instrument as Instrument).note(n.name!, n.seconds!, rate); return n.velocity === 1 ? c : c.gain(n.velocity); };\n // The notes as they are, and each section's that has effects of its own mixed apart and through them.\n const entries: { clip: Clip; at: number }[] = [], apart = new Map();\n for (const n of notes) {\n if (n.piece?.fx) { const list = apart.get(n.piece); if (list) list.push(n); else apart.set(n.piece, [n]); }\n else entries.push({ clip: clipOf(n), at: n.at * FRAME });\n }\n for (const [p, list] of apart) {\n const from = p.from * s.stepFrames;\n const sub = mixdown(list.map((n) => ({ clip: clipOf(n), at: (n.at - from) * FRAME })), p.time.seconds, rate);\n entries.push({ clip: p.fx!(sub, p.time), at: from * FRAME });\n }\n let part = mixdown(entries, least, rate);\n if (t.fx) part = t.fx(part);\n if (t.volume !== 1) part = part.gain(t.volume);\n if (t.pan !== 0) part = part.pan(t.pan);\n parts.push({ clip: part, at: 0 });\n if (t.reverb > 0) sends.push({ clip: part.gain(t.reverb), at: 0 });\n }\n if (sends.length) {\n const r = options.reverb ?? {};\n parts.push({ clip: mixdown(sends, least, rate).reverb({ decay: r.decay ?? 0.7, damping: r.damping ?? 0.45, predelay: r.predelay ?? 0.02, width: r.width ?? 1, mix: 1, dry: 0 }), at: 0 });\n }\n mixed = mixdown(parts, least, rate);\n }\n if (options.master) mixed = options.master(mixed);\n const limit = options.limit ?? true;\n return limit === false ? mixed : mixed.limit(limit === true ? {} : limit);\n}\n\n/* ── Music that follows the game: a song cut into pieces, played piece by piece ── */\n\n/** How a song of sections is cut for the game to play piece by piece (`stems`). */\nexport interface StemsOptions {\n /**\n * Which parts each layer holds, by name — `{ drums: [\"kit\", \"hats\"], bass: [\"bass\"], melody: [\"lead\", \"saw\"] }` — a\n * layer the game can turn on and off where a piece begins. A part in no layer is left out.\n */\n readonly layers: Readonly>;\n /**\n * Beats a piece: where the game can go to another section or turn a layer on or off (a bar's beats when left out).\n * Two in a bar of four halves the wait for a change, for about half as much map again and more sounds at once.\n */\n readonly every?: number;\n /** Samples a second (44 100). */\n readonly rate?: number;\n /** The reverb's room, as `render` takes it. */\n readonly reverb?: ReverbOptions;\n /** What each piece's clip goes through last — an EQ, not a compressor: each is on its own, the game adds them up. */\n readonly master?: (clip: Clip) => Clip;\n /** The loudest the pieces may add up to as the song plays through, every layer on (0.84, −1.5 dB). */\n readonly peak?: number;\n /**\n * Seconds a piece's clip rings on after its notes end (2), its echoes and reverb faded out over the last of them: the\n * pieces after it cover the rest, and a shorter clip is a smaller map. A layer's last piece keeps all of its tail.\n */\n readonly tail?: number;\n /** The Ogg Vorbis quality of the pieces' clips (4: a little smaller than a song's 5, as many play at once). */\n readonly quality?: number;\n /**\n * Sounds to play on the next beat when the game asks (`band.sting`): a crash for a cleared line, a hit for a boss.\n * Turned down with the pieces, which leave room for one on any beat.\n */\n readonly stingers?: Readonly>;\n}\n\n/** A song cut into pieces for the game to play piece by piece, with `conduct` and a `Band`: made when the script is built. */\nexport interface Stems {\n readonly kind: \"stems\";\n /** Frames a bar lasts. */\n readonly barFrames: number;\n readonly bpm: number;\n /** Beats a bar, and beats a piece. */\n readonly beats: number;\n readonly every: number;\n /** Frames each beat of a piece lasts, whole frames (beats of a bar not a whole number of frames take turns). */\n readonly waits: readonly number[];\n /** The layers' names, in the order `band.on` has them. */\n readonly layers: readonly string[];\n /** The sections, in the order the song plays them: their names and how many bars. */\n readonly sections: readonly SongSection[];\n /**\n * Section `s`'s piece `p` in layer `l`: its notes, their tails and echoes and reverb with them, or null where it plays\n * none. A section of `bars` bars has bars × beats / every pieces.\n */\n readonly pieces: readonly (readonly (readonly (Clip | null)[])[])[];\n readonly stingers: readonly Clip[];\n /** A section's place in `sections`, by its name: `band.go(stems.section(\"drop\"))`. */\n section(name: string): number;\n /** A layer's place in `layers`, by its name: `band.mute(stems.layer(\"drums\"))`. */\n layer(name: string): number;\n /** A stinger's place, by its name: `band.sting(stems.stinger(\"crash\"))`. */\n stinger(name: string): number;\n /** The most clips sounding at once as the song plays through, every layer on: of the game's 32, what is left for the rest. */\n readonly voices: number;\n /** The pieces added up as the game plays the song through, every layer on, each on its frame: to hear it, or compare. */\n readonly playthrough: Clip;\n}\n\n/**\n * A song of sections (`arrange`) cut into pieces — a bar each, or `every` beats — a clip each piece of each layer, for\n * music that follows the game: where each piece begins the program says which section plays and which layers (`Band`,\n * `conduct`), and a stinger plays on the next beat. A piece's clip holds the notes that start in it, with their tails,\n * echoes and reverb, so it overlaps the next and nothing is cut off, whichever comes after it; a piece the same as\n * another is the same clip, one file in the map. Each layer is mixed as `render` mixes the song, but with no master\n * and no limiter between the parts — the game adds the clips up — so all of them are turned down together until the\n * song played through stays under `peak`.\n */\nexport function stems(s: Song, options: StemsOptions): Stems {\n const parts = partsOf.get(s), score = scores.get(s);\n if (!parts || !score) throw new Error(\"stems() cuts a song made of sections, by music.arrange.\");\n const rate = options.rate ?? 44100, layers = Object.keys(options.layers), r = options.reverb ?? {}, tail = options.tail ?? 2;\n if (!(tail > 0)) throw new Error(`stems()' tail is seconds after a piece, more than 0, not ${tail}.`);\n if (layers.length === 0 || layers.length > 8) throw new Error(`stems() takes one to eight layers, not ${layers.length}.`);\n const beats = s.beats, every = options.every ?? beats, piece = (s.barFrames * every) / beats;\n if (!Number.isInteger(every) || every < 1 || every > beats || beats % every !== 0) throw new Error(`stems() cuts every few beats of a bar's ${beats}, a number that divides it — not every ${every}.`);\n if (!Number.isInteger(piece)) throw new Error(`A piece of ${every} beats would be ${piece.toFixed(2)} frames: the game plays a sound on a frame. Make it a bar (every: ${beats}), or a tempo whose bar divides into whole frames.`);\n const perBar = beats / every;\n const layerOf = new Map();\n layers.forEach((name, l) => {\n for (const p of options.layers[name]) {\n const found = parts.find((x) => x.name === p);\n if (!found) throw new Error(`The layer ${name} names ${JSON.stringify(p)}, which is no part of the song: its parts are ${parts.map((x) => x.name).join(\", \")}.`);\n if (layerOf.has(found.track)) throw new Error(`${p} is in two layers: a part is in one at most.`);\n layerOf.set(found.track, l);\n }\n });\n const pieceSeconds = piece * FRAME, total = s.sections.reduce((n, x) => n + x.bars, 0) * perBar;\n // The notes of each piece of each layer, by part.\n const cells: Map[][] = Array.from({ length: total }, () => layers.map(() => new Map()));\n for (const n of score) {\n const l = layerOf.get(n.track);\n if (l === undefined) continue;\n const g = Math.min(total - 1, Math.floor((n.at + 1e-6) / piece));\n const m = cells[g][l], list = m.get(n.track);\n if (list) list.push(n); else m.set(n.track, [n]);\n }\n const clipOf = (t: Track, n: Score) => { const c = n.hit ?? (t.instrument as Instrument).note(n.name!, n.seconds!, rate); return n.velocity === 1 ? c : c.gain(n.velocity); };\n // Each layer's last piece with notes: it rings out whole, as the song ends.\n const last = layers.map((_, l) => { let g = total - 1; while (g > 0 && cells[g][l].size === 0) g--; return g; });\n // A piece of a layer, mixed: each part's notes from where it begins, through what the part goes through, and the reverb.\n const raw: (Clip | null)[][] = cells.map((byLayer, g) => byLayer.map((m, l) => {\n const out: { clip: Clip; at: number }[] = [], sends: { clip: Clip; at: number }[] = [];\n // Where the piece's last note ends, its release and all: a riser four bars long is kept whole.\n let end = pieceSeconds;\n for (const [t, notes] of m) for (const n of notes) end = Math.max(end, (n.at - g * piece) * FRAME + clipOf(t, n).seconds);\n for (const [t, notes] of m) {\n const plain = notes.filter((n) => !n.piece?.fx), apart = notes.filter((n) => n.piece?.fx);\n const entries: { clip: Clip; at: number }[] = plain.map((n) => ({ clip: clipOf(t, n), at: (n.at - g * piece) * FRAME }));\n if (apart.length) {\n // A section's effects take the whole section: this piece's notes where they fall in it, then the piece cut out.\n const p = apart[0].piece!, from = p.from * s.stepFrames, into = (g * piece - from) * FRAME;\n const whole = p.fx!(mixdown(apart.map((n) => ({ clip: clipOf(t, n), at: (n.at - from) * FRAME })), undefined, rate), p.time);\n entries.push({ clip: whole.trim(into, whole.seconds), at: 0 });\n }\n let part = mixdown(entries, undefined, rate);\n if (t.fx) part = t.fx(part);\n if (t.volume !== 1) part = part.gain(t.volume);\n if (t.pan !== 0) part = part.pan(t.pan);\n out.push({ clip: part, at: 0 });\n if (t.reverb > 0) sends.push({ clip: part.gain(t.reverb), at: 0 });\n }\n if (out.length === 0) return null;\n if (sends.length) out.push({ clip: mixdown(sends, undefined, rate).reverb({ decay: r.decay ?? 0.7, damping: r.damping ?? 0.45, predelay: r.predelay ?? 0.02, width: r.width ?? 1, mix: 1, dry: 0 }), at: 0 });\n let mixed = mixdown(out, undefined, rate);\n if (options.master) mixed = options.master(mixed);\n // What rings on after the piece's notes, kept for `tail` seconds and faded over the last of them — the pieces\n // after cover the rest — but all of it after the layer's last piece.\n const keep = end + tail;\n return g < last[l] && mixed.seconds > keep ? mixed.trim(0, keep).fade(0, Math.min(1, tail / 2)) : mixed;\n }));\n // All of them turned down together, as the song plays through.\n const placed: { clip: Clip; at: number; g: number; l: number }[] = [];\n raw.forEach((byLayer, g) => byLayer.forEach((c, l) => { if (c) placed.push({ clip: c, at: g * pieceSeconds, g, l }); }));\n if (placed.length === 0) throw new Error(\"stems(): the layers play no notes.\");\n // The stingers may play on any beat: turned down with the pieces, and room left for them over the loudest.\n const waits = Array.from({ length: every }, (_, j) => Math.round(((j + 1) * piece) / every) - Math.round((j * piece) / every));\n const stingerNames = Object.keys(options.stingers ?? {});\n const lines: number[] = [];\n for (let g = 0; g < total; g++) { let at = g * piece; for (const w of waits) { lines.push(at * FRAME); at += w; } }\n const fitted = together(placed, { peak: options.peak ?? 0.84, over: stingerNames.map((k) => ({ clip: options.stingers![k], at: lines })) });\n const grid: (Clip | null)[][] = raw.map((byLayer) => byLayer.map(() => null));\n const quality = options.quality ?? 4;\n placed.forEach((p, k) => { grid[p.g][p.l] = fitted[k].ogg(quality); });\n const stingers = stingerNames.map((_, k) => fitted[placed.length + k]);\n let start = 0;\n const pieces = s.sections.map((sec) => { const n = sec.bars * perBar, rows = grid.slice(start, start + n).map((row) => Object.freeze(row)); start += n; return Object.freeze(rows); });\n const lookup = (what: string, list: readonly string[], name: string) => {\n const k = list.indexOf(name);\n if (k < 0) throw new Error(`There is no ${what} ${JSON.stringify(name)}: there are ${list.map((x) => JSON.stringify(x)).join(\", \") || \"none\"}.`);\n return k;\n };\n let voices = -1;\n return Object.freeze({\n kind: \"stems\" as const, barFrames: s.barFrames, bpm: s.bpm, beats, every, waits: Object.freeze(waits),\n layers: Object.freeze(layers), sections: s.sections, pieces: Object.freeze(pieces), stingers: Object.freeze(stingers),\n section: (name: string) => lookup(\"section\", s.sections.map((x) => x.name), name),\n layer: (name: string) => lookup(\"layer\", layers, name),\n stinger: (name: string) => lookup(\"stinger\", stingerNames, name),\n get playthrough() {\n return mixdown(placed.map((p) => ({ clip: grid[p.g][p.l]!, at: p.at })));\n },\n get voices() {\n if (voices < 0) {\n voices = 0;\n for (const p of placed) { let n = 0; for (const q of placed) if (q.at <= p.at && q.at + q.clip.seconds > p.at) n++; voices = Math.max(voices, n); }\n }\n return voices;\n },\n });\n}\n\n/**\n * The music as the game steers it: which section plays, which piece of it, which layers. Make one inside the program\n * that steers the music and hand it to `conduct`, which plays a piece where each begins as it says:\n *\n * import { music, Band } from \"trigscript/music\";\n * const band = new Band();\n * start(music.conduct(stems, band));\n * band.go(stems.section(\"drop\")); // where the next piece begins\n * band.mute(stems.layer(\"drums\"));\n * band.sting(stems.stinger(\"crash\")); // on the next beat\n *\n * With nothing said, the song plays through its sections in order and starts again.\n */\nexport class Band {\n /** The section playing, its place in `stems.sections`; the piece of it (a bar, unless `stems.every` says), from 0; the beat of the piece. */\n section = 0;\n piece = 0;\n beat = 0;\n /** Pieces played since it started. */\n count = 0;\n /** The game's frame the beat playing began on: how far into the beat it is, for what moves with the music. */\n at = 0;\n /** The section to go to, and whether where the next piece begins (`go`) or when this section ends (`queue`); −1 for none. */\n next = -1;\n now = false;\n /** Whether a section plays again when it ends, instead of going on to the next. */\n loop = false;\n /** Which layers play, by their place in `stems.layers`. */\n on = [true, true, true, true, true, true, true, true];\n /** A stinger to play on the next beat, by its place; −1 for none. */\n stung = -1;\n /** Whether it plays: false stops it where the next piece would begin (what sounds rings out). */\n playing = true;\n\n /** Go to a section where the next piece begins, from its first piece. */\n go(section: number) {\n this.next = section;\n this.now = true;\n }\n /** Go to a section when the one playing ends. */\n queue(section: number) {\n this.next = section;\n this.now = false;\n }\n /** Turn a layer off, or on, from where the next piece begins. */\n mute(layer: number) {\n this.on[layer] = false;\n }\n unmute(layer: number) {\n this.on[layer] = true;\n }\n /** Play a stinger on the next beat. */\n sting(stinger: number) {\n this.stung = stinger;\n }\n /** Stop where the next piece would begin. */\n stop() {\n this.playing = false;\n }\n}\n\n/** Play `stems` as `band` says, beat by beat, until it stops: `start(music.conduct(stems, band))` beside the program. */\nexport const conduct = game(async (s: Stems, band: Band) => {\n band.playing = true;\n band.beat = 0;\n while (band.playing || band.beat > 0) {\n band.at = memory.game.frame;\n if (band.beat === 0) {\n // Where a piece begins: where to be, then the piece's clip of each layer that is on. (Which clip, by test: a\n // clip is no value a program holds.)\n if (band.next >= 0 && (band.now || band.piece === 0)) {\n band.section = band.next;\n band.piece = 0;\n band.next = -1;\n }\n for (let k = 0; k < s.sections.length; k++) if (band.section === k) {\n for (let p = 0; p < s.pieces[k].length; p++) if (band.piece === p) {\n for (let l = 0; l < s.layers.length; l++) if (s.pieces[k][p][l]) if (band.on[l]) playWav(s.pieces[k][p][l]!, 0);\n }\n }\n }\n for (let j = 0; j < s.stingers.length; j++) if (band.stung === j) playWav(s.stingers[j], 0);\n band.stung = -1;\n for (let j = 0; j < s.every; j++) if (band.beat === j) await sleep(frames(s.waits[j]));\n band.beat++;\n if (band.beat >= s.every) {\n band.beat = 0;\n band.count++;\n band.piece++;\n // A section ended: again, or on to the next (after the last, the first).\n for (let k = 0; k < s.sections.length; k++) if (band.section === k) if (band.piece >= s.pieces[k].length) {\n band.piece = 0;\n if (!band.loop) band.section = k + 1 < s.sections.length ? k + 1 : 0;\n }\n if (!band.playing) band.beat = 0;\n }\n }\n});\n\n/**\n * Play a song, note by note, inside an async program: \\`await music.play(song)\\` resolves at its end, or\n * \\`start(music.play(song))\\` plays it beside the program.\n */\nexport const play = game(async (s: Song) => {\n const at = s.at;\n const which = s.which;\n let i = 0;\n let now = 0;\n while (i < at.length) {\n if (at[i] > now) {\n await sleep(frames(at[i] - now));\n now = at[i];\n }\n const k = which[i];\n // Which clip, by its number: a test a clip (a loop over a length known when the script is built is unrolled, and\n // the clips stay the song's, known when it is built — a clip is no value a program holds).\n for (let j = 0; j < s.clips.length; j++) if (k === j) playWav(s.clips[j], 0);\n i++;\n }\n if (s.frames > now) await sleep(frames(s.frames - now));\n});\n\n/** Play a song again and again; start it beside a program: \\`start(music.loop(song))\\`. */\nexport const loop = game(async (s: Song) => {\n // (\\`play\\` written out again, so that the wait at the end of each turn is where the loop shows it — at least a frame,\n // so that a song with nothing left at its end does not turn round within one frame for ever.)\n const at = s.at;\n const which = s.which;\n while (true) {\n let i = 0;\n let now = 0;\n while (i < at.length) {\n if (at[i] > now) {\n await sleep(frames(at[i] - now));\n now = at[i];\n }\n const k = which[i];\n for (let j = 0; j < s.clips.length; j++) if (k === j) playWav(s.clips[j], 0);\n i++;\n }\n await sleep(frames(Math.max(s.frames - now, 1)));\n }\n});\n\n/** Everything above as one name: import { music } from \"trigscript/music\"; music.play(song). */\nexport const music = {\n sampler, synth, fmSynth, strings, presets, drums, kit, track, song, render, play, loop,\n part, section, arrange, transpose, stretch, repeat, beat, chord, over, chords, bars, pump, riser, impact, midi, stems, conduct,\n};\n", diff --git a/compiler/generated/trigscriptPy.ts b/compiler/generated/trigscriptPy.ts index feace8d..e562866 100644 --- a/compiler/generated/trigscriptPy.ts +++ b/compiler/generated/trigscriptPy.ts @@ -1,2 +1,2 @@ // Generated by scripts/embed-python.mts from python/trigscript.py — do not edit. -export const TRIGSCRIPT_PY: string = "\"\"\"\n[trigscript]\nir : /work/files/trigscript.json\n\nTrigScript's programs: the euddraft plugin that lowers the compiler's IR — data, never\ncode — into eudplib. The IR file is what the plugin's compiler wrote (docs/ir.md);\nthis module is handed to the eudplib library plugin as a source with every build, so the\ntwo halves of the IR version are always the same build.\n\nA program is a coroutine the game runs every frame. Its body is lowered to straight-line\neudplib triggers with jumps between labels: `if` / `while` / `switch` become conditional\njumps, and `sleep` stores the label to resume at in a state variable, sets a frame counter,\nand leaves the frame. The frame's entry counts the wait down, then jumps to the stored\nlabel. A program runs for the players it is owned by, as a trigger would: one owner runs\nit as that player while that player is in the game; several owners, All Players or a force\nrun it once each frame for every such player who is in the game, CurrentPlayer set, its\nvariables and its state as 12-slot arrays indexed by the player.\n\nText is written out in the IR: an action's `text` or `wav` is the string itself, which\neudplib adds to the built map's string table, or a number when the script named an index\nof the map's own. The map the user edits never holds a program's strings.\n\nA read is a value of the game taken when the expression is evaluated. What a condition compares\nis read from the game's tables where the table is that value (a player's deaths, kills, ore, gas)\nand otherwise found by asking the condition itself — \"at least 2^31? at least that plus 2^30? …\" —\nso a read means what the condition means, for a force's minerals or the units at a location alike.\nPlayer facts are bytes of the player tables. Text with values in it is printed through eudplib's\nstring buffer, for the player it is for and nobody else.\n\nA unit of the game is a pointer into the game's unit table — 1700 slots of 336 bytes — with the slot's\nuniqueness byte kept beside it: the game gives a dead unit's slot to the next unit made, so a unit kept\nin a variable is checked before every use (a sprite, an order other than \"die\", the same uniqueness\nbyte) and reads 0, and takes no write, once it is gone. A loop over units and a pick walk the whole\ntable with conditions whose address is moved on a slot at a time, as eudplib's own EUDLoopUnit2 does;\na dying unit is passed over. What a unit can be asked and told is what Magenta's probe maps saw\nworking in Remastered: no position, cloak or speed writes. The game's tables (`stats()`) are plain\ncells at addresses the IR carries; a speed is four flingy records, a colour two bytes, a name a string.\n\nWhat the players do — keys, clicks, the mouse, what they type — happens on one computer, and two\nplugins the build adds beside this one bring it to all of them in step. chatEvent (before this\nplugin) finds the line the local player typed; this plugin matches it against the programs'\npatterns, there and then, into a number for the pattern and up to three values; MSQC (after this\nplugin) sends those, the keys and the clicks to every computer as the player they came from, and\nkeeps each player's mouse in a location. They land in arrays registered by name — which is how the\nother two plugins' settings reach them — a cell per player, fresh every frame: an input reads 1 on\nthe frame it arrives. The IR's `input` lists what is asked for; the editor wrote the settings from it.\n\nNumbers keep one contract with the simulator: 32 bits, a `number` signed and a `u32` not, wrapping at\neither end. + - * and the bitwise operators are the same bits whichever way they are read; what reads\nthem one way or the other - a comparison, a division, a shift right, min and max, a printed number -\nsays which in the IR (`unsigned`), so nothing here works a type out. Where the game takes nothing below\nzero the compiler has already written max(v, 0), and a store keeps stopping at the top of what it holds.\nIn here a number known when the map is built is a Python int holding the 32 bits, 0 to 2^32 - 1.\n\nA text is kept one of two ways, a variable at a time (the IR says which). One that only ever holds texts written in\nthe script is the text's id in the built map's string table: a number. One that is made while the map is played is\nthree cells - where its bytes are (UTF-8, ended by a 0), the block of the heap it owns (0: none, the bytes are a\nstring of the table) and its length in characters. A made text is written into one scratch buffer, measured, and\ncopied into a block of just that size, whose first cell says which size; what holds the text owns the block, so\nassigning copies it, a value that was just made is moved, and whatever a variable held goes back to the heap once\nits new value is worked out. compiler/simulateIr.ts takes and gives blocks in the same order, so both run out at\nthe same text. A made text in an action's field goes over a string the build keeps for that kind of field, on the\ncomputer of the player the action is for and nowhere else: the game reads such a string again whenever it draws.\n\"\"\"\nimport base64\nimport json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 26\nFRAMES_PER_SECOND = 24\n# Where the game keeps what a read reads (1.16.1 addresses, which Remastered emulates). The player\n# tables are the ones Magenta's probes 5 and 8 read in the game.\nDEATHS_TABLE = 0x58A364\nKILLS_TABLE = 0x5878A4\nORE_TABLE = 0x57F0F0\nGAS_TABLE = 0x57F120\nPLAYER_BYTES = {\"slot\": 0x57F1B4, \"race\": 0x57F1C0}\n# Per race (144 bytes apart: Zerg, Terran, Protoss), a dword per player, in half supplies.\nSUPPLY_TABLES = {\"provided\": 0x582144, \"used\": 0x582174, \"max\": 0x5821A4}\n# The unit table, and a unit's dwords the scans test (as EPD offsets from the unit's own).\nUNIT_TABLE = 0x59CCA8\nUNIT_SIZE = 336\nUNIT_SLOTS = 1700\nUNIT_END = UNIT_TABLE + UNIT_SIZE * UNIT_SLOTS\nOFF_SPRITE, OFF_POS, OFF_OWNER_ORDER, OFF_TYPE, OFF_UID = 0x0C // 4, 0x28 // 4, 0x4C // 4, 0x64 // 4, 0xA4 // 4\n# The selection a computer keeps: twelve unit addresses; and a Select command, 0x09, the count, a tag a unit (B5).\nSELECTION = 0x6284B8\nSELECT_COMMAND = Db(b\"\\x09\" + bytes(27))\n# units.dat: max hit points (dword, 256 to a point), max shields (word), the group flags a trigger's\n# Men / Buildings / Factories go by (byte), the flingy a type moves as (byte).\nUNITS_MAX_HP = 0x662350\nUNITS_MAX_SHIELDS = 0x660E00\nUNITS_GROUP = 0x6637A0\nUNITS_FLINGY = 0x6644F8\nGROUP_BITS = {230: 0x08, 231: 0x10, 232: 0x20}\n# flingy.dat, by flingy id: movement control (byte), top speed (dword), acceleration (word), halt distance (dword).\nFLINGY_CONTROL, FLINGY_SPEED, FLINGY_ACCELERATION, FLINGY_HALT = 0x6C9858, 0x6C9EF8, 0x6C9C78, 0x6C9930\nMINIMAP_COLOR_OFFSET = 0x581DD6 - 0x581D76\n# The heads of the game's lists of units (1.16.1, which Remastered emulates): the visible units and the dying ones. Each\n# links a record in just behind its head, so behind the head a list runs latest first (scm-sim docs/specs/unit-lists.md).\nVISIBLE_HEAD, DYING_HEAD = 0x628430, 0x62842C\n# How many unit records are in use (a dword): up when a record is taken, down when one is freed.\nRECORDS_IN_USE = 0x6283F0\n# How many units one frame's events pass hands on of each kind; past that the rest are handed on in the next frame.\nEVENT_CAP = 512\n# A unit's hit points (+0x08), and the last player whose attack hurt it (+0x90, a byte; 8 when none has): a unit that died\n# with no hit points and a player there died in a fight, and that player killed it (on.killed).\nOFF_HP, OFF_LAST_ATTACKER = 0x08 // 4, 0x90 // 4\n# The location table, and the one an order borrows for the length of one action (put back after).\nMRGN = 0x58DC60\nSCRATCH_LOCATION = 255\nUNIT_TIMERS = {\"stim\": \"stimTimer\", \"ensnare\": \"ensnareTimer\", \"plague\": \"plagueTimer\", \"lockdown\": \"lockdownTimer\", \"maelstrom\": \"maelstromTimer\", \"irradiate\": \"irradiateTimer\", \"stasis\": \"stasisTimer\"}\nSTATUS_INVINCIBLE, STATUS_HALLUCINATION, STATUS_BURROWED, STATUS_CLOAKED = 0x04000000, 0x40000000, 0x00000010, 0x00000300\nCOND_COMMAND, COND_BRING, COND_ACCUMULATE, COND_KILL, COND_OPPONENTS, COND_DEATHS = 2, 3, 4, 5, 14, 15\nCURRENT_PLAYER = 13\n# What a stack that ran out throws (B4), as JavaScript's does: a RangeError (compiler/ir.ts ERROR_CODES).\nRANGE_ERROR = 2\nSTACK_OVERFLOW_MESSAGE = \"Maximum call stack size exceeded\"\n# The state of a program whose body ended: nothing resumes it.\nDONE = 0xFFFFFFFF\nU32 = 0xFFFFFFFF\nSIGN = 0x80000000\n\nMINUS_SIGN = Db(b\"-\\0\\0\\0\")\nNO_SIGN = Db(b\"\\0\\0\\0\\0\")\n\nwith open(settings[\"ir\"], encoding=\"utf-8\") as _f:\n IR = json.load(_f)\nif IR.get(\"version\") != IR_VERSION:\n raise RuntimeError(\"trigscript: the IR is version %r; this plugin reads version %d\" % (IR.get(\"version\"), IR_VERSION))\n\n\n# ── Static routing (TrigScript 4, the backend's first part) ──\n# eudplib keeps a variable as a trigger of one action, SetDeaths(dest, modifier, value), and reads it by routing:\n# three actions set that action's destination and modifier and the trigger's next pointer, then the walk goes through\n# it. In a played frame of the bullets bench that routing was half the time (scripts/trigger-shapes.mts). Here every\n# copy of a variable this file made (SeqCompute, and everything eudplib does through it: <<, +=, a call's arguments)\n# is lowered as a *site* — whatever came before jumps to a Forward, then a setup trigger with the routing, then the\n# cells, then what follows — and what eudplib itself does to a cell (its vtable, its destination, its mask, its\n# modifier asked for anywhere else) is counted. Once the whole program is lowered (`decide`, at the end of\n# afterTriggerExec), a cell read at exactly one site and touched nowhere else needs no routing at all: its destination,\n# modifier and next pointer are written into the variable buffer as the map is built (they never change), and the site\n# jumps straight into its cells. Any other site keeps its setup. Only variables this file made directly take part:\n# eudplib's own (a function's parameters and results, its pooled temporaries) are shared by many places it cannot see.\n# The `staticRouting` setting \"0\" leaves everything to eudplib, to weigh what this gives.\nimport sys as _sys\nimport eudplib.core.variable.eudv as _eudv\nimport eudplib.core.variable.vbuf as _vbuf\nfrom eudplib.utils import unProxy as _unproxy_r\n\n\nclass _Region:\n \"\"\"Where code is being lowered: a loop of the IR (`loop`), or code that runs apart from what is around it — a\n coroutine (a program's body, a task, a branch of a parallel), a function's body, a scope of triggers apart.\"\"\"\n\n def __init__(self, kind):\n self.kind = kind\n self.waits = False # a loop that sleeps or waits: anything else may run before it goes on\n self.unknown = False # a loop that calls something whose body this cannot see (a function that calls itself)\n self.calls = set() # the functions (EUDFuncN ids) called from here\n self.entry = self.after = None # a loop's way in, which the hoisted routing goes in front of\n self.hoist = []\n self.copies = [] # (variable, its copy for one site of the loop), filled on the way in\n\n\nclass _Routing:\n def __init__(self, enabled):\n self.enabled = enabled\n self.on = False\n self.emitting = 0\n self.reading = 0 # inside a condition's own making: a value read, not written\n self.ours = {} # id -> the variable, for the variables this file made while lowering\n self.touches = {} # id -> where something other than a site asked for one of its routing fields\n self.writes = {} # id -> where its value's address was asked for other than to read it in a condition\n self.sites = {} # id -> the groups it is read in\n self.groups = []\n self.static = {} # a cell's VariableTriggerForward -> (dest, modifier byte, next)\n self.stack = [] # (barrier or loop regions) innermost last\n self.main = _Region(\"main\")\n self.functions = {} # id of an EUDFuncN -> the region of its body\n self.loops = []\n self.names = {} # id -> the name of the script's variable it is, for the report\n self.why_touched = {} # id -> where it was touched, for the report\n self.made_at = {} # id -> where it was made, for the report\n self.stats = {\"groups\": 0, \"bare groups\": 0, \"cells\": 0, \"static\": 0, \"hoisted\": 0, \"replicas\": 0, \"loops\": 0}\n\n def where(self):\n \"\"\"(the region the code runs in apart from the rest, the loops around it in that region, outermost first).\"\"\"\n loops = []\n for r in reversed(self.stack):\n if r.kind != \"loop\":\n return r, tuple(reversed(loops))\n loops.append(r)\n return self.main, tuple(reversed(loops))\n\n def touched(self, v):\n if self.on and not self.emitting:\n k = id(v)\n if k in self.ours:\n self.touches.setdefault(k, []).append(self.where())\n if settings.get(\"routingReport\") and k in self.names:\n f, chain = _sys._getframe(2), []\n while f is not None and len(chain) < 6:\n chain.append(\"%s:%d\" % (f.f_code.co_name, f.f_lineno))\n f = f.f_back\n self.why_touched.setdefault(k, set()).add(\" < \".join(chain))\n\n def wrote(self, v):\n if self.on and not self.emitting and not self.reading:\n k = id(v)\n if k in self.ours:\n self.writes.setdefault(k, []).append(self.where())\n\n def called(self, fn):\n if self.on:\n apart, loops = self.where()\n apart.calls.add(id(fn))\n for r in loops:\n r.calls.add(id(fn))\n\n def waited(self):\n if self.on:\n for r in self.where()[1]:\n r.waits = True\n\n def unknown_call(self):\n if self.on:\n for r in self.where()[1]:\n r.unknown = True\n\n def reached(self, calls):\n \"\"\"The regions of the functions these calls reach, those they call in turn included.\"\"\"\n out, todo = [], list(calls)\n seen = set()\n while todo:\n f = todo.pop()\n if f in seen:\n continue\n seen.add(f)\n r = self.functions.get(f)\n if r is not None:\n out.append(r)\n todo.extend(r.calls)\n return out\n\n def plan_for(self, v, r):\n \"\"\"What loop r can do for v's reads in it: \"hoist\" (one site in it: its routing set once in front of it),\n \"replicas\" (several sites, and nothing in it writes v: a copy of v a site, each copy routed for good and filled\n in front of it), or why not. Nothing that runs between the loop's way in and its way out may route v (or, for\n copies, write it) but those sites: not the loop's own code, not a function it calls, and — when it waits —\n nothing outside it in another coroutine, since anything may run while it waits.\"\"\"\n k = id(v)\n groups, touches, writes = self.sites[k], self.touches.get(k, []), self.writes.get(k, [])\n if r.unknown:\n return \"calls a function that calls itself\"\n inside = [h for h in groups if r in h[\"ctx\"][1]]\n if any(r in t[1] for t in touches):\n return \"touched in the loop\"\n fns = self.reached(r.calls)\n if any(h[\"ctx\"][0] in fns for h in groups) or any(t[0] in fns for t in touches):\n return \"a function the loop calls routes it\"\n if r.waits:\n apart = inside[0][\"ctx\"][0]\n if any(h[\"ctx\"][0] is not apart for h in groups) or any(t[0] is not apart for t in touches):\n return \"waits, and other code routes it\"\n if len(inside) == 1:\n return \"hoist\"\n if any(r in w[1] for w in writes) or any(w[0] in fns for w in writes):\n return \"read at %d sites and written in the loop\" % len(inside)\n if r.waits and any(w[0] is not inside[0][\"ctx\"][0] for w in writes):\n return \"waits, and other code writes it\"\n return \"replicas\"\n\n def choice(self, v, g):\n \"\"\"For v in group g: (\"static\", None), (\\\"hoist\\\" or \"replicas\", the outermost loop that can), or (None, why not).\"\"\"\n k = id(v)\n if len(self.sites[k]) == 1 and not self.touches.get(k):\n return \"static\", None\n whys = []\n for r in g[\"ctx\"][1]:\n how = self.plan_for(v, r)\n if how in (\"hoist\", \"replicas\"):\n return how, r\n whys.append(how)\n return None, \"; \".join(whys) if whys else \"read at %d sites, outside any loop\" % len(self.sites[k])\n\n def decide(self):\n \"\"\"Each group whose cells all need no routing at its site goes in bare: a cell read there and nowhere else is\n routed in the variable buffer for good; one that is the only read inside a loop has its routing set once in\n front of the loop; one read at several sites of a loop that never writes it is read from a copy of its own,\n routed for good, which the loop's way in fills. Every other group keeps its setup.\"\"\"\n if not self.on:\n return\n self.on = False\n report = settings.get(\"routingReport\")\n for g in self.groups:\n self.stats[\"groups\"] += 1\n self.stats[\"cells\"] += len(g[\"cells\"])\n plan = []\n for v, dest, mod, nxt, slot in g[\"cells\"]:\n how, where = self.choice(v, g)\n if how is None:\n if report and g[\"ctx\"][1]:\n print(\"trigscript: routing kept in a loop: %s: %s\" % (self.names.get(id(v)) or \"a temporary made at \" + self.made_at.get(id(v), \"?\"), where))\n for w in sorted(self.why_touched.get(id(v), ()))[:3]:\n print(\"trigscript: touched at \" + w)\n plan = None\n break\n plan.append((how, where))\n if plan is None:\n g[\"entry\"] << g[\"setup\"]\n for v, _, _, _, slot in g[\"cells\"]:\n slot << v.GetVTable()\n continue\n self.stats[\"bare groups\"] += 1\n g[\"entry\"] << g[\"bare\"]\n for (v, dest, mod, nxt, slot), (how, r) in zip(g[\"cells\"], plan):\n self.stats[{\"static\": \"static\", \"hoist\": \"hoisted\", \"replicas\": \"replicas\"}[how]] += 1\n if how == \"static\":\n slot << v.GetVTable()\n self.static[v.GetVTable()] = (dest, mod, nxt)\n elif how == \"hoist\":\n slot << v.GetVTable()\n r.hoist.append((v, dest, mod, nxt))\n else:\n copy = EUDVariable()\n slot << copy.GetVTable()\n self.static[copy.GetVTable()] = (dest, mod, nxt)\n r.copies.append((v, copy))\n # Each loop's way in: straight on, or through triggers that set the routing hoisted in front of it and fill its\n # copies (made here, out of the way of the code being made: what is pending is put aside and handed on after).\n held = Forward()\n SetNextTrigger(held)\n for r in self.loops:\n if not r.hoist and not r.copies:\n r.entry << r.after\n continue\n self.stats[\"loops\"] += 1\n steps = [] # (actions, the cell the trigger goes on to, or None for the next step)\n acts = []\n for v, dest, mod, nxt in r.hoist:\n acts += [SetDeaths(EPD(v.getDestAddr()), SetTo, dest, 0), SetDeathsX(EPD(v.getDestAddr()) + 2, SetTo, mod << 24, 0, 0xFF000000), SetNextPtr(v.GetVTable(), nxt)]\n for i in range(0, len(acts), 63):\n steps.append((acts[i:i + 63], None))\n # A copy is filled by the variable's own cell, routed into the copy and back each time (it may be routed\n # otherwise outside the loop, so nothing of it is assumed).\n for v, copy in r.copies:\n steps.append(([SetDeaths(EPD(v.getDestAddr()), SetTo, EPD(copy.getValueAddr()), 0), SetDeathsX(EPD(v.getDestAddr()) + 2, SetTo, 7 << 24, 0, 0xFF000000)], v))\n firsts = [Forward() for _ in steps]\n for i, (acts_i, cell) in enumerate(steps):\n after = firsts[i + 1] if i + 1 < len(steps) else r.after\n if cell is None:\n firsts[i] << RawTrigger(nextptr=after, actions=acts_i)\n else:\n firsts[i] << RawTrigger(nextptr=cell.GetVTable(), actions=acts_i + [SetNextPtr(cell.GetVTable(), after)])\n r.entry << firsts[0]\n held << NextTrigger()\n print(\"trigscript: static routing — %(bare groups)d of %(groups)d reads without routing: %(static)d cells routed for good, %(hoisted)d routed once in front of a loop, %(replicas)d read from a copy (%(loops)d loops)\" % self.stats)\n\n\nROUTE = _Routing(settings.get(\"staticRouting\", \"1\") != \"0\")\n\nif ROUTE.enabled:\n _var_init = _eudv.EUDVariable.__init__\n _MODULE = globals()\n\n def _routed_init(self, *args, **kwargs):\n _var_init(self, *args, **kwargs)\n # Made by a line of this file itself (not by eudplib on its behalf): ours to route.\n f = _sys._getframe(1)\n if ROUTE.on and f.f_globals is _MODULE:\n ROUTE.ours[id(self)] = self\n if settings.get(\"routingReport\"):\n ROUTE.made_at[id(self)] = \"%s:%d < %s:%d\" % (f.f_code.co_name, f.f_lineno, f.f_back.f_code.co_name, f.f_back.f_lineno)\n\n _eudv.EUDVariable.__init__ = _routed_init\n\n def _watched(name):\n method = getattr(_eudv.EUDVariable, name)\n\n def wrapper(self, *args, **kwargs):\n ROUTE.touched(self)\n return method(self, *args, **kwargs)\n\n wrapper.__name__ = name\n setattr(_eudv.EUDVariable, name, wrapper)\n\n for _name in (\"GetVTable\", \"getDestAddr\", \"getMaskAddr\", \"SetModifier\", \"SetDest\", \"AddDest\", \"SubtractDest\", \"SetDestX\", \"AddDestX\", \"SubtractDestX\"):\n _watched(_name)\n\n # Its value's address asked for is a write unless a condition asks it (a condition only reads the value).\n _value_addr = _eudv.EUDVariable.getValueAddr\n\n def _routed_value_addr(self):\n ROUTE.wrote(self)\n return _value_addr(self)\n\n _eudv.EUDVariable.getValueAddr = _routed_value_addr\n import eudplib.core.variable.vbase as _vbase\n\n def _reading(name):\n method = getattr(_vbase.VariableBase, name)\n\n def wrapper(self, *args, **kwargs):\n ROUTE.reading += 1\n try:\n return method(self, *args, **kwargs)\n finally:\n ROUTE.reading -= 1\n\n wrapper.__name__ = name\n setattr(_vbase.VariableBase, name, wrapper)\n\n for _name in (\"AtLeast\", \"AtMost\", \"Exactly\", \"AtLeastX\", \"AtMostX\", \"ExactlyX\"):\n _reading(_name)\n\n _seqcompute = _eudv.SeqCompute\n _MODS = {id(SetTo): 7, id(Add): 8, id(Subtract): 9}\n\n def _emit_group(cells, consts, memo):\n \"\"\"A site: what came before jumps to `entry`; the setup does the constant stores before it (as eudplib's own\n SeqCompute does, in the same trigger) and routes the cells in a chain, the last back to `after`. The bare way in\n (no routing: `decide` chose) does the constants alone and goes through `slot`s — each the cell itself, or a\n copy of it a loop keeps — whose routing is set elsewhere.\"\"\"\n ROUTE.emitting += 1\n try:\n entry, after = Forward(), Forward()\n SetNextTrigger(entry)\n first = cells[0][0].GetVTable()\n slots = [Forward() for _ in cells]\n # (An action belongs to one trigger: the constants are made twice, for the setup and for the bare way in.)\n const_acts = lambda: [SetDeaths(_eudv._get_computedest(EncodePlayer(_unproxy_r(dst))), mdt, src, 0) for dst, mdt, src in consts]\n bare = RawTrigger(nextptr=slots[0], actions=const_acts()) if consts else slots[0]\n acts, plan = const_acts(), []\n for i, (v, dest, mod) in enumerate(cells):\n nxt = cells[i + 1][0].GetVTable() if i + 1 < len(cells) else after\n # A cell this same SeqCompute routed already keeps what was set then (eudplib's own does the same): it is\n # read at two sites, so neither goes without its setup.\n was = memo.get(id(v))\n if not was or was[0] is not dest:\n acts.append(v.SetDest(dest))\n if not was or was[1] is not mod:\n acts.append(v.SetModifier(mod))\n acts.append(SetNextPtr(v.GetVTable(), nxt))\n memo[id(v)] = (dest, mod)\n plan.append((v, dest, _MODS[id(mod)], slots[i + 1] if i + 1 < len(cells) else after, slots[i]))\n setup = RawTrigger(nextptr=first, actions=acts)\n after << NextTrigger()\n finally:\n ROUTE.emitting -= 1\n g = {\"entry\": entry, \"setup\": setup, \"bare\": bare, \"cells\": plan, \"ctx\": ROUTE.where()}\n ROUTE.groups.append(g)\n for v, _, _, _, _ in plan:\n ROUTE.sites.setdefault(id(v), []).append(g)\n\n def _routed_seqcompute(assignpairs):\n if not ROUTE.on or ROUTE.emitting:\n return _seqcompute(assignpairs)\n plain, group = [], []\n\n def flush_plain():\n if plain:\n _seqcompute(list(plain))\n plain.clear()\n\n consts, memo = [], {}\n\n def flush_group():\n if group:\n _emit_group(list(group), list(consts), memo)\n group.clear()\n consts.clear()\n\n for dst, mdt, src in assignpairs:\n s = _unproxy_r(src)\n if isinstance(s, EUDVariable) and id(s) in ROUTE.ours and id(mdt) in _MODS:\n if any(v is s for v, _, _ in group) or len(group) * 3 + len(consts) >= 61:\n flush_group()\n if not group:\n # The constant stores just before it go with it, as eudplib keeps them in one trigger.\n k = max([i for i, (_, _, x) in enumerate(plain) if isinstance(_unproxy_r(x), EUDVariable)], default=-1)\n consts.extend(plain[k + 1:])\n del plain[k + 1:]\n flush_plain()\n group.append((s, _eudv._get_computedest(EncodePlayer(_unproxy_r(dst))), mdt))\n else:\n flush_group()\n plain.append((dst, mdt, src))\n flush_group()\n flush_plain()\n\n # Every eudplib module that took SeqCompute by name (a call's arguments go through eudplib.core.variable's) calls this one.\n for _mod in list(_sys.modules.values()):\n if getattr(_mod, \"__name__\", \"\").startswith(\"eudplib\") and getattr(_mod, \"SeqCompute\", None) is _seqcompute:\n _mod.SeqCompute = _routed_seqcompute\n SeqCompute = _routed_seqcompute\n\n # Where code runs apart: a function's body (eudplib's and this file's), a scope of triggers this file sets apart.\n from eudplib.core.eudfunc.eudfuncn import EUDFuncN as _EUDFuncN_r\n _fn_call = _EUDFuncN_r.__call__\n _fn_body = _EUDFuncN_r._create_func_body\n\n def _routed_call(self, *args, **kwargs):\n ROUTE.called(self)\n return _fn_call(self, *args, **kwargs)\n\n def _routed_body(self):\n r = ROUTE.functions.setdefault(id(self), _Region(\"fn\"))\n ROUTE.stack.append(r)\n try:\n return _fn_body(self)\n finally:\n ROUTE.stack.pop()\n\n _EUDFuncN_r.__call__ = _routed_call\n _EUDFuncN_r._create_func_body = _routed_body\n _push_scope, _pop_scope = PushTriggerScope, PopTriggerScope\n\n def PushTriggerScope(): # noqa: N802 (this file's own scopes apart: a function that calls itself)\n ROUTE.stack.append(_Region(\"scope\"))\n return _push_scope()\n\n def PopTriggerScope(): # noqa: N802\n ROUTE.stack.pop()\n return _pop_scope()\n\n # The variable buffer writes the cells a decided group reads with their routing in place.\n _create_vartrigger = _vbuf.EUDVarBuffer.create_vartrigger\n\n def _indexed_vartrigger(self, v, initval):\n idx = getattr(self, \"_ts_index\", None)\n if idx is None:\n idx = self._ts_index = {}\n idx[v] = len(self._initvals)\n return _create_vartrigger(self, v, initval)\n\n _vbuf.EUDVarBuffer.create_vartrigger = _indexed_vartrigger\n\n def _static_of(buf):\n return [(i, ROUTE.static[v]) for v, i in getattr(buf, \"_ts_index\", {}).items() if v in ROUTE.static]\n\n _collect_vars = _vbuf.EUDVarBuffer.CollectDependency\n\n def _collect_routed(self, emitbuffer):\n _collect_vars(self, emitbuffer)\n for _, (dest, _, nxt) in _static_of(self):\n for x in (dest, nxt):\n if type(x) is not int:\n emitbuffer.WriteDword(x)\n\n _vbuf.EUDVarBuffer.CollectDependency = _collect_routed\n\n _write_vars = _vbuf.EUDVarBuffer.WritePayload\n\n def _write_routed(self, emitbuffer):\n static = _static_of(self)\n if not static:\n return _write_vars(self, emitbuffer)\n # The buffer as eudplib lays it out (vbuf.py): 324 bytes before the first cell, then 72 a variable — variable i's\n # trigger starts 4 bytes before the buffer plus 72·i, so its next pointer is at 72·i, its action's destination at\n # 340 + 72·i, its value at 344 + 72·i and its modifier byte at 351 + 72·i — then the last triggers' tails.\n n = len(self._initvals)\n blob = bytearray(324)\n block = bytearray(72)\n block[0:4] = b\"\\xFF\\xFF\\xFF\\xFF\"\n block[24:36] = b\"\\0\\0\\x2D\\x07\\0\\0SC\\x04\\0\\0\\0\"\n blob += block * n\n sym = {}\n\n def put(off, x):\n if isinstance(x, int):\n blob[off:off + 4] = (x & 0xFFFFFFFF).to_bytes(4, \"little\")\n else:\n sym[off] = x\n\n for i, iv in enumerate(self._initvals):\n if isinstance(iv, int):\n if iv:\n put(344 + 72 * i, iv)\n else:\n put(344 + 72 * i, iv)\n for i, (dest, mod, nxt) in static:\n put(340 + 72 * i, dest)\n blob[351 + 72 * i] = mod\n put(72 * i, nxt)\n pos = 0\n for off in sorted(sym):\n emitbuffer.WriteBytes(bytes(blob[pos:off]))\n emitbuffer.WriteDword(sym[off])\n pos = off + 4\n emitbuffer.WriteBytes(bytes(blob[pos:]))\n emitbuffer.WriteSpace(32)\n emitbuffer.WriteDword(4) # flags\n emitbuffer.WriteSpace(27)\n emitbuffer.WriteByte(0) # currentAction\n for _i in range(27):\n emitbuffer.WriteSpace(40)\n emitbuffer.WriteDword(4)\n emitbuffer.WriteSpace(27)\n emitbuffer.WriteByte(0)\n\n _vbuf.EUDVarBuffer.WritePayload = _write_routed\n ROUTE.on = True\n\n\ndef where(node):\n \"\"\"\" at main.ts:12:5\" for a node with a position — what the editor parses back into a marker.\"\"\"\n at = node.get(\"at\") if isinstance(node, dict) else None\n if not at:\n return \"\"\n if at.get(\"file\"):\n return \" at %s:%s:%s\" % (at.get(\"file\"), at.get(\"line\"), at.get(\"column\"))\n return \" at %s:%s\" % (at.get(\"line\"), at.get(\"column\"))\n\n\nclass Fail(RuntimeError):\n pass\n\n\nclass Leave(Exception):\n \"\"\"Raised through the lowering when a statement ends the straight line (break / continue / return).\"\"\"\n\n\n# Cells read at an index the game works out (an array, a row of a per-player program or of a task) are an EUDVArray\n# where they fit: a cell is a variable of its own, 72 bytes, which a read jumps to and which writes itself out — a\n# read at a variable index is a jump by the index's bits (4 triggers for 16 cells) where an EUDArray reads the dword\n# a bit at a time (45 triggers), and one at a constant index two triggers where an EUDArray takes as many. Past\n# VARRAY_MOST cells an array stays an EUDArray, 4 bytes a cell (a long table, a video's brushes).\nVARRAY_MOST = 2048\nVARRAY_STRIDE, VARRAY_VALUE = 72, 348 # a cell's size, and where its value is in it\n_VARRAYS = {} # id → the EUDVArray, for the map of variables (which says a cell's place by stride)\n\n\ndef cell_array(values):\n \"\"\"`values` as cells: an EUDVArray up to VARRAY_MOST of them, else an EUDArray.\"\"\"\n values = list(values)\n if len(values) > VARRAY_MOST:\n return EUDArray(values)\n v = EUDVArray(len(values))(values)\n _VARRAYS[id(v)] = v\n return v\n\n\ndef is_varray(store):\n return id(store) in _VARRAYS\n\n\nFILL_WIDTH = 8 # cells a trigger of varray_fill writes\n\n\ndef varray_fill(store, length, value):\n \"\"\"The first `length` cells of an EUDVArray set to `value`: eight a trigger, the Set Deaths' destinations moving on by\n eight cells each time, where a cell at a time worked its address out a bit a trigger (about 250 ns a cell, 23 now).\"\"\"\n acts = [Forward() for _ in range(FILL_WIDTH)]\n start = [SetMemory(a + 16, SetTo, store._epd + VARRAY_VALUE // 4 + VARRAY_STRIDE // 4 * k) for k, a in enumerate(acts)]\n patched = isinstance(unProxy(value), EUDVariable)\n if patched:\n RawTrigger(actions=start)\n for a in acts:\n VProc(value, list(value.QueueAssignTo(EPD(a) + 5)))\n else:\n RawTrigger(actions=start)\n turns = EUDVariable()\n turns << length // FILL_WIDTH\n if EUDWhileNot()(turns == 0):\n RawTrigger(actions=[a << SetMemoryEPD(0, SetTo, 0 if patched else value) for a in acts]\n + [SetMemory(a + 16, Add, VARRAY_STRIDE // 4 * FILL_WIDTH) for a in acts] + [turns.SubtractNumber(1)])\n EUDEndWhile()\n for i in range(length - length % FILL_WIDTH, length):\n cell_write(store, i, value, SetTo)\n\n\ndef cell_write(store, index, value, modifier):\n \"\"\"`store[index]` set to (`SetTo`) or added to (`Add`) `value`, the store an EUDArray or one of cell_array's\n EUDVArrays. Into an EUDVArray at a variable index: a Set Deaths whose destination, the cell's value at\n EPD + 87 + 18 i, is worked out by one trigger an index bit, as eudplib's own get does (eudplib 0.81's EUDVArray\n has no add, and its set is a third dearer).\"\"\"\n if not is_varray(store):\n if modifier == Add:\n store.iadditem(index, value)\n else:\n store[index] = value\n return\n if isinstance(index, int):\n if modifier == SetTo:\n # The variable's value written into the cell (through SeqCompute: a site of static routing).\n SeqCompute([(store._epd + VARRAY_VALUE // 4 + VARRAY_STRIDE // 4 * index, SetTo, value)])\n else:\n Trigger(actions=SetMemoryEPD(store._epd + VARRAY_VALUE // 4 + VARRAY_STRIDE // 4 * index, modifier, value))\n return\n act = Forward()\n # (Through SeqCompute, so that a variable's copy into the action is a site of static routing.)\n pairs = [(EPD(act) + 4, SetTo, store._epd + VARRAY_VALUE // 4)]\n if isinstance(unProxy(value), EUDVariable):\n pairs.append((EPD(act) + 5, SetTo, value))\n value = 0\n SeqCompute(pairs)\n for b in reversed(range(max((store._size - 1).bit_length(), 1))):\n RawTrigger(conditions=index.AtLeastX(1, 1 << b), actions=SetMemory(act + 16, Add, VARRAY_STRIDE // 4 << b))\n RawTrigger(actions=[act << SetMemoryEPD(0, modifier, value)])\n\n\n# ── an index worked out once ──\n# A cell of an EUDVArray at a variable index is 72 bytes a step from the first: eudplib works the place out a bit of\n# the index at a time, a trigger a bit, at every read and every write. A variable whose every store is a constant or\n# itself plus or minus a constant (a loop's counter, `for…of`'s own index) can have the place's offset kept beside it\n# by the same stores — `i++` is two adds — and a read at it is a jump to the first cell plus the offset, a write a Set\n# Deaths whose destination is the first cell's plus the offset: a few triggers whatever the array's length.\n\ndef counter_step(value, id_):\n \"\"\"What a store of `value` into the variable `id_` does to it: (\"set\", c) or (\"add\", c), c a constant (wrapped),\n or None when it is anything else.\"\"\"\n if not isinstance(value, dict):\n return None\n k = value.get(\"kind\")\n if k == \"const\":\n return (\"set\", int(value[\"value\"]) & U32)\n if k == \"binary\" and value.get(\"op\") in (\"+\", \"-\") and not value.get(\"fixed\"):\n left, right = value[\"left\"], value[\"right\"]\n me = lambda e: e.get(\"kind\") == \"var\" and e.get(\"id\") == id_\n if me(left) and right.get(\"kind\") == \"const\":\n c = int(right[\"value\"])\n return (\"add\", (c if value[\"op\"] == \"+\" else -c) & U32)\n if value[\"op\"] == \"+\" and me(right) and left.get(\"kind\") == \"const\":\n return (\"add\", int(left[\"value\"]) & U32)\n return None\n\n\ndef find_counters(program):\n \"\"\"The ids of the variables of numbers whose every store is a constant or the variable plus or minus a constant,\n and which nothing else names but a read: no call's result, no function's saved frame, no loop's own variable.\"\"\"\n ok, bad = set(), set()\n skip = (\"kind\", \"at\", \"label\")\n\n def walk(node):\n if isinstance(node, list):\n for x in node:\n walk(x)\n elif isinstance(node, str):\n bad.add(node) # (an id anywhere but a read, a store or its declaration: something else writes it)\n elif isinstance(node, dict):\n k = node.get(\"kind\")\n if k == \"var\" and isinstance(node.get(\"id\"), str):\n return\n if k == \"assign\" and isinstance(node.get(\"target\"), str):\n t = node[\"target\"]\n (ok if counter_step(node.get(\"value\"), t) else bad).add(t)\n for key, v in node.items():\n if key not in skip and key != \"target\":\n walk(v)\n return\n if k == \"declare\" and isinstance(node.get(\"decl\"), dict):\n d = node[\"decl\"]\n step = counter_step(node.get(\"init\"), d.get(\"id\"))\n plain_number = d.get(\"kind\") == \"number\" and not d.get(\"bits\") and not d.get(\"fixed\") and d.get(\"task\") is None\n (ok if plain_number and step and step[0] == \"set\" else bad).add(d.get(\"id\"))\n for key, v in node.items():\n if key not in skip and key != \"decl\":\n walk(v)\n return\n for key, v in node.items():\n if key not in skip:\n walk(v)\n\n walk([program.get(\"body\", []), program.get(\"functions\", [])])\n return ok - bad\n\n\ndef saved_arrays(program):\n \"\"\"The arrays a function that calls itself saves and takes back (their handles written behind the lowering's back).\"\"\"\n out = set()\n\n def walk(node):\n if isinstance(node, list):\n for x in node:\n walk(x)\n elif isinstance(node, dict):\n if isinstance(node.get(\"saves\"), dict):\n out.update(node[\"saves\"].get(\"arrays\", []))\n for v in node.values():\n if isinstance(v, (list, dict)):\n walk(v)\n\n walk([program.get(\"body\", []), program.get(\"functions\", [])])\n return out\n\n\n# Kinds of statement or value a loop's body may not hold for its reads at the loop's index to go unchecked: what\n# changes an array's length, what calls a function (which may), and what waits (another program may run then).\nUNCHECKED_REFUSE = {\"push\", \"pop\", \"setLength\", \"declareArray\", \"call\", \"sleep\", \"parallel\", \"start\", \"cancel\", \"select\",\n \"syncSend\", \"yield\", \"await\", \"taskHandle\", \"running\"}\n\n\ndef lengths_fixed_in(body, index_id, lists):\n \"\"\"Whether, in a loop's body, no array changes its length and the index is not stored to: nothing in it from\n UNCHECKED_REFUSE, no store into a growing array (`lists`, by id) but at the index itself (a store at the length\n is a push), no store into the index.\"\"\"\n def ok(node):\n if isinstance(node, list):\n return all(ok(x) for x in node)\n if not isinstance(node, dict):\n return True\n k = node.get(\"kind\")\n if k in UNCHECKED_REFUSE:\n return False\n if k == \"assign\" and node.get(\"target\") == index_id:\n return False\n if k == \"store\" and node.get(\"array\") in lists:\n idx = node.get(\"index\", {})\n if not (idx.get(\"kind\") == \"var\" and idx.get(\"id\") == index_id):\n return False\n return all(ok(v) for v in node.values() if isinstance(v, (list, dict)))\n return ok(body)\n\n\ndef lists_at(body, index_id, lists):\n \"\"\"The growing arrays (`lists`, by id) a loop's body reads or writes at the index.\"\"\"\n out = []\n\n def walk(node):\n if isinstance(node, list):\n for x in node:\n walk(x)\n elif isinstance(node, dict):\n idx = node.get(\"index\")\n if node.get(\"kind\") in (\"element\", \"store\") and node.get(\"array\") in lists and isinstance(idx, dict) \\\n and idx.get(\"kind\") == \"var\" and idx.get(\"id\") == index_id and node[\"array\"] not in out:\n out.append(node[\"array\"])\n for v in node.values():\n if isinstance(v, (list, dict)):\n walk(v)\n\n walk(body)\n return out\n\n\nclass Cursors:\n \"\"\"Reads and writes at a loop's counter through registers of their own (the backend's second part): a read is a\n trigger whose next pointer is the cell's address — so it jumps straight to the cell, which writes itself out\n through eudplib's getter — and a write an action whose destination is the cell's value. Each register is set once on\n the way into the loop (the array's first cell plus the counter's place) and moved on by the counter's step at the\n loop's update, one add each, where `varray_read` / `varray_write` work the place out at every access (two variables\n routed into a jump, or into an action, each time). Only for a loop whose body stores nothing into the counter, waits\n for nothing, calls nothing and changes no array's length (`lengths_fixed_in`): the arrays' blocks stay where they\n are, and nothing but the update moves the counter.\"\"\"\n\n def __init__(self, counter, place, step):\n self.counter, self.place, self.step = counter, place, step\n self.reads = [] # (the trigger whose next pointer is the register, the first cell: a constant or a variable)\n self.writes = [] # (the action whose destination is the register, the first cell's value's EPD)\n\n def read(self, base, out):\n from eudplib.core.eudstruct.vararray import _getter, _getter_dst\n after = Forward()\n t = RawTrigger(nextptr=0, actions=[SetNextPtr(_getter, after), SetMemoryEPD(_getter_dst, SetTo, EPD(out.getValueAddr()))])\n after << NextTrigger()\n self.reads.append((t, base))\n\n def write(self, base_epd, value, modifier):\n act = Forward()\n if isinstance(unProxy(value), EUDVariable):\n SeqCompute([(EPD(act) + 5, SetTo, value)])\n value = 0\n RawTrigger(actions=[act << SetMemoryEPD(0, modifier, value)])\n self.writes.append((act, base_epd))\n\n def init(self):\n \"\"\"The registers set from where each array's block starts and the counter's place (on the way in).\"\"\"\n pairs = []\n for t, base in self.reads:\n pairs += [(EPD(t) + 1, SetTo, base), (EPD(t) + 1, Add, self.place.bytes)]\n for act, base_epd in self.writes:\n pairs += [(EPD(act) + 4, SetTo, base_epd), (EPD(act) + 4, Add, self.place.dwords)]\n SeqCompute(pairs)\n\n def advance(self):\n \"\"\"The registers moved on by the counter's step (at the loop's update).\"\"\"\n acts = [SetMemory(t + 4, Add, (VARRAY_STRIDE * self.step) & U32) for t, _ in self.reads]\n acts += [SetMemory(act + 16, Add, (VARRAY_STRIDE // 4 * self.step) & U32) for act, _ in self.writes]\n if acts:\n DoActions(acts)\n\n\nclass Place:\n \"\"\"A counter's offset into EUDVArrays, kept by its stores: in bytes (72 a step, for a read's jump) and in dwords\n (18 a step, for a write's destination).\"\"\"\n\n def __init__(self):\n self.bytes = EUDVariable(0) # initial: the counter's own 0\n self.dwords = EUDVariable(0) # initial: the counter's own 0\n\n def step(self, step):\n op, c = step\n if op == \"set\":\n DoActions([self.bytes.SetNumber((VARRAY_STRIDE * c) & U32), self.dwords.SetNumber((VARRAY_STRIDE // 4 * c) & U32)])\n else:\n DoActions([self.bytes.AddNumber((VARRAY_STRIDE * c) & U32), self.dwords.AddNumber((VARRAY_STRIDE // 4 * c) & U32)])\n\n\ndef varray_read(base, place, out):\n \"\"\"The cell at `base` + the place's bytes into `out`: `base` the first cell (a constant, or a variable holding it).\n The offset's variable adds itself into its own next-trigger pointer and jumps there, to the cell, which writes\n itself out through eudplib's getter, as EUDVArray.get does once it has the place.\"\"\"\n from eudplib.core.eudstruct.vararray import _getter, _getter_dst\n off = place.bytes\n after = Forward()\n tail = [SetNextPtr(_getter, after), SetMemoryEPD(_getter_dst, SetTo, EPD(out.getValueAddr()))]\n if isinstance(base, EUDVariable):\n RawTrigger(nextptr=base.GetVTable(), actions=[SetNextPtr(base.GetVTable(), off.GetVTable()), *base.QueueAssignTo(EPD(off.GetVTable()) + 1),\n *off.QueueAddTo(EPD(off.GetVTable()) + 1), *tail])\n else:\n RawTrigger(nextptr=off.GetVTable(), actions=[SetNextPtr(off.GetVTable(), base), *off.QueueAddTo(EPD(off.GetVTable()) + 1), *tail])\n after << NextTrigger()\n\n\ndef varray_read_at(addr, out):\n \"\"\"The cell whose address the variable `addr` holds into `out`.\"\"\"\n from eudplib.core.eudstruct.vararray import _getter, _getter_dst\n after = Forward()\n RawTrigger(nextptr=addr.GetVTable(), actions=[SetNextPtr(addr.GetVTable(), 0), *addr.QueueAddTo(EPD(addr.GetVTable()) + 1),\n SetNextPtr(_getter, after), SetMemoryEPD(_getter_dst, SetTo, EPD(out.getValueAddr()))])\n after << NextTrigger()\n\n\ndef varray_write_at(value_epd, value, extra=()):\n \"\"\"The cell whose value's EPD the variable `value_epd` holds set to `value`; `extra` actions in the same trigger.\"\"\"\n act = Forward()\n vs, acts = [value_epd], list(value_epd.QueueAssignTo(EPD(act) + 4))\n if isinstance(unProxy(value), EUDVariable):\n vs.append(value)\n acts += value.QueueAssignTo(EPD(act) + 5)\n value = 0\n VProc(vs, acts)\n RawTrigger(actions=[act << SetMemoryEPD(0, SetTo, value), *extra])\n\n\ndef varray_write(value_epd, place, value, modifier):\n \"\"\"The cell whose value is at EPD `value_epd` + the place's dwords set to (or added to) `value`: `value_epd` a\n constant, or a variable holding it.\"\"\"\n act = Forward()\n # (Through SeqCompute, so that each variable's copy into the action is a site of static routing.)\n pairs = [(EPD(act) + 4, SetTo, value_epd), (EPD(act) + 4, Add, place.dwords)]\n if isinstance(unProxy(value), EUDVariable):\n pairs.append((EPD(act) + 5, SetTo, value))\n value = 0\n SeqCompute(pairs)\n RawTrigger(actions=[act << SetMemoryEPD(0, modifier, value)])\n\n\nclass Rows:\n \"\"\"Which of a cell's copies is the one in use: a per-player program has one a player, a task's body one a slot of its\n start (`Pool`), and a task's in a per-player program one a slot within each player's. Called, it is the index.\"\"\"\n\n def __init__(self, lowering, per_player, pool):\n self.lowering, self.per_player, self.pool = lowering, per_player, pool\n self.count = (12 if per_player else 1) * (pool.max if pool else 1)\n\n def __call__(self):\n if self.pool:\n return self.pool.row\n return self.lowering.player_of()\n\n\nclass RowCell:\n \"\"\"A cell of the lowering's own (a coroutine's place, a latch), with a copy for each row of `rows` when it has them.\"\"\"\n\n def __init__(self, rows, initial=0):\n self.rows = rows\n self.store = cell_array([initial] * rows.count) if rows else EUDVariable(initial) # initial: the lowering's own state\n\n def get(self):\n return self.store[self.rows()] if self.rows else self.store\n\n def set(self, value):\n if self.rows:\n cell_write(self.store, self.rows(), value, SetTo)\n else:\n self.store << value\n\n\nclass Storage:\n \"\"\"A variable's cell: plain, or one of rows (`Rows`) - a 12-slot row of a per-player program, a row a task's slot.\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = rows is not None\n self.player_of = rows\n self.store = cell_array([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\n if not self.rowed:\n ROUTE.names[id(self.store)] = decl.get(\"name\", \"?\")\n\n def get(self):\n return self.store[self.player_of()] if self.rowed else self.store\n\n def set(self, value):\n if self.bits:\n value = saturate(value, self.bits)\n if self.rowed:\n cell_write(self.store, self.player_of(), value, SetTo)\n else:\n self.store << value\n\n def add(self, value, op):\n \"\"\"The cell plus (`op` \"+\") or minus (\"-\") `value` where it is, wrapping as numbers do: eudplib's own add, with\n no copy through a temporary (`x = x + 1` was 9 records and 4 triggers through one, and is 1 action). False\n where it cannot: a saturated cell, or a row's cell less a variable (the game's Subtract stops at 0).\"\"\"\n if self.bits:\n return False\n if self.rowed:\n if op == \"-\" and not isinstance(value, int):\n return False\n cell_write(self.store, self.player_of(), value if op == \"+\" else (-value) & U32, Add)\n return True\n if op == \"+\":\n self.store += value\n else:\n self.store -= value\n return True\n\n def mask(self, op, c):\n \"\"\"The cell & c or | c where it is: one masked write (only a variable's own cell, not a row's).\"\"\"\n DoActions(_mask_action(self.store, op, c))\n\n\ndef _mask_action(cell, op, c):\n \"\"\"The masked write that leaves `cell & c` (clear the bits outside c) or `cell | c` (set the bits of c) in a variable.\"\"\"\n return cell.SetNumberX(0, (~c) & U32) if op == \"&\" else cell.SetNumberX(c, c)\n\n\ndef unpack_table(table, length):\n \"\"\"A table the IR carries packed (`compiler/eud.ts#packTable`): `bytes` a value, little-endian, in base64 - as a\n dword a value, the cells of the map.\"\"\"\n width = int(table[\"bytes\"])\n raw = base64.b64decode(table[\"data\"])\n if len(raw) != width * length:\n raise RuntimeError(\"a packed table is %d bytes, not %d values of %d\" % (len(raw), length, width))\n out = bytearray(4 * length)\n for b in range(width):\n out[b::4] = raw[b::width]\n return bytes(out)\n\n\nclass ArrayStorage:\n \"\"\"An array's cells: `length` of them, twelve rows of that in a per-player program, or - `values` - a list\n the script computed when it was built, which is in the map as it loads and which nothing writes. An index\n past either end (read from 0 up, so one below zero too) reads 0 and stores nothing.\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n table = decl.get(\"packed\")\n self.rowed = rows is not None and values is None and table is None\n self.player_of = rows\n if decl.get(\"texts\") is not None:\n # A list of texts the script has: a cell is the text's id in the built map's table.\n values = [text_id(t) for t in decl[\"texts\"]]\n # What a list known when the map is built holds, for what is worked out with it then (an atan2 of constants).\n self.values = [int(v) & U32 for v in values] if values is not None else None\n if table is not None:\n # A long table (a video's brushes): its bytes straight into the map, no list of a value at a time.\n # (An EUDArray is its cells' EPD, so that is what it is made from.)\n raw = unpack_table(table, self.length)\n self.values = [int.from_bytes(raw[i:i + 4], \"little\") for i in range(0, len(raw), 4)]\n self.store = EUDArray(_from=EPD(Db(raw)))\n else:\n self.store = cell_array([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (rows.count if self.rowed else 1))) # initial: a table's values, or cells declareArray sets\n\n def at(self, index):\n \"\"\"The cell's place among the store's: the row first (the player's, the task's).\"\"\"\n if not self.rowed:\n return index\n p = self.player_of()\n row = p * self.length if isinstance(p, int) else f_mul(p, self.length)\n return row + index\n\n def get(self, index):\n if isinstance(index, int):\n return self.store[self.at(index)] if index < self.length else 0\n out = fresh(0)\n if EUDIf()(index <= self.length - 1):\n if is_varray(self.store):\n self.store.get(self.at(index), ret=[out]) # (the cell writes itself into out: no copy through another)\n else:\n out << self.store[self.at(index)]\n EUDEndIf()\n return out\n\n def placeable(self):\n return not self.rowed and is_varray(self.store)\n\n def get_placed(self, index, place, bound=None):\n out = fresh(0)\n if EUDIf()(index <= self.length - 1):\n varray_read(unProxy(self.store), place, out)\n EUDEndIf()\n return out\n\n def set_placed(self, index, place, value, modifier=SetTo, bound=None):\n if self.bits and modifier == SetTo:\n value = saturate(value, self.bits)\n if EUDIf()(index <= self.length - 1):\n varray_write(self.store._epd + VARRAY_VALUE // 4, place, value, modifier)\n EUDEndIf()\n\n def set(self, index, value):\n if self.bits:\n value = saturate(value, self.bits)\n if isinstance(index, int):\n if index < self.length:\n cell_write(self.store, self.at(index), value, SetTo)\n return\n if EUDIf()(index <= self.length - 1):\n cell_write(self.store, self.at(index), value, SetTo)\n EUDEndIf()\n\n def add(self, index, value, op):\n \"\"\"The cell plus (`op` \"+\") or minus (\"-\") the value, by the game's own Add: nothing read. Past either end,\n nothing, as a store.\"\"\"\n # A subtract is the add of the value's negative, which wraps as numbers do: the game's Subtract stops at 0\n # (`a[i] -= 5` of a 3 was 0 in the game and -2 in the simulator).\n if op == \"-\":\n value = (-value) & U32 if isinstance(value, int) else 0 - value\n act = lambda i, v: cell_write(self.store, i, v, Add)\n if isinstance(index, int):\n if index < self.length:\n act(self.at(index), value)\n return\n if EUDIf()(index <= self.length - 1):\n act(self.at(index), value)\n EUDEndIf()\n\n def fill(self, value):\n if self.length <= 16:\n for i in range(self.length):\n self.set(i, value)\n return\n if is_varray(self.store) and not self.rowed:\n varray_fill(self.store, self.length, value)\n return\n v, i = as_var(value), fresh(0)\n if EUDWhile()(i <= self.length - 1):\n self.set(i, v)\n i += 1\n EUDEndWhile()\n\n\nclass SliceStorage:\n \"\"\"A window on another array's cells - a row of a grid: cell i is cell `offset + i` of the array it is a window\n on, `offset` a variable of the program. Past its own end it reads 0 and stores nothing, so a row never reaches\n into the next; what it is a window on keeps its own ends (and its own width, and its row a player).\"\"\"\n\n def __init__(self, decl, lowering):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.lowering = lowering\n\n def of(self):\n return self.lowering.array(self.decl[\"slice\"][\"of\"], self.decl)\n\n def offset(self):\n return self.lowering.var(self.decl[\"slice\"][\"offset\"], self.decl).get()\n\n def get(self, index):\n if isinstance(index, int):\n return self.of().get(self.offset() + index) if index < self.length else 0\n out = fresh(0)\n if EUDIf()(index <= self.length - 1):\n out << self.of().get(self.offset() + index)\n EUDEndIf()\n return out\n\n def set(self, index, value):\n if isinstance(index, int):\n if index < self.length:\n self.of().set(self.offset() + index, value)\n return\n if EUDIf()(index <= self.length - 1):\n self.of().set(self.offset() + index, value)\n EUDEndIf()\n\n def fill(self, value):\n v, i = as_var(value), fresh(0)\n if EUDWhile()(i <= self.length - 1):\n self.set(i, v)\n i += 1\n EUDEndWhile()\n\n\n# The heap the programs' growing arrays share. A block is a power of two of cells, four at least; one given back\n# waits in its size's list (its first cell links the next) for whoever wants that size next; new ground is taken\n# from the bottom up. Cell 0 is never handed out: 0 is \"no block\". compiler/simulateIr.ts counts the same way, so\n# both run out at the same push. (Recursion's stack is an array of its own, below: how deep a function may go is\n# then the same whatever the arrays hold.)\nHEAP_CELLS = min(1 << 20, max(1024, int(IR.get(\"heap\", 16384)))) # the map's script settings, or the default\nHEAP_SMALLEST = 4\nCELL_HEAP_MOST = 1 << 16 # compiler/ir.ts#CELL_HEAP_MOST: up to this many, growing arrays have cells of their own\nHEAP_ROOMS = []\nwhile HEAP_SMALLEST << len(HEAP_ROOMS) <= HEAP_CELLS:\n HEAP_ROOMS.append(HEAP_SMALLEST << len(HEAP_ROOMS))\n_HEAP = {}\n\n\ndef heap():\n \"\"\"The heap's cells and its functions, made when the first growing array is met.\"\"\"\n if _HEAP:\n return _HEAP\n cells = EUDArray(HEAP_CELLS)\n rooms = EUDArray(HEAP_ROOMS + [0xFFFFFFFF]) # initial: the sizes, never written\n free = EUDArray(len(HEAP_ROOMS)) # initial: no block waits\n top = EUDVariable(1) # initial: the heap's own state, for the whole game\n said = EUDVariable(0) # initial: the heap's own state\n\n @EUDFunc\n def take(k):\n \"\"\"A block of size class k: its place among the cells, or 0 when there is none.\"\"\"\n at = EUDVariable()\n at << free[k]\n if EUDIf()(at >= 1):\n free[k] = cells[at]\n EUDReturn(at)\n EUDEndIf()\n end = top + rooms[k]\n if EUDIf()(end <= HEAP_CELLS):\n at << top\n top << end\n EUDReturn(at)\n EUDEndIf()\n EUDReturn(0)\n\n @EUDFunc\n def give(at, k):\n cells[at] = free[k]\n free[k] = at\n\n def growing(copy, write):\n \"\"\"grow and push for one kind of cell: `copy(block, ptr, length)` moves a block's cells, `write(at, value)`\n stores one.\"\"\"\n\n @EUDFunc\n def grow(ptr, length, room, k, need):\n \"\"\"Room for `need` cells: a larger block, the cells copied over, the old block given back. Returns the\n handle as it is now and whether there is room; when the heap has no block left the handle is unchanged.\"\"\"\n if EUDIf()(need <= room):\n EUDReturn(ptr, room, k, 1)\n EUDEndIf()\n nk = EUDVariable()\n nk << 0\n if EUDIf()(ptr >= 1):\n nk << k + 1\n EUDEndIf()\n if EUDWhile()(rooms[nk] < need):\n nk += 1\n EUDEndWhile()\n if EUDIf()(nk >= len(HEAP_ROOMS)):\n EUDReturn(ptr, room, k, 0)\n EUDEndIf()\n block = take(nk)\n if EUDIf()(block == 0):\n EUDReturn(ptr, room, k, 0)\n EUDEndIf()\n if EUDIf()(ptr >= 1):\n if EUDIf()(length >= 1):\n copy(block, ptr, length)\n EUDEndIf()\n give(ptr, k)\n EUDEndIf()\n EUDReturn(block, rooms[nk], nk, 1)\n\n @EUDFunc\n def push(ptr, length, room, k, value):\n \"\"\"One more cell at the end; the handle as it is afterwards. Nothing is pushed when the heap is full.\"\"\"\n ptr, room, k, ok = grow(ptr, length, room, k, length + 1)\n if EUDIf()(ok >= 1):\n write(ptr + length, value)\n length += 1\n if EUDElse()():\n if EUDIf()(said == 0):\n said << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: out of memory - an array could not grow (the programs' arrays share %d cells).\" % HEAP_CELLS)\n EUDEndIf()\n EUDEndIf()\n EUDReturn(ptr, length, room, k)\n\n return grow, push\n\n def copy_dwords(block, ptr, length):\n # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + ptr, length)\n\n def write_dword(at, value):\n cells[at] = value\n\n grow, push = growing(copy_dwords, write_dword)\n _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push, growing=growing)\n return _HEAP\n\n\ndef list_heap():\n \"\"\"The heap with the growing arrays' own cells beside its bytes: blocks are handed out as for texts (the same\n count, so a text and an array still run out at the same push in the game and in the simulator), but an array's\n cells are an EUDVArray of the heap's length, a variable a cell, which a read jumps to instead of reading a dword\n a bit at a time (32 triggers). Made when the first growing array is met; texts never touch these cells.\"\"\"\n h = heap()\n if \"vcells\" in h or HEAP_CELLS > CELL_HEAP_MOST:\n return h # (past CELL_HEAP_MOST, 72 bytes a cell is too much memory: the arrays keep the heap's dwords, as in 3.x)\n vcells = EUDVArray(HEAP_CELLS)()\n _VARRAYS[id(vcells)] = vcells\n\n def copy_cells(block, ptr, length):\n # Both blocks walked a cell at a time: the source by its address, the destination by its value's EPD.\n src, dst, n, cell = EUDVariable(), EUDVariable(), EUDVariable(), EUDVariable()\n src << unProxy(vcells) + ptr * VARRAY_STRIDE\n dst << vcells._epd + VARRAY_VALUE // 4 + block * (VARRAY_STRIDE // 4)\n n << length\n if EUDWhile()(n >= 1):\n varray_read_at(src, cell)\n varray_write_at(dst, cell, [src.AddNumber(VARRAY_STRIDE), dst.AddNumber(VARRAY_STRIDE // 4), n.SubtractNumber(1)])\n EUDEndWhile()\n\n def write_cell(at, value):\n cell_write(vcells, at, value, SetTo)\n\n vgrow, vpush = h[\"growing\"](copy_cells, write_cell)\n h.update(vcells=vcells, vgrow=vgrow, vpush=vpush)\n return h\n\n\n# The stack of the functions that call themselves. A function's variables are cells of the program, one of each, so\n# a call that may come back into the function it is in (the IR's `saves`) puts what that function holds here first\n# and takes it back after, with where the function returns to. One stack serves every program and every player:\n# such a function never sleeps, so the stack is empty whenever a frame ends. It is as many frames of the largest\n# frame as the map's script settings allow calls deep (the IR file's `stack`), and the depth is what is counted -\n# compiler/simulateIr.ts counts the same - so a program stops at the same call in both.\nSTACK_DEPTH = min(1 << 16, max(16, int(IR.get(\"stack\", 1024))))\nSTACK_CELLS_MAX = 1 << 20\nHANDLE = (\"ptr\", \"len\", \"room\", \"k\")\n_STACK = {}\n\n\ndef frame_cells(saves, kinds):\n return 1 + 4 * len(saves.get(\"arrays\", [])) + sum(3 if kinds.get(v) in (\"unit\", \"text\") else 1 for v in saves.get(\"vars\", []))\n\n\ndef largest_frame():\n most = [0]\n\n def walk(node, kinds):\n if isinstance(node, list):\n for x in node:\n walk(x, kinds)\n elif isinstance(node, dict):\n if isinstance(node.get(\"saves\"), dict):\n most[0] = max(most[0], frame_cells(node[\"saves\"], kinds))\n for v in node.values():\n if isinstance(v, (dict, list)):\n walk(v, kinds)\n\n def kinds_of(node, into):\n if isinstance(node, list):\n for x in node:\n kinds_of(x, into)\n elif isinstance(node, dict):\n if isinstance(node.get(\"id\"), str) and node.get(\"kind\") in (\"number\", \"boolean\", \"unit\", \"text\") and \"shared\" in node:\n # A text kept as its id is one cell, as a number is; one that was made is three.\n into[node[\"id\"]] = \"number\" if node[\"kind\"] == \"text\" and node.get(\"text\") == \"id\" else node[\"kind\"]\n for v in node.values():\n if isinstance(v, (dict, list)):\n kinds_of(v, into)\n return into\n\n for program in IR.get(\"programs\", []):\n walk(program.get(\"functions\", []), kinds_of(program, {}))\n return most[0]\n\n\ndef stack():\n \"\"\"The stack's cells, where the next frame goes (an EPD, so a cell is one write) and how deep it is.\"\"\"\n if _STACK:\n return _STACK\n frame = largest_frame()\n if frame * STACK_DEPTH > STACK_CELLS_MAX:\n raise Fail(\"trigscript: the stack would be %d cells - %d calls deep, %d cells a call - and %d is the most: lower the recursion depth in the script's Settings, or keep fewer variables in the function that calls itself\" % (frame * STACK_DEPTH, STACK_DEPTH, frame, STACK_CELLS_MAX))\n cells = EUDArray(max(1, frame) * STACK_DEPTH)\n base = cells # an EUDArray's own value is where it is, as an EPD already\n at = EUDVariable(base) # initial: the stack's own state; it is back here whenever a frame ends\n depth = EUDVariable(0) # initial: the stack's own state; nothing is on it between frames\n _STACK.update(cells=cells, base=base, at=at, depth=depth)\n return _STACK\n\n\nclass ListStorage:\n \"\"\"An array that grows: a handle - where its block is among the heap's cells (0: none yet), how many cells are\n in use, how many the block has room for, and the block's size class - a cell each, or a row of twelve each in\n a per-player program. Reads and stores are bounded by the cells in use; a store at exactly the length is a push.\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = rows is not None\n self.player_of = rows\n make = (lambda: cell_array([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no block\n self.handle = {name: make() for name in (\"ptr\", \"len\", \"room\", \"k\")}\n self.heap = list_heap()\n # Where its block starts, kept as the block moves, for reads and writes at a counter (`Place`): the first\n # cell's address, and its value's EPD. Only when the lowering asks (`keep_base`) before anything is lowered.\n self.base = None\n\n def cells(self):\n \"\"\"Whether the array's cells are the growing arrays' own (`list_heap`), not the heap's dwords.\"\"\"\n return \"vcells\" in self.heap\n\n def keep_base(self):\n if self.base is None and not self.rowed and self.cells():\n v = self.heap[\"vcells\"]\n first = v._epd + VARRAY_VALUE // 4\n self.base = (EUDVariable(unProxy(v)), EUDVariable(first)) # initial: block 0's\n # Where a push writes, base[1] + 18 × the length: a push with room is a write there and two adds.\n self.next = EUDVariable(first) # initial: block 0's first cell, the length 0\n\n def renext(self):\n \"\"\"`next` worked out again from the length (after a block moved, or the length was set).\"\"\"\n if self.base is not None:\n self.next << self.base[1] + self.handle[\"len\"] * (VARRAY_STRIDE // 4)\n\n def placeable(self):\n return self.base is not None\n\n def field(self, name):\n cell = self.handle[name]\n return cell[self.player_of()] if self.rowed else cell\n\n def put(self, name, value):\n if self.rowed:\n self.handle[name][self.player_of()] = value\n else:\n if name == \"ptr\" and self.base is not None:\n # The block moved (a push that grew it, a declaration): its first cell again, only when it did.\n if EUDIfNot()(self.handle[\"ptr\"] == value):\n v = self.heap[\"vcells\"]\n self.base[0] << unProxy(v) + value * VARRAY_STRIDE\n self.base[1] << v._epd + VARRAY_VALUE // 4 + value * (VARRAY_STRIDE // 4)\n EUDEndIf()\n self.handle[name] << value\n\n def get_placed(self, index, place, bound=None):\n \"\"\"`bound`: None, checked against the length; True, known inside (the loop's own array); or a variable,\n 1 when a loop found this array at least as long as its own on the way in, which is checked instead.\"\"\"\n out = fresh(0)\n if bound is True:\n varray_read(self.base[0], place, out)\n return out\n if bound is not None:\n if EUDIf()(bound >= 1):\n varray_read(self.base[0], place, out)\n if EUDElseIf()(as_var(index) < as_var(self.field(\"len\"))):\n varray_read(self.base[0], place, out)\n EUDEndIf()\n return out\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\n varray_read(self.base[0], place, out)\n EUDEndIf()\n return out\n\n def set_placed(self, index, place, value, modifier=SetTo, bound=None):\n if self.bits and modifier == SetTo:\n value = saturate(value, self.bits)\n v = fresh(value) if modifier == SetTo and not isinstance(value, int) else value\n if bound is True:\n varray_write(self.base[1], place, v, modifier)\n return\n i, n = as_var(index), as_var(self.field(\"len\"))\n if bound is not None:\n if EUDIf()(bound >= 1):\n varray_write(self.base[1], place, v, modifier)\n if EUDElseIf()(i < n):\n varray_write(self.base[1], place, v, modifier)\n if EUDElseIf()(i == n):\n self.push(v) # xs[xs.length] = v, as JavaScript has it (xs[n] += v pushes v: xs[n] reads 0)\n EUDEndIf()\n return\n if EUDIf()(i < n):\n varray_write(self.base[1], place, v, modifier)\n if EUDElseIf()(i == n):\n self.push(v) # xs[xs.length] = v, as JavaScript has it (xs[n] += v pushes v: xs[n] reads 0)\n EUDEndIf()\n\n def length(self):\n return self.field(\"len\")\n\n def write(self, at, value):\n \"\"\"Cell `at` of the cells (a place among them, the block's plus the index) set to `value`.\"\"\"\n if self.cells():\n cell_write(self.heap[\"vcells\"], at, value, SetTo)\n else:\n self.heap[\"cells\"][at] = value\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\n if self.cells():\n self.heap[\"vcells\"].get(self.field(\"ptr\") + index, ret=[out])\n else:\n out << self.heap[\"cells\"][self.field(\"ptr\") + index]\n EUDEndIf()\n return out\n\n def push(self, value):\n if self.bits:\n value = saturate(value, self.bits)\n if self.base is None:\n self.push_growing(value)\n return\n if not isinstance(value, int):\n value = fresh(value)\n if EUDIf()(as_var(self.handle[\"len\"]) < as_var(self.handle[\"room\"])):\n varray_write_at(self.next, value, [self.handle[\"len\"].AddNumber(1), self.next.AddNumber(VARRAY_STRIDE // 4)])\n if EUDElse()():\n self.grown()(value)\n EUDEndIf()\n\n def grown(self):\n \"\"\"A push with no room left, as one function of the array's own (the heap's grow, the handle put back, where\n the next push writes worked out again): made once, where every push of the array calls it.\"\"\"\n if getattr(self, \"_grown\", None) is None:\n @EUDFunc\n def grown(value):\n self.push_growing(value)\n self.renext()\n self._grown = grown\n return self._grown\n\n def push_growing(self, value):\n ptr, length, room, k = self.heap[\"vpush\" if self.cells() else \"push\"](self.field(\"ptr\"), self.field(\"len\"), self.field(\"room\"), self.field(\"k\"), value)\n for name, v in ((\"ptr\", ptr), (\"len\", length), (\"room\", room), (\"k\", k)):\n self.put(name, v)\n\n def set(self, index, value):\n if self.bits:\n value = saturate(value, self.bits)\n i, v, n = as_var(index), fresh(value), as_var(self.field(\"len\"))\n if EUDIf()(i < n):\n self.write(self.field(\"ptr\") + i, v)\n if EUDElseIf()(i == n):\n self.push(v) # xs[xs.length] = v, as JavaScript has it\n EUDEndIf()\n\n def pop(self):\n out = fresh(0)\n n = fresh(self.field(\"len\"))\n if EUDIf()(n >= 1):\n n -= 1\n self.put(\"len\", n)\n if self.base is not None:\n self.next -= VARRAY_STRIDE // 4\n if self.cells():\n self.heap[\"vcells\"].get(self.field(\"ptr\") + n, ret=[out])\n else:\n out << self.heap[\"cells\"][self.field(\"ptr\") + n]\n EUDEndIf()\n return out\n\n def set_length(self, value):\n v = as_var(value)\n if EUDIf()(v < as_var(self.field(\"len\"))):\n self.put(\"len\", v)\n self.renext()\n EUDEndIf()\n\n def declare(self, values):\n \"\"\"Declared (again): the block it held goes back, and it starts over with these values.\"\"\"\n ptr = fresh(self.field(\"ptr\"))\n if EUDIf()(ptr >= 1):\n self.heap[\"give\"](ptr, self.field(\"k\"))\n EUDEndIf()\n for name in (\"ptr\", \"len\", \"room\", \"k\"):\n self.put(name, 0)\n self.renext()\n for v in values:\n self.push(v)\n\n def declare_filled(self, value, count):\n \"\"\"The same with one value `count` times over: a loop, where a push a cell would be a push a cell in the map.\"\"\"\n if count <= 8:\n self.declare([value] * count)\n return\n self.declare([])\n v, i = as_var(value), fresh(0)\n if EUDWhile()(i <= count - 1):\n self.push(v)\n i += 1\n EUDEndWhile()\n\n def fill(self, value):\n v, i = as_var(value), fresh(0)\n if EUDWhile()(i < as_var(self.field(\"len\"))):\n self.write(self.field(\"ptr\") + i, v)\n i += 1\n EUDEndWhile()\n\n\nclass InnerListStorage(ListStorage):\n \"\"\"An array that grows inside another: its handle is cell `index` of four arrays the outer one keeps (where the\n block is, how many cells are in use, its room, its size class), `index` a variable of the program. Everything\n else is a growing array's, which only ever asks for its handle's fields and puts them back.\"\"\"\n\n def __init__(self, decl, lowering):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = False\n self.lowering = lowering\n self.heap = list_heap()\n self.base = None\n\n def at(self):\n return self.lowering.var(self.decl[\"through\"][\"index\"], self.decl).get()\n\n def field(self, name):\n return self.lowering.array(self.decl[\"through\"][name], self.decl).get(self.at())\n\n def put(self, name, value):\n self.lowering.array(self.decl[\"through\"][name], self.decl).set(self.at(), value)\n\n\n# ── texts ──\nTEXT_BYTES = 1023 # the most a made text holds; compiler/ir.ts has the same number\nTEXT_FIELD_BYTES = 255 # the room of a string kept for an action's field or a unit type's name\nBUILD_BYTES = 2047 # the most a buffer of buildText holds; compiler/ir.ts has the same number\nUNIT_MAP_STRING = 0x660260 # units.dat: the id of the name a map gives a unit type, a word each\n_TEXT = {}\n_SLOTS = {}\n\n\ndef texts():\n \"\"\"The texts' part of the run time, made when the first text that is made is met.\"\"\"\n if _TEXT:\n return _TEXT\n h = heap()\n cells, rooms, take, give = h[\"cells\"], h[\"rooms\"], h[\"take\"], h[\"give\"]\n # A text being made. It may run one part past its most before it is cut, and a part is at most that long again.\n scratch = Db(TEXT_BYTES + 1 + TEXT_BYTES + 1 + 8)\n empty = Db(4)\n said_cut = EUDVariable(0) # initial: said once a game\n said_full = EUDVariable(0) # initial: said once a game\n said_field = EUDVariable(0) # initial: said once a game\n\n @EUDFunc\n def count(addr):\n \"\"\"How many characters the text at addr is: every byte but those that continue a character (10xxxxxx).\"\"\"\n n = EUDVariable()\n n << 0\n br1.seekoffset(addr)\n if EUDInfLoop()():\n b = br1.readbyte()\n EUDBreakIf(b == 0)\n if EUDIfNot()(b.ExactlyX(0x80, 0xC0)):\n n += 1\n EUDEndIf()\n EUDEndInfLoop()\n EUDReturn(n)\n\n @EUDFunc\n def append(pos, src):\n \"\"\"The text at src written at pos, ended by a 0; where that 0 is. A text of the table may be any length, so no\n more of it than leaves the scratch whole: what is past a text's most is cut by make() anyway.\"\"\"\n br1.seekoffset(src)\n bw1.seekoffset(pos)\n if EUDWhile()(pos <= scratch + TEXT_BYTES + TEXT_BYTES):\n b = br1.readbyte()\n EUDBreakIf(b == 0)\n bw1.writebyte(b)\n pos += 1\n EUDEndWhile()\n bw1.writebyte(0)\n EUDReturn(pos)\n\n @EUDFunc\n def make(end):\n \"\"\"The text in the scratch, which ends at `end`, into a block of its own: where it is, the block, its\n length in characters. Past what a text holds it is cut, never inside a character.\"\"\"\n if EUDIf()(end >= scratch + TEXT_BYTES + 1):\n end << scratch + TEXT_BYTES\n if EUDWhile()(f_bread(end).ExactlyX(0x80, 0xC0)):\n end -= 1\n EUDEndWhile()\n f_bwrite(end, 0)\n if EUDIf()(said_cut == 0):\n said_cut << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: a text was cut off - one that is made holds %d bytes.\" % TEXT_BYTES)\n EUDEndIf()\n EUDEndIf()\n need = f_div(end - scratch, 4)[0] + 2\n k = EUDVariable()\n k << 0\n if EUDWhile()(rooms[k] < need):\n k += 1\n EUDEndWhile()\n block = take(k)\n if EUDIf()(block == 0):\n if EUDIf()(said_full == 0):\n said_full << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: out of memory - a text could not be made (the programs' arrays and texts share %d cells).\" % HEAP_CELLS)\n EUDEndIf()\n EUDReturn(empty, 0, 0)\n EUDEndIf()\n cells[block] = k\n f_repmovsd_epd(cells + block + 1, EPD(scratch), need - 1)\n addr = f_mul(cells + block + 1, 4) + 0x58A364\n EUDReturn(addr, block, count(addr))\n\n @EUDFunc\n def copy(addr, block, length):\n \"\"\"A text as something to keep: itself when it owns no block, else a block of the same size with the same cells.\"\"\"\n if EUDIf()(block == 0):\n EUDReturn(addr, 0, length)\n EUDEndIf()\n k = cells[block]\n mine = take(k)\n if EUDIf()(mine == 0):\n if EUDIf()(said_full == 0):\n said_full << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: out of memory - a text could not be copied (the programs' arrays and texts share %d cells).\" % HEAP_CELLS)\n EUDEndIf()\n EUDReturn(empty, 0, 0)\n EUDEndIf()\n f_repmovsd_epd(cells + mine, cells + block, rooms[k])\n EUDReturn(f_mul(cells + mine + 1, 4) + 0x58A364, mine, length)\n\n @EUDFunc\n def release(block):\n if EUDIf()(block >= 1):\n give(block, cells[block])\n EUDEndIf()\n\n @EUDFunc\n def skip(addr, n):\n \"\"\"Where character n of the text at addr starts; its end when it has fewer.\"\"\"\n br1.seekoffset(addr)\n if EUDInfLoop()():\n b = br1.readbyte()\n EUDBreakIf(b == 0)\n if EUDIfNot()(b.ExactlyX(0x80, 0xC0)):\n EUDBreakIf(n == 0)\n n -= 1\n EUDEndIf()\n addr += 1\n EUDEndInfLoop()\n EUDReturn(addr)\n\n @EUDFunc\n def size(addr):\n \"\"\"The bytes of the character that starts at addr; 0 at the text's end.\"\"\"\n b = f_bread(addr)\n n = EUDVariable()\n n << 1\n if EUDIf()(b == 0):\n n << 0\n if EUDElseIf()(b >= 0xF0):\n n << 4\n if EUDElseIf()(b >= 0xE0):\n n << 3\n if EUDElseIf()(b >= 0xC0):\n n << 2\n EUDEndIf()\n EUDReturn(n)\n\n @EUDFunc\n def slice_(addr, start, end):\n \"\"\"Characters start … end - 1 of the text at addr, made.\"\"\"\n a = skip(addr, start)\n z = EUDVariable()\n z << a\n if EUDIf()(end >= start + 1):\n z << skip(a, end - start)\n EUDEndIf()\n # A text of the table may be longer than one that is made: no more of it than the scratch holds, and make() cuts.\n n = z - a\n if EUDIf()(n >= TEXT_BYTES + 2):\n n << TEXT_BYTES + 1\n EUDEndIf()\n f_memcpy(scratch, a, n)\n f_bwrite(scratch + n, 0)\n EUDReturn(*make(scratch + n))\n\n @EUDFunc\n def pad(addr, length, width, fill, at_start):\n \"\"\"The text with `fill` over and over before or after it until it is `width` characters; as it is when it is that long, or `fill` is empty.\"\"\"\n pos = EUDVariable()\n pos << scratch\n need = EUDVariable()\n need << 0\n if EUDIf()([width >= length + 1, width <= 0x7FFFFFFF, f_bread(fill) >= 1]):\n need << width - length\n EUDEndIf()\n if EUDIf()(at_start == 0):\n pos << append(pos, addr)\n EUDEndIf()\n src = EUDVariable()\n src << fill\n if EUDWhile()([need >= 1, pos <= scratch + TEXT_BYTES]):\n n = size(src)\n if EUDIf()(n == 0):\n src << fill\n n << size(src)\n EUDEndIf()\n f_memcpy(pos, src, n)\n pos += n\n src += n\n need -= 1\n EUDEndWhile()\n f_bwrite(pos, 0)\n if EUDIf()([at_start >= 1, pos <= scratch + TEXT_BYTES]):\n pos << append(pos, addr)\n EUDEndIf()\n EUDReturn(*make(pos))\n\n @EUDFunc\n def repeat(addr, times):\n pos = EUDVariable()\n pos << scratch\n f_bwrite(pos, 0)\n if EUDIf()([times <= 0x7FFFFFFF, f_bread(addr) >= 1]):\n if EUDWhile()([times >= 1, pos <= scratch + TEXT_BYTES]):\n pos << append(pos, addr)\n times -= 1\n EUDEndWhile()\n EUDEndIf()\n EUDReturn(*make(pos))\n\n @EUDFunc\n def starts(addr, find):\n \"\"\"Whether the text at addr starts with the one at find.\"\"\"\n br1.seekoffset(addr)\n br2.seekoffset(find)\n if EUDInfLoop()():\n want = br2.readbyte()\n if EUDIf()(want == 0):\n EUDReturn(1)\n EUDEndIf()\n EUDBreakIfNot(br1.readbyte() == want)\n EUDEndInfLoop()\n EUDReturn(0)\n\n @EUDFunc\n def find_from(addr, find, start):\n \"\"\"The place, in characters, of the first match at or after character `start`; 0xFFFFFFFF when there is none.\"\"\"\n if EUDIf()(start >= count(addr) + 1):\n EUDReturn(0xFFFFFFFF)\n EUDEndIf()\n at = skip(addr, start)\n place = EUDVariable()\n place << start\n if EUDInfLoop()():\n if EUDIf()(starts(at, find) >= 1):\n EUDReturn(place)\n EUDEndIf()\n n = size(at)\n EUDBreakIf(n == 0)\n at += n\n place += 1\n EUDEndInfLoop()\n EUDReturn(0xFFFFFFFF)\n\n @EUDFunc\n def ends(addr, find):\n a, b = f_strlen(addr), f_strlen(find)\n if EUDIf()(b >= a + 1):\n EUDReturn(0)\n EUDEndIf()\n if EUDIf()(f_strcmp(addr + (a - b), find) == 0):\n EUDReturn(1)\n EUDEndIf()\n EUDReturn(0)\n\n @EUDFunc\n def code(addr, index):\n \"\"\"The number of character `index`; 0xFFFFFFFF past either end.\"\"\"\n if EUDIf()(index >= 0x80000000):\n EUDReturn(0xFFFFFFFF)\n EUDEndIf()\n at = skip(addr, index)\n n = size(at)\n if EUDIf()(n == 0):\n EUDReturn(0xFFFFFFFF)\n EUDEndIf()\n br1.seekoffset(at)\n v = EUDVariable()\n v << br1.readbyte()\n if EUDIf()(n == 2):\n v << (v & 0x1F)\n if EUDElseIf()(n == 3):\n v << (v & 0x0F)\n if EUDElseIf()(n == 4):\n v << (v & 0x07)\n EUDEndIf()\n if EUDWhile()(n >= 2):\n v << f_mul(v, 64) + (br1.readbyte() & 0x3F)\n n -= 1\n EUDEndWhile()\n EUDReturn(v)\n\n @EUDFunc\n def show(dst, src):\n \"\"\"At most TEXT_FIELD_BYTES bytes of the text at src over the string at dst, never half a character.\"\"\"\n used = EUDVariable()\n used << 0\n if EUDInfLoop()():\n n = size(src)\n EUDBreakIf(n == 0)\n if EUDIf()(used + n >= TEXT_FIELD_BYTES + 1):\n if EUDIf()(said_field == 0):\n said_field << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: a text was cut off - an action's text and a unit's name show %d bytes of one that is made.\" % TEXT_FIELD_BYTES)\n EUDEndIf()\n EUDBreak()\n EUDEndIf()\n f_memcpy(dst + used, src, n)\n used += n\n src += n\n EUDEndInfLoop()\n f_bwrite(dst + used, 0)\n\n @EUDFunc\n def address(id_):\n \"\"\"Where the text of the table with this id is; an empty one for id 0.\"\"\"\n if EUDIf()(id_ == 0):\n EUDReturn(empty)\n EUDEndIf()\n EUDReturn(GetMapStringAddr(id_))\n\n _TEXT.update(scratch=scratch, empty=empty, append=append, count=count, make=make, copy=copy, release=release, slice=slice_, pad=pad, repeat=repeat,\n starts=starts, ends=ends, find=find_from, code=code, show=show, size=size, address=address)\n return _TEXT\n\n\ndef slot(kind):\n \"\"\"The string of the built map's table kept for one kind of field, made the first time one is asked for: its\n room in bytes that nothing else shares, which is what ForceAddString is for.\"\"\"\n if kind not in _SLOTS:\n text = \"(TrigScript: %s)\" % kind\n _SLOTS[kind] = ForceAddString(text + \" \" * (TEXT_FIELD_BYTES - len(text)))\n return _SLOTS[kind]\n\n\n# The actions whose text the game shows from a string it reads again whenever it draws (played 2026-09-19), by the\n# kind of field: a player has one of each at a time, so one string a kind is enough.\nSLOT_OF_ACTION = {12: \"objectives\", 7: \"transmission\"}\nfor _t in (17, 18, 19, 20, 21, 33, 34, 35, 36, 37, 40):\n SLOT_OF_ACTION[_t] = \"leaderboard\"\n\n\ndef text_id(text):\n \"\"\"The id of a text written in the script; 0 for the empty one, which the table does not hold.\"\"\"\n return EncodeString(text) if text else 0\n\n\n_BUFFERS = {}\n\n\ndef buffer(name):\n \"\"\"A buffer of buildText: its bytes and where the writing has got to. One for the whole game, used up within the\n frame it is built in. It may run a part past its most before it is cut, and a part is at most a text's room.\"\"\"\n if name not in _BUFFERS:\n _BUFFERS[name] = (Db(BUILD_BYTES + 1 + TEXT_BYTES + 1 + 8), EUDVariable())\n return _BUFFERS[name]\n\n\n_BUILD = {}\n\n\ndef builds():\n \"\"\"What every buffer shares: copying a text onto the end of one, and cutting one that ran past its room.\"\"\"\n if _BUILD:\n return _BUILD\n said_cut = EUDVariable(0) # initial: said once a game\n\n @EUDFunc\n def append(pos, src, last):\n \"\"\"The text at src written at pos, ended by a 0; where that 0 is. Never past `last`.\"\"\"\n br1.seekoffset(src)\n bw1.seekoffset(pos)\n if EUDWhile()(pos <= last):\n b = br1.readbyte()\n EUDBreakIf(b == 0)\n bw1.writebyte(b)\n pos += 1\n EUDEndWhile()\n bw1.writebyte(0)\n EUDReturn(pos)\n\n @EUDFunc\n def cut(pos, end):\n \"\"\"A buffer whose writing got to `pos`, cut at `end` (never inside a character) when it went past it.\"\"\"\n if EUDIf()(pos > end):\n pos << end\n if EUDWhile()(f_bread(pos).ExactlyX(0x80, 0xC0)):\n pos -= 1\n EUDEndWhile()\n f_bwrite(pos, 0)\n if EUDIf()(said_cut == 0):\n said_cut << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: a text was cut off - one put together piece by piece holds %d bytes.\" % BUILD_BYTES)\n EUDEndIf()\n EUDEndIf()\n EUDReturn(pos)\n\n _BUILD.update(append=append, cut=cut)\n return _BUILD\n\n\nclass TextVal:\n \"\"\"A text as the lowering holds it: where it is, the block it owns (the int 0: none), and its length in\n characters - an int, a variable, or None when nobody has counted yet. `taken`: the block is the value's own, so\n what receives the value keeps the block or gives it back; a variable's is only looked at.\"\"\"\n\n def __init__(self, addr, block, length, taken):\n self.addr, self.block, self._length, self.taken = addr, block, length, taken\n\n def length(self):\n if self._length is None:\n self._length = texts()[\"count\"](self.addr)\n return self._length\n\n\nclass TextStorage:\n \"\"\"A text variable that is made: three cells, or three rows (of twelve in a per-player program, of a task's slots).\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.rowed = rows is not None\n self.player_of = rows\n make = (lambda: cell_array([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no text yet, and no block\n self.addr, self.block, self.len = make(), make(), make()\n\n def cell(self, cell):\n return cell[self.player_of()] if self.rowed else cell\n\n def get(self):\n # A variable that was never given a text is an empty one: its address is 0 until then.\n addr = fresh(self.cell(self.addr))\n if EUDIf()(addr == 0):\n addr << texts()[\"empty\"]\n EUDEndIf()\n return TextVal(addr, self.cell(self.block), self.cell(self.len), False)\n\n def write(self, addr, block, length):\n for cell, value in ((self.addr, addr), (self.block, block), (self.len, length)):\n if self.rowed:\n cell[self.player_of()] = value\n else:\n cell << value\n\n def take_out(self):\n \"\"\"The text, and its block with it: the variable holds no block from here on.\"\"\"\n v = self.get()\n out = TextVal(v.addr, fresh(v.block), fresh(v.length()), True)\n self.write(out.addr, 0, out.length())\n return out\n\n\nclass UnitRef:\n \"\"\"A unit of the game as the lowering holds it: the pointer, its EPD, and the slot's uniqueness byte\n (as it sits in its dword, masked 0xFF00). `uid` None is the unit of a loop's turn, there by\n construction; a pointer that is the int 0 is no unit at all.\"\"\"\n\n def __init__(self, ptr, epd, uid=None):\n self.ptr, self.epd, self.uid = ptr, epd, uid\n\n @property\n def none(self):\n return isinstance(self.ptr, int) and self.ptr == 0\n\n\nNO_UNIT = UnitRef(0, 0, 0)\n\n\nclass UnitStorage:\n \"\"\"A unit variable: three cells, or three rows (12-slot of a per-player program, of a task's slots).\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.rowed = rows is not None\n self.player_of = rows\n make = (lambda: cell_array([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no unit\n self.ptr, self.epd, self.uid = make(), make(), make()\n\n def get(self):\n if self.rowed:\n p = self.player_of()\n return UnitRef(self.ptr[p], self.epd[p], self.uid[p])\n return UnitRef(self.ptr, self.epd, self.uid)\n\n def set(self, ref):\n uid = ref.uid\n if uid is None:\n # The unit of a loop's turn is there now: this is when its slot's uniqueness byte is taken.\n uid = f_maskread_epd(ref.epd + OFF_UID, 0xFF00)\n for cell, value in ((self.ptr, ref.ptr), (self.epd, ref.epd), (self.uid, uid)):\n if self.rowed:\n cell[self.player_of()] = value\n else:\n cell << value\n\n\nclass LoopUnit:\n \"\"\"The variable of a loop over units: the scan's own pointer, never kept past the loop.\"\"\"\n\n def __init__(self, ref):\n self.ref = ref\n\n def get(self):\n return self.ref\n\n def set(self, ref):\n raise Fail(\"trigscript: the unit of a loop's turn cannot be assigned\")\n\n\ndef saturate(value, bits):\n top = (1 << bits) - 1\n if isinstance(value, int):\n return min(value, top)\n v = EUDVariable()\n v << value\n if EUDIf()(v >= top + 1):\n v << top\n EUDEndIf()\n return v\n\n\n# ── fixed-point numbers (16.16, TrigScript 4's B6): a cell is the value times 65 536, read signed ──\nFIXED_ONE = 1 << 16\nFIXED_PI = 205887 # round(pi * 65536); compiler/fixedMath.ts has the same\nFIXED_HALF_PI = 102944\nDIGIT_TEXTS = Db(b\"0\\x001\\x002\\x003\\x004\\x005\\x006\\x007\\x008\\x009\\x00\") # a digit d's text at 2 * d\nDOT_TEXT = Db(b\".\\0\\0\\0\")\n_FIXED = {}\n\n\ndef fixed_text(v):\n \"\"\"A fixed number known when the map is built, as a program prints it: compiler/fixedMath.ts#fixedText.\"\"\"\n v = signed(v)\n m = abs(v)\n whole, places = m >> 16, ((m & 0xFFFF) * 10000 + 32768) >> 16\n if places == 10000:\n whole, places = whole + 1, 0\n sign = \"-\" if v < 0 and (whole or places) else \"\"\n return sign + str(whole) + (\".\" + (\"%04d\" % places).rstrip(\"0\") if places else \"\")\n\n\ndef magnitudes(a, b):\n \"\"\"a and b made their distances from 0 in place, and 1 when exactly one of them was below zero.\"\"\"\n neg = fresh(0)\n if EUDIf()(a >= SIGN):\n a << 0 - a\n neg << 1\n EUDEndIf()\n if EUDIf()(b >= SIGN):\n b << 0 - b\n neg << 1 - neg\n EUDEndIf()\n return neg\n\n\ndef chain_fdiv(a, b):\n \"\"\"a * 65536 / b, both read from 0 up and b below 2^24: eudplib's division chain (the divisor doubled into each step's\n condition and action, set up once), run for the whole part and then twice more for eight bits of the fraction each:\n what is left is less than b, so 256 times it still fits, and its quotient is below 256. The same digits as sixteen\n steps of a bit each, which cost a dozen triggers a step. `a` is the caller's own copy, used up.\"\"\"\n whole, frac, x = EUDVariable(), EUDVariable(), EUDVariable()\n SeqCompute([(whole, SetTo, 0), (frac, SetTo, 0), (x, SetTo, b)])\n main = [(Forward(), Forward()) for _ in range(32)]\n parts = [[(Forward(), Forward()) for _ in range(8)] for _ in range(2)]\n start = [Forward() for _ in range(32)]\n for i in range(32):\n cells = [main[i]] + ([parts[0][i], parts[1][i]] if i < 8 else [])\n SeqCompute([(EPD(c), SetTo, x) for pair in cells for c in pair])\n # Past 2^31 the next doubling would wrap: the chain starts here.\n p1, p2, p3 = Forward(), Forward(), Forward()\n p1 << RawTrigger(nextptr=p2, conditions=x.AtLeastX(1, 0x80000000), actions=SetNextPtr(p1, p3))\n p3 << RawTrigger(nextptr=start[i], actions=SetNextPtr(p1, p2))\n p2 << NextTrigger()\n SeqCompute([(x, Add, x)])\n\n def steps(chain, top, into, scale):\n for i in range(top, -1, -1):\n c, act = Forward(), Forward()\n t = RawTrigger(conditions=[c << a.AtLeast(0)], actions=[act << a.SubtractNumber(0), into.AddNumber((1 << i) * scale)])\n if chain is main:\n start[i] << t\n chain[i][0] << c + 8\n chain[i][1] << act + 20\n\n steps(main, 31, whole, 1)\n for part, scale in ((parts[0], 256), (parts[1], 1)):\n for _ in range(8):\n a += a\n steps(part, 7, frac, scale)\n return shift_const(whole, \"<<\", 16) + frac\n\n\ndef fixed_fns():\n \"\"\"The fixed numbers' functions, made when the first one is met: one body each in the map, called.\"\"\"\n if \"fdiv\" in _FIXED:\n return _FIXED\n\n @EUDFunc\n def fmul(a, b):\n \"\"\"a * b / 65536 towards zero, wrapping: the four products of the 16-bit halves of the magnitudes. A half is a\n masked copy (the low) or a bit a trigger (the high), where a division by 65 536 was a 32-step loop; each product\n has its smaller side first, which eudplib's goes over the bits of and stops past the highest.\"\"\"\n neg = magnitudes(a, b)\n al, bl = fresh(a), fresh(b)\n DoActions([al.SetNumberX(0, 0xFFFF0000), bl.SetNumberX(0, 0xFFFF0000)])\n ah, bh = shift_const(a, \">>>\", 16), shift_const(b, \">>>\", 16)\n r = shift_const(f_mul(ah, bh), \"<<\", 16)\n r += f_mul(ah, bl)\n r += f_mul(bh, al)\n r += shift_const(f_mul(al, bl), \">>>\", 16)\n if EUDIf()(neg == 1):\n r << 0 - r\n EUDEndIf()\n EUDReturn(r)\n\n @EUDFunc\n def fdiv(a, b):\n \"\"\"a * 65536 / b towards zero, wrapping; a divisor of 0 gives 0. The whole part by eudplib's division, then\n sixteen more bits of the quotient, one a step, from what is left over.\"\"\"\n if EUDIf()(b == 0):\n EUDReturn(0)\n EUDEndIf()\n neg = magnitudes(a, b)\n q = EUDVariable()\n if EUDIf()(b <= (1 << 24) - 1):\n q << chain_fdiv(a, b)\n if EUDElse()():\n # (A divisor of 256 or more: the quotient's sixteen bits past the point one a step, from what is left over.)\n w, r = [fresh(x) for x in f_div(a, b)]\n for _ in range(16):\n r += r\n w += w\n if EUDIf()(r >= b):\n r -= b\n w += 1\n EUDEndIf()\n q << w\n EUDEndIf()\n if EUDIf()(neg == 1):\n q << 0 - q\n EUDEndIf()\n EUDReturn(q)\n\n def root4(x, pairs, zeros):\n \"\"\"root() kept as four times the root found so far, t: the step's trial 4r + 1 is then rem > t, no copy of r\n made a step. Only while 4r fits in 32 bits: sqrt_fixed's root is below 2^24.\"\"\"\n t, rem = fresh(0), fresh(0)\n for i in range(pairs + zeros):\n rem += rem\n rem += rem\n if i < pairs:\n for weight, bit in ((2, 31 - 2 * i), (1, 30 - 2 * i)):\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=rem.AddNumber(weight))\n if EUDIf()(rem > t):\n rem -= t\n rem -= 1\n t += t\n t += 4\n if EUDElse()():\n t += t\n EUDEndIf()\n return shift_const(t, \">>>\", 2)\n\n def root(x, pairs, zeros):\n \"\"\"The largest r with r * r <= x * 4 ** zeros, x read as `pairs` pairs of bits from the top: two bits a step.\"\"\"\n r, rem = fresh(0), fresh(0)\n for i in range(pairs + zeros):\n rem += rem\n rem += rem\n if i < pairs:\n # The next two bits of x, read where they are (x was doubled twice a step to bring them to the top).\n for weight, bit in ((2, 31 - 2 * i), (1, 30 - 2 * i)):\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=rem.AddNumber(weight))\n trial = fresh(r)\n trial += trial\n trial += trial\n trial += 1\n r += r\n if EUDIf()(rem >= trial):\n rem -= trial\n r += 1\n EUDEndIf()\n return r\n\n @EUDFunc\n def sqrt_fixed(x):\n \"\"\"The square root of a fixed number, rounded down: isqrt(x * 65536); 0 below zero.\"\"\"\n if EUDIf()(x >= SIGN):\n EUDReturn(0)\n EUDEndIf()\n EUDReturn(root4(x, 16, 8))\n\n @EUDFunc\n def sqrt_whole(x):\n \"\"\"The square root of a whole number read from 0 up, as a fixed one: isqrt(x * 2 ** 32). From 2 ** 30 up it\n is past what a fixed number holds, and gives the largest.\"\"\"\n if EUDIf()(x >= 1 << 30):\n EUDReturn(0x7FFFFFFF)\n EUDEndIf()\n EUDReturn(root(x, 16, 16))\n\n _FIXED.update(fmul=fmul, fdiv=fdiv, sqrt_fixed=sqrt_fixed, sqrt_whole=sqrt_whole)\n return _FIXED\n\n\n# A trigger's first condition's number, and its first action's destination, number and modifier (a SetDeaths, its\n# modifier the top byte of the dword: 7 SetTo, 8 Add), from the trigger's address: 8 bytes of links, 16 conditions of\n# 20 bytes, then the actions. A cell of our own is a trigger of one action that adds its number to itself: running it\n# doubles the number, and nothing routes it while only the loop it sits in runs it (TrigScript 4, the backend, part 3).\nCOND_VALUE, ACT_DEST, ACT_VALUE, ACT_MODIFIER_DWORD = 16, 344, 348, 352\n\n\n_CONST_FDIV = {}\n\n\ndef const_fdiv(c, x, bound=None):\n \"\"\"fdiv(c, x) — c × 65 536 / x towards zero — for a constant c and a variable x, the very bits fixed_fns()'s fdiv\n gives (compiler/fixedMath.ts#fixedDiv). `bound`: the most |x| can be, when ranges.ts knows. One body for each c\n (and bound) in the map, called.\"\"\"\n c = signed(c & U32)\n if c == 0:\n return 0\n key = (abs(c), bound)\n if key not in _CONST_FDIV:\n _CONST_FDIV[key] = make_const_fdiv(abs(c), bound)\n q = fresh(_CONST_FDIV[key](as_var(x)))\n if c < 0:\n q << 0 - q\n return q\n\n\ndef make_const_fdiv(c, bound):\n \"\"\"The divisor, not the dividend, stays where it is: long division the way it is done on paper, the remainder\n doubled a step and the divisor taken from it when it fits, so the divisor is written into the one comparison and\n the one subtraction of a loop of four triggers, where eudplib's division writes it, doubled, into every step it\n has (each a copy, the cost of all the rest). The dividend c × 65 536 is known: where the divisor's highest bit is\n (a search of comparisons) says how far down the dividend the first step that can take the divisor away is, and\n what the remainder is there (a constant), so only the steps that make a bit of the quotient are run. The quotient\n is counted in a cell with a bit put above it, which reaches the top on the last step: the loop's test of its end.\n Quotients of 2^31 and more (a divisor of a few 65 536ths) are fixed_fns()'s fdiv's.\"\"\"\n d = c << 16\n top_d = d.bit_length() - 1\n t_most = 31 if bound is None else max(0, min(31, bound.bit_length() - 1))\n\n def plan(t):\n \"\"\"For a divisor whose highest bit is t: (steps, the remainder before them, the dividend's bits still to\n come at the top of a cell), or None when the loop cannot (no step: the quotient is 0; too many: wraps).\"\"\"\n n = top_d - t + 1\n if n <= 0:\n return \"zero\"\n if n > 31:\n return None\n return (n, d >> n, ((d & ((1 << n) - 1)) << (32 - n)) & U32)\n\n plans = {t: plan(t) for t in range(t_most + 1)}\n with_bits = any(isinstance(v, tuple) and v[2] for v in plans.values())\n\n @EUDFunc\n def fdiv_by(x):\n out = EUDVariable()\n if EUDIf()(x == 0):\n EUDReturn(0)\n EUDEndIf()\n neg = fresh(0)\n if EUDIf()(x >= SIGN):\n x << 0 - x\n neg << 1\n EUDEndIf()\n # The quotient, the remainder and the dividend's bits still to come: three cells of our own in one trigger, the\n # three numbers doubled by one run of it.\n cells, take_bit, test, end, leave, after = Forward(), Forward(), Forward(), Forward(), Forward(), Forward()\n q, r, bits = cells + ACT_VALUE, cells + ACT_VALUE + 32, cells + ACT_VALUE + 64\n # The divisor, where the loop compares and subtracts.\n SeqCompute([(EPD(test + COND_VALUE), SetTo, x), (EPD(test + ACT_VALUE), SetTo, x)])\n # The quotient's cell back to doubling itself (the last call sent it to `out`).\n DoActions([SetMemory(cells + ACT_DEST, SetTo, EPD(q)),\n SetMemoryX(cells + ACT_MODIFIER_DWORD, SetTo, 8 << 24, 0xFF000000),\n SetNextPtr(cells, take_bit if with_bits else test)])\n how = fresh(0) # 0: the loop, 1: the quotient is 0, 2: fdiv's own way\n\n def leaf(t):\n p = plans[t]\n if p is None:\n DoActions(how.SetNumber(2))\n elif p == \"zero\":\n DoActions(how.SetNumber(1))\n else:\n n, r0, rest = p\n # (The dividend's bits a place lower than where they are tested: the cells double them first.)\n acts = [SetMemory(r, SetTo, r0), SetMemory(q, SetTo, 1 << (31 - n))]\n if with_bits:\n acts.append(SetMemory(bits, SetTo, rest >> 1))\n DoActions(acts)\n\n def tree(lo, hi):\n if lo == hi:\n leaf(lo)\n return\n mid = (lo + hi + 1) // 2\n if EUDIf()(x >= (1 << mid)):\n tree(mid, hi)\n if EUDElse()():\n tree(lo, mid - 1)\n EUDEndIf()\n\n tree(0, t_most)\n if EUDIf()(how == 0):\n cells << RawTrigger(actions=[SetMemory(q, Add, 0), SetMemory(r, Add, 0)] + ([SetMemory(bits, Add, 0)] if with_bits else []))\n if with_bits:\n # The dividend's next bit into the remainder, from the top of its cell.\n take_bit << RawTrigger(conditions=Memory(bits, AtLeast, SIGN), actions=SetMemory(r, Add, 1))\n test << RawTrigger(conditions=Memory(r, AtLeast, 0), actions=[SetMemory(r, Subtract, 0), SetMemory(q, Add, 1)])\n end << RawTrigger(nextptr=cells, conditions=Memory(q, AtLeast, SIGN), actions=SetNextPtr(end, leave))\n # The quotient's cell run once more, its number written into `out`; the bit above it taken off after.\n leave << RawTrigger(nextptr=cells, actions=[\n SetNextPtr(end, cells),\n SetMemory(cells + ACT_DEST, SetTo, EPD(out.getValueAddr())),\n SetMemoryX(cells + ACT_MODIFIER_DWORD, SetTo, 7 << 24, 0xFF000000),\n SetNextPtr(cells, after),\n ])\n after << RawTrigger(actions=out.AddNumber(SIGN))\n if EUDElseIf()(how == 1):\n out << 0\n if EUDElse()():\n out << fixed_fns()[\"fdiv\"](c, x)\n EUDEndIf()\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n return fdiv_by\n\n\n_FIXED_BY_CONST = {}\n\n\ndef fixed_by_const(name, x, c, bound=None):\n \"\"\"fmul(x, c) or fdiv(x, c) of a variable x and a constant c: the very bits fixed_fns()'s give\n (compiler/fixedMath.ts), from comparisons with constants and constants added — nothing copied. `bound`: the most\n |x| can be, when ranges.ts knows. One body for each constant (and bound) in the map, called.\"\"\"\n c = signed(c & U32)\n if c == 0:\n return 0\n key = (name, abs(c), bound)\n if key not in _FIXED_BY_CONST:\n _FIXED_BY_CONST[key] = (make_fmul_by if name == \"fmul\" else make_fdiv_by_const)(abs(c), 31 if bound is None else max(0, min(31, bound.bit_length() - 1)))\n q = fresh(_FIXED_BY_CONST[key](as_var(x)))\n if c < 0:\n q << 0 - q\n return q\n\n\ndef magnitude_of(x):\n \"\"\"x made its distance from 0 in place; 1 when it was below 0.\"\"\"\n neg = fresh(0)\n if EUDIf()(x >= SIGN):\n x << 0 - x\n neg << 1\n EUDEndIf()\n return neg\n\n\ndef make_fmul_by(c, top):\n \"\"\"x × c / 65 536 towards zero: each bit of |x| adds c times its place, split at the point — the whole part added\n into the result and the part below it gathered apart, whose whole part is added at the end (so the result is the\n product's, not a sum of rounded parts). `top`: the highest bit |x| can have.\"\"\"\n parts = [((c << j) >> 16, (c << j) & 0xFFFF) for j in range(top + 1)]\n low_bits = sum(lo for _, lo in parts).bit_length()\n\n @EUDFunc\n def fmul_by(x):\n neg = magnitude_of(x)\n out, low = fresh(0), fresh(0)\n for j, (hi, lo) in enumerate(parts):\n acts = ([out.AddNumber(hi & U32)] if hi & U32 else []) + ([low.AddNumber(lo)] if lo else [])\n if acts:\n RawTrigger(conditions=x.AtLeastX(1, 1 << j), actions=acts)\n for k in range(16, low_bits):\n RawTrigger(conditions=low.AtLeastX(1, 1 << k), actions=out.AddNumber(1 << (k - 16)))\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n return fmul_by\n\n\ndef make_fdiv_by_const(c, top):\n \"\"\"x × 65 536 / c towards zero: long division with the divisor doubled into each step when the map is built, so a\n step is one comparison with a constant. The whole part's quotient bits go straight to their place 16 higher; the\n remainder, below c, is then moved up as far as stays under 2^32 (a bit tested, its new place added) and divided\n again, until the sixteen bits below the point are found. `top`: the highest bit |x| can have.\"\"\"\n c_top = c.bit_length() - 1\n chunk = max(1, 31 - c_top)\n\n @EUDFunc\n def fdiv_by_const(x):\n neg = magnitude_of(x)\n out = fresh(0)\n # The whole part: steps from the highest multiple of c that |x| can reach.\n for i in range(min(31 - c_top, top - c_top) if top >= c_top else -1, -1, -1):\n add = (1 << (i + 16)) & U32\n RawTrigger(conditions=x.AtLeast(c << i), actions=[x.SubtractNumber(c << i)] + ([out.AddNumber(add)] if add else []))\n # Below the point: x < c now.\n done = 0\n while done < 16:\n k = min(chunk, 16 - done)\n for bit in range(c_top, -1, -1): # x <<= k, from its top bit down (each lands where nothing is yet)\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=x.AddNumber(((1 << (bit + k)) - (1 << bit)) & U32))\n done += k\n for i in range(k - 1, -1, -1):\n RawTrigger(conditions=x.AtLeast(c << i), actions=[x.SubtractNumber(c << i), out.AddNumber(1 << (i + 16 - done))])\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n return fdiv_by_const\n\n\ndef fixed_vars():\n \"\"\"fmul and fdiv of two variables (TrigScript 4, the backend, part 3): loops over cells of our own, the other side\n written once into the step that adds or subtracts it, where eudplib's ways write it, shifted, into every step.\n One body each in the map, called.\"\"\"\n if \"fmul_vars\" in _FIXED:\n return _FIXED\n\n def take_out(cells, out, after, k=0):\n \"\"\"The actions that send the cell `k` of `cells` (its first, by default) into `out` when the trigger runs next,\n and go on to `after`; the cell is put back to doubling itself when its loop starts again.\"\"\"\n return [SetMemory(cells + ACT_DEST + 32 * k, SetTo, EPD(out.getValueAddr())),\n SetMemoryX(cells + ACT_MODIFIER_DWORD + 32 * k, SetTo, 7 << 24, 0xFF000000),\n SetNextPtr(cells, after)]\n\n def put_back(cells, nxt):\n \"\"\"The cell (the first action from `cells`, an action's offset from the trigger added) back to doubling itself;\n with `nxt`, the trigger going on to it.\"\"\"\n return [SetMemory(cells + ACT_DEST, SetTo, EPD(cells + ACT_VALUE)),\n SetMemoryX(cells + ACT_MODIFIER_DWORD, SetTo, 8 << 24, 0xFF000000)] + ([SetNextPtr(cells, nxt)] if nxt is not None else [])\n\n @EUDFunc\n def fmul_vars(a, b):\n \"\"\"a × b / 65 536 towards zero. With the 16-bit halves of the magnitudes, a × b / 65 536 is ah × b + al × bh\n plus al × bl / 65 536: one loop of sixteen steps reads a bit of ah and the same bit of al (a doubled a step,\n its bits 31 and 15 tested), and doubles two sums — w gathering ah × b + al × bh, z gathering al × bl, whose\n bits from 16 up are added to w at the end.\"\"\"\n neg = magnitudes(a, b)\n bh = shift_const(b, \">>>\", 16)\n bl = fresh(b)\n DoActions(bl.SetNumberX(0, 0xFFFF0000))\n out, count = EUDVariable(), EUDVariable()\n cells, high, low, end, leave, after = (Forward() for _ in range(6))\n w, z, x = cells + ACT_VALUE, cells + ACT_VALUE + 32, cells + ACT_VALUE + 64\n SeqCompute([(EPD(high + ACT_VALUE), SetTo, b), (EPD(low + ACT_VALUE), SetTo, bh), (EPD(low + ACT_VALUE + 32), SetTo, bl), (EPD(x), SetTo, a)])\n DoActions(put_back(cells, high) + [SetMemory(w, SetTo, 0), SetMemory(z, SetTo, 0), count.SetNumber(15)])\n # A step: the bits added, then (but on the last step) the sums and a doubled.\n high << RawTrigger(conditions=Memory(x, AtLeast, SIGN), actions=SetMemory(w, Add, 0))\n low << RawTrigger(conditions=MemoryX(x, AtLeast, 1, 0x8000), actions=[SetMemory(w, Add, 0), SetMemory(z, Add, 0)])\n end << RawTrigger(conditions=count.Exactly(0), actions=SetNextPtr(end, leave))\n cells << RawTrigger(nextptr=high, actions=[SetMemory(w, Add, 0), SetMemory(z, Add, 0), SetMemory(x, Add, 0), count.SubtractNumber(1)])\n leave << RawTrigger(actions=SetNextPtr(end, cells))\n for k in range(16, 32):\n RawTrigger(conditions=MemoryX(z, AtLeast, 1, 1 << k), actions=SetMemory(w, Add, 1 << (k - 16)))\n RawTrigger(nextptr=cells, actions=take_out(cells, out, after))\n after << NextTrigger()\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n @EUDFunc\n def fdiv_vars(a, b):\n \"\"\"a × 65 536 / b towards zero; a divisor of 0 gives 0. Long division the way it is done on paper: the dividend's\n bits shifted into the remainder from the top, the divisor taken away when it fits, the quotient doubled a step\n (wrapping, as fdiv's does). The divisor is written into the one comparison and the one subtraction of the loop.\n The dividend's leading zeros first: the loop's cells run on their own until its top bit is in place, each run\n a step with nothing in it (the quotient and remainder are 0, and stay so).\"\"\"\n if EUDIf()([b == 0]):\n EUDReturn(0)\n EUDEndIf()\n if EUDIf()([a == 0]):\n EUDReturn(0)\n EUDEndIf()\n neg = magnitudes(a, b)\n out, count = EUDVariable(), EUDVariable()\n if EUDIf()(a >= SIGN):\n # (2^31, the magnitude of -2^31 only: its top bit is past where the loop starts.)\n # (fdiv's own way reads it as -2^31, and b as -2^31 too when it is 2^31: the quotient of the magnitudes is\n # its quotient, negated when only a read below zero.)\n out << fixed_fns()[\"fdiv\"](a, b)\n if EUDIf()(b <= 0x7FFFFFFF):\n out << 0 - out\n EUDEndIf()\n if EUDElse()():\n cells, ready, take_bit, test, end, leave, after = (Forward() for _ in range(7))\n q, r, d = cells + ACT_VALUE, cells + ACT_VALUE + 32, cells + ACT_VALUE + 64\n SeqCompute([(EPD(test + COND_VALUE), SetTo, b), (EPD(test + ACT_VALUE), SetTo, b), (EPD(d), SetTo, a)])\n # 47 steps: the dividend's bits 30 … 0 and sixteen 0s.\n DoActions(put_back(cells, ready) + [SetMemory(q, SetTo, 0), SetMemory(r, SetTo, 0), count.SetNumber(47)])\n # Until the dividend's top bit is at bit 30: then a run of the cells brings it to 31, where it is taken.\n ready << RawTrigger(nextptr=cells, conditions=Memory(d, AtLeast, 1 << 30), actions=SetNextPtr(cells, take_bit))\n cells << RawTrigger(nextptr=ready, actions=[SetMemory(q, Add, 0), SetMemory(r, Add, 0), SetMemory(d, Add, 0), count.SubtractNumber(1)])\n take_bit << RawTrigger(conditions=Memory(d, AtLeast, SIGN), actions=SetMemory(r, Add, 1))\n test << RawTrigger(conditions=Memory(r, AtLeast, 0), actions=[SetMemory(r, Subtract, 0), SetMemory(q, Add, 1)])\n end << RawTrigger(nextptr=cells, conditions=count.Exactly(0), actions=SetNextPtr(end, leave))\n leave << RawTrigger(nextptr=cells, actions=[SetNextPtr(end, cells)] + take_out(cells, out, after))\n after << NextTrigger()\n EUDEndIf()\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n @EUDFunc\n def udivmod_vars(a, b):\n \"\"\"a // b and a % b of two numbers read from 0 up, b not 0: fdiv_vars' loop with no bits past the point, the\n quotient and the remainder each sent out by its cell's own action. A dividend from 2^31 up (its top bit is past\n where the loop starts) is eudplib's division.\"\"\"\n q_out, r_out, count = EUDVariable(), EUDVariable(), EUDVariable()\n if EUDIf()(a >= SIGN):\n q_, r_ = f_div(a, b)\n SeqCompute([(q_out, SetTo, q_), (r_out, SetTo, r_)])\n if EUDElseIf()(a == 0):\n # (No top bit for the loop's start to wait for.)\n DoActions([q_out.SetNumber(0), r_out.SetNumber(0)])\n if EUDElse()():\n cells, ready, take_bit, test, end, leave, after = (Forward() for _ in range(7))\n q, r, d = cells + ACT_VALUE, cells + ACT_VALUE + 32, cells + ACT_VALUE + 64\n SeqCompute([(EPD(test + COND_VALUE), SetTo, b), (EPD(test + ACT_VALUE), SetTo, b), (EPD(d), SetTo, a)])\n # 31 steps: the dividend's bits 30 … 0, its leading zeros run off by the cells alone.\n DoActions(put_back(cells, ready) + put_back(cells + 32, None) + [SetMemory(q, SetTo, 0), SetMemory(r, SetTo, 0), count.SetNumber(31)])\n ready << RawTrigger(nextptr=cells, conditions=Memory(d, AtLeast, 1 << 30), actions=SetNextPtr(cells, take_bit))\n cells << RawTrigger(nextptr=ready, actions=[SetMemory(q, Add, 0), SetMemory(r, Add, 0), SetMemory(d, Add, 0), count.SubtractNumber(1)])\n take_bit << RawTrigger(conditions=Memory(d, AtLeast, SIGN), actions=SetMemory(r, Add, 1))\n test << RawTrigger(conditions=Memory(r, AtLeast, 0), actions=[SetMemory(r, Subtract, 0), SetMemory(q, Add, 1)])\n end << RawTrigger(nextptr=cells, conditions=count.Exactly(0), actions=SetNextPtr(end, leave))\n # The quotient's cell into q_out and the remainder's into r_out, by one run of the cells (another would\n # double the one not sent out yet).\n leave << RawTrigger(nextptr=cells, actions=[SetNextPtr(end, cells)] + take_out(cells, q_out, after) + take_out(cells, r_out, after, 1)[:2])\n after << NextTrigger()\n EUDEndIf()\n EUDReturn(q_out, r_out)\n\n _FIXED.update(fmul_vars=fmul_vars, fdiv_vars=fdiv_vars, udivmod_vars=udivmod_vars)\n return _FIXED\n\n\ndef divmod_vars(a, b, unsigned):\n \"\"\"(a / b, a % b) of two variables, b not 0: read from 0 up, or (`unsigned` false) towards zero, the remainder with\n the dividend's sign — udivmod_vars of the magnitudes, the signs after.\"\"\"\n f = fixed_vars()[\"udivmod_vars\"]\n if unsigned:\n return [fresh(x) for x in f(a, b)]\n x, y = fresh(a), fresh(b)\n neg_x, neg_q = fresh(0), fresh(0)\n if EUDIf()(x >= SIGN):\n x << 0 - x\n DoActions([neg_x.SetNumber(1), neg_q.SetNumber(1)])\n EUDEndIf()\n if EUDIf()(y >= SIGN):\n y << 0 - y\n neg_q << 1 - neg_q\n EUDEndIf()\n q, r = [fresh(v) for v in f(x, y)]\n if EUDIf()(neg_q == 1):\n q << 0 - q\n EUDEndIf()\n if EUDIf()(neg_x == 1):\n r << 0 - r\n EUDEndIf()\n return q, r\n\n\ndef as_var(value):\n if isinstance(value, int):\n return EUDVariable(value & U32) # initial: a constant, never written\n return value\n\n\ndef fresh(value=0):\n \"\"\"A temporary that starts from `value` every time the code runs. `EUDVariable(n)` is not\n that: n is the cell's value when the map loads, so a temporary built that way and then\n written keeps what the last run left in it — `ticks + 1` went 1, 2, 4, 8 (found in the\n first played probe, 2026-09-18). Anything the lowering writes to starts here.\"\"\"\n v = EUDVariable()\n v << value\n return v\n\n\nclass Coroutine:\n \"\"\"Where a coroutine is: the label it goes on from (0 its start, DONE once it ended) and the frames it still waits,\n a cell each, a row of twelve in a per-player program, a row a slot in a task's body. The program is one; each branch\n of a Promise.all / race is another, which runs inside the frame of what waits at that statement; a task is one too.\n `task`: the coroutine is a task's own, which a cancel() may have ended while it ran: parking leaves an end an end.\"\"\"\n\n def __init__(self, lowering, rows, initial=0, task=False):\n self.lowering = lowering\n self.state = RowCell(rows, initial)\n self.wait = RowCell(rows, 0)\n self.task = task\n self.resumes = [] # (index, Forward) for every place it can go on from\n self.frame_end = None\n\n def get_state(self):\n return self.state.get()\n\n def set_state(self, value):\n self.state.set(value)\n\n def get_wait(self):\n return self.wait.get()\n\n def set_wait(self, value):\n self.wait.set(value)\n\n def park(self, index, wait):\n \"\"\"Stop here for this frame, to go on from `index` once `wait` more frames have passed.\"\"\"\n if self.task:\n if EUDIfNot()(as_var(self.get_state()) == DONE):\n self.set_state(index)\n EUDEndIf()\n else:\n self.set_state(index)\n self.set_wait(wait)\n EUDJump(self.frame_end)\n\n def resume_point(self):\n \"\"\"A place to go on from: its number, which the state takes, and its label, to be placed where it is.\"\"\"\n index = len(self.resumes) + 1\n label = Forward()\n self.resumes.append((index, label))\n return index, label\n\n\nclass Pool:\n \"\"\"A `start` statement's tasks: `max` slots, each a row of the cells of the task's body - its coroutine's place\n (DONE: the slot is free), the generation of the task in it, the frame it last had its turn in. `slot` is the slot\n being run or started, `row` its row among the cells (the player's rows first in a per-player program, from `base`).\"\"\"\n\n def __init__(self, lowering, st):\n self.st = st\n self.task = int(st[\"task\"])\n self.max = int(st[\"max\"])\n self.slot = EUDVariable(0) # initial: the pool's own state\n self.row = EUDVariable(0) # initial: the pool's own state\n self.base = EUDVariable(0) # initial: the pool's own state\n self.said = EUDVariable(0) # initial: nothing said yet\n self.rows = Rows(lowering, lowering.per_player, self)\n self.co = Coroutine(lowering, self.rows, DONE, task=True)\n self.gen = RowCell(self.rows, 0)\n self.ran = RowCell(self.rows, 0)\n # How many of its slots are running (the player's, in a per-player program), and that count where the tasks'\n # turn can test it every time round: the slots after the last running one are not looked at.\n self.live = RowCell(Rows(lowering, True, None) if lowering.per_player else None, 0)\n self.count = self.live.store if not lowering.per_player else EUDVariable(0) # initial: the pool's own state\n # A listener's start (on.created, ...): set when one of its tasks is started and has not had its first turn, so\n # that without it and with nothing heard the tasks' turn passes the whole start by.\n self.fresh = RowCell(Rows(lowering, True, None) if lowering.per_player else None, 0) if st.get(\"listen\") is not None else None\n\n def add(self, n):\n \"\"\"One more task running (1) or one fewer (-1).\"\"\"\n if self.count is self.live.store:\n self.count += n\n return\n self.live.set(as_var(self.live.get()) + n)\n self.count += n\n\n def select(self, i):\n self.slot << i\n self.row << self.base + i\n\n def at(self, cell, slot):\n \"\"\"A cell of the slot `slot`, without selecting it: the one being run stays where it is.\"\"\"\n return cell.store[self.base + slot]\n\n\nclass SlotTable(EUDObject):\n \"\"\"A dword for each of the game's 1700 unit slots, 336 bytes apart as the slots are: a unit's own EPD plus one number is\n its cell, as eudplib's EUDLoopNewUnit keeps its marks. The space between the cells is left for anything else.\"\"\"\n\n def GetDataSize(self):\n return 4 + UNIT_SIZE * (UNIT_SLOTS - 1)\n\n def WritePayload(self, buf):\n buf.WriteDword(0)\n for _ in range(UNIT_SLOTS - 1):\n buf.WriteSpace(UNIT_SIZE - 4)\n buf.WriteDword(0)\n\n\nclass Events:\n \"\"\"The frame's events pass (on.created / on.died), before the programs' turns: the units that came onto the map since\n the pass before, and the ones that died. A slot's cell of `seen` holds the uniqueness byte of the unit seen in it (as\n it sits in its dword, masked 0xFF00) and, once that unit's death has been handed on, one more: so a unit is new when\n its slot's cell is not its byte, and a death is handed on once, and only for a unit that was seen on the map. Each\n list on the map is walked from its head to the record that was behind the head at the pass before, the units linked\n in since lying in between; when that record has left the list the walk goes to the end, which costs more and misses\n nothing. The dying list is walked whole, both ways from the record its address gives: in 1.16.1 that is the list's\n head, but Remastered links its dying list the other way round and hands an EUD map its tail there (its own code\n links a dying unit in behind the other end, which no map can read), so only a walk both ways sees every record in\n both. A record stays on it only until its sprite is gone. Nothing of the game is written.\"\"\"\n\n def __init__(self, created, died, ordered=False, morphed=False):\n self.created, self.died, self.ordered, self.morphed = created, died, ordered, morphed\n self.seen = SlotTable()\n # on.order / on.morphed: each slot's order (masked 0xFF00, as it sits in its dword) and type as the pass before found\n # them, and this frame's units that changed, each with what it was.\n self.orders = SlotTable() if ordered else None\n self.types = SlotTable() if morphed else None\n self.turned, self.turned_was = (EUDArray(EVENT_CAP), EUDArray(EVENT_CAP)) if ordered else (None, None)\n self.turned_n = EUDVariable(0) # initial: the pass's own state, set at its start\n self.changed, self.changed_was = (EUDArray(EVENT_CAP), EUDArray(EVENT_CAP)) if morphed else (None, None)\n self.changed_n = EUDVariable(0) # initial: the pass's own state, set at its start\n # Each a unit's pointer, which f_cunitepdread_epd reads back cheaply with its EPD: within the frame a slot is not\n # given to another unit (that takes a dying pass), so its uniqueness byte need not be kept.\n self.made = EUDArray(EVENT_CAP) if created else None\n self.made_n = EUDVariable(0) # initial: the pass's own state, set at its start\n self.gone = EUDArray(EVENT_CAP) if died else None\n self.gone_n = EUDVariable(0) # initial: the pass's own state, set at its start\n # For each list, as the pass before left it: the record behind the head (pointer, EPD, uniqueness byte) and the\n # head (pointer, EPD). Nothing is linked in but behind the head, so with both the same nothing came in since.\n self.bounds = {VISIBLE_HEAD: [EUDVariable(0) for _ in range(5)]} # initial: no pass before\n\n def cell(self, epd, table=None):\n return epd + (EPD(table or self.seen) - EPD(UNIT_TABLE))\n\n def walk(self, head, step):\n \"\"\"`step(ptr, epd, uid, cell, stop)` for each record from the head to the one behind the head at the pass before.\"\"\"\n bptr, bepd, buid, hptr, hepd = self.bounds[head]\n top, done, stop = Forward(), Forward(), Forward()\n # Nothing linked in since the pass before: the same head, the same record behind it (or none).\n walk = Forward()\n EUDJumpIfNot(Memory(head, Exactly, hptr), walk)\n EUDJumpIf(hptr == 0, stop)\n if EUDIf()(bptr == 0):\n EUDJumpIf(MemoryEPD(hepd + 1, Exactly, 0), stop)\n if EUDElse()():\n EUDJumpIf([MemoryEPD(hepd + 1, Exactly, bptr), MemoryXEPD(bepd + OFF_UID, Exactly, buid, 0xFF00)], stop)\n EUDEndIf()\n walk << NextTrigger()\n ptr, epd = f_cunitepdread_epd(EPD(head))\n first = fresh(1)\n top << NextTrigger()\n EUDJumpIf(ptr == 0, done)\n uid = f_maskread_epd(epd + OFF_UID, 0xFF00)\n EUDJumpIf([first == 0, ptr == bptr, uid == buid], done)\n first << 0\n step(ptr, epd, uid, self.cell(epd), stop)\n f_cunitepdread_epd(epd + 1, ret=[ptr, epd])\n EUDJump(top)\n done << NextTrigger()\n # What is behind the head now is where the next pass stops.\n f_cunitepdread_epd(EPD(head), ret=[hptr, hepd])\n bptr << 0\n if EUDIf()(hptr >= 1):\n f_cunitepdread_epd(hepd + 1, ret=[bptr, bepd])\n if EUDIf()(bptr >= 1):\n buid << f_maskread_epd(bepd + OFF_UID, 0xFF00)\n EUDEndIf()\n EUDEndIf()\n stop << NextTrigger()\n\n def walk_all(self, head, step):\n \"\"\"`step(ptr, epd, uid, cell, stop)` for every record of a list: the one at its address, those after it (+4), then\n those before it (+0).\"\"\"\n top, done, stop = Forward(), Forward(), Forward()\n EUDJumpIf(Memory(head, Exactly, 0), stop)\n ptr, epd = f_cunitepdread_epd(EPD(head))\n sepd = EUDVariable()\n sepd << epd\n link = fresh(1)\n top << NextTrigger()\n EUDJumpIf(ptr == 0, done)\n uid = f_maskread_epd(epd + OFF_UID, 0xFF00)\n step(ptr, epd, uid, self.cell(epd), stop)\n if EUDIf()(link == 1):\n f_cunitepdread_epd(epd + 1, ret=[ptr, epd])\n if EUDElse()():\n f_cunitepdread_epd(epd, ret=[ptr, epd])\n EUDEndIf()\n EUDJump(top)\n done << NextTrigger()\n # The way after it is done: now the way before it, from the record before the first.\n EUDJumpIf(link == 0, stop)\n link << 0\n f_cunitepdread_epd(sepd, ret=[ptr, epd])\n EUDJump(top)\n stop << NextTrigger()\n\n def watch(self):\n \"\"\"on.order / on.morphed: every unit on the map, its order and its type against what the pass before found. A unit\n not seen before (it comes on in this pass) only has them kept; the units that came on are taken after this. The\n type is read as its low byte (there are 228). Whether the unit was seen is asked only when something changed.\"\"\"\n top, done = Forward(), Forward()\n ptr, epd = f_cunitepdread_epd(EPD(VISIBLE_HEAD))\n top << NextTrigger()\n EUDJumpIf(ptr == 0, done)\n for table, buf, was, n, off, mask in ((self.orders, self.turned, self.turned_was, self.turned_n, OFF_OWNER_ORDER, 0xFF00),\n (self.types, self.changed, self.changed_was, self.changed_n, OFF_TYPE, 0xFF)):\n if table is None:\n continue\n now = f_maskread_epd(epd + off, mask)\n cell = self.cell(epd, table)\n if EUDIfNot()(MemoryEPD(cell, Exactly, now)):\n if EUDIf()([MemoryXEPD(epd + OFF_UID, Exactly, f_dwread_epd(self.cell(epd)), 0xFF00), n < EVENT_CAP]):\n buf[n] = ptr\n old = f_dwread_epd(cell)\n was[n] = f_bitrshift(old, 8) if mask == 0xFF00 else old\n n += 1\n EUDEndIf()\n f_dwwrite_epd(cell, now)\n EUDEndIf()\n f_cunitepdread_epd(epd + 1, ret=[ptr, epd])\n EUDJump(top)\n done << NextTrigger()\n\n def run(self):\n self.made_n << 0\n self.gone_n << 0\n self.turned_n << 0\n self.changed_n << 0\n if self.ordered or self.morphed:\n self.watch()\n\n def came(ptr, epd, uid, cell, stop):\n if EUDIfNot()(MemoryEPD(cell, Exactly, uid)):\n EUDJumpIf(self.made_n >= EVENT_CAP, stop)\n f_dwwrite_epd(cell, uid)\n if self.created:\n self.made[self.made_n] = ptr\n self.made_n += 1\n EUDEndIf()\n self.walk(VISIBLE_HEAD, came)\n if self.died:\n def went(ptr, epd, uid, cell, stop):\n if EUDIf()(MemoryEPD(cell, Exactly, uid)):\n EUDJumpIf(self.gone_n >= EVENT_CAP, stop)\n f_dwwrite_epd(cell, uid + 1)\n self.gone[self.gone_n] = ptr\n self.gone_n += 1\n EUDEndIf()\n self.walk_all(DYING_HEAD, went)\n\n\ndef uses_source(node, which):\n \"\"\"Whether a piece of the IR has a loop over the events pass's units of that kind.\"\"\"\n if isinstance(node, list):\n return any(uses_source(x, which) for x in node)\n if isinstance(node, dict):\n if node.get(\"kind\") == \"unitLoop\" and node.get(\"source\") == which:\n return True\n return any(uses_source(v, which) for v in node.values() if isinstance(v, (list, dict)))\n return False\n\n\ndef mentions(node, kind):\n \"\"\"Whether a node of that kind is anywhere in a piece of the IR.\"\"\"\n if isinstance(node, list):\n return any(mentions(x, kind) for x in node)\n if not isinstance(node, dict):\n return False\n if node.get(\"kind\") == kind:\n return True\n return any(mentions(v, kind) for v in node.values() if isinstance(v, (dict, list)))\n\n\ndef called_ids(node, out):\n \"\"\"The ids of the functions a piece of the IR calls.\"\"\"\n if isinstance(node, list):\n for x in node:\n called_ids(x, out)\n elif isinstance(node, dict):\n if node.get(\"kind\") in (\"call\", \"textCall\") and isinstance(node.get(\"call\"), dict) and node[\"call\"].get(\"fn\") is not None:\n out.add(node[\"call\"][\"fn\"])\n for v in node.values():\n if isinstance(v, (dict, list)):\n called_ids(v, out)\n return out\n\n\ndef throwing_functions(program):\n \"\"\"The called functions that may come back with an error in flight (B4): one that throws, or calls itself (its stack may\n run out), or calls one that may. Each call of one is followed by a look at the error in flight.\"\"\"\n if not program.get(\"errors\"):\n return set()\n fns = {f[\"id\"]: f for f in program.get(\"functions\", [])}\n out = {i for i, f in fns.items() if f.get(\"recursive\") or mentions(f[\"body\"], \"throw\")}\n callees = {i: called_ids(f[\"body\"], set()) for i, f in fns.items()}\n grew = True\n while grew:\n grew = False\n for i in fns:\n if i not in out and callees[i] & out:\n out.add(i)\n grew = True\n return out\n\n\ndef collect_decls(node, out):\n \"\"\"Every variable declaration in a piece of the IR, by id: what a `declare` and a loop declare, a call's\n parameters and result, a function's.\"\"\"\n if isinstance(node, list):\n for x in node:\n collect_decls(x, out)\n return\n if not isinstance(node, dict):\n return\n if isinstance(node.get(\"id\"), str) and node.get(\"kind\") in (\"number\", \"boolean\", \"unit\", \"text\") and \"shared\" in node:\n out.setdefault(node[\"id\"], node)\n for v in node.values():\n if isinstance(v, (list, dict)):\n collect_decls(v, out)\n\n\ndef mentions_array(e, array):\n \"\"\"Whether an expression reads the array anywhere in it.\"\"\"\n if isinstance(e, dict):\n if e.get(\"array\") == array:\n return True\n return any(mentions_array(x, array) for x in e.values())\n if isinstance(e, list):\n return any(mentions_array(x, array) for x in e)\n return False\n\n\ndef mentions_var(e, id_):\n \"\"\"Whether an expression reads the variable anywhere in it.\"\"\"\n if isinstance(e, dict):\n if e.get(\"kind\") == \"var\" and e.get(\"id\") == id_:\n return True\n return any(mentions_var(x, id_) for x in e.values())\n if isinstance(e, list):\n return any(mentions_var(x, id_) for x in e)\n return False\n\n\n# Expressions that read and do nothing else: worked out at any point of a statement, they give the same answer.\nPLAIN_KINDS = {\"const\", \"var\", \"cast\", \"unary\", \"binary\", \"element\", \"length\"}\n\n\ndef plain(e):\n if not isinstance(e, dict) or \"kind\" not in e:\n return True # (not an expression: a number, a position)\n if e.get(\"kind\") not in PLAIN_KINDS:\n return False\n return all(plain(v) for k, v in e.items() if isinstance(v, dict))\n\n\nclass Lowering:\n def __init__(self, program):\n self.p = program\n self.per_player = bool(program.get(\"perPlayer\"))\n self.owner = int(program.get(\"owner\", 0))\n self.slots = owner_slots(program)\n self.player = None\n self.vars = {}\n # Every variable of the program by id, and those made before their declaration was lowered: a function is\n # lowered where it is first called, which may come before a variable it uses is declared (a function kept\n # as a value, B3, is lowered with every other of its dispatcher's).\n self.decls = {}\n collect_decls(program, self.decls)\n self.early = set()\n # The `start` statements' tasks, by their number, in that order; the one whose body is being lowered.\n starts = []\n each_start([program.get(\"body\", []), program.get(\"functions\", [])], starts.append)\n self.pools = {}\n self.pool = None\n for st in sorted(starts, key=lambda st: int(st[\"task\"])):\n self.pools[int(st[\"task\"])] = Pool(self, st)\n self.tick = EUDVariable(0) # initial: frames the tasks have run\n self.again = EUDVariable(0) # initial: no task started during the tasks' turn\n self.arrays = {}\n for a in program.get(\"arrays\", []):\n if a.get(\"slice\"):\n self.arrays[a[\"id\"]] = SliceStorage(a, self)\n elif a.get(\"through\"):\n self.arrays[a[\"id\"]] = InnerListStorage(a, self)\n else:\n self.arrays[a[\"id\"]] = (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.rows_of(a))\n # Counters whose offset into the arrays they index is kept beside them (`Place`), by id; a growing array read\n # at one keeps where its block starts.\n self.places = {}\n # Inside a loop over a growing array at a counter, the arrays read there unchecked: index id → {array id: True,\n # or the flag the loop set on the way in}.\n self.inside = {}\n # The loops whose accesses at their counter go through registers (`Cursors`), by the counter's id.\n self.cursors = {}\n counters, saved = find_counters(program), saved_arrays(program)\n\n def find_places(node):\n if isinstance(node, list):\n for x in node:\n find_places(x)\n elif isinstance(node, dict):\n idx = node.get(\"index\")\n if node.get(\"kind\") in (\"element\", \"store\") and isinstance(idx, dict) and idx.get(\"kind\") == \"var\" and idx.get(\"id\") in counters:\n d, a = self.decls.get(idx[\"id\"]), self.arrays.get(node.get(\"array\"))\n if d is not None and self.rows_of(d) is None:\n if type(a) is ListStorage and node[\"array\"] not in saved:\n a.keep_base()\n if a is not None and getattr(a, \"placeable\", lambda: False)() and idx[\"id\"] not in self.places:\n self.places[idx[\"id\"]] = Place()\n for v in node.values():\n if isinstance(v, (list, dict)):\n find_places(v)\n\n find_places([program.get(\"body\", []), program.get(\"functions\", [])])\n # Every plain growing array keeps where its block starts, for a push with room to be a write and two adds.\n for id_, a in self.arrays.items():\n if type(a) is ListStorage and id_ not in saved:\n a.keep_base()\n # A task's parameters are set where it is started, which may be lowered after its body: theirs are made now.\n for pool in self.pools.values():\n for prm in pool.st[\"call\"][\"params\"]:\n self.declare(prm[\"decl\"])\n # The program's own coroutine, and the one being lowered (a branch of a Promise.all / race inside it, a task).\n self.top = Coroutine(self, self.rows())\n self.co = self.top\n self.latches = {}\n # The functions that are called, by id, and each one's EUDFunc once something has called it.\n self.functions = {f[\"id\"]: f for f in program.get(\"functions\", [])}\n self.made = {}\n # B4: the error in flight's variables (IR `errors`), the catches the lowering is inside, innermost last — or, in a\n # called function's body, its end, which it returns from with the error still in flight — and the functions whose\n # calls are followed by a look at it. The line an error was thrown at is a cell of the lowering's own.\n self.errors = program.get(\"errors\")\n self.handlers = []\n self.throwing = throwing_functions(program)\n if self.errors:\n self.declare({\"id\": \"(error line)\", \"name\": \"(error line)\", \"kind\": \"number\", \"shared\": True})\n self.in_function = 0\n # The function being lowered, when it is one that calls itself: whose return address a frame keeps.\n self.within = []\n\n # ── storage ──\n def player_of(self):\n if self.player is None:\n raise Fail(\"trigscript: a per-player variable outside the player loop\")\n return self.player\n\n def rows(self):\n \"\"\"The rows a cell of the lowering's own has where the lowering is: a player's, a task's slot's, or none.\"\"\"\n return Rows(self, self.per_player, self.pool) if self.per_player or self.pool else None\n\n def rows_of(self, decl):\n \"\"\"The rows of a variable's or an array's cells: one a player unless it is shared, one a slot of its task.\"\"\"\n pool = self.pools.get(decl[\"task\"]) if decl.get(\"task\") is not None else None\n per_player = self.per_player and not decl.get(\"shared\")\n return Rows(self, per_player, pool) if per_player or pool else None\n\n def declare(self, decl):\n made = self.vars.get(decl[\"id\"])\n if made is not None and decl.get(\"task\") is not None:\n return made # (a task's parameter, made with its pool)\n if made is not None and decl[\"id\"] in self.early:\n self.early.discard(decl[\"id\"])\n return made # (used by a function lowered before the declaration was)\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, Rows(self, self.per_player, self.pools.get(decl.get(\"task\"))) if self.per_player or decl.get(\"task\") is not None else None)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.rows_of(decl))\n else:\n s = Storage(decl, self.rows_of(decl))\n self.vars[decl[\"id\"]] = s\n return s\n\n def array(self, id_, node=None):\n a = self.arrays.get(id_)\n if a is None:\n raise Fail(\"trigscript: unknown array %r%s\" % (id_, where(node)))\n return a\n\n def var(self, id_, node=None):\n s = self.vars.get(id_)\n if s is None and id_ in self.decls:\n s = self.declare(self.decls[id_])\n self.early.add(id_)\n if s is None:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def place_of(self, index, a):\n \"\"\"The counter's `Place` when `index` is one and the array reads and writes at it; else None.\"\"\"\n if index.get(\"kind\") != \"var\" or index.get(\"id\") not in self.places:\n return None\n return self.places[index[\"id\"]] if getattr(a, \"placeable\", lambda: False)() else None\n\n def bound_of(self, index, array_id):\n return self.inside.get(index.get(\"id\"), {}).get(array_id) if index.get(\"kind\") == \"var\" else None\n\n def step_place(self, id_, value):\n place = self.places.get(id_)\n if place is not None:\n place.step(counter_step(value, id_))\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\n def cell_step(self, st, a):\n \"\"\"(op, step) when a store is the cell it stores into plus or minus a number - the same array, the same index,\n an index that reads nothing else (a constant or a variable) - into a plain array of whole numbers; else None.\"\"\"\n v, i = st[\"value\"], st[\"index\"]\n placed_list = type(a) is ListStorage and a.placeable() and self.place_of(i, a) is not None\n if not (isinstance(a, ArrayStorage) or placed_list) or a.bits or getattr(a, \"values\", None) is not None or a.decl[\"kind\"] != \"number\" or a.decl.get(\"fixed\"):\n return None\n if v.get(\"kind\") != \"binary\" or v.get(\"op\") not in (\"+\", \"-\") or v.get(\"unsigned\"):\n return None\n left = v[\"left\"]\n if left.get(\"kind\") != \"element\" or left.get(\"array\") != st[\"array\"] or i.get(\"kind\") not in (\"const\", \"var\") or left.get(\"index\") != i:\n return None\n if mentions_array(v[\"right\"], st[\"array\"]):\n return None\n return (v[\"op\"], v[\"right\"])\n\n def assign_in_place(self, target, st):\n \"\"\"A store worked out in the variable's own cell, step by step: `x = x + e` an add where it is (`x = e + x`,\n `x = x - e` too), `x = y + e` a copy and an add, `x = x & c` / `x | c` a masked write of the cell, and these\n nested (`k = (k + 1) & 15` an add and a mask). Each `e` only reads and does not read x, so the order of the\n reads cannot matter. False when the store is not of that shape (nothing is emitted then).\"\"\"\n if not isinstance(target, Storage) or target.bits:\n return False\n steps = self.into_steps(target, st[\"value\"], st[\"target\"])\n if not steps or steps == [(\"set\", st[\"value\"])]:\n return False\n for step in steps:\n if step[0] == \"set\":\n target.set(self.num(step[1]))\n elif step[0] == \"add\":\n target.add(self.num(step[2]), step[1])\n else:\n target.mask(step[1], step[2])\n return True\n\n def into_steps(self, target, v, id_):\n \"\"\"The steps that work `v` out in the target's own cell, or None when they cannot.\"\"\"\n is_me = lambda e: e.get(\"kind\") == \"var\" and e.get(\"id\") == id_\n if is_me(v):\n return []\n k, op = v.get(\"kind\"), v.get(\"op\")\n if k in (\"const\", \"var\"):\n return None if target.rowed else [(\"set\", v)]\n if k != \"binary\" or op not in (\"+\", \"-\", \"&\", \"|\"):\n return None\n left, right = v[\"left\"], v[\"right\"]\n if op in (\"+\", \"-\"):\n if op == \"+\" and is_me(right) and not is_me(left):\n left, right = right, left\n if not plain(right) or mentions_var(right, id_) or not plain(left):\n return None\n first = self.into_steps(target, left, id_)\n if first is None or (target.rowed and (first or (op == \"-\" and right.get(\"kind\") != \"const\"))):\n return None\n return first + [(\"add\", op, right)]\n # & | with a constant: a masked write of the cell (the other side worked out into it first).\n if target.rowed:\n return None\n c, other = (right, left) if right.get(\"kind\") == \"const\" else (left, right) if left.get(\"kind\") == \"const\" else (None, None)\n if c is None or not plain(other):\n return None\n first = self.into_steps(target, other, id_)\n return None if first is None else first + [(\"mask\", op, int(c[\"value\"]) & U32)]\n\n def num(self, e):\n k = e[\"kind\"]\n if k in (\"textLength\", \"textIndexOf\", \"textCode\"):\n return self.text_number(e)\n if k == \"input\":\n return INPUT.read(e[\"input\"], self, e)\n if k == \"const\":\n return int(e[\"value\"]) & U32\n if k == \"var\":\n return self.var(e[\"id\"], e).get()\n if k == \"element\":\n a = self.array(e[\"array\"], e)\n place = self.place_of(e[\"index\"], a)\n if place is not None:\n cur = self.cursor_at(e[\"index\"], a)\n if cur is not None:\n return self.cursor_read(cur, a, e)\n return a.get_placed(self.num(e[\"index\"]), place, self.bound_of(e[\"index\"], e[\"array\"]))\n return a.get(self.num(e[\"index\"]))\n if k == \"length\":\n a = self.array(e[\"array\"], e)\n return a.length() if isinstance(a, ListStorage) else a.length\n if k == \"pop\":\n return self.array(e[\"array\"], e).pop()\n if k == \"cast\":\n return self.num(e[\"expr\"])\n if k == \"unary\":\n x = self.num(e[\"expr\"])\n return (-x) & U32 if isinstance(x, int) else 0 - x\n if k == \"binary\":\n return self.binary(e)\n if k == \"read\":\n return self.read(e)\n if k == \"taskHandle\":\n # The task whose turn it is, as start() numbered it; 0 in the program's own turn.\n pool = self.pool\n if pool is None:\n return 0\n return pool.slot + pool.task * 256 + f_bitlshift(fresh(pool.gen.get()), 16)\n if k == \"randomInt\":\n # 0 … n - 1; an n of 0 gives 0, as a 0 divisor does.\n n = self.num(e[\"bound\"])\n if isinstance(n, int):\n if n <= 1:\n return 0\n return f_div(f_dwrand(), n)[1]\n out = fresh(0)\n if EUDIf()(n >= 1):\n out << f_div(f_dwrand(), n)[1]\n EUDEndIf()\n return out\n if k == \"unitField\":\n return self.unit_field(e)\n if k == \"unitPart\":\n return self.unit_part(e)\n if k == \"tableRead\":\n return self.table_read(e[\"cell\"], e)\n if k == \"ternary\":\n t = EUDVariable()\n if EUDIf()(self.cond(e[\"cond\"])):\n t << self.num(e[\"whenTrue\"])\n if EUDElse()():\n t << self.num(e[\"whenFalse\"])\n EUDEndIf()\n return t\n if k == \"intrinsic\" and e[\"name\"] == \"abs\":\n x = self.num(e[\"args\"][0])\n if isinstance(x, int):\n return abs(signed(x)) & U32\n if top_of(e[\"args\"][0]) is not None:\n return x\n t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\" and e[\"name\"] in (\"fmul\", \"fdiv\", \"sqrt\", \"atan2\", \"sin\", \"cos\"):\n return self.fixed_op(e)\n if k == \"intrinsic\":\n a, b = self.nums(e[\"args\"])\n name = e[\"name\"]\n unsigned = bool(e.get(\"unsigned\"))\n if isinstance(a, int) and isinstance(b, int):\n key = (lambda v: v) if unsigned else signed\n return min(a, b, key=key) if name == \"min\" else max(a, b, key=key)\n # One side always the smaller (ranges.ts): no comparison. Read unsigned, only when both are never below zero.\n ra, rb = rng(e[\"args\"][0]), rng(e[\"args\"][1])\n if ra is not None and rb is not None and (not unsigned or (ra[0] >= 0 and rb[0] >= 0)):\n if ra[1] <= rb[0]:\n return a if name == \"min\" else b\n if rb[1] <= ra[0]:\n return b if name == \"min\" else a\n if isinstance(a, int) or isinstance(b, int):\n return clamp_const(b if isinstance(a, int) else a, a if isinstance(a, int) else b, name, unsigned)\n t = EUDVariable()\n if EUDIf()(self.ordered(a, \"<=\" if name == \"min\" else \">=\", b, unsigned)):\n t << a\n if EUDElse()():\n t << b\n EUDEndIf()\n return t\n if k == \"call\":\n return self.call(e[\"call\"])\n raise Fail(\"trigscript: unknown expression %r%s\" % (k, where(e)))\n\n def fixed_op(self, e):\n \"\"\"fmul / fdiv / sqrt / atan2 of fixed numbers (raw cells): worked out here when every argument is known, else a\n call of the function fixed_fns() makes. compiler/fixedMath.ts is the same arithmetic for the simulator.\"\"\"\n name = e[\"name\"]\n args = self.nums(e[\"args\"])\n if all(isinstance(x, int) for x in args):\n return fixed_const(name, [signed(x) for x in args], e.get(\"unsigned\"), [signed(v) for v in self.array(e[\"table\"], e).values] if name in (\"atan2\", \"sin\", \"cos\") else None) & U32\n fns = fixed_fns()\n bound = lambda i: (lambda r: None if r is None else max(abs(r[0]), abs(r[1])))(rng(e[\"args\"][i]))\n if name == \"fmul\":\n if isinstance(args[1], int):\n return fixed_by_const(\"fmul\", args[0], args[1], bound(0))\n if isinstance(args[0], int):\n return fixed_by_const(\"fmul\", args[1], args[0], bound(1))\n return fresh(fixed_vars()[\"fmul_vars\"](*[as_var(x) for x in args]))\n if name == \"fdiv\":\n if isinstance(args[0], int):\n return const_fdiv(args[0], args[1], bound(1))\n if isinstance(args[1], int):\n return fixed_by_const(\"fdiv\", args[0], args[1], bound(0))\n return fresh(fixed_vars()[\"fdiv_vars\"](*[as_var(x) for x in args]))\n if name == \"sqrt\":\n return fresh(fns[\"sqrt_whole\" if e.get(\"unsigned\") else \"sqrt_fixed\"](as_var(args[0])))\n if name in (\"sin\", \"cos\"):\n return self.sine(args[0], args[1], self.array(e[\"table\"], e), name == \"cos\")\n return self.atan2(args[0], args[1], self.array(e[\"table\"], e), e)\n\n @staticmethod\n def between(table, i, f, bits):\n \"\"\"table[i] + (table[i + 1] - table[i]) * f / 2 ** bits, the part of the step towards zero: a table read between\n its cells (compiler/fixedMath.ts#between). `i + 1` past the end reads 0, as any cell past the end does.\"\"\"\n a, b = fresh(table.get(i)), fresh(table.get(i + 1))\n d, out = fresh(b - a), fresh(a)\n if EUDIf()(d >= SIGN):\n d << 0 - d\n out -= f_div(f_mul(d, f), 1 << bits)[0]\n if EUDElse()():\n out += f_div(f_mul(d, f), 1 << bits)[0]\n EUDEndIf()\n return out\n\n def sine(self, x, scale, table, cos):\n \"\"\"sin(x) of a fixed number of radians (x * scale the place in the 4096 cells of the table), or with `cos` a\n quarter turn on: compiler/fixedMath.ts#fixedSin. The next cell is cell 0 again at the end of the turn.\"\"\"\n at = fresh(fixed_fns()[\"fmul\"](as_var(x), as_var(scale)))\n f = fresh(at & 0xFFFF)\n i = fresh(f_bitrshift(at, 16))\n if cos:\n i += 1024\n i << (i & 4095)\n # Cell 4096 is cell 0: the table read with its first cell after its last.\n a, b = fresh(table.get(i)), fresh(0)\n if EUDIf()(i == 4095):\n b << table.get(0)\n if EUDElse()():\n b << table.get(i + 1)\n EUDEndIf()\n d, out = fresh(b - a), fresh(a)\n if EUDIf()(d >= SIGN):\n d << 0 - d\n out -= f_div(f_mul(d, f), 1 << 16)[0]\n if EUDElse()():\n out += f_div(f_mul(d, f), 1 << 16)[0]\n EUDEndIf()\n return out\n\n def atan2(self, y, x, table, node):\n \"\"\"atan2(y, x) of fixed numbers: the smaller side over the larger to the nearest 1/1024, read from the table\n (atan(i / 1024)), then put in its octant: compiler/fixedMath.ts#fixedAtan2.\"\"\"\n fns = fixed_fns()\n ax, ay, t = fresh(x), fresh(y), fresh(0)\n if EUDIf()(ax >= SIGN):\n ax << 0 - ax\n EUDEndIf()\n if EUDIf()(ay >= SIGN):\n ay << 0 - ay\n EUDEndIf()\n if EUDIfNot()(EUDAnd(ax == 0, ay == 0)):\n if EUDIf()(ay <= ax):\n i, f = [fresh(v) for v in f_div(fns[\"fdiv\"](ay, ax), 64)]\n t << self.between(table, i, f, 6)\n if EUDElse()():\n i, f = [fresh(v) for v in f_div(fns[\"fdiv\"](ax, ay), 64)]\n t << FIXED_HALF_PI - self.between(table, i, f, 6)\n EUDEndIf()\n if EUDIf()(as_var(x) >= SIGN):\n t << FIXED_PI - t\n EUDEndIf()\n if EUDIf()(as_var(y) >= SIGN):\n t << 0 - t\n EUDEndIf()\n EUDEndIf()\n return t\n\n def fixed_pieces(self, v):\n \"\"\"A fixed number as printed pieces: its sign, its whole part, and the point and up to four places, without the\n zeros at the end. The most it comes to is 11 bytes.\"\"\"\n if isinstance(v, int):\n return [fixed_text(v)]\n sign, m = fresh(NO_SIGN), fresh(v)\n if EUDIf()(m >= SIGN):\n sign << MINUS_SIGN\n m << 0 - m\n EUDEndIf()\n whole, frac = [fresh(x) for x in f_div(m, FIXED_ONE)]\n places = fresh(f_div(f_mul(frac, 10000) + 32768, FIXED_ONE)[0])\n if EUDIf()(places == 10000):\n whole += 1\n places << 0\n EUDEndIf()\n if EUDIf()(EUDAnd(whole == 0, places == 0)):\n sign << NO_SIGN\n EUDEndIf()\n # The four places as digits, then each one's text, or none once only zeros are left after it.\n digits = []\n rest = fresh(places)\n for unit in (1000, 100, 10):\n d, rest2 = [fresh(x) for x in f_div(rest, unit)]\n digits.append(d)\n rest << rest2\n digits.append(rest)\n texts = [fresh(NO_SIGN) for _ in range(4)]\n dot = fresh(NO_SIGN)\n shown = fresh(0)\n for i in (3, 2, 1, 0):\n if EUDIf()(digits[i] >= 1):\n shown << 1\n EUDEndIf()\n if EUDIf()(shown == 1):\n texts[i] << DIGIT_TEXTS + digits[i] + digits[i]\n EUDEndIf()\n if EUDIf()(shown == 1):\n dot << DOT_TEXT\n EUDEndIf()\n return [ptr2s(sign), whole, ptr2s(dot)] + [ptr2s(t) for t in texts]\n\n def nums(self, exprs):\n \"\"\"Numbers worked out left to right, as JavaScript does: a value that is not a constant is copied when an\n expression after it holds a call, which may change what it reads (`a + bump()` read a after bump() did).\"\"\"\n out = []\n for i, x in enumerate(exprs):\n v = self.num(x)\n if not isinstance(v, int) and has_call(exprs[i + 1:]):\n v = fresh(v)\n out.append(v)\n return out\n\n def binary(self, e):\n a, b = self.nums([e[\"left\"], e[\"right\"]])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b, top_of(e[\"left\"]))\n both = isinstance(a, int) and isinstance(b, int)\n if op == \"+\":\n return (a + b) & U32 if both else a + b\n if op == \"-\":\n return (a - b) & U32 if both else a - b\n if op == \"*\":\n if both:\n return (a * b) & U32\n # By a power of two, a shift: x * 32 is x << 5, a fifth of the cost of eudplib's general product.\n k, v = (b, a) if isinstance(b, int) else (a, b) if isinstance(a, int) else (None, None)\n vtop = top_of(e[\"left\"] if k is b else e[\"right\"]) if k is not None else None\n if k is not None and k > 0 and k & (k - 1) == 0:\n return v if k == 1 else shift_const(v, \"<<\", k.bit_length() - 1, vtop)\n if k is not None and vtop is not None and mul_bits_pays(vtop, k):\n # The other side known to be 0 ... vtop (ranges.ts): a trigger a bit it can have.\n return mul_bits(as_var(v), vtop, k)\n if k is not None:\n # eudplib's product goes over its first operand's bits and stops past the highest that is set: whichever\n # of the two is smaller first, the constant by its distance from 0 (a number below zero has every top\n # bit set), negated after.\n m = signed(k)\n kv = EUDVariable(abs(m)) # initial: a constant, never written\n r = EUDVariable()\n if EUDIf()(v <= abs(m) - 1):\n f_mul(v, kv, ret=[r])\n if EUDElse()():\n f_mul(kv, v, ret=[r])\n EUDEndIf()\n return 0 - r if m < 0 else r\n # eudplib's product goes over its first operand's bits, up to the highest set: the side with fewer first.\n ta, tb = top_of(e[\"left\"]), top_of(e[\"right\"])\n if tb is not None and (ta is None or tb < ta):\n a, b = b, a\n return f_mul(as_var(a), as_var(b))\n # / and %: towards zero, the remainder with the dividend's sign, unless both sides are u32s. A divisor of 0 gives 0.\n unsigned = bool(e.get(\"unsigned\"))\n pick = (lambda q, r: q) if op == \"/\" else (lambda q, r: r)\n # Both sides known never below zero (ranges.ts): the division is the same read either way, and takes only the\n # bits the dividend can have. A dividend that is a constant is its own top.\n atop = a if isinstance(a, int) and a < SIGN else top_of(e[\"left\"])\n brange = (signed(b), signed(b)) if isinstance(b, int) else rng(e[\"right\"])\n if atop is not None and not both and brange is not None and brange[0] >= 1 and (atop >> 1).bit_length() <= 24:\n if isinstance(b, int):\n return pick(*div_bits(as_var(a), b, atop))\n return pick(*div_var_bits(as_var(a), b, atop))\n if isinstance(b, int):\n if b == 0:\n raise Fail(\"trigscript: division by zero%s\" % where(e))\n if both:\n if unsigned:\n return pick(a // b, a % b)\n x, y = signed(a), signed(b)\n q = abs(x) // abs(y) * (-1 if (x < 0) != (y < 0) else 1)\n return pick(q, x - q * y) & U32\n return pick(*(f_div(a, b) if unsigned else f_div_towards_zero(fresh(a), signed(b))))\n out = fresh(0)\n if EUDIf()(b >= 1):\n if atop is not None and brange is not None and brange[0] >= 0 and (atop >> 1).bit_length() <= 24:\n q, r = div_var_bits(as_var(a), b, atop)\n else:\n q, r = divmod_vars(as_var(a), b, unsigned)\n out << pick(q, r)\n EUDEndIf()\n return out\n\n def ordered(self, a, op, b, unsigned):\n \"\"\"`a op b` as one condition, the two read as u32s (`unsigned` true), as signed numbers (false), or one of\n each (\"left\" / \"right\" names the u32) - exactly: a number below zero is smaller than any u32. At least one\n side is a variable. A signed order is the unsigned one with the top bit of both sides flipped.\"\"\"\n if unsigned in (\"left\", \"right\"):\n # s is the signed side, u the u32, and the comparison is turned to read s op u.\n s_, u_ = (b, a) if unsigned == \"left\" else (a, b)\n if unsigned == \"left\":\n op = FLIPPED[op]\n if isinstance(s_, int):\n if s_ >= SIGN:\n return always(op in (\"<\", \"<=\", \"!=\"))\n return self.ordered(s_, op, u_, True)\n sv = as_var(s_)\n # The signed side at or above zero, and the order between the two read from 0 up: one trigger's conditions,\n # ANDed by the game (EUDAnd is a branch an item). < <= != are the other three turned round and negated.\n if op in (\"<\", \"<=\", \"!=\"):\n return not_all([sv <= SIGN - 1, relation(sv, {\"<\": \">=\", \"<=\": \">\", \"!=\": \"==\"}[op], u_)])\n return [sv <= SIGN - 1, relation(sv, op, u_)]\n if isinstance(a, int):\n a, b, op = b, a, FLIPPED[op]\n if unsigned or op in (\"==\", \"!=\"):\n return relation(as_var(a), op, b)\n if not isinstance(b, int):\n return signed_order(as_var(a), op, as_var(b))\n return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b, atop=None):\n \"\"\"& | ^ << >> >>> over 32 bits. `>>` keeps the sign of what it shifts and `>>>` does not; a shift by 32 or\n more leaves nothing but that sign. `atop`: the left side is known to be 0 ... atop (ranges.ts).\"\"\"\n if op in (\"<<\", \">>\", \">>>\") and isinstance(b, int) and not isinstance(a, int) and 0 < b < 32:\n return shift_const(a, op, b, atop)\n if op in (\"<<\", \">>\", \">>>\"):\n if isinstance(a, int) and isinstance(b, int):\n if op == \"<<\":\n return 0 if b >= 32 else (a << b) & U32\n if op == \">>>\":\n return 0 if b >= 32 else a >> b\n return (signed(a) >> min(b, 31)) & U32\n if isinstance(b, int) and b == 0:\n return a\n if isinstance(b, int) and b >= 32 and op != \">>\":\n return 0\n if op == \"<<\":\n return f_bitlshift(fresh(a), b)\n if op == \">>>\":\n return f_bitrshift(fresh(a), b)\n # The sign kept: a number below zero is shifted as its complement, which has zeros where it has ones.\n x, below = fresh(a), fresh(0)\n if EUDIf()(x >= SIGN):\n below << 1\n x << ~x\n EUDEndIf()\n r = fresh(f_bitrshift(x, b)) if not (isinstance(b, int) and b >= 32) else fresh(0)\n if EUDIf()(below >= 1):\n r << ~r\n EUDEndIf()\n return r\n if isinstance(a, int) and isinstance(b, int):\n return {\"&\": a & b, \"|\": a | b, \"^\": a ^ b}[op]\n if op in (\"&\", \"|\") and (isinstance(a, int) or isinstance(b, int)):\n # Against a constant: a copy and one masked write (eudplib's & of two variables tests a bit a trigger).\n c, x = (a, b) if isinstance(a, int) else (b, a)\n t = fresh(x)\n DoActions(_mask_action(t, op, c))\n return t\n # Copies: eudplib computes in place into an operand nothing else refers to, and ours may be a variable's own cell.\n x, y = fresh(a), fresh(b)\n return x & y if op == \"&\" else x | y if op == \"|\" else x ^ y\n\n # ── reads: a value of the game ──\n def current(self):\n \"\"\"The player the program is running as: a slot, or the player loop's variable.\"\"\"\n return self.player if self.per_player else self.slots[0]\n\n def one_player(self, p, node):\n if p == CURRENT_PLAYER:\n return self.current()\n if 0 <= p < 12:\n return p\n raise Fail(\"trigscript: a read takes one player%s\" % where(node))\n\n def read(self, e):\n r = e[\"read\"]\n source = r.get(\"source\")\n if source == \"condition\":\n return self.read_condition(r[\"record\"], e)\n if source == \"player\" and r.get(\"fact\") == \"left\":\n return self.read_left(r, e)\n if source == \"player\":\n base = PLAYER_BYTES.get(r.get(\"fact\"))\n if base is None:\n raise Fail(\"trigscript: unknown player fact %r%s\" % (r.get(\"fact\"), where(e)))\n value = f_bread(base + self.one_player(r[\"player\"], e))\n if r.get(\"fact\") == \"slot\":\n # A computer of a Use Map Settings game keeps the map's own number, 5; a melee computer is 1\n # (the slice 2 probe read 5 for one). Both are \"a computer\": one number for the script.\n kind = fresh(value)\n if EUDIf()(kind == 5):\n kind << 1\n EUDEndIf()\n return kind\n return value\n if source == \"supply\":\n return self.read_supply(r, e)\n raise Fail(\"trigscript: unknown read %r%s\" % (source, where(e)))\n\n def read_left(self, r, e):\n \"\"\"1 once a player who was in the map's settings as a human or a computer is gone. Asked the way\n eudplib's f_playerexist asks (the player's trigger list), which the player loop already relies on.\n The byte table at 0x581D62 read 0 for players who were there, as it should, but nobody has seen it\n turn 1 in a game — a computer never leaves — so it is not what this rests on.\"\"\"\n p = self.one_player(r[\"player\"], e)\n if isinstance(p, int) and (p >= 8 or GetPlayerInfo(p).typestr not in (\"Human\", \"Computer\")):\n return 0\n gone = fresh(1)\n if EUDIf()(f_playerexist(p)):\n gone << 0\n EUDEndIf()\n return gone\n\n def read_supply(self, r, e):\n base = SUPPLY_TABLES.get(r.get(\"of\"))\n if base is None:\n raise Fail(\"trigscript: unknown supply %r%s\" % (r.get(\"of\"), where(e)))\n p = self.one_player(r[\"player\"], e)\n half = fresh(0)\n if r.get(\"race\") is None:\n # The race the player plays; a slot with none (neutral, empty) has no supply.\n race = f_bread(PLAYER_BYTES[\"race\"] + p)\n if EUDIf()(race <= 2):\n half << f_dwread_epd(EPD(base) + race * 36 + p)\n EUDEndIf()\n else:\n half << f_dwread_epd(EPD(base) + int(r[\"race\"]) * 36 + p)\n # As the top bar shows it: half a supply in use counts as one.\n if r[\"of\"] == \"used\":\n half += 1\n return half // 2\n\n def read_condition(self, rec, e):\n t, player, unit = rec[\"type\"], rec[\"player\"], rec[\"unitId\"]\n one = player == CURRENT_PLAYER or not 12 <= player <= 26 # not a group; beyond 26 is an EUD offset, read as it stands\n table = None\n if one and t in (COND_DEATHS, COND_KILL) and not 229 <= unit <= 232:\n table = (DEATHS_TABLE if t == COND_DEATHS else KILLS_TABLE, unit * 12)\n elif one and t == COND_ACCUMULATE and rec[\"resource\"] in (0, 1):\n table = (ORE_TABLE if rec[\"resource\"] == 0 else GAS_TABLE, 0)\n if table is not None:\n p = self.current() if player == CURRENT_PLAYER else player\n return f_dwread_epd(EPD(table[0]) + table[1] + p)\n # No table holds it (a group's sum, units counted at a location, a score, a clock): ask the\n # condition, a bit at a time from the top. A unit count stays under 2^12, a number of players under 2^4.\n bits = 12 if t in (COND_BRING, COND_COMMAND) else 4 if t == COND_OPPONENTS else 32\n found = fresh(0)\n for bit in reversed(range(bits)):\n probe = found + (1 << bit)\n if EUDIf()(Condition(rec[\"location\"], player, probe, unit, 0, t, rec[\"resource\"], rec[\"flags\"], eudx=rec.get(\"mask\", 0) or 0)):\n found << probe\n EUDEndIf()\n return found\n\n # ── booleans: an eudplib condition ──\n def cond(self, e):\n k = e[\"kind\"]\n if k == \"const\":\n return always(e[\"value\"])\n if k == \"cond\":\n return condition(e[\"record\"])\n if k == \"var\":\n return as_var(self.var(e[\"id\"], e).get()) >= 1\n if k == \"element\":\n return as_var(self.num(e)) >= 1\n if k == \"pop\":\n return as_var(self.array(e[\"array\"], e).pop()) >= 1\n if k == \"test\":\n return as_var(self.num(e[\"expr\"])) >= 1\n if k == \"compare\":\n a, b = self.nums([e[\"left\"], e[\"right\"]])\n op, unsigned = e[\"op\"], e.get(\"unsigned\", False)\n if isinstance(a, int) and isinstance(b, int):\n x = a if unsigned in (True, \"left\") else signed(a)\n y = b if unsigned in (True, \"right\") else signed(b)\n return always(compare(x, op, y))\n # One comparison, built once: a comparison between variables writes into its own\n # condition, and one that is built and dropped is an orphan eudplib refuses.\n return self.ordered(a, op, b, unsigned)\n if k in (\"and\", \"or\"):\n items = e[\"items\"]\n # JavaScript stops at the first item that settles it: an item after it that runs something — a call, a\n # pop, a random number — must not run. Each such item is worked out inside the test of the ones before.\n if any(has_effect(c) for c in items[1:]):\n t = fresh(0)\n self.short_circuit(items, k == \"and\", t)\n return t >= 1\n if k == \"and\":\n # Every item is worked out either way (none of them does anything): one trigger's conditions, ANDed by the game.\n return [self.cond(c) for c in items]\n return EUDOr(*[self.cond(c) for c in items])\n if k == \"not\":\n return EUDNot(self.cond(e[\"expr\"]))\n if k == \"random\":\n return (f_rand() & 1) >= 1\n if k in (\"unitAlive\", \"unitFlag\", \"unitSame\"):\n return as_var(self.truth(e)) >= 1\n if k == \"running\":\n return as_var(self.running(e)) >= 1\n if k == \"edge\":\n return self.edge(e)\n if k == \"ternary\":\n t = EUDVariable()\n if EUDIf()(self.cond(e[\"cond\"])):\n t << self.truth(e[\"whenTrue\"])\n if EUDElse()():\n t << self.truth(e[\"whenFalse\"])\n EUDEndIf()\n return t >= 1\n if k == \"call\":\n return as_var(self.call(e[\"call\"])) >= 1\n if k in (\"textCompare\", \"textTest\"):\n return as_var(self.text_truth(e)) >= 1\n raise Fail(\"trigscript: unknown condition %r%s\" % (k, where(e)))\n\n def short_circuit(self, items, conj, t):\n \"\"\"`a && b && …` (conj) or `a || b || …` into t, each item worked out only when those before it did not settle it.\"\"\"\n if not items:\n if conj:\n t << 1\n return\n if conj:\n if EUDIf()(self.cond(items[0])):\n self.short_circuit(items[1:], conj, t)\n EUDEndIf()\n else:\n if EUDIf()(self.cond(items[0])):\n t << 1\n if EUDElse()():\n self.short_circuit(items[1:], conj, t)\n EUDEndIf()\n\n def truth(self, e):\n \"\"\"A boolean expression as 0 / 1.\"\"\"\n if e[\"kind\"] == \"const\":\n return 1 if e[\"value\"] else 0\n if e[\"kind\"] == \"var\":\n return self.var(e[\"id\"], e).get()\n if e[\"kind\"] == \"element\":\n return self.array(e[\"array\"], e).get(self.num(e[\"index\"]))\n if e[\"kind\"] == \"pop\":\n return self.array(e[\"array\"], e).pop()\n if e[\"kind\"] == \"unitAlive\":\n ref = self.unit(e[\"unit\"])\n t = fresh(0)\n if not ref.none:\n self.when_alive(ref, lambda: t << 1)\n return t\n if e[\"kind\"] == \"unitFlag\":\n return self.unit_flag(e)\n if e[\"kind\"] == \"running\":\n return self.running(e)\n if e[\"kind\"] == \"unitSame\":\n a, b = self.unit(e[\"left\"]), self.unit(e[\"right\"])\n t = fresh(0)\n if not a.none and not b.none:\n ap, bp = as_var(a.ptr), as_var(b.ptr)\n if EUDIf()([ap >= 1, ap == bp]):\n t << 1\n EUDEndIf()\n return t\n t = fresh(0)\n if EUDIf()(self.cond(e)):\n t << 1\n EUDEndIf()\n return t\n\n def edge(self, e):\n \"\"\"rose(c): true on the frame c becomes true; once(c): true the first time it holds.\"\"\"\n key = id(e)\n if key not in self.latches:\n self.latches[key] = RowCell(self.rows(), 0) # initial: the latch\n latch = self.latches[key]\n get, put = latch.get, latch.set\n\n fired = fresh(0)\n held = self.truth(e[\"cond\"])\n if EUDIf()(as_var(held) >= 1):\n if EUDIf()(as_var(get()) == 0):\n fired << 1\n put(1)\n EUDEndIf()\n if EUDElse()():\n if e[\"edge\"] == \"rose\":\n put(0)\n EUDEndIf()\n return fired >= 1\n\n # ── units: a pointer into the game's unit table, checked before use ──\n def unit(self, e):\n k = e[\"kind\"]\n if k == \"unitNull\":\n return NO_UNIT\n if k == \"unitVar\":\n return self.var(e[\"id\"], e).get()\n if k == \"pick\":\n return self.pick(e)\n if k == \"unitAt\":\n # A unit kept as three numbers of the program (a cell each of an array of units): re-checked like any kept unit.\n ptr = self.num(e[\"ptr\"])\n if isinstance(ptr, int) and ptr == 0:\n return NO_UNIT\n return UnitRef(fresh(ptr), fresh(self.num(e[\"epd\"])), fresh(self.num(e[\"uid\"])))\n if k == \"call\":\n return self.call(e[\"call\"])\n if k == \"selected\":\n # A slot of this computer's selection (B5): the unit's address, as Remastered's emulator gives it, and the\n # slot's uniqueness byte taken now, so a use later re-checks it like any kept unit.\n ptr = f_dwread(SELECTION + 4 * int(e[\"index\"]))\n epd, uid = fresh(0), fresh(0)\n if EUDIf()(ptr >= 1):\n epd << (ptr - 0x58A364) // 4\n uid << (f_dwread_epd(epd + OFF_UID) & 0xFF00)\n EUDEndIf()\n return UnitRef(fresh(ptr), epd, uid)\n raise Fail(\"trigscript: unknown unit expression %r%s\" % (k, where(e)))\n\n def unit_part(self, e):\n \"\"\"One of the three numbers a unit is kept as. The unit of a loop's turn has no uniqueness byte taken yet: it is read here.\"\"\"\n ref = self.unit(e[\"unit\"])\n if ref.none:\n return 0\n part = e[\"part\"]\n if part == \"ptr\":\n return ref.ptr\n if part == \"epd\":\n return ref.epd\n return f_maskread_epd(ref.epd + OFF_UID, 0xFF00) if ref.uid is None else ref.uid\n\n def when_alive(self, ref, body):\n \"\"\"`body()` when the unit is still the one that was kept: the slot has a sprite, its order is not\n \"die\", and its uniqueness byte is the one taken with the pointer.\"\"\"\n if ref.none:\n return\n if ref.uid is None:\n body()\n return\n ptr, epd = as_var(ref.ptr), as_var(ref.epd)\n if EUDIf()([ptr >= 1, MemoryEPD(epd + OFF_SPRITE, AtLeast, 1), MemoryXEPD(epd + OFF_OWNER_ORDER, AtLeast, 0x100, 0xFF00), MemoryXEPD(epd + OFF_UID, Exactly, ref.uid, 0xFF00)]):\n body()\n EUDEndIf()\n\n @staticmethod\n def cunit(ref):\n return CUnit(ref.epd, ptr=ref.ptr)\n\n def unit_field(self, e):\n ref = self.unit(e[\"unit\"])\n out = fresh(0)\n self.when_alive(ref, lambda: out << self.field_of(self.cunit(ref), e[\"field\"], e))\n return out\n\n def field_of(self, cu, field, node):\n \"\"\"A unit's number, in the script's units: whole points for hit points (as the game shows them,\n a started point counting), shields and energy.\"\"\"\n if field == \"hp\":\n return f_div(cu.hp + 255, 256)[0]\n if field == \"maxHp\":\n return f_div(f_dwread_epd(EPD(UNITS_MAX_HP) + cu.unitType), 256)[0]\n if field == \"shields\":\n return f_div(cu.shield, 256)[0]\n if field == \"maxShields\":\n return f_wread(UNITS_MAX_SHIELDS + cu.unitType * 2)\n if field == \"energy\":\n return f_div(cu.energy, 256)[0]\n if field in UNIT_TIMERS:\n return getattr(cu, UNIT_TIMERS[field])\n name = {\"owner\": \"owner\", \"type\": \"unitType\", \"x\": \"posX\", \"y\": \"posY\", \"kills\": \"killCount\", \"orderId\": \"orderID\", \"cooldown\": \"groundWeaponCooldown\", \"resources\": \"resourceAmount\"}.get(field)\n if name is None:\n raise Fail(\"trigscript: unknown unit field %r%s\" % (field, where(node)))\n return getattr(cu, name)\n\n def unit_flag(self, e):\n ref = self.unit(e[\"unit\"])\n out = fresh(0)\n\n def body():\n cu = self.cunit(ref)\n flag = e[\"flag\"]\n if flag == \"underAttack\":\n held = cu.attackNotifyTimer >= 1\n else:\n mask = {\"hallucinated\": STATUS_HALLUCINATION, \"cloaked\": STATUS_CLOAKED, \"burrowed\": STATUS_BURROWED, \"invincible\": STATUS_INVINCIBLE}.get(flag)\n if mask is None:\n raise Fail(\"trigscript: unknown unit flag %r%s\" % (flag, where(e)))\n held = cu.check_status_flag(mask)\n if EUDIf()(held):\n out << 1\n EUDEndIf()\n\n self.when_alive(ref, body)\n return out\n\n def location_bounds(self, number):\n base = EPD(MRGN + (int(number) - 1) * 20)\n return [f_dwread_epd(base + i) for i in range(4)]\n\n def scan(self, flt, node, body):\n \"\"\"The game's unit table in the order the game hands its places out — slot 0, then 1699 downwards, the units\n oldest first — stopping once it has met as many units as the game says are in use (0x6283F0): the places in use\n lie at the top, so the walk costs what the map holds, not the 1 700 places (about 135 ns a place, 229 µs a loop).\n `body(ref, next_, exit_)` for every unit the filter matches. Every test is one condition whose address moves on\n with the slot, so a slot that does not match costs a trigger or two. (Counted are the places with a sprite: never\n more than the game's count, and at most one fewer in a 12 000-frame game of four, when the walk goes to the end.)\"\"\"\n ptr, epd = EUDVariable(), EUDVariable()\n empty = MemoryEPD(0, Exactly, 0)\n dying = MemoryXEPD(0, Exactly, 0, 0xFF00)\n moving = [(empty, OFF_SPRITE), (dying, OFF_OWNER_ORDER)]\n match, values = [], []\n owner = flt.get(\"owner\")\n if owner is not None:\n who = self.current() if owner == CURRENT_PLAYER else int(owner)\n c = MemoryXEPD(0, Exactly, who if isinstance(who, int) else 0, 0xFF)\n match.append(c)\n moving.append((c, OFF_OWNER_ORDER))\n if not isinstance(who, int):\n values.append((c, who))\n kind = flt.get(\"type\")\n if kind is not None and kind < 228:\n c = MemoryXEPD(0, Exactly, int(kind), 0xFFFF)\n match.append(c)\n moving.append((c, OFF_TYPE))\n elif kind is not None and kind not in GROUP_BITS:\n raise Fail(\"trigscript: unknown unit type %r%s\" % (kind, where(node)))\n if flt.get(\"at\"):\n left, top, right, bottom = self.location_bounds(flt[\"at\"])\n # A location reaching past the map's edge starts below zero, which a unit's position never is.\n for v in (left, top):\n if EUDIf()(v >= 0x80000000):\n v << 0\n EUDEndIf()\n for comparison, mask, value in ((AtLeast, 0xFFFF, left), (AtMost, 0xFFFF, right), (AtLeast, 0xFFFF0000, f_bitlshift(top, 16)), (AtMost, 0xFFFF0000, f_bitlshift(bottom, 16))):\n c = MemoryXEPD(0, comparison, 0, mask)\n match.append(c)\n moving.append((c, OFF_POS))\n values.append((c, value))\n # Slot 0 first; past it the walk jumps to slot 1699 and goes down.\n remaining = fresh(f_dwread(RECORDS_IN_USE))\n DoActions([ptr.SetNumber(UNIT_TABLE), epd.SetNumber(EPD(UNIT_TABLE))] + [SetMemory(c + 4, SetTo, EPD(UNIT_TABLE) + off) for c, off in moving])\n for c, value in values:\n f_dwwrite_epd(EPD(c + 8), value)\n top_slot = UNIT_TABLE + UNIT_SIZE * (UNIT_SLOTS - 1)\n head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(remaining == 0, exit_)\n EUDJumpIf(empty, next_)\n remaining -= 1\n EUDJumpIf(dying, next_)\n if match:\n matched = Forward()\n EUDJumpIf(match, matched)\n EUDJump(next_)\n matched << NextTrigger()\n if kind in GROUP_BITS:\n group = fresh(f_bread(UNITS_GROUP + f_maskread_epd(epd + OFF_TYPE, 0xFFFF)))\n EUDJumpIf((group & GROUP_BITS[kind]) == 0, next_)\n body(UnitRef(ptr, epd, None), next_, exit_)\n next_ << NextTrigger()\n down = Forward()\n EUDJumpIfNot(ptr == UNIT_TABLE, down)\n DoActions([SetMemory(c + 4, SetTo, EPD(top_slot) + off) for c, off in moving] + [ptr.SetNumber(top_slot), epd.SetNumber(EPD(top_slot))])\n EUDJump(head)\n down << NextTrigger()\n DoActions([SetMemory(c + 4, Add, (-(UNIT_SIZE // 4)) & U32) for c, _ in moving] + [ptr.SubtractNumber(UNIT_SIZE), epd.SubtractNumber(UNIT_SIZE // 4)])\n EUDJumpIfNot(ptr == UNIT_TABLE, head)\n exit_ << NextTrigger()\n\n def pick(self, e):\n \"\"\"One unit among the matching: the first, the nearest to a location's centre, or one at random.\"\"\"\n by, flt = e[\"by\"], e.get(\"filter\", {})\n found_ptr, found_epd = fresh(0), fresh(0)\n\n def take(ref):\n found_ptr << ref.ptr\n found_epd << ref.epd\n\n if by == \"first\":\n def body(ref, next_, exit_):\n take(ref)\n EUDJump(exit_)\n self.scan(flt, e, body)\n elif by == \"nearest\":\n if e.get(\"mouse\") is not None:\n # A player's mouse, and nothing farther from it than `within`.\n cx = as_var(INPUT.read({\"source\": \"mouse\", \"axis\": \"x\", \"player\": e[\"mouse\"]}, self, e))\n cy = as_var(INPUT.read({\"source\": \"mouse\", \"axis\": \"y\", \"player\": e[\"mouse\"]}, self, e))\n least = fresh(int(e.get(\"within\", 48)) + 1)\n else:\n left, top, right, bottom = self.location_bounds(e[\"near\"])\n cx, cy = f_div(left + right, 2)[0], f_div(top + bottom, 2)[0]\n least = fresh(U32)\n\n def body(ref, next_, exit_):\n cu = self.cunit(ref)\n # |dx| + |dy| is enough to say which is nearest, and never overflows.\n d = distance(cu.posX, cx) + distance(cu.posY, cy)\n if EUDIf()(d < least):\n least << d\n take(ref)\n EUDEndIf()\n self.scan(flt, e, body)\n elif by == \"random\":\n # Count the matching, draw one, take the drawn one on a second pass.\n count = fresh(0)\n self.scan(flt, e, lambda ref, next_, exit_: count.__iadd__(1))\n none = Forward()\n EUDJumpIf(count == 0, none)\n drawn = f_div(f_dwrand(), count)[1]\n i = fresh(0)\n\n def body(ref, next_, exit_):\n hit = Forward()\n EUDJumpIf(i == drawn, hit)\n i.__iadd__(1)\n EUDJump(next_)\n hit << NextTrigger()\n take(ref)\n EUDJump(exit_)\n self.scan(flt, e, body)\n none << NextTrigger()\n else:\n raise Fail(\"trigscript: unknown pick %r%s\" % (by, where(e)))\n uid = fresh(0)\n if EUDIf()(found_ptr >= 1):\n uid << f_maskread_epd(found_epd + OFF_UID, 0xFF00)\n EUDEndIf()\n return UnitRef(found_ptr, found_epd, uid)\n\n def unit_loop(self, st, ctx):\n def body(ref, next_, exit_):\n self.vars[st[\"decl\"][\"id\"]] = LoopUnit(ref)\n self.straight(st[\"body\"], dict(ctx, **{\"break\": exit_, \"continue\": next_}))\n if st.get(\"source\"):\n self.event_loop(st[\"source\"], st.get(\"filter\", {}), st, body)\n return\n self.scan(st.get(\"filter\", {}), st, body)\n\n def event_loop(self, source, flt, node, body):\n \"\"\"The units of this frame's events pass, in the order it found them: `body(ref, next_, exit_)` for each the\n filter matches. One that came on and has gone since is passed over; one that died is the dying record, read as\n it died (its slot is not given to another unit before the next frame's dying pass).\"\"\"\n if EVENTS is None:\n raise Fail(\"trigscript: a loop over the events pass's units, and no pass%s\" % where(node))\n cells, was, count = {\"created\": (EVENTS.made, None, EVENTS.made_n), \"died\": (EVENTS.gone, None, EVENTS.gone_n),\n \"killed\": (EVENTS.gone, None, EVENTS.gone_n), \"ordered\": (EVENTS.turned, EVENTS.turned_was, EVENTS.turned_n),\n \"morphed\": (EVENTS.changed, EVENTS.changed_was, EVENTS.changed_n)}[source]\n event = node.get(\"event\") or {}\n i = fresh(0)\n head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(i >= count, exit_)\n ptr, epd = f_cunitepdread_epd(EPD(cells) + i)\n if source in (\"created\", \"ordered\", \"morphed\"):\n # Gone since the pass: killed or removed by a program before this turn (its slot is still its own).\n EUDJumpIfNot([MemoryEPD(epd + OFF_SPRITE, AtLeast, 1), MemoryXEPD(epd + OFF_OWNER_ORDER, AtLeast, 0x100, 0xFF00)], next_)\n match = []\n if source == \"killed\":\n # Died in a fight: no hit points, and one of the eight players the last whose attack hurt it.\n match += [MemoryEPD(epd + OFF_HP, Exactly, 0), MemoryXEPD(epd + OFF_LAST_ATTACKER, AtMost, 7, 0xFF)]\n by = event.get(\"by\")\n if by is not None:\n match.append(MemoryXEPD(epd + OFF_LAST_ATTACKER, Exactly, self.current() if by == CURRENT_PLAYER else int(by), 0xFF))\n if event.get(\"order\") is not None:\n match.append(MemoryXEPD(epd + OFF_OWNER_ORDER, Exactly, int(event[\"order\"]) << 8, 0xFF00))\n owner = flt.get(\"owner\")\n if owner is not None:\n who = self.current() if owner == CURRENT_PLAYER else int(owner)\n match.append(MemoryXEPD(epd + OFF_OWNER_ORDER, Exactly, who, 0xFF))\n kind = flt.get(\"type\")\n if kind is not None and kind < 228:\n match.append(MemoryXEPD(epd + OFF_TYPE, Exactly, int(kind), 0xFFFF))\n elif kind is not None and kind not in GROUP_BITS:\n raise Fail(\"trigscript: unknown unit type %r%s\" % (kind, where(node)))\n if flt.get(\"at\"):\n left, top, right, bottom = self.location_bounds(flt[\"at\"])\n for v in (left, top):\n if EUDIf()(v >= 0x80000000):\n v << 0\n EUDEndIf()\n match += [MemoryXEPD(epd + OFF_POS, AtLeast, left, 0xFFFF), MemoryXEPD(epd + OFF_POS, AtMost, right, 0xFFFF),\n MemoryXEPD(epd + OFF_POS, AtLeast, f_bitlshift(top, 16), 0xFFFF0000), MemoryXEPD(epd + OFF_POS, AtMost, f_bitlshift(bottom, 16), 0xFFFF0000)]\n if match:\n EUDJumpIfNot(match, next_)\n if kind in GROUP_BITS:\n group = fresh(f_bread(UNITS_GROUP + f_maskread_epd(epd + OFF_TYPE, 0xFFFF)))\n EUDJumpIf((group & GROUP_BITS[kind]) == 0, next_)\n if node.get(\"was\"):\n # What the unit was: the killer, or the order or the type the pass before found.\n self.declare(node[\"was\"]).set(f_maskread_epd(epd + OFF_LAST_ATTACKER, 0xFF) if source == \"killed\" else was[i])\n body(UnitRef(ptr, epd, None), next_, exit_)\n next_ << NextTrigger()\n i += 1\n EUDJump(head)\n exit_ << NextTrigger()\n\n def unit_write(self, st):\n ref = self.unit(st[\"unit\"])\n field = st[\"field\"]\n if field == \"invincible\":\n on = self.truth(st[\"value\"])\n\n def flag():\n cu = self.cunit(ref)\n if isinstance(on, int):\n cu.set_invincible() if on else cu.clear_invincible()\n return\n if EUDIf()(as_var(on) >= 1):\n cu.set_invincible()\n if EUDElse()():\n cu.clear_invincible()\n EUDEndIf()\n self.when_alive(ref, flag)\n return\n value = self.num(st[\"value\"])\n\n def write():\n cu = self.cunit(ref)\n if field == \"hp\":\n # Hit points at 0 are a dead unit, so that is what the write makes of it.\n if isinstance(value, int):\n if value == 0:\n cu.die()\n else:\n cu.hp = min(value, 0xFFFFFF) * 256\n return\n if EUDIf()(value == 0):\n cu.die()\n if EUDElse()():\n cu.hp = f_mul(saturate(value, 24), 256)\n EUDEndIf()\n elif field == \"shields\":\n cu.shield = points(value, 24)\n elif field == \"energy\":\n cu.energy = points(value, 8)\n elif field == \"kills\":\n cu.killCount = saturate(value, 8)\n elif field == \"resources\":\n cu.resourceAmount = saturate(value, 16)\n elif field == \"cooldown\":\n frames = saturate(value, 8)\n cu.groundWeaponCooldown = frames\n cu.airWeaponCooldown = frames\n cu.spellCooldown = frames\n elif field in UNIT_TIMERS:\n setattr(cu, UNIT_TIMERS[field], saturate(value, 8))\n else:\n raise Fail(\"trigscript: a unit's %s takes no write%s\" % (field, where(st)))\n self.when_alive(ref, write)\n\n def unit_do(self, st):\n ref = self.unit(st[\"unit\"])\n verb = st[\"verb\"]\n do = verb[\"do\"]\n amount = self.num(verb[\"amount\"]) if do in (\"damage\", \"heal\") else 0\n\n def act():\n cu = self.cunit(ref)\n if do == \"kill\":\n cu.die()\n elif do == \"remove\":\n cu.remove()\n elif do == \"give\":\n to = int(verb[\"to\"])\n cu.cgive(self.current() if to == CURRENT_PLAYER else to)\n forget_counts()\n elif do == \"order\":\n self.order(cu, verb, st)\n elif do == \"locate\":\n self.locate(cu, int(verb[\"location\"]))\n elif do in (\"damage\", \"heal\"):\n self.adjust(cu, do, amount, bool(verb.get(\"percent\")))\n else:\n raise Fail(\"trigscript: unknown unit verb %r%s\" % (do, where(st)))\n self.when_alive(ref, act)\n\n def order(self, cu, verb, node):\n \"\"\"The game's own Order, reaching this unit alone: a location is made a small box around the unit\n for the length of the action (the game did nothing with a box of no size), then put back.\"\"\"\n kind = {\"move\": Move, \"patrol\": Patrol, \"attack\": Attack}.get(verb[\"order\"])\n if kind is None:\n raise Fail(\"trigscript: unknown order %r%s\" % (verb[\"order\"], where(node)))\n base = EPD(MRGN + (SCRATCH_LOCATION - 1) * 20)\n kept = [f_dwread_epd(base + i) for i in range(5)]\n x, y = cu.posX, cu.posY\n for i, v in enumerate((x - 2, y - 2, x + 2, y + 2, 0)):\n f_dwwrite_epd(base + i, v)\n DoActions(Order(cu.unitType, cu.owner, SCRATCH_LOCATION, kind, int(verb[\"target\"])))\n for i, v in enumerate(kept):\n f_dwwrite_epd(base + i, v)\n\n def locate(self, cu, number):\n \"\"\"A location centred on the unit, its size kept.\"\"\"\n base = EPD(MRGN + (number - 1) * 20)\n left, top, right, bottom = [f_dwread_epd(base + i) for i in range(4)]\n width, height = right - left, bottom - top\n x, y = cu.posX - f_div(width, 2)[0], cu.posY - f_div(height, 2)[0]\n for i, v in enumerate((x, y, x + width, y + height)):\n f_dwwrite_epd(base + i, v)\n forget_counts()\n\n def select(self, st):\n \"\"\"select() (B5): on the player's own computer the selection becomes the units still there — the twelve slots\n (0x6284B8), the panel told it changed (0x59723C, the flag Remastered's panel refresh tests) — and a Select\n command of their tags (place + 1, uniqueness byte << 11) goes to every computer, as MSQC puts a selection back.\"\"\"\n p = self.one_player(st[\"player\"], st)\n refs = [self.unit(u) for u in st[\"units\"]]\n if EUDIf()(f_getuserplayerid() == p):\n n = fresh(0)\n for ref in refs:\n def keep(ref=ref):\n f_dwwrite(SELECTION + n * 4, ref.ptr)\n # The unit's tag: its place in the table from 1, its uniqueness byte above bit 11.\n uid = as_var(ref.uid) if ref.uid is not None else f_dwread_epd(as_var(ref.epd) + OFF_UID) & 0xFF00\n tag = f_div(as_var(ref.ptr) - 0x59CCA8, 336)[0] + 1 + uid * 8\n f_wwrite(SELECT_COMMAND + 2 + n * 2, tag)\n n.__iadd__(1)\n self.when_alive(ref, keep)\n if EUDIf()(n >= 1):\n i = fresh(n)\n if EUDWhile()(i <= 11):\n f_dwwrite(SELECTION + i * 4, 0)\n i.__iadd__(1)\n EUDEndWhile()\n DoActions(SetMemoryX(0x59723C, SetTo, 1, 0xFF))\n f_bwrite(SELECT_COMMAND + 1, n)\n QueueGameCommand(SELECT_COMMAND, n * 2 + 2)\n EUDEndIf()\n EUDEndIf()\n\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept - or the size given, when nothing of the old one is read.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 1) * 20)\n size = st.get(\"size\")\n if size:\n width, height = self.num(size[\"width\"]), self.num(size[\"height\"])\n if isinstance(width, int) and isinstance(height, int):\n # Each side is the centre plus a constant: all four in one go, the centre routed into the two sides that\n # take it, the constants added where they land (four temporaries made and copied was about 0.75 µs —\n # every brush of the fog library is one of these).\n sides = []\n for i, (c, d) in enumerate(((x, -(width // 2)), (y, -(height // 2)), (x, width - width // 2), (y, height - height // 2))):\n if isinstance(c, int):\n sides.append((base + i, SetTo, (c + d) & U32))\n else:\n sides.append((base + i, SetTo, c))\n if d & U32:\n sides.append((base + i, Add, d & U32))\n SeqCompute(sides)\n forget_counts()\n return\n else:\n nx, ny = x - f_div(width, 2)[0], y - f_div(height, 2)[0]\n for i, v in enumerate((nx, ny, nx + width, ny + height)):\n f_dwwrite_epd(base + i, v)\n forget_counts()\n return\n left, top, right, bottom = [f_dwread_epd(base + i) for i in range(4)]\n width, height = right - left, bottom - top\n nx, ny = x - f_div(width, 2)[0], y - f_div(height, 2)[0]\n for i, v in enumerate((nx, ny, nx + width, ny + height)):\n f_dwwrite_epd(base + i, v)\n forget_counts()\n\n def adjust(self, cu, do, amount, percent):\n \"\"\"Hit points down — at 0 the unit dies — or up to the type's maximum, in the game's own units (256 to a point).\"\"\"\n now = fresh(cu.hp)\n top = f_dwread_epd(EPD(UNITS_MAX_HP) + cu.unitType)\n if percent:\n step = f_div(f_mul(top, as_var(amount)), 100)[0]\n else:\n step = amount * 256 if isinstance(amount, int) else f_mul(saturate(amount, 24), 256)\n if do == \"damage\":\n if EUDIf()(now <= step):\n cu.die()\n if EUDElse()():\n cu.hp = now - step\n EUDEndIf()\n return\n new = fresh(now + step)\n if EUDIf()(new >= top):\n new << top\n EUDEndIf()\n cu.hp = new\n\n # ── the game's tables ──\n def cell_address(self, c):\n index = c[\"index\"]\n if c.get(\"player\") and index == CURRENT_PLAYER:\n index = self.current()\n base = int(c[\"base\"]) + int(c.get(\"key\") or 0)\n return base + index * int(c[\"stride\"])\n\n def table_read(self, c, node):\n if c.get(\"special\"):\n raise Fail(\"trigscript: %s cannot be read%s\" % (c.get(\"name\"), where(node)))\n addr = self.cell_address(c)\n width = c[\"width\"]\n if width == \"bit\":\n v = fresh(0)\n if EUDIf()(MemoryX(addr, AtLeast, 1, 1 << int(c[\"bit\"]))):\n v << 1\n EUDEndIf()\n return v\n v = f_dwread(addr) if width == 4 else f_wread(addr) if width == 2 else f_bread(addr)\n scale = int(c.get(\"scale\") or 1)\n return f_div(v, scale)[0] if scale > 1 else v\n\n def table_write(self, st):\n c = st[\"cell\"]\n addr = self.cell_address(c)\n special = c.get(\"special\")\n if special == \"name\":\n if self.has_id(st[\"value\"]):\n f_wwrite(addr, self.text_id_of(st[\"value\"]))\n return\n # A name that was made: over the string kept for this unit type, on every computer - a name is nobody's in particular.\n made = self.text(st[\"value\"])\n sid = slot(\"the name of unit type %d\" % int(c[\"index\"]))\n texts()[\"show\"](GetMapStringAddr(sid), made.addr)\n self.used(made)\n f_wwrite(addr, sid)\n return\n width = c[\"width\"]\n if st.get(\"boolean\"):\n value = self.truth(st[\"value\"])\n else:\n value = self.num(st[\"value\"])\n scale = int(c.get(\"scale\") or 1)\n if scale > 1 and not st.get(\"scaled\"):\n value = value * scale if isinstance(value, int) else f_mul(value, scale)\n top = 1 if width == \"bit\" else (1 << (8 * width)) - 1\n if isinstance(value, int):\n value = min(value, top)\n elif width != 4 and width != \"bit\":\n value = saturate(value, 8 * width)\n if special == \"speed\":\n self.write_speed(int(c[\"index\"]), value)\n elif special == \"color\":\n f_bwrite(addr, value)\n f_bwrite(addr + MINIMAP_COLOR_OFFSET, value)\n elif width == \"bit\":\n mask = 1 << int(c[\"bit\"])\n if isinstance(value, int):\n DoActions(SetMemoryX(addr, SetTo, mask if value else 0, mask))\n else:\n if EUDIf()(value >= 1):\n DoActions(SetMemoryX(addr, SetTo, mask, mask))\n if EUDElse()():\n DoActions(SetMemoryX(addr, SetTo, 0, mask))\n EUDEndIf()\n elif width == 4:\n f_dwwrite(addr, value)\n elif width == 2:\n f_wwrite(addr, value)\n else:\n f_bwrite(addr, value)\n\n def write_speed(self, unit, speed):\n \"\"\"A unit type's top speed is its flingy's: the flingy is switched to table control and given the\n speed, with acceleration about a seventeenth of it and the braking distance v² / 2a to match\n (the Vulture's own figures), as Magenta writes it. Units made afterwards move at the new speed.\"\"\"\n flingy = f_bread(UNITS_FLINGY + unit)\n if isinstance(speed, int):\n acceleration = max(1, round(speed / 17))\n halt = max(1, round(speed * speed / (2 * acceleration)))\n else:\n acceleration = fresh(f_div(speed, 17)[0])\n if EUDIf()(acceleration == 0):\n acceleration << 1\n EUDEndIf()\n halt = fresh(f_div(f_mul(speed, speed), acceleration * 2)[0])\n if EUDIf()(halt == 0):\n halt << 1\n EUDEndIf()\n acceleration = saturate(acceleration, 16)\n f_bwrite(FLINGY_CONTROL + flingy, 0)\n f_dwwrite(FLINGY_SPEED + flingy * 4, speed)\n f_wwrite(FLINGY_ACCELERATION + flingy * 2, acceleration)\n f_dwwrite(FLINGY_HALT + flingy * 4, halt)\n\n # ── statements ──\n def block(self, statements, ctx):\n for st in statements:\n self.statement(st, ctx)\n\n def statement(self, st, ctx):\n k = st[\"kind\"]\n if k == \"declare\":\n s = self.declare(st[\"decl\"])\n if not st.get(\"failed\"):\n self.put(s, st[\"init\"], st[\"decl\"][\"kind\"])\n self.step_place(st[\"decl\"][\"id\"], st[\"init\"])\n elif k == \"assignText\":\n self.put(self.var(st[\"target\"], st), st[\"value\"], \"text\")\n elif k == \"storeText\":\n self.store_text(st)\n elif k == \"releaseText\":\n block, i = self.array(st[\"block\"], st), fresh(self.num(st[\"index\"]))\n texts()[\"release\"](as_var(block.get(i)))\n block.set(i, 0)\n elif k == \"textLoop\":\n self.text_loop(st, ctx)\n elif k == \"assignUnit\":\n self.var(st[\"target\"], st).set(self.unit(st[\"value\"]))\n elif k == \"unitLoop\":\n self.unit_loop(st, ctx)\n elif k == \"unitWrite\":\n self.unit_write(st)\n elif k == \"unitDo\":\n self.unit_do(st)\n elif k == \"tableWrite\":\n self.table_write(st)\n elif k == \"assign\":\n target = self.var(st[\"target\"], st)\n if not self.assign_in_place(target, st):\n target.set(self.num(st[\"value\"]))\n self.step_place(st[\"target\"], st[\"value\"])\n elif k == \"assignBool\":\n self.var(st[\"target\"], st).set(self.truth(st[\"value\"]))\n elif k == \"declareArray\":\n a = self.array(st[\"array\"], st)\n cell = self.num if a.decl[\"kind\"] == \"number\" else self.truth\n if isinstance(a, ListStorage):\n if st.get(\"fill\") is not None:\n v = cell(st[\"fill\"])\n a.declare_filled(v if isinstance(v, int) else fresh(v), int(a.decl[\"length\"]))\n else:\n values = [cell(v) for v in st.get(\"init\", [])]\n a.declare([v if isinstance(v, int) else fresh(v) for v in values])\n elif st.get(\"fill\") is not None:\n a.fill(cell(st[\"fill\"]))\n else:\n # Every value first, then the stores: [b, a] of two cells of the array itself is a swap.\n values = [cell(v) for v in st.get(\"init\", [])]\n values = [v if isinstance(v, int) else fresh(v) for v in values]\n for i, v in enumerate(values):\n a.set(i, v)\n elif k == \"push\":\n a = self.array(st[\"array\"], st)\n a.push((self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"]))\n elif k == \"pop\":\n self.array(st[\"array\"], st).pop()\n elif k == \"setLength\":\n self.array(st[\"array\"], st).set_length(self.num(st[\"value\"]))\n elif k == \"store\":\n a = self.array(st[\"array\"], st)\n # `xs[i] = xs[i] + v` (`xs[i] += v`, a count): the game adds to the cell itself, nothing read.\n step = self.cell_step(st, a)\n place = self.place_of(st[\"index\"], a)\n if step is not None:\n if place is not None:\n v = self.num(step[1])\n if step[0] == \"-\":\n v = (-v) & U32 if isinstance(v, int) else 0 - v\n cur = self.cursor_at(st[\"index\"], a)\n if cur is not None:\n self.cursor_write(cur, a, st, v, Add)\n return\n a.set_placed(self.num(st[\"index\"]), place, v, Add, self.bound_of(st[\"index\"], st[\"array\"]))\n return\n a.add(self.num(st[\"index\"]), self.num(step[1]), step[0])\n return\n value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\n if place is not None:\n cur = self.cursor_at(st[\"index\"], a)\n if cur is not None:\n self.cursor_write(cur, a, st, value, SetTo)\n return\n a.set_placed(self.num(st[\"index\"]), place, value, SetTo, self.bound_of(st[\"index\"], st[\"array\"]))\n return\n a.set(self.num(st[\"index\"]), value)\n elif k == \"if\":\n self.if_(st, ctx)\n elif k == \"while\":\n self.while_(st, ctx)\n elif k == \"do\":\n self.do_(st, ctx)\n elif k == \"for\":\n self.for_(st, ctx)\n elif k == \"unrolled\":\n self.unrolled(st, ctx)\n elif k == \"switch\":\n self.switch(st, ctx)\n elif k == \"break\":\n if \"break\" not in ctx:\n raise Fail(\"trigscript: break outside a loop%s\" % where(st))\n EUDJump(ctx[\"break\"])\n raise Leave()\n elif k == \"continue\":\n if \"continue\" not in ctx:\n raise Fail(\"trigscript: continue outside a loop%s\" % where(st))\n EUDJump(ctx[\"continue\"])\n raise Leave()\n elif k == \"return\":\n fn = ctx.get(\"fn\")\n if fn is None:\n raise Fail(\"trigscript: return outside a function%s\" % where(st))\n if st.get(\"value\") is not None and fn[\"result\"] is not None:\n self.put(fn[\"result\"], st[\"value\"], fn[\"kind\"])\n EUDJump(fn[\"end\"])\n raise Leave()\n elif k == \"sleep\":\n self.sleep(st)\n elif k == \"parallel\":\n self.parallel(st)\n elif k == \"start\":\n self.start(st)\n elif k == \"cancel\":\n self.cancel(st)\n elif k == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"buildText\":\n self.build_text(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"select\":\n self.select(st)\n elif k == \"syncSend\":\n INPUT.send_sync(int(st[\"slot\"]), self.one_player(st[\"player\"], st), self.num(st[\"value\"]))\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"try\":\n self.try_(st, ctx)\n elif k == \"throw\":\n if not st.get(\"rethrow\"):\n self.var(\"(error line)\").set(int((st.get(\"at\") or {}).get(\"line\", 0)))\n EUDJump(self.handler(st))\n raise Leave()\n elif k == \"uncaught\":\n self.uncaught(st)\n elif k == \"remark\":\n pass\n elif k == \"graph\":\n self.graph(st, ctx)\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\n\n # ── the emitter's graphs (compiler/backend/graph.ts) ──\n GRAPH_COMPARE = {\"atLeast\": AtLeast, \"atMost\": AtMost, \"exactly\": Exactly}\n GRAPH_MODIFIER = {\"set\": SetTo, \"add\": Add, \"subtract\": Subtract}\n\n def graph(self, st, ctx):\n \"\"\"A loop the emitter lowered (TrigScript 4, the backend: the emitter): its triggers made as they are, where the\n loop was. A dword of the graph is a game address, a variable's cell, a dword of the graph's own data (one in the\n map for each name), or a field of one of its triggers; the copies into it on the way in are routed reads of the\n variables, as any read is. A statement lowered twice (a `finally` copied to each way out) makes it twice.\"\"\"\n g = st[\"graph\"]\n if not hasattr(self, \"graph_data\"):\n self.graph_data = {}\n labels = {t[\"label\"]: Forward() for t in g[\"triggers\"]}\n end = Forward()\n labels[\"@end\"] = end\n\n def element0(id_):\n \"\"\"The EPD of the value of an array's first cell, and of the next cell's less it.\"\"\"\n store = self.array(id_, st).store\n if is_varray(store):\n return EPD(unProxy(store) + VARRAY_VALUE), VARRAY_STRIDE // 4\n return store._epd, 1\n\n # The lists the graph reads where they are: laid out as the emitter thought, or nothing is made.\n for a in g.get(\"arrays\", []):\n if element0(a[\"id\"])[1] != a[\"stride\"]:\n raise Fail(\"trigscript: the emitter read %s's cells %d EPD apart, which are %d%s\" % (a[\"id\"], a[\"stride\"], element0(a[\"id\"])[1], where(st)))\n\n def epd(d):\n if \"game\" in d:\n return EPD(d[\"game\"])\n if \"param\" in d:\n return EPD(self.graph_function(d[\"param\"])._fargs[d[\"index\"]].getValueAddr())\n if \"var\" in d:\n return EPD(self.var(d[\"var\"], st).get().getValueAddr())\n if \"data\" in d:\n if d[\"data\"] not in self.graph_data:\n self.graph_data[d[\"data\"]] = Db(4)\n return EPD(self.graph_data[d[\"data\"]])\n if \"element\" in d:\n return element0(d[\"element\"])[0]\n if \"getter\" in d:\n from eudplib.core.eudstruct.vararray import _getter, _getter_dst\n return _getter_dst if d[\"getter\"] == \"dest\" else EPD(_getter) + 1\n if \"temp\" in d:\n return EPD(graph_temp(d[\"temp\"]).getValueAddr())\n if \"counter\" in d:\n # The offset this side keeps beside a counter an array is indexed by, or (it keeps none) a dword nothing reads.\n place = self.places.get(d[\"counter\"])\n if place is None:\n if (\"junk\",) not in self.graph_data:\n self.graph_data[(\"junk\",)] = Db(4)\n return EPD(self.graph_data[(\"junk\",)])\n return EPD((place.bytes if d[\"part\"] == \"bytes\" else place.dwords).getValueAddr())\n base, f = labels[d[\"at\"]], d[\"field\"]\n if f == \"next\":\n return EPD(base + 4)\n if \"cond\" in f:\n return EPD(base + COND_VALUE + 20 * f[\"cond\"] - (4 if f.get(\"part\") == \"player\" else 0))\n return EPD(base + ACT_DEST - 16 + 32 * f[\"act\"] + {\"value\": 20, \"dest\": 16, \"modifier\": 24}[f[\"part\"]])\n\n def value(v):\n if isinstance(v, dict):\n if \"label\" in v:\n return labels[v[\"label\"]]\n if \"epd\" in v:\n return epd(v[\"epd\"])\n if \"addrOf\" in v:\n # (The first cell of an EUDVArray, a trigger: a jump into cell i goes 72 i bytes on.)\n return unProxy(self.array(v[\"addrOf\"], st).store) + v[\"plus\"]\n return element0(v[\"epdOf\"])[0] + v[\"plus\"]\n return v\n\n def graph_temp(name):\n \"\"\"A scratch cell of the graph's: a variable of this side's, one for each name (written before every read\n of it, so no initial value).\"\"\"\n key = (\"temp\", name)\n if key not in self.graph_data:\n self.graph_data[key] = EUDVariable()\n return self.graph_data[key]\n\n def copy(id_, into, top=None):\n \"\"\"The variable's cell into each dword (times its `scale`, plus its `add`): routed reads, a multiple worked\n out once for all the dwords that take it. `@name`: a scratch cell of the graph's. `top`: the most the value\n can be (value ranges), a multiple then worked out over only its bits.\"\"\"\n v = graph_temp(id_[1:]) if id_.startswith(\"@\") else self.var(id_, st).get()\n times = {1: v}\n for e in into:\n k = int(e.get(\"scale\", 1)) & U32\n if k not in times:\n # (A power of two is a shift, a bit a trigger; another multiple eudplib's product by a constant.)\n if top is not None and k != U32:\n times[k] = mul_bits(v, top, k)\n else:\n times[k] = fresh(0 - v) if k == U32 else shift_const(v, \"<<\", k.bit_length() - 1, top) if k & (k - 1) == 0 else fresh(v * k)\n for i in range(0, len(into), 16):\n group = []\n for e in into[i:i + 16]:\n d = epd(e[\"into\"] if \"into\" in e else e[\"dword\"])\n group.append((d, SetTo, times[int(e.get(\"scale\", 1)) & U32]))\n if e[\"add\"]:\n group.append((d, Add, value(e[\"add\"])))\n SeqCompute(group)\n\n entries = {}\n for e in g[\"entries\"]:\n entries.setdefault(e[\"from\"], []).append(e)\n for id_, es in entries.items():\n copy(id_, es)\n origins = globals().get(\"_ORIGINS\")\n for t in g[\"triggers\"]:\n # (With the map of triggers on, each trigger says the line of the statement it comes from.)\n if origins is not None and t.get(\"at\"):\n origins.append(_origin_of(t[\"at\"]))\n try:\n self.graph_trigger(t, labels, epd, value, copy, ctx)\n finally:\n if origins is not None and t.get(\"at\"):\n origins.pop()\n end << NextTrigger()\n\n def graph_trigger(self, t, labels, epd, value, copy, ctx):\n \"\"\"One trigger of a graph, or what stands in its place (a copy, an island, a call).\"\"\"\n if \"copy\" in t:\n # (In place of a trigger: the walk goes on to the one made after it.)\n labels[t[\"label\"]] << NextTrigger()\n if t[\"copy\"][\"into\"]:\n copy(t[\"copy\"][\"from\"], t[\"copy\"][\"into\"], t[\"copy\"].get(\"top\"))\n return\n if \"island\" in t:\n # A statement the emitter left to this side, lowered here as anywhere (nothing in it leaves it).\n labels[t[\"label\"]] << NextTrigger()\n self.statement(t[\"island\"], ctx)\n return\n if \"call\" in t:\n # A fixed-point function, its parameters written by the triggers before: it goes back to the trigger\n # after this one, its result written where the graph says (an EPD, as a call's return is).\n f, back = self.graph_function(t[\"call\"]), Forward()\n labels[t[\"label\"]] << RawTrigger(nextptr=f._fstart, actions=[SetMemory(f._nptr, SetTo, back), SetMemoryEPD(EPD(f._frets[0].getDestAddr()), SetTo, epd(t[\"call\"][\"into\"]))])\n back << NextTrigger()\n return\n conds = [MemoryXEPD(epd(c[\"dword\"]), self.GRAPH_COMPARE[c[\"compare\"]], c[\"value\"], c[\"mask\"]) if \"mask\" in c else MemoryEPD(epd(c[\"dword\"]), self.GRAPH_COMPARE[c[\"compare\"]], c[\"value\"]) for c in t[\"conds\"]]\n acts = [SetMemoryXEPD(epd(a[\"dword\"]), self.GRAPH_MODIFIER[a[\"modifier\"]], value(a[\"value\"]), a[\"mask\"]) if \"mask\" in a else SetMemoryEPD(epd(a[\"dword\"]), self.GRAPH_MODIFIER[a[\"modifier\"]], value(a[\"value\"])) for a in t[\"acts\"]]\n if \"next\" in t:\n trg = RawTrigger(nextptr=labels[t[\"next\"]], conditions=conds, actions=acts)\n else:\n trg = RawTrigger(conditions=conds, actions=acts)\n labels[t[\"label\"]] << trg\n # (With the map of triggers on, the line of each action whose statement is not the trigger's.)\n made = globals().get(\"_ACT_ORIGINS\")\n if made is not None and any(a.get(\"at\") for a in t[\"acts\"]):\n mine = _origin_of(t.get(\"at\"))\n made[trg] = [_origin_of(a[\"at\"]) if a.get(\"at\") else mine for a in t[\"acts\"]]\n\n def graph_function(self, spec):\n \"\"\"The fixed-point function a graph calls (compiler/backend/graph.ts, FixedFn), its body made.\"\"\"\n fn, c = spec[\"fn\"], int(spec.get(\"c\", 0))\n if fn in (\"fmul_vars\", \"fdiv_vars\"):\n f = fixed_vars()[fn]\n elif fn == \"fdiv_by\":\n if (c, None) not in _CONST_FDIV:\n _CONST_FDIV[(c, None)] = make_const_fdiv(c, None)\n f = _CONST_FDIV[(c, None)]\n else:\n name = \"fmul\" if fn == \"fmul_by\" else \"fdiv\"\n if (name, c, None) not in _FIXED_BY_CONST:\n _FIXED_BY_CONST[(name, c, None)] = (make_fmul_by if name == \"fmul\" else make_fdiv_by_const)(c, 31)\n f = _FIXED_BY_CONST[(name, c, None)]\n if f._fstart is None:\n f._create_func_body()\n return f\n\n def try_(self, st, ctx):\n \"\"\"The body with its catch as the place a throw goes to; the catch after it, reached only that way.\"\"\"\n caught, end = Forward(), Forward()\n self.handlers.append(caught)\n try:\n reached = self.straight(st[\"body\"], ctx)\n finally:\n self.handlers.pop()\n if reached:\n EUDJump(end)\n caught << NextTrigger()\n self.straight(st[\"catch\"], ctx)\n end << NextTrigger()\n\n def handler(self, node):\n \"\"\"Where an error goes from here: the nearest catch, or the end of the called function the lowering is in.\"\"\"\n if not self.handlers:\n raise Fail(\"trigscript: an error with nothing to go to%s\" % where(node))\n return self.handlers[-1]\n\n def uncaught(self, st):\n \"\"\"The error in flight, said in red where the player sees the game's own faults, and given up.\"\"\"\n e = self.errors\n at = st.get(\"at\")\n self.print_({\"kind\": \"print\", \"to\": CURRENT_PLAYER, \"position\": \"chat\", \"at\": at, \"label\": st.get(\"label\", \"\"), \"parts\": [\n {\"kind\": \"text\", \"text\": \"\\x06TrigScript: uncaught \"},\n {\"kind\": \"value\", \"text\": {\"kind\": \"textOf\", \"array\": e[\"names\"], \"index\": {\"kind\": \"var\", \"id\": e[\"thrown\"]}, \"at\": at}},\n {\"kind\": \"text\", \"text\": \": \"},\n {\"kind\": \"value\", \"text\": {\"kind\": \"textVar\", \"id\": e[\"message\"]}},\n {\"kind\": \"text\", \"text\": \", line \"},\n {\"kind\": \"number\", \"expr\": {\"kind\": \"var\", \"id\": \"(error line)\"}},\n {\"kind\": \"text\", \"text\": \".\"},\n ]})\n self.var(e[\"thrown\"], st).set(0)\n\n def straight(self, statements, ctx):\n \"\"\"A statement list whose end may not be reached (a break inside): Leave stops it. True when the end was reached.\"\"\"\n try:\n self.block(statements, ctx)\n return True\n except Leave:\n return False\n\n def if_(self, st, ctx):\n else_l, end = Forward(), Forward()\n EUDJumpIfNot(self.cond(st[\"cond\"]), else_l)\n if self.straight(st[\"then\"], ctx):\n EUDJump(end)\n else_l << NextTrigger()\n if st.get(\"else\"):\n self.straight(st[\"else\"], ctx)\n end << NextTrigger()\n\n def while_(self, st, ctx):\n head, exit_ = Forward(), Forward()\n head << NextTrigger()\n if st.get(\"cond\") is not None:\n EUDJumpIfNot(self.cond(st[\"cond\"]), exit_)\n if self.straight(st[\"body\"], dict(ctx, **{\"break\": exit_, \"continue\": head})):\n EUDJump(head)\n exit_ << NextTrigger()\n\n def do_(self, st, ctx):\n body, check, exit_ = Forward(), Forward(), Forward()\n body << NextTrigger()\n self.straight(st[\"body\"], dict(ctx, **{\"break\": exit_, \"continue\": check}))\n check << NextTrigger()\n EUDJumpIf(self.cond(st[\"cond\"]), body)\n exit_ << NextTrigger()\n\n def unchecked_loop(self, st):\n \"\"\"For a loop `for (…; i < xs.length; …)` over a growing array at a counter, whose body changes no array's length\n and stores nothing into i: the arrays its body reads and writes at i, unchecked — xs itself (the condition has\n just compared), each other one under a flag set on the way in when it is at least as long as xs. None when\n the loop is not of that shape.\"\"\"\n c = st.get(\"cond\")\n if not isinstance(c, dict) or c.get(\"kind\") != \"compare\" or c.get(\"op\") != \"<\":\n return None\n left, right = c.get(\"left\", {}), c.get(\"right\", {})\n if left.get(\"kind\") != \"var\" or left.get(\"id\") not in self.places or right.get(\"kind\") != \"length\":\n return None\n own = self.arrays.get(right.get(\"array\"))\n if type(own) is not ListStorage or not own.placeable():\n return None\n lists = {id_ for id_, a in self.arrays.items() if isinstance(a, ListStorage)}\n if not lengths_fixed_in(st[\"body\"], left[\"id\"], lists):\n return None\n out = {right[\"array\"]: True}\n for id_ in lists_at(st[\"body\"], left[\"id\"], lists):\n a = self.arrays[id_]\n if id_ in out or type(a) is not ListStorage or not a.placeable():\n continue\n flag = EUDVariable()\n flag << 0\n if EUDIf()(as_var(a.field(\"len\")) >= as_var(own.field(\"len\"))):\n flag << 1\n EUDEndIf()\n out[id_] = flag\n return left[\"id\"], out\n\n def for_(self, st, ctx):\n unchecked = self.unchecked_loop(st)\n cursor = self.cursors_for(st)\n flags = [] if unchecked is None else [f for f in unchecked[1].values() if f is not True]\n if unchecked is not None and cursor is not None and flags and len(flags) <= 16:\n # Unswitched: every other array the body reads at the counter found as long as the loop's own on the way in\n # (the usual case: a record's fields grow together) runs a copy of the loop that knows it, with no flag\n # tested at each access; otherwise the copy that tests them.\n self._copies = getattr(self, \"_copies\", 0) + 1\n try:\n if EUDIf()([f >= 1 for f in flags]):\n self.for_copy(st, ctx, (unchecked[0], {k: True for k in unchecked[1]}), True)\n if EUDElse()():\n self.for_copy(st, ctx, unchecked, True)\n EUDEndIf()\n finally:\n self._copies -= 1\n return\n self.for_copy(st, ctx, unchecked, cursor is not None)\n\n def for_copy(self, st, ctx, unchecked, cursors):\n \"\"\"One copy of a `for` loop: `unchecked` what `unchecked_loop` found (or the same with every flag known true),\n `cursors` whether its accesses at the counter go through registers (`Cursors`). An unswitched loop has two, each\n a region of its own for static routing, with a way in of its own.\"\"\"\n head, update, exit_ = Forward(), Forward(), Forward()\n region = None\n if ROUTE.enabled and ROUTE.on and ROUTE.stack and ROUTE.stack[-1].kind == \"loop\" and getattr(self, \"_copies\", 0):\n region = _Region(\"loop\")\n region.entry, region.after = Forward(), Forward()\n SetNextTrigger(region.entry)\n region.after << NextTrigger()\n ROUTE.loops.append(region)\n cursor = self.cursors_for(st) if cursors else None\n if cursor is not None:\n # The way in, which the registers' setting goes in front of once the body has said which there are.\n pre, pre_after = Forward(), Forward()\n SetNextTrigger(pre)\n pre_after << NextTrigger()\n self.cursors[cursor.counter] = cursor\n if region is not None:\n ROUTE.stack.append(region)\n try:\n if unchecked is not None:\n was = self.inside.get(unchecked[0])\n self.inside[unchecked[0]] = unchecked[1]\n try:\n self.for_body(st, ctx, head, update, exit_, cursor)\n finally:\n if was is None:\n self.inside.pop(unchecked[0], None)\n else:\n self.inside[unchecked[0]] = was\n else:\n self.for_body(st, ctx, head, update, exit_, cursor)\n finally:\n if region is not None:\n ROUTE.stack.pop()\n if cursor is not None:\n del self.cursors[cursor.counter]\n if cursor is not None:\n # Made out of the way of the code being made (what is pending is put aside and handed on after), and as\n # code in front of the loop, not in it.\n outer = ROUTE.stack.pop() if ROUTE.enabled and ROUTE.stack and ROUTE.stack[-1].kind == \"loop\" else None\n held = Forward()\n SetNextTrigger(held)\n if cursor.reads or cursor.writes:\n pre << NextTrigger()\n cursor.init()\n SetNextTrigger(pre_after)\n else:\n pre << pre_after\n held << NextTrigger()\n if outer is not None:\n ROUTE.stack.append(outer)\n\n def cursors_for(self, st):\n \"\"\"`Cursors` for a loop `for (…; …; i = i ± c)` at a counter with a place, when that store is its whole update and\n its body is as `lengths_fixed_in` wants it (no wait, no call, no length changed, nothing stored into i).\"\"\"\n upd = st.get(\"update\") or []\n if len(upd) != 1 or upd[0].get(\"kind\") != \"assign\":\n return None\n i = upd[0].get(\"target\")\n if i not in self.places:\n return None\n step = counter_step(upd[0].get(\"value\"), i)\n if step is None or step[0] != \"add\":\n return None\n lists = {a[\"id\"] for a in self.p.get(\"arrays\", []) if a.get(\"dynamic\") or a.get(\"through\")}\n if not lengths_fixed_in(st[\"body\"], i, lists):\n return None\n return Cursors(i, self.places[i], step[1])\n\n def cursor_at(self, index, a):\n \"\"\"The loop's `Cursors` an access at `index` of `a` can go through, or None: the index is a cursor loop's counter,\n and the array keeps its block where it is — a fixed one, or a growing one the loop found at least as long as its\n own (`inside`: known, or a flag set on the way in).\"\"\"\n if index.get(\"kind\") != \"var\":\n return None\n cur = self.cursors.get(index.get(\"id\"))\n if cur is None or self.places.get(cur.counter) is not cur.place:\n return None\n if type(a) is ArrayStorage and a.placeable():\n return cur\n if type(a) is ListStorage and a.placeable() and self.bound_of(index, a.decl[\"id\"]) is not None:\n return cur\n return None\n\n def cursor_read(self, cur, a, e):\n # (A read that always happens fills its temporary whole: it need not start from 0, as `fresh` does.)\n if type(a) is ArrayStorage:\n r = rng(e[\"index\"])\n if r is not None and r[0] >= 0 and r[1] <= a.length - 1:\n out = EUDVariable()\n cur.read(unProxy(a.store), out) # (inside, the ranges say)\n return out\n out = fresh(0)\n if EUDIf()(as_var(self.num(e[\"index\"])) <= a.length - 1):\n cur.read(unProxy(a.store), out)\n EUDEndIf()\n return out\n bound = self.bound_of(e[\"index\"], e[\"array\"])\n if bound is True:\n out = EUDVariable()\n cur.read(a.base[0], out)\n return out\n out = fresh(0)\n if EUDIf()(bound >= 1):\n cur.read(a.base[0], out)\n if EUDElseIf()(as_var(self.num(e[\"index\"])) < as_var(a.field(\"len\"))):\n varray_read(a.base[0], cur.place, out)\n EUDEndIf()\n return out\n\n def cursor_write(self, cur, a, st, value, modifier):\n if a.bits and modifier == SetTo:\n value = saturate(value, a.bits)\n v = fresh(value) if modifier == SetTo and not isinstance(value, int) else value\n if type(a) is ArrayStorage:\n r = rng(st[\"index\"])\n if r is not None and r[0] >= 0 and r[1] <= a.length - 1:\n cur.write(a.store._epd + VARRAY_VALUE // 4, v, modifier)\n else:\n if EUDIf()(as_var(self.num(st[\"index\"])) <= a.length - 1):\n cur.write(a.store._epd + VARRAY_VALUE // 4, v, modifier)\n EUDEndIf()\n return\n bound = self.bound_of(st[\"index\"], st[\"array\"])\n if bound is True:\n cur.write(a.base[1], v, modifier)\n return\n i, n = as_var(self.num(st[\"index\"])), as_var(a.field(\"len\"))\n if EUDIf()(bound >= 1):\n cur.write(a.base[1], v, modifier)\n if EUDElseIf()(i < n):\n varray_write(a.base[1], cur.place, v, modifier)\n if EUDElseIf()(i == n):\n a.push(v)\n EUDEndIf()\n\n def for_body(self, st, ctx, head, update, exit_, cursor=None):\n head << NextTrigger()\n if st.get(\"cond\") is not None:\n EUDJumpIfNot(self.cond(st[\"cond\"]), exit_)\n self.straight(st[\"body\"], dict(ctx, **{\"break\": exit_, \"continue\": update}))\n update << NextTrigger()\n if cursor is not None and self.cursor_update(st, cursor):\n EUDJump(head)\n elif self.straight(st[\"update\"], ctx):\n if cursor is not None:\n cursor.advance()\n EUDJump(head)\n exit_ << NextTrigger()\n\n def cursor_update(self, st, cursor):\n \"\"\"A cursor loop's update `i = i ± c` of a variable of the program's own: the counter, its place and every\n register moved on in one trigger, where they were three. False when the counter is kept otherwise (a row).\"\"\"\n target = self.vars.get(cursor.counter)\n if not isinstance(target, Storage) or target.rowed or target.bits:\n return False\n c = cursor.step\n acts = [target.store.AddNumber(c & U32), cursor.place.bytes.AddNumber((VARRAY_STRIDE * c) & U32), cursor.place.dwords.AddNumber((VARRAY_STRIDE // 4 * c) & U32)]\n acts += [SetMemory(t + 4, Add, (VARRAY_STRIDE * c) & U32) for t, _ in cursor.reads]\n acts += [SetMemory(act + 16, Add, (VARRAY_STRIDE // 4 * c) & U32) for act, _ in cursor.writes]\n DoActions(acts)\n return True\n\n def unrolled(self, st, ctx):\n exit_ = Forward()\n for statements in st[\"iterations\"]:\n nxt = Forward()\n self.straight(statements, dict(ctx, **{\"break\": exit_, \"continue\": nxt}))\n nxt << NextTrigger()\n exit_ << NextTrigger()\n\n def switch(self, st, ctx):\n v = as_var(self.num(st[\"value\"]))\n exit_ = Forward()\n labels = [Forward() for _ in st[\"cases\"]]\n default = None\n for c, label in zip(st[\"cases\"], labels):\n if c[\"value\"] is None:\n default = label\n continue\n n = c[\"value\"]\n if n != n or n < -SIGN or n > U32: # NaN, or a value the variable never holds\n continue\n EUDJumpIf(v == int(n) & U32, label)\n EUDJump(default if default is not None else exit_)\n for c, label in zip(st[\"cases\"], labels):\n label << NextTrigger()\n self.straight(c[\"body\"], dict(ctx, **{\"break\": exit_}))\n exit_ << NextTrigger()\n\n def sleep(self, st):\n if self.in_function:\n raise Fail(\"trigscript: sleep inside a function that is called%s\" % where(st))\n if st.get(\"cycles\") is not None:\n frames = int(st[\"cycles\"])\n else:\n frames = max(1, int(round(float(st.get(\"ms\", 0)) * FRAMES_PER_SECOND / 1000)))\n co = self.co\n index, resume = co.resume_point()\n # The frame after this one is one frame later: sleep(frames(1)) goes on in the next frame, as the\n # simulator has it. (Until 3.4 the wait was one frame longer, so a loop sleeping a frame ran every other.)\n co.park(index, frames - 1)\n resume << NextTrigger()\n\n def parallel(self, st):\n \"\"\"Promise.all / race: each branch a coroutine of its own, started here and run from here, in order, every frame\n the program waits at this statement; the program goes on when every branch has ended (all) or the first has\n (race, which leaves the others where they are). The program waits here through a place of its own to go on\n from, which this same statement is: the first frame falls into it, the later ones jump to it.\"\"\"\n if self.in_function:\n raise Fail(\"trigscript: await inside a function that is called%s\" % where(st))\n parent = self.co\n kids = [Coroutine(self, self.rows()) for _ in st[\"branches\"]]\n for k in kids:\n k.set_state(0)\n k.set_wait(0)\n index, again = parent.resume_point()\n again << NextTrigger()\n after = Forward()\n for k, body in zip(kids, st[\"branches\"]):\n skip = Forward()\n EUDJumpIf(as_var(k.get_state()) == DONE, skip)\n self.coroutine(k, body)\n if st[\"mode\"] == \"race\":\n EUDJumpIfNot(as_var(k.get_state()) == DONE, skip)\n for other in kids:\n if other is not k:\n other.set_state(DONE)\n EUDJump(after)\n skip << NextTrigger()\n if st[\"mode\"] == \"all\":\n EUDJumpIf([as_var(k.get_state()) == DONE for k in kids], after)\n parent.park(index, 0)\n after << NextTrigger()\n\n def action(self, st):\n r = st[\"record\"]\n fields = dict(locid1=r[\"location\"], strid=string_of(r[\"text\"]), wavid=string_of(r[\"wav\"]), time=r[\"time\"], player1=r[\"player\"], player2=r[\"target\"], unitid=r[\"unitId\"], acttype=r[\"type\"], amount=r[\"modifier\"], flags=r[\"flags\"])\n shown = None\n if st.get(\"text\") is not None:\n if self.has_id(st[\"text\"]):\n fields[\"strid\"] = self.text_id_of(st[\"text\"])\n else:\n kind = SLOT_OF_ACTION.get(int(r[\"type\"]))\n if kind is None:\n raise Fail(\"trigscript: this action's text cannot be one that is made%s\" % where(st))\n # Over the string kept for this kind of field - on the computer of the player the action is for, which is\n # the one the program is running as: the game reads the string again whenever it draws, and another\n # player's text must not be there when it does. Nothing of the script can read the string back.\n shown = self.text(st[\"text\"])\n if EUDIf()(IsUserCP()):\n texts()[\"show\"](GetMapStringAddr(slot(kind)), shown.addr)\n EUDEndIf()\n self.used(shown)\n fields[\"strid\"] = slot(kind)\n count = None\n for variable in st.get(\"variables\") or []:\n value = as_var(self.num(variable[\"expr\"]))\n field = variable[\"field\"]\n if field == \"modifier\":\n # A unit count: the byte field is not a variable's place, so the action is done once per\n # unit — as many as the variable says, 0 being none (in the record, 0 means \"all\").\n count = value\n continue\n name = {\"target\": \"player2\", \"time\": \"time\", \"player\": \"player1\", \"location\": \"locid1\", \"text\": \"strid\", \"wav\": \"wavid\", \"unitId\": \"unitid\"}.get(field)\n if name is None:\n raise Fail(\"trigscript: no variable can stand in the %s field%s\" % (field, where(st)))\n if name == \"unitid\":\n # A unit type the game has: past the table, an action reads what is not a unit.\n value = fresh(value)\n if EUDIf()(value >= 228):\n value << 228\n EUDEndIf()\n fields[name] = value\n moves = int(r[\"type\"]) in MOVES_UNITS\n if count is None:\n DoActions(Action(**fields))\n if moves:\n forget_counts()\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\n if moves:\n forget_counts()\n\n def print_(self, st):\n \"\"\"Text with values in it: every value first, then the text, shown only on the screen of the player it is for.\"\"\"\n args = []\n temps = []\n for part in st[\"parts\"]:\n k = part[\"kind\"]\n if k == \"text\":\n args.append(part[\"text\"])\n elif k == \"value\":\n v = self.text(part[\"text\"])\n temps.append(v)\n args.append(ptr2s(v.addr))\n elif k == \"number\" and part.get(\"fixed\"):\n args.extend(self.fixed_pieces(self.num(part[\"expr\"])))\n elif k == \"number\":\n v = self.num(part[\"expr\"])\n if isinstance(v, int):\n args.append(str(v if part.get(\"unsigned\") else signed(v)))\n elif part.get(\"unsigned\"):\n args.append(v)\n else:\n # A number below zero: its minus sign, then how far below it is.\n sign, size = fresh(NO_SIGN), fresh(v)\n if EUDIf()(size >= SIGN):\n sign << MINUS_SIGN\n size << 0 - size\n EUDEndIf()\n args.append(ptr2s(sign))\n args.append(size)\n elif k == \"name\":\n args.append(PName(self.one_player(part[\"player\"], st)))\n elif k == \"color\":\n args.append(PColor(self.one_player(part[\"player\"], st)))\n elif k == \"char\":\n args.append(self.char(part[\"expr\"]))\n else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n if not args:\n return\n # eudplib prints for the current player, and only on that player's own computer.\n show = (lambda: f_eprintln(*args)) if st.get(\"position\") == \"center\" else (lambda: GetGlobalStringBuffer().print(*args))\n to = int(st.get(\"to\", CURRENT_PLAYER))\n if to == CURRENT_PLAYER:\n show()\n self.used(*temps)\n return\n for p in group_slots(to, st):\n f_setcurpl(p)\n show()\n f_setcurpl(self.current())\n self.used(*temps)\n\n # ── texts ──\n def has_id(self, e):\n \"\"\"Whether a text is one of the built map's table whatever happens in the game (compiler/ir.ts#textHasId).\"\"\"\n k = e[\"kind\"]\n if k in (\"text\", \"textOf\"):\n return True\n if k == \"textVar\":\n return not isinstance(self.var(e[\"id\"], e), TextStorage)\n if k == \"textTernary\":\n return self.has_id(e[\"whenTrue\"]) and self.has_id(e[\"whenFalse\"])\n return False\n\n def text_id_of(self, e):\n \"\"\"The id of a text that has one: an int, or a variable.\"\"\"\n k = e[\"kind\"]\n if k == \"text\":\n return text_id(e[\"text\"])\n if k == \"textVar\":\n return self.var(e[\"id\"], e).get()\n if k == \"textOf\":\n return self.array(e[\"array\"], e).get(self.num(e[\"index\"]))\n if k == \"textTernary\":\n t = EUDVariable()\n if EUDIf()(self.cond(e[\"cond\"])):\n t << self.text_id_of(e[\"whenTrue\"])\n if EUDElse()():\n t << self.text_id_of(e[\"whenFalse\"])\n EUDEndIf()\n return t\n raise Fail(\"trigscript: this text has no id%s\" % where(e))\n\n def used(self, *values):\n \"\"\"Values that have been used: a block that was the value's own goes back to the heap.\"\"\"\n for v in values:\n if v.taken and not isinstance(v.block, int):\n texts()[\"release\"](v.block)\n\n def texts_of(self, exprs):\n \"\"\"Several texts in the order they are written. A variable's text is only looked at - unless working a later\n one out runs a call, which may give that variable another text and its block back to the heap: then it is a copy.\"\"\"\n out = []\n for i, e in enumerate(exprs):\n v = self.text(e)\n if not v.taken and any(has_call(x) for x in exprs[i + 1:]):\n v = self.own(v)\n out.append(v)\n return out\n\n def own(self, v):\n \"\"\"A value as something to keep: its own block as it is, a copy of a variable's.\"\"\"\n if v.taken:\n return v\n addr, block, length = texts()[\"copy\"](v.addr, v.block, v.length())\n return TextVal(addr, block, length, True)\n\n def store_text(self, st):\n \"\"\"A text into cell i of three arrays (a row's): worked out first - it may be made from what the cells hold -\n then the block they held goes back, then they take the text, a copy of one that was only looked at.\"\"\"\n v = self.own(self.text(st[\"value\"]))\n addr, block, length = fresh(v.addr), fresh(v.block), fresh(v.length())\n i = fresh(self.num(st[\"index\"]))\n held = self.array(st[\"block\"], st)\n texts()[\"release\"](as_var(held.get(i)))\n self.array(st[\"addr\"], st).set(i, addr)\n held.set(i, block)\n self.array(st[\"chars\"], st).set(i, length)\n\n def put(self, storage, e, kind):\n \"\"\"A value into a variable. A text is worked out first - it may be made from what the variable holds - and only then does the block the variable held go back.\"\"\"\n if kind != \"text\":\n storage.set(self.value(e, kind))\n return\n if not isinstance(storage, TextStorage):\n storage.set(self.text_id_of(e))\n return\n v = self.own(self.text(e))\n addr, block, length = fresh(v.addr), fresh(v.block), fresh(v.length())\n texts()[\"release\"](storage.cell(storage.block))\n storage.write(addr, block, length)\n\n def char(self, e):\n \"\"\"One byte from a number, as something to print: a colour code chosen when the map is played; 0 is nothing.\"\"\"\n v = self.num(e)\n if isinstance(v, int):\n return chr(v & 0xFF) if v & 0xFF else \"\"\n one = Db(4)\n f_bwrite(one, v & 0xFF)\n return ptr2s(one)\n\n def written(self, parts, st):\n \"\"\"The parts of a text in the scratch, and where it ends. Every value is worked out first, since working one\n out may use the scratch itself; then the parts are written a group at a time, a group being what cannot\n come to more than a text holds, so that checking where the writing has got to between groups keeps it inside.\"\"\"\n T = texts()\n items, temps = self.items(parts, st)\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n self.write_items(items, pos, T[\"scratch\"] + TEXT_BYTES, lambda at, value: T[\"append\"](at, value.addr), True)\n return pos, temps\n\n def build_text(self, st):\n \"\"\"Parts written onto the end of a buffer (emptied first on `reset`): a text put together a piece at a time.\"\"\"\n data, pos = buffer(st[\"buffer\"])\n B = builds()\n items, temps = self.items(st[\"parts\"], st)\n if st.get(\"reset\"):\n pos << data\n f_bwrite(data, 0)\n last = data + BUILD_BYTES + TEXT_BYTES\n self.write_items(items, pos, data + BUILD_BYTES, lambda at, value: B[\"append\"](at, value.addr, last), False)\n pos << B[\"cut\"](pos, data + BUILD_BYTES)\n self.used(*temps)\n\n def items(self, parts, st):\n \"\"\"The parts of a text as what f_dbstr_print takes, each with the most bytes it can come to; a value of the\n program as itself, to be copied in. Every value is worked out here, before anything is written.\"\"\"\n items, temps = [], []\n for part in parts:\n k = part[\"kind\"]\n if k == \"text\":\n data = part[\"text\"].encode(\"utf-8\")[:TEXT_BYTES].decode(\"utf-8\", \"ignore\")\n items.append(([data], len(data.encode(\"utf-8\")), None))\n elif k == \"number\" and part.get(\"fixed\"):\n pieces = self.fixed_pieces(self.num(part[\"expr\"]))\n items.append((pieces, 11 if len(pieces) > 1 else len(pieces[0]), None))\n elif k == \"number\":\n v = self.num(part[\"expr\"])\n if isinstance(v, int):\n data = str(v if part.get(\"unsigned\") else signed(v))\n items.append(([data], len(data), None))\n elif part.get(\"unsigned\"):\n items.append(([fresh(v)], 10, None))\n else:\n sign, size = fresh(NO_SIGN), fresh(v)\n if EUDIf()(size >= SIGN):\n sign << MINUS_SIGN\n size << 0 - size\n EUDEndIf()\n items.append(([ptr2s(sign), size], 11, None))\n elif k == \"name\":\n items.append(([PName(self.one_player(part[\"player\"], st))], 25, None))\n elif k == \"color\":\n items.append(([PColor(self.one_player(part[\"player\"], st))], 1, None))\n elif k == \"value\":\n v = self.text(part[\"text\"])\n if not v.taken and has_call(parts[parts.index(part) + 1:]):\n v = self.own(v)\n temps.append(v)\n items.append((None, TEXT_BYTES, v))\n elif k == \"char\":\n c = self.char(part[\"expr\"])\n if c != \"\":\n items.append(([c], 1, None))\n else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n return items, temps\n\n def write_items(self, items, pos, end, append, first_free):\n \"\"\"Items written at pos, which moves to where the text ends: a group at a time, a group being what cannot come\n to more than a text holds, so that checking pos against `end` between groups keeps the writing inside the room\n behind it. `first_free`: pos starts at the beginning, so the first group needs no check.\"\"\"\n group, room, first = [], 0, [first_free]\n\n def flush():\n if not group:\n return\n if first[0]:\n pos << f_dbstr_print(pos, *group)\n else:\n if EUDIf()(pos <= end):\n pos << f_dbstr_print(pos, *group)\n EUDEndIf()\n first[0] = False\n del group[:]\n\n for args, size, value in items:\n if value is not None:\n flush()\n room = 0\n if first[0]:\n pos << append(pos, value)\n else:\n if EUDIf()(pos <= end):\n pos << append(pos, value)\n EUDEndIf()\n first[0] = False\n continue\n if room + size > TEXT_BYTES + 1:\n flush()\n room = 0\n group.extend(args)\n room += size\n flush()\n\n def text(self, e):\n \"\"\"A text: where it is, its block, its length (TextVal).\"\"\"\n T = texts()\n k = e[\"kind\"]\n if k == \"text\":\n return TextVal(fresh(GetMapStringAddr(e[\"text\"]) if e[\"text\"] else T[\"empty\"]), 0, len(e[\"text\"]), True)\n if k == \"textVar\" and isinstance(self.var(e[\"id\"], e), TextStorage):\n return self.var(e[\"id\"], e).get()\n if k == \"textAt\":\n # Cells that were never given a text are 0: the empty one, as a variable's are.\n i = fresh(self.num(e[\"index\"]))\n addr = fresh(self.array(e[\"addr\"], e).get(i))\n if EUDIf()(addr == 0):\n addr << T[\"empty\"]\n EUDEndIf()\n return TextVal(addr, fresh(self.array(e[\"block\"], e).get(i)), fresh(self.array(e[\"chars\"], e).get(i)), False)\n if self.has_id(e):\n return TextVal(T[\"address\"](as_var(self.text_id_of(e))), 0, None, True)\n if k == \"built\":\n # Only looked at: what keeps it takes a copy, since the buffer is built again next time.\n return TextVal(fresh(buffer(e[\"buffer\"])[0]), 0, None, False)\n if k == \"template\":\n pos, temps = self.written(e[\"parts\"], e)\n addr, block, length = T[\"make\"](pos)\n self.used(*temps)\n return TextVal(addr, block, length, True)\n if k == \"textTernary\":\n addr, block, length = EUDVariable(), EUDVariable(), EUDVariable()\n if EUDIf()(self.cond(e[\"cond\"])):\n v = self.own(self.text(e[\"whenTrue\"]))\n addr << v.addr\n block << v.block\n length << v.length()\n if EUDElse()():\n v = self.own(self.text(e[\"whenFalse\"]))\n addr << v.addr\n block << v.block\n length << v.length()\n EUDEndIf()\n return TextVal(addr, block, length, True)\n if k == \"textSlice\":\n of = self.text(e[\"of\"])\n start = self.num(e[\"start\"]) if e.get(\"start\") is not None else 0\n end = self.num(e[\"end\"]) if e.get(\"end\") is not None else 0x7FFFFFFF\n addr, block, length = T[\"slice\"](of.addr, start, end)\n self.used(of)\n return TextVal(addr, block, length, True)\n if k == \"textPad\":\n of, fill = self.texts_of([e[\"of\"], e[\"with\"]])\n width = self.num(e[\"width\"])\n addr, block, length = T[\"pad\"](of.addr, of.length(), width, fill.addr, 1 if e[\"side\"] == \"start\" else 0)\n self.used(of, fill)\n return TextVal(addr, block, length, True)\n if k == \"textRepeat\":\n of = self.text(e[\"of\"])\n addr, block, length = T[\"repeat\"](of.addr, self.num(e[\"count\"]))\n self.used(of)\n return TextVal(addr, block, length, True)\n if k == \"textCall\":\n self.call(e[\"call\"])\n result = e[\"call\"].get(\"result\")\n if result is None:\n return TextVal(fresh(T[\"empty\"]), 0, 0, True)\n return self.var(result[\"decl\"][\"id\"], e).take_out()\n raise Fail(\"trigscript: unknown text %r%s\" % (k, where(e)))\n\n def text_number(self, e):\n T = texts()\n k = e[\"kind\"]\n if k == \"textIndexOf\":\n of, find = self.texts_of([e[\"of\"], e[\"find\"]])\n start = self.num(e[\"from\"]) if e.get(\"from\") is not None else 0\n out = T[\"find\"](of.addr, find.addr, start)\n self.used(of, find)\n return out\n of = self.text(e[\"of\"])\n if k == \"textLength\":\n n = of.length()\n out = n if isinstance(n, int) else fresh(n)\n self.used(of)\n return out\n out = T[\"code\"](of.addr, self.num(e[\"index\"]))\n self.used(of)\n return out\n\n def text_truth(self, e):\n \"\"\"A comparison of two texts, or a test of one, as 0 / 1.\"\"\"\n T = texts()\n t = fresh(0)\n if e[\"kind\"] == \"textTest\":\n of, find = self.texts_of([e[\"of\"], e[\"find\"]])\n if e[\"test\"] == \"startsWith\":\n t << T[\"starts\"](of.addr, find.addr)\n elif e[\"test\"] == \"endsWith\":\n t << T[\"ends\"](of.addr, find.addr)\n else:\n if EUDIfNot()(T[\"find\"](of.addr, find.addr, 0) == 0xFFFFFFFF):\n t << 1\n EUDEndIf()\n self.used(of, find)\n return t\n op = e[\"op\"]\n if op in (\"==\", \"!=\") and self.has_id(e[\"left\"]) and self.has_id(e[\"right\"]):\n # Two texts of the table: the same text is the same id.\n a, b = self.text_id_of(e[\"left\"]), self.text_id_of(e[\"right\"])\n if isinstance(a, int) and isinstance(b, int):\n return 1 if (a == b) == (op == \"==\") else 0\n if EUDIf()(as_var(a) == b):\n t << 1\n EUDEndIf()\n else:\n a, b = self.texts_of([e[\"left\"], e[\"right\"]])\n # The difference of the first bytes that differ, as a number of 32 bits: 0, below zero, or above.\n d = f_strcmp(a.addr, b.addr)\n self.used(a, b)\n if op in (\"==\", \"!=\"):\n if EUDIf()(d == 0):\n t << 1\n EUDEndIf()\n else:\n below = (lambda: d >= SIGN)\n if op in (\"<\", \">=\"):\n if EUDIf()(below()):\n t << 1\n EUDEndIf()\n else: # \"<=\" and \">\": whether it is not above\n if EUDIf()(EUDOr(d == 0, below())):\n t << 1\n EUDEndIf()\n return flipped(t) if op in (\">=\", \">\") else t\n return flipped(t) if op == \"!=\" else t\n\n def text_loop(self, st, ctx):\n \"\"\"`for (const ch of s)`: the text walked once, a character a turn, each a made text of its own.\"\"\"\n T = texts()\n turn = self.declare(st[\"decl\"])\n over = self.own(self.text(st[\"of\"]))\n held = fresh(over.block)\n at = fresh(over.addr)\n head, done = Forward(), Forward()\n inner = dict(ctx)\n inner[\"break\"], inner[\"continue\"] = done, head\n leave = None\n if ctx.get(\"fn\") is not None:\n # A return inside the loop leaves through here, so that the text the loop holds goes back first.\n leave = Forward()\n inner[\"fn\"] = dict(ctx[\"fn\"], end=leave)\n head << NextTrigger()\n n = T[\"size\"](at)\n EUDJumpIf(n == 0, done)\n f_memcpy(T[\"scratch\"], at, n)\n f_bwrite(T[\"scratch\"] + n, 0)\n addr, block, length = T[\"make\"](T[\"scratch\"] + n)\n T[\"release\"](turn.cell(turn.block))\n turn.write(addr, block, length)\n at += n\n # An error out of the body leaves through here too (B4), so that the text the loop holds goes back first.\n unwind = Forward() if self.handlers else None\n if unwind is not None:\n outer_handler = self.handlers[-1]\n self.handlers.append(unwind)\n try:\n if self.straight(st[\"body\"], inner):\n EUDJump(head)\n finally:\n if unwind is not None:\n self.handlers.pop()\n done << NextTrigger()\n T[\"release\"](held)\n if unwind is not None:\n past = Forward()\n EUDJump(past)\n unwind << NextTrigger()\n T[\"release\"](held)\n EUDJump(outer_handler)\n past << NextTrigger()\n if leave is not None:\n after = Forward()\n EUDJump(after)\n leave << NextTrigger()\n T[\"release\"](held)\n EUDJump(ctx[\"fn\"][\"end\"])\n after << NextTrigger()\n\n def value(self, e, kind):\n \"\"\"An expression as what a variable of `kind` holds.\"\"\"\n return self.num(e) if kind == \"number\" else self.unit(e) if kind == \"unit\" else self.truth(e)\n\n def function(self, id_, node):\n \"\"\"A called function: its parameters' cells, its result's, and its one body — an EUDFunc of no\n arguments, since the cells are the program's own (a row a player, as any variable is).\"\"\"\n made = self.made.get(id_)\n if made is not None:\n return made\n f = self.functions.get(id_)\n if f is None:\n raise Fail(\"trigscript: unknown function %r%s\" % (id_, where(node)))\n params = [self.declare(d) for d in f[\"params\"]]\n kind = f[\"result\"][\"kind\"] if f.get(\"result\") else \"void\"\n result = self.declare(f[\"result\"][\"decl\"]) if f.get(\"result\") else None\n lowering = self\n if f.get(\"recursive\"):\n return self.recursive_function(id_, f, params, kind, result)\n\n @EUDFunc\n def body():\n # A text result keeps what it held until `return` puts the next one in it, which is when that block goes back.\n if result is not None and kind != \"text\":\n result.set(NO_UNIT if kind == \"unit\" else 0)\n end = Forward()\n lowering.in_function += 1\n outer = lowering.handlers\n lowering.handlers = [end]\n try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\n lowering.handlers = outer\n end << NextTrigger()\n\n made = self.made[id_] = (body, params, result)\n return made\n\n def recursive_function(self, id_, f, params, kind, result):\n \"\"\"A function that calls itself. eudplib's EUDFunc keeps one return address and is not there to be called\n until its body is whole, so this one is triggers of our own: the body in a scope apart, ended by a trigger\n whose next-trigger field is the return address - a cell a call writes, and a frame keeps. It is on record\n before its body is lowered, so a call of it met in that body finds it.\"\"\"\n start, tail = Forward(), Forward()\n # The return address a second time, in a variable: a frame keeps it from there, since reading a cell of\n # the game's memory back costs some thirty triggers and writing a variable out costs two.\n back = EUDVariable()\n\n def body():\n ROUTE.unknown_call()\n after = Forward()\n DoActions([SetNextPtr(tail, after), back.SetNumber(after)])\n EUDJump(start)\n after << NextTrigger()\n\n made = self.made[id_] = (body, params, result)\n entry = {\"f\": f, \"tail\": tail, \"back\": back}\n PushTriggerScope()\n start << NextTrigger()\n if result is not None and kind != \"text\":\n result.set(NO_UNIT if kind == \"unit\" else 0)\n end = Forward()\n self.in_function += 1\n self.within.append(entry)\n outer = self.handlers\n self.handlers = [end]\n try:\n self.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n self.within.pop()\n self.in_function -= 1\n self.handlers = outer\n end << NextTrigger()\n tail << RawTrigger()\n PopTriggerScope()\n return made\n\n def saved_cells(self, saves, node):\n \"\"\"What a frame keeps, as (read, write) pairs a cell: the variables, a unit three cells, an array's handle four.\"\"\"\n pairs = []\n for id_ in saves.get(\"vars\", []):\n s = self.vars.get(id_)\n if s is None:\n # Declared further down the body: it has no cell yet, and the declaration that makes it sets it.\n continue\n if isinstance(s, UnitStorage):\n for cell in (s.ptr, s.epd, s.uid):\n pairs.append(self.cell_pair(cell, s.player_of if s.rowed else None))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.player_of if s.rowed else None))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.player_of if s.rowed else None))\n for id_ in saves.get(\"arrays\", []):\n a = self.array(id_, node)\n if isinstance(a, ListStorage):\n for name in HANDLE:\n pairs.append(self.cell_pair(a.handle[name], a.player_of if a.rowed else None))\n return pairs\n\n def saved_texts(self, saves):\n \"\"\"The made texts among what a frame keeps: each owns a block, which the frame keeps for it.\"\"\"\n return [s for s in (self.vars.get(id_) for id_ in saves.get(\"vars\", [])) if isinstance(s, TextStorage)]\n\n def cell_pair(self, cell, rows):\n if rows:\n return (lambda: cell[rows()]), (lambda v: cell.__setitem__(rows(), v))\n return (lambda: cell), (lambda v: cell << v)\n\n def keep(self, call):\n \"\"\"Before a call that may come back into the function it is in: that function's cells and its return\n address onto the stack, the handles of its arrays set to \"no block\". Past the depth the map allows the\n program says so and stops for good.\"\"\"\n saves = call[\"saves\"]\n st = stack()\n if not self.within:\n raise Fail(\"trigscript: a call keeps a frame outside a function that calls itself%s\" % where(call))\n mine = self.within[-1]\n if self.errors:\n # B4: a RangeError, as JavaScript's, where the call is and before anything of it is kept: the function this\n # call is in returns with it in flight.\n if EUDIf()(st[\"depth\"] >= STACK_DEPTH):\n self.put(self.var(self.errors[\"message\"], call), {\"kind\": \"text\", \"text\": STACK_OVERFLOW_MESSAGE}, \"text\")\n self.var(\"(error line)\").set(int((call.get(\"at\") or {}).get(\"line\", 0)))\n self.var(self.errors[\"thrown\"], call).set(RANGE_ERROR)\n EUDJump(self.handler(call))\n elif EUDIf()(st[\"depth\"] >= STACK_DEPTH):\n GetGlobalStringBuffer().print(\"\\x06TrigScript: stack overflow in %s, line %s - %d calls deep. The program has stopped.\" % (saves.get(\"within\", \"a function\"), (call.get(\"at\") or {}).get(\"line\", \"?\"), STACK_DEPTH))\n st[\"depth\"] << 0\n st[\"at\"] << st[\"base\"]\n # The program stops for good, or, in a task, the task does.\n stopped = self.pool.co if self.pool else self.top\n stopped.set_state(DONE)\n EUDJump(stopped.frame_end)\n EUDEndIf()\n pairs = self.saved_cells(saves, call)\n at = st[\"at\"]\n f_dwwrite_epd(at, mine[\"back\"])\n at += 1\n for read, _ in pairs:\n f_dwwrite_epd(at, read())\n at += 1\n st[\"depth\"] += 1\n for id_ in saves.get(\"arrays\", []):\n a = self.array(id_, call)\n if isinstance(a, ListStorage):\n for name in HANDLE:\n a.put(name, 0)\n # A text the function holds keeps its block on the stack; the variable holds none for the length of the call,\n # so that the inner run's first text does not give the outer run's block back.\n held = self.saved_texts(saves)\n for s in held:\n s.write(0, 0, 0)\n return pairs, mine, held\n\n def bring_back(self, call, kept):\n pairs, mine, held = kept\n st = stack()\n # The block the inner run left in a text goes back to the heap before the text is the outer run's again.\n for s in held:\n texts()[\"release\"](s.cell(s.block))\n # The block the inner run left in a handle goes back to the heap before the handle is the outer run's again.\n for id_ in call[\"saves\"].get(\"arrays\", []):\n a = self.array(id_, call)\n if isinstance(a, ListStorage):\n ptr = fresh(a.field(\"ptr\"))\n if EUDIf()(ptr >= 1):\n a.heap[\"give\"](ptr, a.field(\"k\"))\n EUDEndIf()\n at = st[\"at\"]\n for _, write in reversed(pairs):\n at -= 1\n write(f_dwread_epd(at))\n at -= 1\n mine[\"back\"] << f_dwread_epd(at)\n f_dwwrite_epd(EPD(mine[\"tail\"]) + 1, mine[\"back\"])\n st[\"depth\"] -= 1\n\n def call_function(self, call, result):\n body, params, returned = self.function(call[\"fn\"], call)\n if len(params) != len(call[\"params\"]):\n raise Fail(\"trigscript: %s takes %d values, not %d%s\" % (call[\"fn\"], len(params), len(call[\"params\"]), where(call)))\n # Every argument first, then the parameters: an argument may be a call of this same function. A variable's\n # own cell is copied when a later argument holds a call, which may be of a function that writes that variable.\n values = []\n for i, p in enumerate(call[\"params\"]):\n if p[\"decl\"][\"kind\"] == \"text\":\n # A text is a copy of its own by the time the parameter takes it: three numbers, read now.\n t = self.own(self.text(p[\"init\"]))\n values.append((fresh(t.addr), fresh(t.block), fresh(t.length())))\n continue\n v = self.value(p[\"init\"], p[\"decl\"][\"kind\"])\n later = any(has_call(q[\"init\"]) for q in call[\"params\"][i + 1:])\n if later and p[\"decl\"][\"kind\"] != \"unit\" and not isinstance(v, int):\n v = fresh(v)\n values.append(v)\n # A call that may come back here keeps this function's frame: after the arguments are worked out (they read\n # it), before the parameters are set (they may be this function's own).\n kept = None\n if call.get(\"saves\"):\n # Copies: an argument may be one of this function's own parameters, which the ones set before it write over.\n values = [v if isinstance(v, (int, tuple)) else UnitRef(v.ptr if isinstance(v.ptr, int) else fresh(v.ptr), v.epd if isinstance(v.epd, int) else fresh(v.epd), v.uid if v.uid is None or isinstance(v.uid, int) else fresh(v.uid)) if isinstance(v, UnitRef) else fresh(v) for v in values]\n kept = self.keep(call)\n for s, v in zip(params, values):\n if isinstance(v, tuple):\n # What the parameter held from the call before goes back; then it holds this call's text.\n texts()[\"release\"](s.cell(s.block))\n s.write(*v)\n else:\n s.set(v)\n body()\n if kept is not None:\n self.bring_back(call, kept)\n if call[\"fn\"] in self.throwing:\n # It came back with an error in flight: on to the nearest catch, nothing of the result used.\n EUDJumpIf(as_var(self.var(self.errors[\"thrown\"], call).get()) >= 1, self.handler(call))\n if result is None:\n return 0\n if isinstance(result, TextStorage):\n # The function's text moves to the call's own result, from where a textCall takes it.\n v = returned.take_out()\n texts()[\"release\"](result.cell(result.block))\n result.write(v.addr, v.block, v.length())\n return 0\n result.set(returned.get())\n return result.get()\n\n def call(self, call):\n result = self.declare(call[\"result\"][\"decl\"]) if call.get(\"result\") else None\n if call.get(\"fn\") is not None:\n return self.call_function(call, result)\n # A text result keeps what it held until `return` puts the next one in it, which is when that block goes back.\n if result is not None and call[\"result\"][\"kind\"] != \"text\":\n result.set(NO_UNIT if call[\"result\"][\"kind\"] == \"unit\" else 0)\n for p in call[\"params\"]:\n s = self.declare(p[\"decl\"])\n self.put(s, p[\"init\"], p[\"decl\"][\"kind\"])\n end = Forward()\n self.straight(call[\"body\"], {\"fn\": {\"result\": result, \"kind\": call[\"result\"][\"kind\"] if call.get(\"result\") else \"void\", \"end\": end}})\n end << NextTrigger()\n return result.get() if result is not None else 0\n\n # ── one frame ──\n def frame(self):\n if self.pools:\n self.tick += 1\n if self.per_player:\n for pool in self.pools.values():\n pool.base << f_mul(self.player, pool.max)\n self.coroutine(self.top, self.p[\"body\"])\n self.run_tasks()\n\n def run_tasks(self):\n \"\"\"The tasks' turn, after the program's: each slot whose task is running and has not had its turn in this frame\n goes on until it sleeps, waits or ends - by the number of its start, then by slot. A task started meanwhile\n sets `again`, and the slots are gone through once more, so that it starts in this frame too.\"\"\"\n if not self.pools:\n return\n if self.per_player:\n for pool in self.pools.values():\n pool.count << pool.live.get()\n sweep = Forward()\n sweep << NextTrigger()\n self.again << 0\n for pool in self.pools.values():\n passed = Forward()\n if pool.fresh is not None:\n go = Forward()\n EUDJumpIf(as_var(pool.fresh.get()) >= 1, go)\n EUDJumpIfNot(self.heard(pool.st[\"listen\"]), passed)\n go << NextTrigger()\n pool.fresh.set(0)\n i, seen = EUDVariable(), EUDVariable()\n i << 0\n seen << 0\n # `seen`: the running slots so far, less those that ended on their turn - so while it is below the count,\n # a running slot is still ahead (or one started meanwhile, which the next time round finds).\n if EUDWhile()([i <= pool.max - 1, seen < pool.count]):\n pool.select(i)\n skip = Forward()\n EUDJumpIf(as_var(pool.co.get_state()) == DONE, skip)\n seen += 1\n EUDJumpIf(as_var(pool.ran.get()) == self.tick, skip)\n pool.ran.set(self.tick)\n listen = pool.st.get(\"listen\")\n if listen is not None:\n # A listener (on.created, on.chat, ...) that has begun waits at the top of its loop, where a turn\n # with nothing to hand on does nothing: it is not given one.\n go = Forward()\n EUDJumpIf(as_var(pool.co.get_state()) == 0, go)\n EUDJumpIfNot(self.heard(listen), skip)\n go << NextTrigger()\n outer = self.pool\n self.pool = pool\n end = Forward()\n call = pool.st[\"call\"]\n result = self.declare(call[\"result\"][\"decl\"]) if call.get(\"result\") else None\n self.coroutine(pool.co, call[\"body\"], {\"fn\": {\"result\": result, \"kind\": call[\"result\"][\"kind\"] if call.get(\"result\") else \"void\", \"end\": end}}, end)\n self.pool = outer\n if EUDIf()(as_var(pool.co.get_state()) == DONE):\n pool.add(-1)\n seen -= 1\n EUDEndIf()\n skip << NextTrigger()\n i += 1\n EUDEndWhile()\n passed << NextTrigger()\n EUDJumpIf(self.again >= 1, sweep)\n\n def heard(self, listen):\n \"\"\"Whether a listener has anything to hand on this frame: units the events pass found, or one of its inputs.\"\"\"\n source = listen.get(\"source\")\n if source is not None:\n return {\"created\": EVENTS.made_n, \"died\": EVENTS.gone_n, \"killed\": EVENTS.gone_n, \"ordered\": EVENTS.turned_n, \"morphed\": EVENTS.changed_n}[source] >= 1\n found = fresh(0)\n for inp in listen.get(\"inputs\", []):\n if EUDIf()(as_var(INPUT.read(inp, self, listen)) >= 1):\n found << 1\n EUDEndIf()\n return found >= 1\n\n def start(self, st):\n \"\"\"A task into a free slot of its start: the arguments worked out as things are now, then the slot taken - its\n generation one more, its parameters set, its place the body's start - and the number the task is known by\n into `handle`. With every slot running nothing starts, and the game says so once.\"\"\"\n pool = self.pools[int(st[\"task\"])]\n call = st[\"call\"]\n values = []\n for prm in call[\"params\"]:\n kind = prm[\"decl\"][\"kind\"]\n target = self.var(prm[\"decl\"][\"id\"], st)\n if kind == \"unit\":\n ref = self.unit(prm[\"init\"])\n uid = ref.uid if ref.uid is not None else f_maskread_epd(ref.epd + OFF_UID, 0xFF00)\n values.append(UnitRef(fresh(ref.ptr), fresh(ref.epd), fresh(uid)))\n elif kind == \"text\" and isinstance(target, TextStorage):\n v = self.own(self.text(prm[\"init\"]))\n values.append(TextVal(fresh(v.addr), fresh(v.block), fresh(v.length()), True))\n elif kind == \"text\":\n values.append(fresh(self.text_id_of(prm[\"init\"])))\n else:\n values.append(fresh(self.value(prm[\"init\"], kind)))\n found, after = Forward(), Forward()\n i = fresh(0)\n if EUDWhile()(i <= pool.max - 1):\n pool.select(i)\n EUDJumpIf(as_var(pool.co.get_state()) == DONE, found)\n i += 1\n EUDEndWhile()\n # Every slot is running.\n for v in values:\n if isinstance(v, TextVal):\n texts()[\"release\"](v.block)\n if EUDIf()(pool.said == 0):\n pool.said << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: %s did not start, line %s - all %d of this start's tasks are running.\" % (call.get(\"name\") or \"a task\", (st.get(\"at\") or {}).get(\"line\", \"?\"), pool.max))\n EUDEndIf()\n if st.get(\"handle\"):\n self.var(st[\"handle\"], st).set(0)\n EUDJump(after)\n found << NextTrigger()\n gen = fresh(pool.gen.get())\n if EUDIf()(gen >= 0xFFFF):\n gen << 0\n EUDEndIf()\n gen += 1\n pool.gen.set(gen)\n for prm, v in zip(call[\"params\"], values):\n target = self.var(prm[\"decl\"][\"id\"], st)\n if isinstance(v, UnitRef):\n target.set(v)\n elif isinstance(v, TextVal):\n texts()[\"release\"](target.cell(target.block))\n target.write(v.addr, v.block, v.length())\n else:\n target.set(v)\n pool.co.set_state(0)\n pool.co.set_wait(0)\n pool.ran.set(self.tick - 1)\n pool.add(1)\n if pool.fresh is not None:\n pool.fresh.set(1)\n self.again << 1\n if st.get(\"handle\"):\n self.var(st[\"handle\"], st).set(pool.slot + pool.task * 256 + f_bitlshift(fresh(gen), 16))\n after << NextTrigger()\n\n def task_slot(self, e, act):\n \"\"\"The slot a task's number names, when the task is in it: `act(pool, slot)` runs for it, the slot being run left as it is.\"\"\"\n h = fresh(self.num(e))\n slot = self.bitwise(\"&\", h, 0xFF)\n task = self.bitwise(\"&\", self.bitwise(\">>>\", h, 8), 0xFF)\n gen = self.bitwise(\">>>\", h, 16)\n for pool in self.pools.values():\n if EUDIf()([as_var(task) == pool.task, as_var(slot) <= pool.max - 1]):\n if EUDIf()([EUDNot(as_var(pool.at(pool.co.state, slot)) == DONE), as_var(pool.at(pool.gen, slot)) == gen]):\n act(pool, slot)\n EUDEndIf()\n EUDEndIf()\n\n def running(self, e):\n out = fresh(0)\n self.task_slot(e[\"task\"], lambda pool, slot: out << 1)\n return out\n\n def cancel(self, st):\n def stop(pool, slot):\n pool.co.state.store[pool.base + slot] = DONE\n # The task whose turn this is ends on it: the tasks' turn counts it then.\n if self.pool is pool:\n if EUDIfNot()(as_var(slot) == pool.slot):\n pool.add(-1)\n EUDEndIf()\n else:\n pool.add(-1)\n self.task_slot(st[\"task\"], stop)\n\n def coroutine(self, co, body, ctx=None, end=None):\n \"\"\"One frame of a coroutine: count its wait down and leave, or go on from where it stopped until it sleeps or\n ends. Its body is lowered here, once, and the places to go on from are recorded as they are met; the jump\n table is filled in after.\"\"\"\n outer = self.co\n self.co = co\n co.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(co.get_wait()) >= 1, skip)\n co.set_wait(as_var(co.get_wait()) - 1)\n EUDJump(co.frame_end)\n skip << NextTrigger()\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(body, ctx or {})\n except Leave:\n pass\n if end is not None:\n end << NextTrigger() # (where a `return` in a task's body goes)\n # The body ended: it stops for good, as the classic backend's program does.\n co.set_state(DONE)\n EUDJump(co.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(co.get_state()) == DONE, co.frame_end)\n for index, resume in co.resumes:\n EUDJumpIf(as_var(co.get_state()) == index, resume)\n EUDJump(start)\n co.frame_end << NextTrigger()\n self.co = outer\n\n def run(self):\n if self.per_player:\n for p in loop_players(self.slots):\n self.player = p\n f_setcurpl(p)\n self.frame()\n self.player = None\n else:\n # As a trigger owned by that player would: not at all once the player has left.\n if EUDIf()(f_playerexist(self.slots[0])):\n f_setcurpl(self.slots[0])\n self.frame()\n EUDEndIf()\n\n\ndef flipped(t):\n \"\"\"1 for 0 and 0 for anything else, in a variable of its own.\"\"\"\n out = fresh(0)\n if EUDIf()(t == 0):\n out << 1\n EUDEndIf()\n return out\n\n\ndef each_start(node, visit):\n \"\"\"Every `start` statement in a piece of the IR, outer ones before those in their bodies.\"\"\"\n if isinstance(node, list):\n for x in node:\n each_start(x, visit)\n return\n if not isinstance(node, dict):\n return\n if node.get(\"kind\") == \"start\" and isinstance(node.get(\"task\"), int) and isinstance(node.get(\"call\"), dict):\n visit(node)\n for v in node.values():\n if isinstance(v, (dict, list)):\n each_start(v, visit)\n\n\ndef has_effect(node):\n \"\"\"Whether working out a piece of the IR does something beyond reading: a call, a pop, a random number.\"\"\"\n if isinstance(node, list):\n return any(has_effect(x) for x in node)\n if not isinstance(node, dict):\n return False\n if node.get(\"kind\") in (\"pop\", \"random\", \"randomInt\") or (node.get(\"kind\") in (\"call\", \"textCall\") and isinstance(node.get(\"call\"), dict)):\n return True\n return any(has_effect(v) for v in node.values() if isinstance(v, (dict, list)))\n\n\ndef has_call(node):\n \"\"\"Whether a call is anywhere in a piece of the IR.\"\"\"\n if isinstance(node, list):\n return any(has_call(x) for x in node)\n if not isinstance(node, dict):\n return False\n if node.get(\"kind\") in (\"call\", \"textCall\") and isinstance(node.get(\"call\"), dict):\n return True\n return any(has_call(v) for v in node.values() if isinstance(v, (dict, list)))\n\n\ndef string_of(value):\n \"\"\"An action's text or sound: the string itself (eudplib adds it to the map), or an index the script named.\"\"\"\n if isinstance(value, str):\n return EncodeString(value) if value else 0\n return int(value or 0)\n\n\ndef group_slots(group, node):\n \"\"\"The player slots a player number stands for: a slot is itself, All Players and a force their human and computer players.\"\"\"\n playing = lambda p: GetPlayerInfo(p).typestr in (\"Human\", \"Computer\")\n if 0 <= group < 8:\n return [group]\n if group == 17:\n return [p for p in range(8) if playing(p)]\n if 18 <= group <= 21:\n return [p for p in range(8) if playing(p) and GetPlayerInfo(p).force == group - 18]\n raise Fail(\"trigscript: expected players 1 to 8, All Players or a force%s\" % where(node))\n\n\ndef owner_slots(program):\n \"\"\"The player slots a program runs for, from its owners and the map's player settings: a slot is\n itself, All Players and a force are their human and computer players — who a trigger runs for.\"\"\"\n slots = []\n for o in program.get(\"owners\", [program.get(\"owner\", 0)]):\n for p in group_slots(int(o), program):\n if p not in slots:\n slots.append(p)\n if not slots:\n raise Fail(\"trigscript: no human or computer player of this map is among the program's owners%s\" % where(program))\n return slots\n\n\ndef loop_players(slots):\n \"\"\"Each of `slots` who is in the game, as an EUDVariable: EUDLoopPlayer, for a list of our own.\"\"\"\n start, end = min(slots), max(slots)\n v = EUDVariable()\n v << start\n if EUDWhile()(v <= end):\n for i in range(start, end):\n if i not in slots:\n EUDContinueIf(v == i)\n EUDContinueIfNot(f_playerexist(v))\n yield v\n EUDSetContinuePoint()\n v += 1\n EUDEndWhile()\n\n\nBITWISE = (\"&\", \"|\", \"^\", \"<<\", \">>\", \">>>\")\nFLIPPED = {\"<\": \">\", \"<=\": \">=\", \">\": \"<\", \">=\": \"<=\", \"==\": \"==\", \"!=\": \"!=\"}\n\n\ndef points(value, bits):\n \"\"\"Whole points as the game stores them, 256 to a point; `bits` is how many bits of points the cell holds.\"\"\"\n if isinstance(value, int):\n return min(value, (1 << bits) - 1) * 256\n return f_mul(saturate(value, bits), 256)\n\n\ndef uses_random(node):\n if isinstance(node, dict):\n return node.get(\"kind\") in (\"random\", \"randomInt\") or (node.get(\"kind\") == \"pick\" and node.get(\"by\") == \"random\") or any(uses_random(v) for v in node.values())\n return isinstance(node, list) and any(uses_random(v) for v in node)\n\n\ndef distance(a, b):\n \"\"\"|a - b| of two places on the map, both from 0 up.\"\"\"\n d = fresh(a)\n d -= b\n if EUDIf()(d >= SIGN):\n d << 0 - d\n EUDEndIf()\n return d\n\n\ndef signed(v):\n \"\"\"The 32 bits as a signed number.\"\"\"\n v &= U32\n return v - (1 << 32) if v >= SIGN else v\n\n\ndef isqrt(n):\n r = int(n ** 0.5)\n while r * r > n:\n r -= 1\n while (r + 1) * (r + 1) <= n:\n r += 1\n return r\n\n\ndef between_wrapped(table, i, f):\n \"\"\"The sine table read between cell i and the next, cell 0 after the last.\"\"\"\n a, b = table[i], table[(i + 1) & 4095]\n part = (abs(b - a) * f) >> 16\n return a - part if b < a else a + part\n\n\ndef fixed_const(name, args, unsigned=False, table=None):\n \"\"\"A fixed operation of numbers known when the map is built, exactly as the functions of fixed_fns() compute it.\"\"\"\n def towards_zero(m, negative):\n return (-(m & U32)) & U32 if negative else m & U32\n if name == \"fmul\":\n a, b = args\n return towards_zero((abs(a) * abs(b)) >> 16, (a < 0) != (b < 0))\n if name == \"fdiv\":\n a, b = args\n return 0 if b == 0 else towards_zero((abs(a) << 16) // abs(b), (a < 0) != (b < 0))\n if name == \"sqrt\":\n x = args[0] & U32 if unsigned else args[0]\n if unsigned:\n return 0x7FFFFFFF if x >= 1 << 30 else isqrt(x << 32)\n return 0 if x <= 0 else isqrt(x << 16)\n def between(i, f, bits):\n a = table[i] if i < len(table) else 0\n b = table[i + 1] if i + 1 < len(table) else 0\n part = (abs(b - a) * f) >> bits\n return a - part if b < a else a + part\n if name in (\"sin\", \"cos\"):\n x, scale = args\n at = fixed_const(\"fmul\", [x, scale])\n i = ((at >> 16) + (1024 if name == \"cos\" else 0)) & 4095\n return between_wrapped(table, i, at & 0xFFFF)\n y, x = args\n ax, ay = abs(x), abs(y)\n if ax == 0 and ay == 0:\n return 0\n read = lambda small, large: between(((small << 16) // large) >> 6, ((small << 16) // large) & 63, 6)\n t = read(ay, ax) if ay <= ax else FIXED_HALF_PI - read(ax, ay)\n if x < 0:\n t = FIXED_PI - t\n if y < 0:\n t = -t\n return t\n\n\ndef rng(node):\n \"\"\"The range compiler/ranges.ts found for a number of the IR - its signed reading, lowest and highest - or None.\"\"\"\n if not isinstance(node, dict):\n return None\n if node.get(\"kind\") == \"const\":\n v = signed(int(node[\"value\"]) & U32)\n return (v, v)\n r = node.get(\"range\")\n return (int(r[0]), int(r[1])) if r else None\n\n\ndef top_of(node):\n \"\"\"The most a number of the IR can be, when ranges.ts found it never below zero; None otherwise.\"\"\"\n r = rng(node)\n return r[1] if r is not None and r[0] >= 0 else None\n\n\ndef mul_bits(v, top, k):\n \"\"\"v * k, wrapping, of a variable known to be 0 ... top and a constant k of either sign: a trigger a bit v can have,\n each adding k shifted to that bit (eudplib's product by a constant goes over all 32, through a call).\"\"\"\n r = EUDVariable()\n RawTrigger(actions=r.SetNumber(0))\n for bit in range(top.bit_length()):\n add = (k << bit) & U32\n if add:\n RawTrigger(conditions=v.AtLeastX(1, 1 << bit), actions=r.AddNumber(add))\n return r\n\n\ndef mul_bits_pays(top, k):\n \"\"\"Whether mul_bits beats eudplib's product for a side of 0 ... top and the constant k, by Remastered's weights: about\n 18 ns a bit of top, against a call (80 ns) and about 95 ns a bit of the smaller side for eudplib's.\"\"\"\n m = abs(signed(k))\n return 18 * top.bit_length() < 80 + 95 * min(m.bit_length(), top.bit_length())\n\n\ndef div_bits(a, d, top):\n \"\"\"(a // d, a % d) of a value known to be 0 ... top and a constant d >= 1: a trigger a bit the quotient can have,\n where eudplib's goes over every bit a 32-bit dividend could give, through a call. `a` is copied, not used up.\"\"\"\n if d == 1:\n return a, 0\n if d & (d - 1) == 0:\n k = d.bit_length() - 1\n rem = fresh(a)\n DoActions(rem.SetNumberX(0, (~(d - 1)) & U32))\n return shift_const(as_var(a), \">>>\", k, top), rem\n rem, q = fresh(a), EUDVariable()\n RawTrigger(actions=q.SetNumber(0))\n for i in range((top // d).bit_length() - 1, -1, -1):\n RawTrigger(conditions=rem.AtLeast(d << i), actions=[rem.SubtractNumber(d << i), q.AddNumber(1 << i)])\n return q, rem\n\n\ndef div_var_bits(a, b, top):\n \"\"\"(a // b, a % b) of a value known to be 0 ... top and a variable b >= 1, both read from 0 up: eudplib's division\n chain (the divisor doubled into each step's condition and action, then the steps from the highest down), with only\n as many steps as top has bits, and the chain started as soon as the next doubling would pass top - where eudplib's\n doubles the divisor until it would pass 2^31 and keeps 32 steps.\"\"\"\n n = max(1, top.bit_length())\n q, x, rem = EUDVariable(), EUDVariable(), fresh(a)\n SeqCompute([(q, SetTo, 0), (x, SetTo, b)])\n cells = [(Forward(), Forward()) for _ in range(n)]\n chain = [Forward() for _ in range(n)]\n half = (top >> 1) + 1\n for i in range(n):\n SeqCompute([(EPD(c), SetTo, x) for c in cells[i]])\n if i == n - 1:\n break\n # Past top / 2 the next doubling is past top, whose steps take nothing: the chain starts here.\n p1, p2, p3 = Forward(), Forward(), Forward()\n p1 << RawTrigger(nextptr=p2, conditions=x.AtLeast(half), actions=SetNextPtr(p1, p3))\n p3 << RawTrigger(nextptr=chain[i], actions=SetNextPtr(p1, p2))\n p2 << NextTrigger()\n SeqCompute([(x, Add, x)])\n for i in range(n - 1, -1, -1):\n c, act = Forward(), Forward()\n chain[i] << RawTrigger(conditions=[c << rem.AtLeast(0)], actions=[act << rem.SubtractNumber(0), q.AddNumber(1 << i)])\n cells[i][0] << c + 8\n cells[i][1] << act + 20\n return q, rem\n\n\ndef shift_const(x, op, k, top=None):\n \"\"\"x << k, x >> k (the sign kept) or x >>> k for a constant k from 1 to 31, one trigger a bit that moves: a bit of x\n tested, its place in the result added. A short shift left is doublings instead (a doubling is about two triggers).\n `top`: x is known to be 0 ... top (ranges.ts), so only its bits move - and `>>` is `>>>`, the sign bit being 0.\"\"\"\n if top is not None:\n if op == \">>\":\n op = \">>>\"\n if op == \">>>\" and top >> k == 0:\n return 0\n if op == \"<<\" and k <= 4:\n t = fresh(x)\n for _ in range(k):\n t += t\n return t\n r = EUDVariable()\n RawTrigger(actions=r.SetNumber(0))\n if op == \"<<\":\n for bit in range(32 - k if top is None else min(32 - k, top.bit_length())):\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=r.AddNumber(1 << (bit + k)))\n return r\n # From the top, eight bits a group: a group with none of its bits set is jumped over (a small number skips most).\n # Known to be at most `top`, the bits above it are never set, and a group of eight or fewer needs no jump.\n highest = 31 if top is None else top.bit_length() - 1\n if highest - k < 8:\n for bit in range(highest, k - 1, -1):\n add = 1 << (bit - k)\n if op == \">>\" and bit == 31:\n add = (add - (1 << (32 - k))) & U32\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=r.AddNumber(add))\n return r\n for top in range(highest, k - 1, -8):\n bits = range(top, max(k, top - 7) - 1, -1)\n group = sum(1 << bit for bit in bits)\n past = Forward()\n EUDJumpIf(x.ExactlyX(0, group), past)\n for bit in bits:\n add = 1 << (bit - k)\n if op == \">>\" and bit == 31:\n add = (add - (1 << (32 - k))) & U32 # the sign bit: its place, and every bit above the result's top\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=r.AddNumber(add))\n past << NextTrigger()\n return r\n\n\ndef clamp_const(v, c, name, unsigned):\n \"\"\"min(v, c) or max(v, c) of a variable and a constant: a copy of v, set to c by a trigger whose conditions say it\n passes c — one or two comparisons with constants, where a comparison of two values and a branch was about 0.17 µs\n (the front end puts a max(…, 0) around every coordinate of a centerLocation, so the fog library's every brush).\"\"\"\n t = fresh(v)\n c &= U32\n if unsigned:\n if name == \"max\" and c > 0:\n RawTrigger(conditions=t.AtMost(c - 1), actions=t.SetNumber(c))\n elif name == \"min\" and c < U32:\n RawTrigger(conditions=t.AtLeast(c + 1), actions=t.SetNumber(c))\n return t\n # Signed: below zero is from 2^31 up, read from 0 up.\n if name == \"max\":\n if c < SIGN:\n RawTrigger(conditions=t.AtLeast(SIGN), actions=t.SetNumber(c))\n if c > 0:\n RawTrigger(conditions=t.AtMost(c - 1), actions=t.SetNumber(c))\n elif c > SIGN:\n RawTrigger(conditions=[t.AtLeast(SIGN), t.AtMost(c - 1)], actions=t.SetNumber(c))\n else:\n if c < SIGN:\n if c < SIGN - 1:\n RawTrigger(conditions=[t.AtMost(SIGN - 1), t.AtLeast(c + 1)], actions=t.SetNumber(c))\n else:\n RawTrigger(conditions=t.AtMost(SIGN - 1), actions=t.SetNumber(c))\n if c < U32:\n RawTrigger(conditions=t.AtLeast(c + 1), actions=t.SetNumber(c))\n return t\n\n\ndef signed_order(a, op, b):\n \"\"\"`a op b` of two signed variables, with no addition a side: a < b read as signed is a < b read from 0 up, turned\n over once for each side below zero (the top bits differ only when exactly one is). A count of those three is a\n condition on its low bit; > <= >= are < with the sides swapped, or the count's other answer.\"\"\"\n x, y, holds = (a, b, 1) if op == \"<\" else (b, a, 1) if op == \">\" else (b, a, 0) if op == \"<=\" else (a, b, 0)\n n = EUDVariable()\n lt = relation(x, \"<\", y)\n RawTrigger(actions=n.SetNumber(0))\n RawTrigger(conditions=lt, actions=n.AddNumber(1))\n RawTrigger(conditions=x.AtLeast(SIGN), actions=n.AddNumber(1))\n RawTrigger(conditions=y.AtLeast(SIGN), actions=n.AddNumber(1))\n return n.ExactlyX(holds, 1)\n\n\ndef not_all(conds):\n \"\"\"!(c1 && c2 && ...) as a light bool: set by one trigger, cleared by a second that has the conditions. (EUDNot of a\n list takes an if around the second, a third trigger.)\"\"\"\n v = EUDLightBool()\n RawTrigger(actions=v.Set())\n # (Trigger, not RawTrigger: a condition whose amount is a variable is patched before, as EUDIf does.)\n Trigger(conditions=conds, actions=v.Clear())\n return v\n\n\ndef forget_counts():\n \"\"\"Bring keeps the counts it found for a location number until the trigger pass ends or a query of another kind or\n location is asked: writing a location's sides, or making units in it, leaves the next Bring of it reading the old\n counts (Remastered and the lifted game alike, probes/location-stale.py). A Command in between makes it count again\n (about 30 ns, and the next Bring is a scan, as it would be for a location that moved). Only in a map whose programs\n ask Bring at all: the fog library moves a location and makes a unit at it for every brush, and asks nothing.\"\"\"\n if _asks_bring():\n RawTrigger(conditions=Command(P1, AtLeast, 0, 0))\n\n\n_BRING = []\n\n\ndef _asks_bring():\n \"\"\"Whether any program of the map reads a Bring condition: tests one (an `if`, an `until`) or counts with one.\"\"\"\n if not _BRING:\n def walk(node):\n if isinstance(node, list):\n return any(walk(x) for x in node)\n if not isinstance(node, dict):\n return False\n rec = node.get(\"record\")\n if isinstance(rec, dict) and (node.get(\"kind\") == \"cond\" or node.get(\"source\") == \"condition\") and rec.get(\"type\") == COND_BRING:\n return True\n return any(walk(v) for v in node.values() if isinstance(v, (list, dict)))\n _BRING.append(walk(IR.get(\"programs\", [])))\n return _BRING[0]\n\n\n# Actions that put units where a Bring already counted, or change whose they are: Create Unit with Properties, Move\n# Unit, Create Unit, Give Units to Player.\nMOVES_UNITS = {11, 39, 44, 48}\n\n\ndef always(truth):\n \"\"\"A condition that is known when the map is built.\"\"\"\n return EUDVariable(1 if truth else 0) >= 1 # initial: a constant, never written\n\n\ndef relation(av, op, b):\n \"\"\"`av op b` between a variable and a variable or an int, both read from 0 up.\"\"\"\n if op == \"==\":\n return av == b\n if op == \"!=\":\n return av != b\n if op == \"<\":\n return av < b\n if op == \">\":\n return av > b\n if isinstance(unProxy(b), EUDVariable):\n # eudplib orders two variables with < and > only: a >= b is b - a, which the game's Subtract stops at 0, being\n # 0 - worked out into the condition's own first dword, as eudplib's < does it.\n return floored_zero(b, av) if op == \">=\" else floored_zero(av, b)\n if op == \"<=\":\n return av <= b\n return av >= b\n\n\ndef floored_zero(x, y):\n \"\"\"x - y, stopped at 0, is 0: x <= y read from 0 up, one condition and a sum written into it.\"\"\"\n cell = Forward()\n cond = MemoryEPD(cell, Exactly, 0)\n cell << EPD(cond)\n SeqCompute(((cell, SetTo, x), (cell, Subtract, y)))\n return cond\n\n\ndef compare(a, op, b):\n return {\"==\": a == b, \"!=\": a != b, \"<\": a < b, \"<=\": a <= b, \">\": a > b, \">=\": a >= b}[op]\n\n\ndef condition(r):\n return Condition(r[\"location\"], r[\"player\"], r[\"amount\"], r[\"unitId\"], r[\"comparison\"], r[\"type\"], r[\"resource\"], r[\"flags\"], eudx=r.get(\"mask\", 0) or 0)\n\n\nclass Input:\n \"\"\"What the players do, as it reaches every computer: the cells chatEvent and MSQC write, by the\n names the editor put in their settings (compiler/input.ts, INPUT_NAMES), and the reads of them.\"\"\"\n\n MAX_NUMBER = 0xFFFFF\n\n def __init__(self, plan):\n self.plan = plan or {}\n self.keys = list(self.plan.get(\"keys\", []))\n self.held = list(self.plan.get(\"held\", []))\n self.buttons = list(self.plan.get(\"buttons\", []))\n self.chats = list(self.plan.get(\"chats\", []))\n self.mouse_base = self.plan.get(\"mouseBase\")\n # A person can sit in the map's human slots, which is where MSQC counts the mouse locations from.\n self.humans = [p for p in range(8) if GetPlayerInfo(p).typestr == \"Human\"]\n if plan and not self.humans:\n raise Fail(\"trigscript: a program reads keys, clicks, the mouse or chat, and the map has no human player to give any\")\n self.key_cells = [self.register(\"tsin_key%d\" % i, EUDArray(12)) for i in range(len(self.keys))]\n self.held_cells = [self.register(\"tsin_held%d\" % i, EUDArray(12)) for i in range(len(self.held))]\n # A held key is a message each frame it is down, but a multiplayer game carries commands in turns: two frames'\n # messages may arrive on one frame and none on the next. What arrives is banked and paid out a frame at a time,\n # so keyHeld() is true on as many frames as the key was down, however the turns grouped them.\n self.held_bank = [EUDArray(12) for _ in self.held]\n self.held_now = [EUDArray(12) for _ in self.held]\n # And a message can go missing for a frame (seen in the lifted game, the second frame of every hold): a key is\n # still held until HELD_GRACE frames have passed with no message, which delays a release by as much.\n self.held_quiet = [EUDArray(12) for _ in self.held]\n # With held keys every key is sent as held (compiler/input.ts#inputPlan): a press is held now and not before.\n self.held_pressed = [EUDArray(12) for _ in self.held]\n self.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\n # sync() (B5): each slot a value put on one computer (local) and what arrived of it, by player (everyone's).\n self.syncs = int(self.plan.get(\"syncs\", 0))\n self.sync_local = [self.register(\"tsin_sync%d\" % i, EUDVariable()) for i in range(self.syncs)]\n self.sync_in = [self.register(\"tsin_sync%d_in\" % i, EUDArray(12)) for i in range(self.syncs)]\n self.captures = max([len(c[\"captures\"]) for c in self.chats] or [0])\n if self.chats:\n # Local: what chatEvent found on this computer, and what the patterns made of it.\n self.heard = self.register(\"tsin_heard\", EUDVariable())\n self.pointer = self.register(\"tsin_pointer\", EUDVariable())\n self.length = self.register(\"tsin_length\", EUDVariable())\n self.register(\"tsin_pattern\", EUDVariable())\n self.chat_local = self.register(\"tsin_chat\", EUDVariable())\n self.capture_local = [self.register(\"tsin_capture%d\" % i, EUDVariable()) for i in range(self.captures)]\n # Everyone's: what MSQC delivered this frame, by player; 0xFFFFFFFF on a frame with nothing.\n self.chat_in = self.register(\"tsin_chat_in\", EUDArray(12))\n self.capture_in = [self.register(\"tsin_capture%d_in\" % i, EUDArray(12)) for i in range(self.captures)]\n self.unit_table = None\n\n SYNC_MOST = 1048574 # what MSQC's val carries, less the 1 that tells a value from nothing\n\n def sync_frame(self):\n \"\"\"Once a frame, before the programs: what was put to be sent has gone (MSQC sends in its beforeTriggerExec).\"\"\"\n if self.sync_local:\n DoActions([c.SetNumber(0) for c in self.sync_local])\n\n def send_sync(self, slot, p, value):\n \"\"\"sync()'s first half: on every computer the slot's arrival for the player cleared; on the player's own the\n value, kept to what MSQC carries, plus 1, put where MSQC sends it from.\"\"\"\n if slot >= self.syncs:\n raise Fail(\"trigscript: the IR's input plan has no sync slot %d\" % slot)\n self.sync_in[slot][p] = 0\n if EUDIf()(f_getuserplayerid() == p):\n v = fresh(value)\n if EUDIf()(v >= 0x80000000):\n v << 0\n EUDEndIf()\n if EUDIf()(v >= self.SYNC_MOST + 1):\n v << self.SYNC_MOST\n EUDEndIf()\n self.sync_local[slot] << v + 1\n EUDEndIf()\n\n HELD_BANK_MOST = 8 # a burst after a lag spike pays out at most this many frames\n HELD_GRACE = 2 # frames with no message before a held key counts as let go\n\n def frame(self):\n \"\"\"Once a frame, before the programs: this frame's held messages banked, one frame of each paid out, and a key\n with nothing left to pay still held for HELD_GRACE quiet frames.\"\"\"\n for cells, bank, now, quiet, pressed in zip(self.held_cells, self.held_bank, self.held_now, self.held_quiet, self.held_pressed):\n for p in self.humans:\n was = fresh(now[p])\n b = fresh(bank[p] + cells[p])\n if EUDIf()(b >= self.HELD_BANK_MOST + 1):\n b << self.HELD_BANK_MOST\n EUDEndIf()\n if EUDIf()(b >= 1):\n now[p] = 1\n bank[p] = b - 1\n quiet[p] = 0\n if EUDElse()():\n bank[p] = 0\n q = fresh(quiet[p] + 1)\n quiet[p] = q\n if EUDIf()([now[p] >= 1, q <= self.HELD_GRACE]):\n now[p] = 1\n if EUDElse()():\n now[p] = 0\n EUDEndIf()\n EUDEndIf()\n if EUDIf()([now[p] >= 1, was == 0]):\n pressed[p] = 1\n if EUDElse()():\n pressed[p] = 0\n EUDEndIf()\n\n @staticmethod\n def register(name, cell):\n EUDRegisterObjectToNamespace(name, cell)\n return cell\n\n # ── reads ──\n def read(self, i, low, node):\n source = i.get(\"source\")\n p = low.one_player(i[\"player\"], node)\n if source == \"key\" and i.get(\"held\"):\n return self.cell(self.held_now, self.held, i[\"key\"], node)[p]\n if source == \"key\" and self.held:\n return self.cell(self.held_pressed, self.held, i[\"key\"], node)[p]\n if source == \"key\":\n return self.cell(self.key_cells, self.keys, i[\"key\"], node)[p]\n if source == \"click\":\n return self.cell(self.button_cells, self.buttons, i[\"button\"], node)[p]\n if source == \"mouse\":\n return self.mouse(p, 0 if i[\"axis\"] == \"x\" else 1, node)\n if source == \"sync\":\n slot = int(i[\"slot\"])\n if slot >= self.syncs:\n raise Fail(\"trigscript: the IR's input plan has no sync slot %d%s\" % (slot, where(node)))\n return self.sync_in[slot][p]\n if source == \"chat\":\n number = self.chat_number(i[\"pattern\"], node)\n out = fresh(0)\n if EUDIf()(self.chat_in[p] == number):\n out << (1 if i.get(\"capture\") is None else self.capture_in[int(i[\"capture\"])][p])\n EUDEndIf()\n return out\n raise Fail(\"trigscript: unknown input %r%s\" % (source, where(node)))\n\n @staticmethod\n def cell(cells, names, name, node):\n if name not in names:\n raise Fail(\"trigscript: the IR's input plan has no %r%s\" % (name, where(node)))\n return cells[names.index(name)]\n\n def chat_number(self, pattern, node):\n for index, c in enumerate(self.chats):\n if c[\"pattern\"] == pattern:\n return index + 1\n raise Fail(\"trigscript: the IR's input plan has no pattern %r%s\" % (pattern, where(node)))\n\n def mouse(self, p, axis, node):\n \"\"\"MSQC keeps the mouse of the map's first human slot in location `mouseBase`, the next slot's in the next.\"\"\"\n if self.mouse_base is None:\n raise Fail(\"trigscript: the IR's input plan keeps no mouse%s\" % where(node))\n first = min(self.humans)\n cell = lambda h: EPD(MRGN + (int(self.mouse_base) - 1 + h - first) * 20) + axis\n if isinstance(p, int):\n return f_dwread_epd(cell(p)) if p in self.humans else 0\n out = fresh(0)\n for h in self.humans:\n if EUDIf()(p == h):\n out << f_dwread_epd(cell(h))\n EUDEndIf()\n return out\n\n # ── the typed line, on the computer it was typed on ──\n @staticmethod\n def hash_of(data):\n \"\"\"What `hashed` makes of the same bytes: capitals A to Z as small letters, h = h × 31 + byte.\"\"\"\n h = 0\n for b in data:\n if 65 <= b <= 90:\n b += 32\n h = (h * 31 + b) & U32\n return h\n\n def hashed(self, pos, stop_at_space):\n \"\"\"The hash of the line from `pos` to its end or, with `stop_at_space`, to the next space; `pos` moves past it.\"\"\"\n h = fresh(0)\n if EUDWhile()(pos < self.length):\n ch = fresh(f_bread(self.pointer + pos))\n if stop_at_space:\n EUDBreakIf(ch == 32)\n if EUDIf()([ch >= 65, ch <= 90]):\n ch += 32\n EUDEndIf()\n h << f_mul(h, 31) + ch\n pos += 1\n EUDEndWhile()\n return h\n\n def units(self):\n \"\"\"Unit names by hash, in hash order, for a search by halves: two arrays side by side.\"\"\"\n if self.unit_table is None:\n by_hash = {}\n for name, unit in self.plan.get(\"unitNames\", []):\n by_hash.setdefault(self.hash_of(name.encode(\"utf-8\")), int(unit))\n ordered = sorted(by_hash.items())\n self.unit_table = (EUDArray([h for h, _ in ordered] or [0]), EUDArray([u for _, u in ordered] or [0]), len(ordered))\n return self.unit_table\n\n def capture(self, c, pos, fail):\n \"\"\"One value out of the line at `pos`, or a jump to `fail`.\"\"\"\n value = fresh(0)\n if c[\"kind\"] == \"number\":\n digits = fresh(0)\n if EUDWhile()(pos < self.length):\n ch = f_bread(self.pointer + pos)\n EUDBreakIf(ch <= 47)\n EUDBreakIf(ch >= 58)\n value << f_mul(value, 10) + ch - 48\n if EUDIf()(value >= self.MAX_NUMBER + 1):\n value << self.MAX_NUMBER\n EUDEndIf()\n pos += 1\n digits += 1\n EUDEndWhile()\n EUDJumpIf(digits == 0, fail)\n elif c[\"kind\"] == \"word\":\n h = self.hashed(pos, True)\n found = fresh(0)\n for index, word in enumerate(c[\"words\"]):\n if EUDIf()(h == self.hash_of(word.encode(\"utf-8\"))):\n value << index\n found << 1\n EUDEndIf()\n EUDJumpIf(found == 0, fail)\n elif c[\"kind\"] == \"unit\":\n h = self.hashed(pos, False)\n hashes, units, count = self.units()\n lo, hi = fresh(0), fresh(count)\n if EUDWhile()(lo < hi):\n mid = f_div(lo + hi, 2)[0]\n if EUDIf()(hashes[mid] < h):\n lo << mid + 1\n if EUDElse()():\n hi << mid\n EUDEndIf()\n EUDEndWhile()\n EUDJumpIf(lo >= count, fail)\n EUDJumpIfNot(hashes[lo] == h, fail)\n value << units[lo]\n else:\n raise Fail(\"trigscript: unknown chat capture %r\" % (c[\"kind\"],))\n return value\n\n def match_line(self):\n \"\"\"The line the local player typed against every pattern, in order: the first that fits the whole\n line gives its number and values to the cells MSQC sends from. Nothing fits: nothing is sent.\"\"\"\n DoActions([self.chat_local.SetNumber(0)] + [c.SetNumber(0) for c in self.capture_local])\n if EUDIf()(self.heard >= 1):\n done = Forward()\n for index, chat in enumerate(self.chats):\n fail = Forward()\n pos = fresh(0)\n values = []\n for seg in chat[\"segments\"]:\n if isinstance(seg, str):\n data = seg.encode(\"utf-8\")\n EUDJumpIf(pos + len(data) >= self.length + 1, fail)\n EUDJumpIfNot(f_memcmp(self.pointer + pos, Db(data + b\"\\0\"), len(data)) == 0, fail)\n pos += len(data)\n else:\n values.append(self.capture(chat[\"captures\"][int(seg)], pos, fail))\n EUDJumpIfNot(pos == self.length, fail)\n DoActions(self.chat_local.SetNumber(index + 1))\n for cell, value in zip(self.capture_local, values):\n cell << value\n EUDJump(done)\n fail << NextTrigger()\n done << NextTrigger()\n EUDEndIf()\n\n\n# Where each trigger came from, for a run in the lifted game to name the line a trigger belongs to (TrigScript 4,\n# A1). Only when the build asks for it (the `triggerMap` setting): every RawTrigger made is tagged with the\n# statement being lowered, or with the eudplib function whose body it is, and once the payload is laid out one\n# header line and lines of rows say where each trigger sits in it:\n# TRIGSCRIPT-MAP {\"payload\": , \"count\": }\n# TRIGSCRIPT-ROWS [[offset, conditions, actions, origin], ...] (400 a line, as many lines as it takes)\n# An origin is \"line:column\" (or \"file:line:column\"), \"fn:\" for a function of eudplib's, \"\" for what no\n# statement made (a program's frame, its sleeps), or \"eudplib\" for what eudplib made before this file was read. The payload sits at the end of the strings section, so a run\n# finds it at the section's address plus its length less the payload's.\nif settings.get(\"triggerMap\"):\n import eudplib.core.allocator.payload as _payload\n from eudplib.core.eudfunc.eudfuncn import EUDFuncN as _EUDFuncN\n from eudplib.core.rawtrigger.rawtriggerdef import RawTrigger as _RawTrigger\n\n _ORIGINS = []\n _MADE = {}\n _ACT_ORIGINS = {}\n\n def _origin_of(at):\n if not at:\n return \"\"\n if at.get(\"file\"):\n return \"%s:%s:%s\" % (at.get(\"file\"), at.get(\"line\"), at.get(\"column\"))\n return \"%s:%s\" % (at.get(\"line\"), at.get(\"column\"))\n\n _trigger_init = _RawTrigger.__init__\n\n def _tagged_trigger(self, *args, **kwargs):\n _trigger_init(self, *args, **kwargs)\n _MADE[self] = _ORIGINS[-1] if _ORIGINS else \"\"\n\n _RawTrigger.__init__ = _tagged_trigger\n\n _func_body = _EUDFuncN._create_func_body\n\n def _tagged_body(self):\n _ORIGINS.append(\"fn:\" + getattr(self._bodyfunc, \"__name__\", \"?\"))\n try:\n return _func_body(self)\n finally:\n _ORIGINS.pop()\n\n _EUDFuncN._create_func_body = _tagged_body\n\n _statement = Lowering.statement\n\n def _tagged_statement(self, st, ctx):\n _ORIGINS.append(_origin_of(st.get(\"at\")) or (_ORIGINS[-1] if _ORIGINS else \"\"))\n try:\n return _statement(self, st, ctx)\n finally:\n _ORIGINS.pop()\n\n Lowering.statement = _tagged_statement\n\n _construct = _payload._construct_payload\n\n def _construct_and_say():\n built = _construct()\n found = _payload._found_objects_dict\n rows = []\n # (Every trigger in the payload, also those eudplib made before this file was read, at its own import:\n # those say \"eudplib\".)\n for trg, i in found.items():\n if not isinstance(trg, _RawTrigger):\n continue\n origin = _MADE.get(trg, \"eudplib\")\n row = [_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin]\n # (A trigger of the emitter's holding several statements' work: each action's line.)\n if trg in _ACT_ORIGINS:\n row.append(_ACT_ORIGINS[trg])\n rows.append(row)\n rows.sort()\n # (In parts, so that no one line of the log runs to megabytes.)\n print(\"TRIGSCRIPT-MAP \" + json.dumps({\"payload\": len(built.data), \"count\": len(rows)}, separators=(\",\", \":\")))\n for k in range(0, len(rows), 400):\n print(\"TRIGSCRIPT-ROWS \" + json.dumps(rows[k:k + 400], separators=(\",\", \":\")))\n return built\n\n _payload._construct_payload = _construct_and_say\n\n\n# Where each program's variables are, for a test in the lifted game to read them as `sim.program()` reads the\n# simulator's (TrigScript 4, A2). Only when the build asks for it (the `variableMap` setting): once the payload is\n# placed, a header line and a line a program give the address of every cell:\n# TRIGSCRIPT-VARS {\"programs\": , \"heap\":
, \"cells\":
, \"strings\":
}\n# TRIGSCRIPT-PROGRAM {\"index\", \"name\", \"perPlayer\", \"owners\", \"vars\": [...], \"arrays\": [...]}\n# A variable is its id (what the simulator keys it by), its kind, and its cells: `value` (a number, a boolean, a text\n# of the table by its id), `addr` + `len` (a text that is made), `ptr` + `uid` (a unit). An array is its cells' `at`,\n# or, growing, its handle's `ptr` + `len` among the growing arrays' cells. A cell of a per-player program's own (`rowed`) is a\n# row of twelve, one a player; an address is null for what nothing in the map uses, which eudplib leaves out.\nif settings.get(\"variableMap\"):\n import eudplib.core.allocator.payload as _vpayload\n import eudplib.maprw.injector.apply_injector as _injector\n from eudplib.core.allocator.constexpr import Evaluate as _evaluate\n from eudplib.core.mapdata.stringmap import get_string_section_name as _strings_name\n from eudplib.core.variable.vbuf import get_current_varbuffer as _varbuffer\n from eudplib.utils import unProxy as _unproxy\n\n _PLACES = {}\n\n def _offset(expr):\n \"\"\"A payload-relative address as a byte offset into the payload: eudplib's own (relocated whole) or an EPD\n (relocated a quarter at a time, as EPD(x) = (x - 0x58A364) / 4 is).\"\"\"\n r = _evaluate(expr)\n if r.rlocmode == 4:\n return r.offset\n if r.rlocmode == 1:\n return r.offset * 4 + 0x58A364\n return None\n\n def _place(obj):\n \"\"\"Where a variable's value or an array's first cell is in the payload; None for what it left out.\"\"\"\n if is_varray(obj):\n try:\n at = _offset(_unproxy(obj))\n except Exception:\n return None\n return None if at is None else at + VARRAY_VALUE\n if isinstance(obj, EUDVariable):\n vb = _varbuffer()\n if vb is None or obj.GetVTable() not in vb._vdict:\n return None\n return _offset(obj.getValueAddr())\n # An EUDArray is a proxy of its data's EPD (or of the data itself), the data an object of its own.\n data = _unproxy(obj)\n while not isinstance(data, EUDObject) and hasattr(data, \"_value\"):\n data = _unproxy(data)\n try:\n return _offset(data)\n except Exception:\n return None # (nothing in the map uses it, so eudplib did not place it)\n\n def _cells_of(s):\n if isinstance(s, UnitStorage):\n return {\"ptr\": s.ptr, \"uid\": s.uid}\n if isinstance(s, TextStorage):\n return {\"addr\": s.addr, \"len\": s.len}\n return {\"value\": s.store}\n\n def _program_places(k, low):\n p = low.p\n out = {\"index\": k, \"name\": p.get(\"name\"), \"perPlayer\": low.per_player, \"owners\": p.get(\"owners\", [low.owner]), \"vars\": [], \"arrays\": []}\n for id_, s in low.vars.items():\n # (A loop's unit is the scan's own pointer for one turn, kept nowhere: nothing to read.)\n d = getattr(s, \"decl\", None)\n if d is None or d.get(\"temp\") or d.get(\"task\") is not None:\n continue # (a task's variable is a row a slot: nothing a test reads by name)\n cells_ = _cells_of(s)\n out[\"vars\"].append({\"id\": id_, \"kind\": d.get(\"kind\"), \"text\": d.get(\"text\"), \"bits\": d.get(\"bits\"), \"unsigned\": bool(d.get(\"unsigned\")),\n \"rowed\": bool(s.rowed), \"cells\": {name: _place(cell) for name, cell in cells_.items()},\n \"stride\": VARRAY_STRIDE if any(is_varray(c) for c in cells_.values()) else 4})\n for id_, a in low.arrays.items():\n if isinstance(a, (InnerListStorage, SliceStorage)) or a.decl.get(\"task\") is not None:\n continue # (a row of another's cells, or a list inside one: read through what it is in, not here)\n d = a.decl\n row = {\"id\": id_, \"kind\": d.get(\"kind\"), \"bits\": d.get(\"bits\"), \"unsigned\": bool(d.get(\"unsigned\")), \"length\": int(d.get(\"length\", 0)), \"rowed\": bool(a.rowed)}\n if isinstance(a, ListStorage):\n row.update(dynamic=True, ptr=_place(a.handle[\"ptr\"]), len=_place(a.handle[\"len\"]), stride=VARRAY_STRIDE if is_varray(a.handle[\"ptr\"]) else 4)\n else:\n row.update(at=_place(a.store), stride=VARRAY_STRIDE if is_varray(a.store) else 4)\n out[\"arrays\"].append(row)\n return out\n\n _vconstruct = _vpayload._construct_payload\n\n def _construct_and_place():\n built = _vconstruct()\n # (eudplib works addresses out only while it writes the payload: for as long as this takes, it is again.)\n was = _vpayload.phase\n _vpayload.phase = _vpayload.PHASE_WRITING\n try:\n _PLACES[\"programs\"] = [_program_places(k, low) for k, low in enumerate(PROGRAMS)]\n _PLACES[\"heap\"] = _place(_HEAP[\"cells\"]) if _HEAP else None\n _PLACES[\"cells\"] = _place(_HEAP[\"vcells\"]) if _HEAP and \"vcells\" in _HEAP else None\n finally:\n _vpayload.phase = was\n _PLACES[\"bytes\"] = len(built.data)\n return built\n\n _vpayload._construct_payload = _construct_and_place\n\n _initialize = _injector.initialize_payload\n\n def _initialize_and_say(chkt, payload, *args, **kwargs):\n out = _initialize(chkt, payload, *args, **kwargs)\n # The payload is the end of the strings section, which Remastered keeps at 0x191943C8.\n base = 0x191943C8 + len(chkt.getsection(_strings_name())) - _PLACES[\"bytes\"]\n\n def at(o):\n return None if o is None else base + o\n\n print(\"TRIGSCRIPT-VARS \" + json.dumps({\"programs\": len(_PLACES[\"programs\"]), \"heap\": at(_PLACES[\"heap\"]), \"cells\": at(_PLACES.get(\"cells\")), \"strings\": 0x191943C8}, separators=(\",\", \":\")))\n for prog in _PLACES[\"programs\"]:\n for v in prog[\"vars\"]:\n v[\"cells\"] = {name: at(o) for name, o in v[\"cells\"].items()}\n for a in prog[\"arrays\"]:\n for key in (\"at\", \"ptr\", \"len\"):\n if key in a:\n a[key] = at(a[key])\n print(\"TRIGSCRIPT-PROGRAM \" + json.dumps(prog, separators=(\",\", \":\")))\n return out\n\n _injector.initialize_payload = _initialize_and_say\n\n\n# The loops of the IR and the code that runs apart from them, for static routing (above): a loop is a region with a way\n# in (`entry`, what came before jumps there; `after`, the loop's first trigger), a sleep or a parallel makes the loops\n# around it wait, a coroutine (a program's body, a task, a branch) runs apart, and a call of a function that calls itself\n# is one whose body this cannot follow.\nif ROUTE.enabled:\n _ROUTED_LOOPS = (\"while\", \"do\", \"for\", \"unitLoop\", \"textLoop\")\n _lower_statement = Lowering.statement\n\n def _routed_statement(self, st, ctx):\n k = st[\"kind\"]\n if k in (\"sleep\", \"parallel\"):\n ROUTE.waited()\n if k not in _ROUTED_LOOPS or not ROUTE.on:\n return _lower_statement(self, st, ctx)\n r = _Region(\"loop\")\n r.entry, r.after = Forward(), Forward()\n SetNextTrigger(r.entry)\n r.after << NextTrigger()\n ROUTE.loops.append(r)\n ROUTE.stack.append(r)\n try:\n return _lower_statement(self, st, ctx)\n finally:\n ROUTE.stack.pop()\n\n Lowering.statement = _routed_statement\n _lower_coroutine = Lowering.coroutine\n\n def _routed_coroutine(self, *args, **kwargs):\n ROUTE.stack.append(_Region(\"coroutine\"))\n try:\n return _lower_coroutine(self, *args, **kwargs)\n finally:\n ROUTE.stack.pop()\n\n Lowering.coroutine = _routed_coroutine\n\nINPUT = Input(IR.get(\"input\"))\n_SOURCES = {s: uses_source(IR.get(\"programs\", []), s) for s in (\"created\", \"died\", \"killed\", \"ordered\", \"morphed\")}\nEVENTS = Events(_SOURCES[\"created\"], _SOURCES[\"died\"] or _SOURCES[\"killed\"], _SOURCES[\"ordered\"], _SOURCES[\"morphed\"]) if any(_SOURCES.values()) else None\nPROGRAMS = [Lowering(p) for p in IR.get(\"programs\", [])]\n\n\ndef onPluginStart():\n # eudplib's generator starts from the same seed in every game; the game's own randomness (a switch randomized) seeds it.\n if uses_random(IR.get(\"programs\", [])):\n f_randomize()\n\n\ndef beforeTriggerExec():\n # After chatEvent has looked for a typed line, before MSQC sends what it was.\n if INPUT.chats:\n INPUT.match_line()\n\n\ndef afterTriggerExec():\n INPUT.sync_frame()\n if INPUT.held:\n INPUT.frame()\n if EVENTS is not None:\n EVENTS.run()\n for prog in PROGRAMS:\n prog.run()\n # Everything this file makes is made by now: which reads need no routing (above).\n ROUTE.decide()\n"; +export const TRIGSCRIPT_PY: string = "\"\"\"\n[trigscript]\nir : /work/files/trigscript.json\n\nTrigScript's programs: the euddraft plugin that lowers the compiler's IR — data, never\ncode — into eudplib. The IR file is what the plugin's compiler wrote (docs/ir.md);\nthis module is handed to the eudplib library plugin as a source with every build, so the\ntwo halves of the IR version are always the same build.\n\nA program is a coroutine the game runs every frame. Its body is lowered to straight-line\neudplib triggers with jumps between labels: `if` / `while` / `switch` become conditional\njumps, and `sleep` stores the label to resume at in a state variable, sets a frame counter,\nand leaves the frame. The frame's entry counts the wait down, then jumps to the stored\nlabel. A program runs for the players it is owned by, as a trigger would: one owner runs\nit as that player while that player is in the game; several owners, All Players or a force\nrun it once each frame for every such player who is in the game, CurrentPlayer set, its\nvariables and its state as 12-slot arrays indexed by the player.\n\nText is written out in the IR: an action's `text` or `wav` is the string itself, which\neudplib adds to the built map's string table, or a number when the script named an index\nof the map's own. The map the user edits never holds a program's strings.\n\nA read is a value of the game taken when the expression is evaluated. What a condition compares\nis read from the game's tables where the table is that value (a player's deaths, kills, ore, gas)\nand otherwise found by asking the condition itself — \"at least 2^31? at least that plus 2^30? …\" —\nso a read means what the condition means, for a force's minerals or the units at a location alike.\nPlayer facts are bytes of the player tables. Text with values in it is printed through eudplib's\nstring buffer, for the player it is for and nobody else.\n\nA unit of the game is a pointer into the game's unit table — 1700 slots of 336 bytes — with the slot's\nuniqueness byte kept beside it: the game gives a dead unit's slot to the next unit made, so a unit kept\nin a variable is checked before every use (a sprite, an order other than \"die\", the same uniqueness\nbyte) and reads 0, and takes no write, once it is gone. A loop over units and a pick walk the whole\ntable with conditions whose address is moved on a slot at a time, as eudplib's own EUDLoopUnit2 does;\na dying unit is passed over. What a unit can be asked and told is what Magenta's probe maps saw\nworking in Remastered: no position, cloak or speed writes. The game's tables (`stats()`) are plain\ncells at addresses the IR carries; a speed is four flingy records, a colour two bytes, a name a string.\n\nWhat the players do — keys, clicks, the mouse, what they type — happens on one computer, and two\nplugins the build adds beside this one bring it to all of them in step. chatEvent (before this\nplugin) finds the line the local player typed; this plugin matches it against the programs'\npatterns, there and then, into a number for the pattern and up to three values; MSQC (after this\nplugin) sends those, the keys and the clicks to every computer as the player they came from, and\nkeeps each player's mouse in a location. They land in arrays registered by name — which is how the\nother two plugins' settings reach them — a cell per player, fresh every frame: an input reads 1 on\nthe frame it arrives. The IR's `input` lists what is asked for; the editor wrote the settings from it.\n\nNumbers keep one contract with the simulator: 32 bits, a `number` signed and a `u32` not, wrapping at\neither end. + - * and the bitwise operators are the same bits whichever way they are read; what reads\nthem one way or the other - a comparison, a division, a shift right, min and max, a printed number -\nsays which in the IR (`unsigned`), so nothing here works a type out. Where the game takes nothing below\nzero the compiler has already written max(v, 0), and a store keeps stopping at the top of what it holds.\nIn here a number known when the map is built is a Python int holding the 32 bits, 0 to 2^32 - 1.\n\nA text is kept one of two ways, a variable at a time (the IR says which). One that only ever holds texts written in\nthe script is the text's id in the built map's string table: a number. One that is made while the map is played is\nthree cells - where its bytes are (UTF-8, ended by a 0), the block of the heap it owns (0: none, the bytes are a\nstring of the table) and its length in characters. A made text is written into one scratch buffer, measured, and\ncopied into a block of just that size, whose first cell says which size; what holds the text owns the block, so\nassigning copies it, a value that was just made is moved, and whatever a variable held goes back to the heap once\nits new value is worked out. compiler/simulateIr.ts takes and gives blocks in the same order, so both run out at\nthe same text. A made text in an action's field goes over a string the build keeps for that kind of field, on the\ncomputer of the player the action is for and nowhere else: the game reads such a string again whenever it draws.\n\"\"\"\nimport base64\nimport json\n\nfrom eudplib import *\nfrom eudplib.core.mapdata.stringmap import ForceAddString\nfrom eudplib.memio.rwcommon import br1, br2, bw1\n\nIR_VERSION = 26\nFRAMES_PER_SECOND = 24\n# Where the game keeps what a read reads (1.16.1 addresses, which Remastered emulates). The player\n# tables are the ones Magenta's probes 5 and 8 read in the game.\nDEATHS_TABLE = 0x58A364\nKILLS_TABLE = 0x5878A4\nORE_TABLE = 0x57F0F0\nGAS_TABLE = 0x57F120\nPLAYER_BYTES = {\"slot\": 0x57F1B4, \"race\": 0x57F1C0}\n# Per race (144 bytes apart: Zerg, Terran, Protoss), a dword per player, in half supplies.\nSUPPLY_TABLES = {\"provided\": 0x582144, \"used\": 0x582174, \"max\": 0x5821A4}\n# The unit table, and a unit's dwords the scans test (as EPD offsets from the unit's own).\nUNIT_TABLE = 0x59CCA8\nUNIT_SIZE = 336\nUNIT_SLOTS = 1700\nUNIT_END = UNIT_TABLE + UNIT_SIZE * UNIT_SLOTS\nOFF_SPRITE, OFF_POS, OFF_OWNER_ORDER, OFF_TYPE, OFF_UID = 0x0C // 4, 0x28 // 4, 0x4C // 4, 0x64 // 4, 0xA4 // 4\n# The selection a computer keeps: twelve unit addresses; and a Select command, 0x09, the count, a tag a unit (B5).\nSELECTION = 0x6284B8\nSELECT_COMMAND = Db(b\"\\x09\" + bytes(27))\n# units.dat: max hit points (dword, 256 to a point), max shields (word), the group flags a trigger's\n# Men / Buildings / Factories go by (byte), the flingy a type moves as (byte).\nUNITS_MAX_HP = 0x662350\nUNITS_MAX_SHIELDS = 0x660E00\nUNITS_GROUP = 0x6637A0\nUNITS_FLINGY = 0x6644F8\nGROUP_BITS = {230: 0x08, 231: 0x10, 232: 0x20}\n# flingy.dat, by flingy id: movement control (byte), top speed (dword), acceleration (word), halt distance (dword).\nFLINGY_CONTROL, FLINGY_SPEED, FLINGY_ACCELERATION, FLINGY_HALT = 0x6C9858, 0x6C9EF8, 0x6C9C78, 0x6C9930\nMINIMAP_COLOR_OFFSET = 0x581DD6 - 0x581D76\n# The heads of the game's lists of units (1.16.1, which Remastered emulates): the visible units and the dying ones. Each\n# links a record in just behind its head, so behind the head a list runs latest first (scm-sim docs/specs/unit-lists.md).\nVISIBLE_HEAD, DYING_HEAD = 0x628430, 0x62842C\n# How many unit records are in use (a dword): up when a record is taken, down when one is freed.\nRECORDS_IN_USE = 0x6283F0\n# How many units one frame's events pass hands on of each kind; past that the rest are handed on in the next frame.\nEVENT_CAP = 512\n# A unit's hit points (+0x08), and the last player whose attack hurt it (+0x90, a byte; 8 when none has): a unit that died\n# with no hit points and a player there died in a fight, and that player killed it (on.killed).\nOFF_HP, OFF_LAST_ATTACKER = 0x08 // 4, 0x90 // 4\n# The location table, and the one an order borrows for the length of one action (put back after).\nMRGN = 0x58DC60\nSCRATCH_LOCATION = 255\nUNIT_TIMERS = {\"stim\": \"stimTimer\", \"ensnare\": \"ensnareTimer\", \"plague\": \"plagueTimer\", \"lockdown\": \"lockdownTimer\", \"maelstrom\": \"maelstromTimer\", \"irradiate\": \"irradiateTimer\", \"stasis\": \"stasisTimer\"}\nSTATUS_INVINCIBLE, STATUS_HALLUCINATION, STATUS_BURROWED, STATUS_CLOAKED = 0x04000000, 0x40000000, 0x00000010, 0x00000300\nCOND_COMMAND, COND_BRING, COND_ACCUMULATE, COND_KILL, COND_OPPONENTS, COND_DEATHS = 2, 3, 4, 5, 14, 15\nCURRENT_PLAYER = 13\n# What a stack that ran out throws (B4), as JavaScript's does: a RangeError (compiler/ir.ts ERROR_CODES).\nRANGE_ERROR = 2\nSTACK_OVERFLOW_MESSAGE = \"Maximum call stack size exceeded\"\n# The state of a program whose body ended: nothing resumes it.\nDONE = 0xFFFFFFFF\nU32 = 0xFFFFFFFF\nSIGN = 0x80000000\n\nMINUS_SIGN = Db(b\"-\\0\\0\\0\")\nNO_SIGN = Db(b\"\\0\\0\\0\\0\")\n\nwith open(settings[\"ir\"], encoding=\"utf-8\") as _f:\n IR = json.load(_f)\nif IR.get(\"version\") != IR_VERSION:\n raise RuntimeError(\"trigscript: the IR is version %r; this plugin reads version %d\" % (IR.get(\"version\"), IR_VERSION))\n\n\n# ── Static routing (TrigScript 4, the backend's first part) ──\n# eudplib keeps a variable as a trigger of one action, SetDeaths(dest, modifier, value), and reads it by routing:\n# three actions set that action's destination and modifier and the trigger's next pointer, then the walk goes through\n# it. In a played frame of the bullets bench that routing was half the time (scripts/trigger-shapes.mts). Here every\n# copy of a variable this file made (SeqCompute, and everything eudplib does through it: <<, +=, a call's arguments)\n# is lowered as a *site* — whatever came before jumps to a Forward, then a setup trigger with the routing, then the\n# cells, then what follows — and what eudplib itself does to a cell (its vtable, its destination, its mask, its\n# modifier asked for anywhere else) is counted. Once the whole program is lowered (`decide`, at the end of\n# afterTriggerExec), a cell read at exactly one site and touched nowhere else needs no routing at all: its destination,\n# modifier and next pointer are written into the variable buffer as the map is built (they never change), and the site\n# jumps straight into its cells. Any other site keeps its setup. Only variables this file made directly take part:\n# eudplib's own (a function's parameters and results, its pooled temporaries) are shared by many places it cannot see.\n# The `staticRouting` setting \"0\" leaves everything to eudplib, to weigh what this gives.\nimport sys as _sys\nimport eudplib.core.variable.eudv as _eudv\nimport eudplib.core.variable.vbuf as _vbuf\nfrom eudplib.utils import unProxy as _unproxy_r\n\n\nclass _Region:\n \"\"\"Where code is being lowered: a loop of the IR (`loop`), or code that runs apart from what is around it — a\n coroutine (a program's body, a task, a branch of a parallel), a function's body, a scope of triggers apart.\"\"\"\n\n def __init__(self, kind):\n self.kind = kind\n self.waits = False # a loop that sleeps or waits: anything else may run before it goes on\n self.unknown = False # a loop that calls something whose body this cannot see (a function that calls itself)\n self.calls = set() # the functions (EUDFuncN ids) called from here\n self.entry = self.after = None # a loop's way in, which the hoisted routing goes in front of\n self.hoist = []\n self.copies = [] # (variable, its copy for one site of the loop), filled on the way in\n\n\nclass _Routing:\n def __init__(self, enabled):\n self.enabled = enabled\n self.on = False\n self.emitting = 0\n self.reading = 0 # inside a condition's own making: a value read, not written\n self.ours = {} # id -> the variable, for the variables this file made while lowering\n self.touches = {} # id -> where something other than a site asked for one of its routing fields\n self.writes = {} # id -> where its value's address was asked for other than to read it in a condition\n self.sites = {} # id -> the groups it is read in\n self.groups = []\n self.static = {} # a cell's VariableTriggerForward -> (dest, modifier byte, next)\n self.stack = [] # (barrier or loop regions) innermost last\n self.main = _Region(\"main\")\n self.functions = {} # id of an EUDFuncN -> the region of its body\n self.loops = []\n self.names = {} # id -> the name of the script's variable it is, for the report\n self.why_touched = {} # id -> where it was touched, for the report\n self.made_at = {} # id -> where it was made, for the report\n self.stats = {\"groups\": 0, \"bare groups\": 0, \"cells\": 0, \"static\": 0, \"hoisted\": 0, \"replicas\": 0, \"loops\": 0}\n\n def where(self):\n \"\"\"(the region the code runs in apart from the rest, the loops around it in that region, outermost first).\"\"\"\n loops = []\n for r in reversed(self.stack):\n if r.kind != \"loop\":\n return r, tuple(reversed(loops))\n loops.append(r)\n return self.main, tuple(reversed(loops))\n\n def touched(self, v):\n if self.on and not self.emitting:\n k = id(v)\n if k in self.ours:\n self.touches.setdefault(k, []).append(self.where())\n if settings.get(\"routingReport\") and k in self.names:\n f, chain = _sys._getframe(2), []\n while f is not None and len(chain) < 6:\n chain.append(\"%s:%d\" % (f.f_code.co_name, f.f_lineno))\n f = f.f_back\n self.why_touched.setdefault(k, set()).add(\" < \".join(chain))\n\n def wrote(self, v):\n if self.on and not self.emitting and not self.reading:\n k = id(v)\n if k in self.ours:\n self.writes.setdefault(k, []).append(self.where())\n\n def called(self, fn):\n if self.on:\n apart, loops = self.where()\n apart.calls.add(id(fn))\n for r in loops:\n r.calls.add(id(fn))\n\n def waited(self):\n if self.on:\n for r in self.where()[1]:\n r.waits = True\n\n def unknown_call(self):\n if self.on:\n for r in self.where()[1]:\n r.unknown = True\n\n def reached(self, calls):\n \"\"\"The regions of the functions these calls reach, those they call in turn included.\"\"\"\n out, todo = [], list(calls)\n seen = set()\n while todo:\n f = todo.pop()\n if f in seen:\n continue\n seen.add(f)\n r = self.functions.get(f)\n if r is not None:\n out.append(r)\n todo.extend(r.calls)\n return out\n\n def plan_for(self, v, r):\n \"\"\"What loop r can do for v's reads in it: \"hoist\" (one site in it: its routing set once in front of it),\n \"replicas\" (several sites, and nothing in it writes v: a copy of v a site, each copy routed for good and filled\n in front of it), or why not. Nothing that runs between the loop's way in and its way out may route v (or, for\n copies, write it) but those sites: not the loop's own code, not a function it calls, and — when it waits —\n nothing outside it in another coroutine, since anything may run while it waits.\"\"\"\n k = id(v)\n groups, touches, writes = self.sites[k], self.touches.get(k, []), self.writes.get(k, [])\n if r.unknown:\n return \"calls a function that calls itself\"\n inside = [h for h in groups if r in h[\"ctx\"][1]]\n if any(r in t[1] for t in touches):\n return \"touched in the loop\"\n fns = self.reached(r.calls)\n if any(h[\"ctx\"][0] in fns for h in groups) or any(t[0] in fns for t in touches):\n return \"a function the loop calls routes it\"\n if r.waits:\n apart = inside[0][\"ctx\"][0]\n if any(h[\"ctx\"][0] is not apart for h in groups) or any(t[0] is not apart for t in touches):\n return \"waits, and other code routes it\"\n if len(inside) == 1:\n return \"hoist\"\n if any(r in w[1] for w in writes) or any(w[0] in fns for w in writes):\n return \"read at %d sites and written in the loop\" % len(inside)\n if r.waits and any(w[0] is not inside[0][\"ctx\"][0] for w in writes):\n return \"waits, and other code writes it\"\n return \"replicas\"\n\n def choice(self, v, g):\n \"\"\"For v in group g: (\"static\", None), (\\\"hoist\\\" or \"replicas\", the outermost loop that can), or (None, why not).\"\"\"\n k = id(v)\n if len(self.sites[k]) == 1 and not self.touches.get(k):\n return \"static\", None\n whys = []\n for r in g[\"ctx\"][1]:\n how = self.plan_for(v, r)\n if how in (\"hoist\", \"replicas\"):\n return how, r\n whys.append(how)\n return None, \"; \".join(whys) if whys else \"read at %d sites, outside any loop\" % len(self.sites[k])\n\n def decide(self):\n \"\"\"Each group whose cells all need no routing at its site goes in bare: a cell read there and nowhere else is\n routed in the variable buffer for good; one that is the only read inside a loop has its routing set once in\n front of the loop; one read at several sites of a loop that never writes it is read from a copy of its own,\n routed for good, which the loop's way in fills. Every other group keeps its setup.\"\"\"\n if not self.on:\n return\n self.on = False\n report = settings.get(\"routingReport\")\n for g in self.groups:\n self.stats[\"groups\"] += 1\n self.stats[\"cells\"] += len(g[\"cells\"])\n plan = []\n for v, dest, mod, nxt, slot in g[\"cells\"]:\n how, where = self.choice(v, g)\n if how is None:\n if report and g[\"ctx\"][1]:\n print(\"trigscript: routing kept in a loop: %s: %s\" % (self.names.get(id(v)) or \"a temporary made at \" + self.made_at.get(id(v), \"?\"), where))\n for w in sorted(self.why_touched.get(id(v), ()))[:3]:\n print(\"trigscript: touched at \" + w)\n plan = None\n break\n plan.append((how, where))\n if plan is None:\n g[\"entry\"] << g[\"setup\"]\n for v, _, _, _, slot in g[\"cells\"]:\n slot << v.GetVTable()\n continue\n self.stats[\"bare groups\"] += 1\n g[\"entry\"] << g[\"bare\"]\n for (v, dest, mod, nxt, slot), (how, r) in zip(g[\"cells\"], plan):\n self.stats[{\"static\": \"static\", \"hoist\": \"hoisted\", \"replicas\": \"replicas\"}[how]] += 1\n if how == \"static\":\n slot << v.GetVTable()\n self.static[v.GetVTable()] = (dest, mod, nxt)\n elif how == \"hoist\":\n slot << v.GetVTable()\n r.hoist.append((v, dest, mod, nxt))\n else:\n copy = EUDVariable()\n slot << copy.GetVTable()\n self.static[copy.GetVTable()] = (dest, mod, nxt)\n r.copies.append((v, copy))\n # Each loop's way in: straight on, or through triggers that set the routing hoisted in front of it and fill its\n # copies (made here, out of the way of the code being made: what is pending is put aside and handed on after).\n held = Forward()\n SetNextTrigger(held)\n for r in self.loops:\n if not r.hoist and not r.copies:\n r.entry << r.after\n continue\n self.stats[\"loops\"] += 1\n steps = [] # (actions, the cell the trigger goes on to, or None for the next step)\n acts = []\n for v, dest, mod, nxt in r.hoist:\n acts += [SetDeaths(EPD(v.getDestAddr()), SetTo, dest, 0), SetDeathsX(EPD(v.getDestAddr()) + 2, SetTo, mod << 24, 0, 0xFF000000), SetNextPtr(v.GetVTable(), nxt)]\n for i in range(0, len(acts), 63):\n steps.append((acts[i:i + 63], None))\n # A copy is filled by the variable's own cell, routed into the copy and back each time (it may be routed\n # otherwise outside the loop, so nothing of it is assumed).\n for v, copy in r.copies:\n steps.append(([SetDeaths(EPD(v.getDestAddr()), SetTo, EPD(copy.getValueAddr()), 0), SetDeathsX(EPD(v.getDestAddr()) + 2, SetTo, 7 << 24, 0, 0xFF000000)], v))\n firsts = [Forward() for _ in steps]\n for i, (acts_i, cell) in enumerate(steps):\n after = firsts[i + 1] if i + 1 < len(steps) else r.after\n if cell is None:\n firsts[i] << RawTrigger(nextptr=after, actions=acts_i)\n else:\n firsts[i] << RawTrigger(nextptr=cell.GetVTable(), actions=acts_i + [SetNextPtr(cell.GetVTable(), after)])\n r.entry << firsts[0]\n held << NextTrigger()\n print(\"trigscript: static routing — %(bare groups)d of %(groups)d reads without routing: %(static)d cells routed for good, %(hoisted)d routed once in front of a loop, %(replicas)d read from a copy (%(loops)d loops)\" % self.stats)\n\n\nROUTE = _Routing(settings.get(\"staticRouting\", \"1\") != \"0\")\n\nif ROUTE.enabled:\n _var_init = _eudv.EUDVariable.__init__\n _MODULE = globals()\n\n def _routed_init(self, *args, **kwargs):\n _var_init(self, *args, **kwargs)\n # Made by a line of this file itself (not by eudplib on its behalf): ours to route.\n f = _sys._getframe(1)\n if ROUTE.on and f.f_globals is _MODULE:\n ROUTE.ours[id(self)] = self\n if settings.get(\"routingReport\"):\n ROUTE.made_at[id(self)] = \"%s:%d < %s:%d\" % (f.f_code.co_name, f.f_lineno, f.f_back.f_code.co_name, f.f_back.f_lineno)\n\n _eudv.EUDVariable.__init__ = _routed_init\n\n def _watched(name):\n method = getattr(_eudv.EUDVariable, name)\n\n def wrapper(self, *args, **kwargs):\n ROUTE.touched(self)\n return method(self, *args, **kwargs)\n\n wrapper.__name__ = name\n setattr(_eudv.EUDVariable, name, wrapper)\n\n for _name in (\"GetVTable\", \"getDestAddr\", \"getMaskAddr\", \"SetModifier\", \"SetDest\", \"AddDest\", \"SubtractDest\", \"SetDestX\", \"AddDestX\", \"SubtractDestX\"):\n _watched(_name)\n\n # Its value's address asked for is a write unless a condition asks it (a condition only reads the value).\n _value_addr = _eudv.EUDVariable.getValueAddr\n\n def _routed_value_addr(self):\n ROUTE.wrote(self)\n return _value_addr(self)\n\n _eudv.EUDVariable.getValueAddr = _routed_value_addr\n import eudplib.core.variable.vbase as _vbase\n\n def _reading(name):\n method = getattr(_vbase.VariableBase, name)\n\n def wrapper(self, *args, **kwargs):\n ROUTE.reading += 1\n try:\n return method(self, *args, **kwargs)\n finally:\n ROUTE.reading -= 1\n\n wrapper.__name__ = name\n setattr(_vbase.VariableBase, name, wrapper)\n\n for _name in (\"AtLeast\", \"AtMost\", \"Exactly\", \"AtLeastX\", \"AtMostX\", \"ExactlyX\"):\n _reading(_name)\n\n _seqcompute = _eudv.SeqCompute\n _MODS = {id(SetTo): 7, id(Add): 8, id(Subtract): 9}\n\n def _emit_group(cells, consts, memo):\n \"\"\"A site: what came before jumps to `entry`; the setup does the constant stores before it (as eudplib's own\n SeqCompute does, in the same trigger) and routes the cells in a chain, the last back to `after`. The bare way in\n (no routing: `decide` chose) does the constants alone and goes through `slot`s — each the cell itself, or a\n copy of it a loop keeps — whose routing is set elsewhere.\"\"\"\n ROUTE.emitting += 1\n try:\n entry, after = Forward(), Forward()\n SetNextTrigger(entry)\n first = cells[0][0].GetVTable()\n slots = [Forward() for _ in cells]\n # (An action belongs to one trigger: the constants are made twice, for the setup and for the bare way in.)\n const_acts = lambda: [SetDeaths(_eudv._get_computedest(EncodePlayer(_unproxy_r(dst))), mdt, src, 0) for dst, mdt, src in consts]\n bare = RawTrigger(nextptr=slots[0], actions=const_acts()) if consts else slots[0]\n acts, plan = const_acts(), []\n for i, (v, dest, mod) in enumerate(cells):\n nxt = cells[i + 1][0].GetVTable() if i + 1 < len(cells) else after\n # A cell this same SeqCompute routed already keeps what was set then (eudplib's own does the same): it is\n # read at two sites, so neither goes without its setup.\n was = memo.get(id(v))\n if not was or was[0] is not dest:\n acts.append(v.SetDest(dest))\n if not was or was[1] is not mod:\n acts.append(v.SetModifier(mod))\n acts.append(SetNextPtr(v.GetVTable(), nxt))\n memo[id(v)] = (dest, mod)\n plan.append((v, dest, _MODS[id(mod)], slots[i + 1] if i + 1 < len(cells) else after, slots[i]))\n setup = RawTrigger(nextptr=first, actions=acts)\n after << NextTrigger()\n finally:\n ROUTE.emitting -= 1\n g = {\"entry\": entry, \"setup\": setup, \"bare\": bare, \"cells\": plan, \"ctx\": ROUTE.where()}\n ROUTE.groups.append(g)\n for v, _, _, _, _ in plan:\n ROUTE.sites.setdefault(id(v), []).append(g)\n\n def _routed_seqcompute(assignpairs):\n if not ROUTE.on or ROUTE.emitting:\n return _seqcompute(assignpairs)\n plain, group = [], []\n\n def flush_plain():\n if plain:\n _seqcompute(list(plain))\n plain.clear()\n\n consts, memo = [], {}\n\n def flush_group():\n if group:\n _emit_group(list(group), list(consts), memo)\n group.clear()\n consts.clear()\n\n for dst, mdt, src in assignpairs:\n s = _unproxy_r(src)\n if isinstance(s, EUDVariable) and id(s) in ROUTE.ours and id(mdt) in _MODS:\n if any(v is s for v, _, _ in group) or len(group) * 3 + len(consts) >= 61:\n flush_group()\n if not group:\n # The constant stores just before it go with it, as eudplib keeps them in one trigger.\n k = max([i for i, (_, _, x) in enumerate(plain) if isinstance(_unproxy_r(x), EUDVariable)], default=-1)\n consts.extend(plain[k + 1:])\n del plain[k + 1:]\n flush_plain()\n group.append((s, _eudv._get_computedest(EncodePlayer(_unproxy_r(dst))), mdt))\n else:\n flush_group()\n plain.append((dst, mdt, src))\n flush_group()\n flush_plain()\n\n # Every eudplib module that took SeqCompute by name (a call's arguments go through eudplib.core.variable's) calls this one.\n for _mod in list(_sys.modules.values()):\n if getattr(_mod, \"__name__\", \"\").startswith(\"eudplib\") and getattr(_mod, \"SeqCompute\", None) is _seqcompute:\n _mod.SeqCompute = _routed_seqcompute\n SeqCompute = _routed_seqcompute\n\n # Where code runs apart: a function's body (eudplib's and this file's), a scope of triggers this file sets apart.\n from eudplib.core.eudfunc.eudfuncn import EUDFuncN as _EUDFuncN_r\n _fn_call = _EUDFuncN_r.__call__\n _fn_body = _EUDFuncN_r._create_func_body\n\n def _routed_call(self, *args, **kwargs):\n ROUTE.called(self)\n return _fn_call(self, *args, **kwargs)\n\n def _routed_body(self):\n r = ROUTE.functions.setdefault(id(self), _Region(\"fn\"))\n ROUTE.stack.append(r)\n try:\n return _fn_body(self)\n finally:\n ROUTE.stack.pop()\n\n _EUDFuncN_r.__call__ = _routed_call\n _EUDFuncN_r._create_func_body = _routed_body\n _push_scope, _pop_scope = PushTriggerScope, PopTriggerScope\n\n def PushTriggerScope(): # noqa: N802 (this file's own scopes apart: a function that calls itself)\n ROUTE.stack.append(_Region(\"scope\"))\n return _push_scope()\n\n def PopTriggerScope(): # noqa: N802\n ROUTE.stack.pop()\n return _pop_scope()\n\n # The variable buffer writes the cells a decided group reads with their routing in place.\n _create_vartrigger = _vbuf.EUDVarBuffer.create_vartrigger\n\n def _indexed_vartrigger(self, v, initval):\n idx = getattr(self, \"_ts_index\", None)\n if idx is None:\n idx = self._ts_index = {}\n idx[v] = len(self._initvals)\n return _create_vartrigger(self, v, initval)\n\n _vbuf.EUDVarBuffer.create_vartrigger = _indexed_vartrigger\n\n def _static_of(buf):\n return [(i, ROUTE.static[v]) for v, i in getattr(buf, \"_ts_index\", {}).items() if v in ROUTE.static]\n\n _collect_vars = _vbuf.EUDVarBuffer.CollectDependency\n\n def _collect_routed(self, emitbuffer):\n _collect_vars(self, emitbuffer)\n for _, (dest, _, nxt) in _static_of(self):\n for x in (dest, nxt):\n if type(x) is not int:\n emitbuffer.WriteDword(x)\n\n _vbuf.EUDVarBuffer.CollectDependency = _collect_routed\n\n _write_vars = _vbuf.EUDVarBuffer.WritePayload\n\n def _write_routed(self, emitbuffer):\n static = _static_of(self)\n if not static:\n return _write_vars(self, emitbuffer)\n # The buffer as eudplib lays it out (vbuf.py): 324 bytes before the first cell, then 72 a variable — variable i's\n # trigger starts 4 bytes before the buffer plus 72·i, so its next pointer is at 72·i, its action's destination at\n # 340 + 72·i, its value at 344 + 72·i and its modifier byte at 351 + 72·i — then the last triggers' tails.\n n = len(self._initvals)\n blob = bytearray(324)\n block = bytearray(72)\n block[0:4] = b\"\\xFF\\xFF\\xFF\\xFF\"\n block[24:36] = b\"\\0\\0\\x2D\\x07\\0\\0SC\\x04\\0\\0\\0\"\n blob += block * n\n sym = {}\n\n def put(off, x):\n if isinstance(x, int):\n blob[off:off + 4] = (x & 0xFFFFFFFF).to_bytes(4, \"little\")\n else:\n sym[off] = x\n\n for i, iv in enumerate(self._initvals):\n if isinstance(iv, int):\n if iv:\n put(344 + 72 * i, iv)\n else:\n put(344 + 72 * i, iv)\n for i, (dest, mod, nxt) in static:\n put(340 + 72 * i, dest)\n blob[351 + 72 * i] = mod\n put(72 * i, nxt)\n pos = 0\n for off in sorted(sym):\n emitbuffer.WriteBytes(bytes(blob[pos:off]))\n emitbuffer.WriteDword(sym[off])\n pos = off + 4\n emitbuffer.WriteBytes(bytes(blob[pos:]))\n emitbuffer.WriteSpace(32)\n emitbuffer.WriteDword(4) # flags\n emitbuffer.WriteSpace(27)\n emitbuffer.WriteByte(0) # currentAction\n for _i in range(27):\n emitbuffer.WriteSpace(40)\n emitbuffer.WriteDword(4)\n emitbuffer.WriteSpace(27)\n emitbuffer.WriteByte(0)\n\n _vbuf.EUDVarBuffer.WritePayload = _write_routed\n ROUTE.on = True\n\n\ndef where(node):\n \"\"\"\" at main.ts:12:5\" for a node with a position — what the editor parses back into a marker.\"\"\"\n at = node.get(\"at\") if isinstance(node, dict) else None\n if not at:\n return \"\"\n if at.get(\"file\"):\n return \" at %s:%s:%s\" % (at.get(\"file\"), at.get(\"line\"), at.get(\"column\"))\n return \" at %s:%s\" % (at.get(\"line\"), at.get(\"column\"))\n\n\nclass Fail(RuntimeError):\n pass\n\n\nclass Leave(Exception):\n \"\"\"Raised through the lowering when a statement ends the straight line (break / continue / return).\"\"\"\n\n\n# Cells read at an index the game works out (an array, a row of a per-player program or of a task) are an EUDVArray\n# where they fit: a cell is a variable of its own, 72 bytes, which a read jumps to and which writes itself out — a\n# read at a variable index is a jump by the index's bits (4 triggers for 16 cells) where an EUDArray reads the dword\n# a bit at a time (45 triggers), and one at a constant index two triggers where an EUDArray takes as many. Past\n# VARRAY_MOST cells an array stays an EUDArray, 4 bytes a cell (a long table, a video's brushes).\nVARRAY_MOST = 2048\nVARRAY_STRIDE, VARRAY_VALUE = 72, 348 # a cell's size, and where its value is in it\n_VARRAYS = {} # id → the EUDVArray, for the map of variables (which says a cell's place by stride)\n\n\ndef cell_array(values):\n \"\"\"`values` as cells: an EUDVArray up to VARRAY_MOST of them, else an EUDArray.\"\"\"\n values = list(values)\n if len(values) > VARRAY_MOST:\n return EUDArray(values)\n v = EUDVArray(len(values))(values)\n _VARRAYS[id(v)] = v\n return v\n\n\ndef is_varray(store):\n return id(store) in _VARRAYS\n\n\nFILL_WIDTH = 8 # cells a trigger of varray_fill writes\n\n\ndef varray_fill(store, length, value):\n \"\"\"The first `length` cells of an EUDVArray set to `value`: eight a trigger, the Set Deaths' destinations moving on by\n eight cells each time, where a cell at a time worked its address out a bit a trigger (about 250 ns a cell, 23 now).\"\"\"\n acts = [Forward() for _ in range(FILL_WIDTH)]\n start = [SetMemory(a + 16, SetTo, store._epd + VARRAY_VALUE // 4 + VARRAY_STRIDE // 4 * k) for k, a in enumerate(acts)]\n patched = isinstance(unProxy(value), EUDVariable)\n if patched:\n RawTrigger(actions=start)\n for a in acts:\n VProc(value, list(value.QueueAssignTo(EPD(a) + 5)))\n else:\n RawTrigger(actions=start)\n turns = EUDVariable()\n turns << length // FILL_WIDTH\n if EUDWhileNot()(turns == 0):\n RawTrigger(actions=[a << SetMemoryEPD(0, SetTo, 0 if patched else value) for a in acts]\n + [SetMemory(a + 16, Add, VARRAY_STRIDE // 4 * FILL_WIDTH) for a in acts] + [turns.SubtractNumber(1)])\n EUDEndWhile()\n for i in range(length - length % FILL_WIDTH, length):\n cell_write(store, i, value, SetTo)\n\n\ndef cell_write(store, index, value, modifier):\n \"\"\"`store[index]` set to (`SetTo`) or added to (`Add`) `value`, the store an EUDArray or one of cell_array's\n EUDVArrays. Into an EUDVArray at a variable index: a Set Deaths whose destination, the cell's value at\n EPD + 87 + 18 i, is worked out by one trigger an index bit, as eudplib's own get does (eudplib 0.81's EUDVArray\n has no add, and its set is a third dearer).\"\"\"\n if not is_varray(store):\n if modifier == Add:\n store.iadditem(index, value)\n else:\n store[index] = value\n return\n if isinstance(index, int):\n if modifier == SetTo:\n # The variable's value written into the cell (through SeqCompute: a site of static routing).\n SeqCompute([(store._epd + VARRAY_VALUE // 4 + VARRAY_STRIDE // 4 * index, SetTo, value)])\n else:\n Trigger(actions=SetMemoryEPD(store._epd + VARRAY_VALUE // 4 + VARRAY_STRIDE // 4 * index, modifier, value))\n return\n act = Forward()\n # (Through SeqCompute, so that a variable's copy into the action is a site of static routing.)\n pairs = [(EPD(act) + 4, SetTo, store._epd + VARRAY_VALUE // 4)]\n if isinstance(unProxy(value), EUDVariable):\n pairs.append((EPD(act) + 5, SetTo, value))\n value = 0\n SeqCompute(pairs)\n for b in reversed(range(max((store._size - 1).bit_length(), 1))):\n RawTrigger(conditions=index.AtLeastX(1, 1 << b), actions=SetMemory(act + 16, Add, VARRAY_STRIDE // 4 << b))\n RawTrigger(actions=[act << SetMemoryEPD(0, modifier, value)])\n\n\n# ── an index worked out once ──\n# A cell of an EUDVArray at a variable index is 72 bytes a step from the first: eudplib works the place out a bit of\n# the index at a time, a trigger a bit, at every read and every write. A variable whose every store is a constant or\n# itself plus or minus a constant (a loop's counter, `for…of`'s own index) can have the place's offset kept beside it\n# by the same stores — `i++` is two adds — and a read at it is a jump to the first cell plus the offset, a write a Set\n# Deaths whose destination is the first cell's plus the offset: a few triggers whatever the array's length.\n\ndef counter_step(value, id_):\n \"\"\"What a store of `value` into the variable `id_` does to it: (\"set\", c) or (\"add\", c), c a constant (wrapped),\n or None when it is anything else.\"\"\"\n if not isinstance(value, dict):\n return None\n k = value.get(\"kind\")\n if k == \"const\":\n return (\"set\", int(value[\"value\"]) & U32)\n if k == \"binary\" and value.get(\"op\") in (\"+\", \"-\") and not value.get(\"fixed\"):\n left, right = value[\"left\"], value[\"right\"]\n me = lambda e: e.get(\"kind\") == \"var\" and e.get(\"id\") == id_\n if me(left) and right.get(\"kind\") == \"const\":\n c = int(right[\"value\"])\n return (\"add\", (c if value[\"op\"] == \"+\" else -c) & U32)\n if value[\"op\"] == \"+\" and me(right) and left.get(\"kind\") == \"const\":\n return (\"add\", int(left[\"value\"]) & U32)\n return None\n\n\ndef find_counters(program):\n \"\"\"The ids of the variables of numbers whose every store is a constant or the variable plus or minus a constant,\n and which nothing else names but a read: no call's result, no function's saved frame, no loop's own variable.\"\"\"\n ok, bad = set(), set()\n skip = (\"kind\", \"at\", \"label\")\n\n def walk(node):\n if isinstance(node, list):\n for x in node:\n walk(x)\n elif isinstance(node, str):\n bad.add(node) # (an id anywhere but a read, a store or its declaration: something else writes it)\n elif isinstance(node, dict):\n k = node.get(\"kind\")\n if k == \"var\" and isinstance(node.get(\"id\"), str):\n return\n if k == \"assign\" and isinstance(node.get(\"target\"), str):\n t = node[\"target\"]\n (ok if counter_step(node.get(\"value\"), t) else bad).add(t)\n for key, v in node.items():\n if key not in skip and key != \"target\":\n walk(v)\n return\n if k == \"declare\" and isinstance(node.get(\"decl\"), dict):\n d = node[\"decl\"]\n step = counter_step(node.get(\"init\"), d.get(\"id\"))\n plain_number = d.get(\"kind\") == \"number\" and not d.get(\"bits\") and not d.get(\"fixed\") and d.get(\"task\") is None\n (ok if plain_number and step and step[0] == \"set\" else bad).add(d.get(\"id\"))\n for key, v in node.items():\n if key not in skip and key != \"decl\":\n walk(v)\n return\n for key, v in node.items():\n if key not in skip:\n walk(v)\n\n walk([program.get(\"body\", []), program.get(\"functions\", [])])\n return ok - bad\n\n\ndef saved_arrays(program):\n \"\"\"The arrays a function that calls itself saves and takes back (their handles written behind the lowering's back).\"\"\"\n out = set()\n\n def walk(node):\n if isinstance(node, list):\n for x in node:\n walk(x)\n elif isinstance(node, dict):\n if isinstance(node.get(\"saves\"), dict):\n out.update(node[\"saves\"].get(\"arrays\", []))\n for v in node.values():\n if isinstance(v, (list, dict)):\n walk(v)\n\n walk([program.get(\"body\", []), program.get(\"functions\", [])])\n return out\n\n\n# Kinds of statement or value a loop's body may not hold for its reads at the loop's index to go unchecked: what\n# changes an array's length, what calls a function (which may), and what waits (another program may run then).\nUNCHECKED_REFUSE = {\"push\", \"pop\", \"setLength\", \"declareArray\", \"call\", \"sleep\", \"parallel\", \"start\", \"cancel\", \"select\",\n \"syncSend\", \"yield\", \"await\", \"taskHandle\", \"running\"}\n\n\ndef lengths_fixed_in(body, index_id, lists):\n \"\"\"Whether, in a loop's body, no array changes its length and the index is not stored to: nothing in it from\n UNCHECKED_REFUSE, no store into a growing array (`lists`, by id) but at the index itself (a store at the length\n is a push), no store into the index.\"\"\"\n def ok(node):\n if isinstance(node, list):\n return all(ok(x) for x in node)\n if not isinstance(node, dict):\n return True\n k = node.get(\"kind\")\n if k in UNCHECKED_REFUSE:\n return False\n if k == \"assign\" and node.get(\"target\") == index_id:\n return False\n if k == \"store\" and node.get(\"array\") in lists:\n idx = node.get(\"index\", {})\n if not (idx.get(\"kind\") == \"var\" and idx.get(\"id\") == index_id):\n return False\n return all(ok(v) for v in node.values() if isinstance(v, (list, dict)))\n return ok(body)\n\n\ndef lists_at(body, index_id, lists):\n \"\"\"The growing arrays (`lists`, by id) a loop's body reads or writes at the index.\"\"\"\n out = []\n\n def walk(node):\n if isinstance(node, list):\n for x in node:\n walk(x)\n elif isinstance(node, dict):\n idx = node.get(\"index\")\n if node.get(\"kind\") in (\"element\", \"store\") and node.get(\"array\") in lists and isinstance(idx, dict) \\\n and idx.get(\"kind\") == \"var\" and idx.get(\"id\") == index_id and node[\"array\"] not in out:\n out.append(node[\"array\"])\n for v in node.values():\n if isinstance(v, (list, dict)):\n walk(v)\n\n walk(body)\n return out\n\n\nclass Cursors:\n \"\"\"Reads and writes at a loop's counter through registers of their own (the backend's second part): a read is a\n trigger whose next pointer is the cell's address — so it jumps straight to the cell, which writes itself out\n through eudplib's getter — and a write an action whose destination is the cell's value. Each register is set once on\n the way into the loop (the array's first cell plus the counter's place) and moved on by the counter's step at the\n loop's update, one add each, where `varray_read` / `varray_write` work the place out at every access (two variables\n routed into a jump, or into an action, each time). Only for a loop whose body stores nothing into the counter, waits\n for nothing, calls nothing and changes no array's length (`lengths_fixed_in`): the arrays' blocks stay where they\n are, and nothing but the update moves the counter.\"\"\"\n\n def __init__(self, counter, place, step):\n self.counter, self.place, self.step = counter, place, step\n self.reads = [] # (the trigger whose next pointer is the register, the first cell: a constant or a variable)\n self.writes = [] # (the action whose destination is the register, the first cell's value's EPD)\n\n def read(self, base, out):\n from eudplib.core.eudstruct.vararray import _getter, _getter_dst\n after = Forward()\n t = RawTrigger(nextptr=0, actions=[SetNextPtr(_getter, after), SetMemoryEPD(_getter_dst, SetTo, EPD(out.getValueAddr()))])\n after << NextTrigger()\n self.reads.append((t, base))\n\n def write(self, base_epd, value, modifier):\n act = Forward()\n if isinstance(unProxy(value), EUDVariable):\n SeqCompute([(EPD(act) + 5, SetTo, value)])\n value = 0\n RawTrigger(actions=[act << SetMemoryEPD(0, modifier, value)])\n self.writes.append((act, base_epd))\n\n def init(self):\n \"\"\"The registers set from where each array's block starts and the counter's place (on the way in).\"\"\"\n pairs = []\n for t, base in self.reads:\n pairs += [(EPD(t) + 1, SetTo, base), (EPD(t) + 1, Add, self.place.bytes)]\n for act, base_epd in self.writes:\n pairs += [(EPD(act) + 4, SetTo, base_epd), (EPD(act) + 4, Add, self.place.dwords)]\n SeqCompute(pairs)\n\n def advance(self):\n \"\"\"The registers moved on by the counter's step (at the loop's update).\"\"\"\n acts = [SetMemory(t + 4, Add, (VARRAY_STRIDE * self.step) & U32) for t, _ in self.reads]\n acts += [SetMemory(act + 16, Add, (VARRAY_STRIDE // 4 * self.step) & U32) for act, _ in self.writes]\n if acts:\n DoActions(acts)\n\n\nclass Place:\n \"\"\"A counter's offset into EUDVArrays, kept by its stores: in bytes (72 a step, for a read's jump) and in dwords\n (18 a step, for a write's destination).\"\"\"\n\n def __init__(self):\n self.bytes = EUDVariable(0) # initial: the counter's own 0\n self.dwords = EUDVariable(0) # initial: the counter's own 0\n\n def step(self, step):\n op, c = step\n if op == \"set\":\n DoActions([self.bytes.SetNumber((VARRAY_STRIDE * c) & U32), self.dwords.SetNumber((VARRAY_STRIDE // 4 * c) & U32)])\n else:\n DoActions([self.bytes.AddNumber((VARRAY_STRIDE * c) & U32), self.dwords.AddNumber((VARRAY_STRIDE // 4 * c) & U32)])\n\n\ndef varray_read(base, place, out):\n \"\"\"The cell at `base` + the place's bytes into `out`: `base` the first cell (a constant, or a variable holding it).\n The offset's variable adds itself into its own next-trigger pointer and jumps there, to the cell, which writes\n itself out through eudplib's getter, as EUDVArray.get does once it has the place.\"\"\"\n from eudplib.core.eudstruct.vararray import _getter, _getter_dst\n off = place.bytes\n after = Forward()\n tail = [SetNextPtr(_getter, after), SetMemoryEPD(_getter_dst, SetTo, EPD(out.getValueAddr()))]\n if isinstance(base, EUDVariable):\n RawTrigger(nextptr=base.GetVTable(), actions=[SetNextPtr(base.GetVTable(), off.GetVTable()), *base.QueueAssignTo(EPD(off.GetVTable()) + 1),\n *off.QueueAddTo(EPD(off.GetVTable()) + 1), *tail])\n else:\n RawTrigger(nextptr=off.GetVTable(), actions=[SetNextPtr(off.GetVTable(), base), *off.QueueAddTo(EPD(off.GetVTable()) + 1), *tail])\n after << NextTrigger()\n\n\ndef varray_read_at(addr, out):\n \"\"\"The cell whose address the variable `addr` holds into `out`.\"\"\"\n from eudplib.core.eudstruct.vararray import _getter, _getter_dst\n after = Forward()\n RawTrigger(nextptr=addr.GetVTable(), actions=[SetNextPtr(addr.GetVTable(), 0), *addr.QueueAddTo(EPD(addr.GetVTable()) + 1),\n SetNextPtr(_getter, after), SetMemoryEPD(_getter_dst, SetTo, EPD(out.getValueAddr()))])\n after << NextTrigger()\n\n\ndef varray_write_at(value_epd, value, extra=()):\n \"\"\"The cell whose value's EPD the variable `value_epd` holds set to `value`; `extra` actions in the same trigger.\"\"\"\n act = Forward()\n vs, acts = [value_epd], list(value_epd.QueueAssignTo(EPD(act) + 4))\n if isinstance(unProxy(value), EUDVariable):\n vs.append(value)\n acts += value.QueueAssignTo(EPD(act) + 5)\n value = 0\n VProc(vs, acts)\n RawTrigger(actions=[act << SetMemoryEPD(0, SetTo, value), *extra])\n\n\ndef varray_write(value_epd, place, value, modifier):\n \"\"\"The cell whose value is at EPD `value_epd` + the place's dwords set to (or added to) `value`: `value_epd` a\n constant, or a variable holding it.\"\"\"\n act = Forward()\n # (Through SeqCompute, so that each variable's copy into the action is a site of static routing.)\n pairs = [(EPD(act) + 4, SetTo, value_epd), (EPD(act) + 4, Add, place.dwords)]\n if isinstance(unProxy(value), EUDVariable):\n pairs.append((EPD(act) + 5, SetTo, value))\n value = 0\n SeqCompute(pairs)\n RawTrigger(actions=[act << SetMemoryEPD(0, modifier, value)])\n\n\nclass Rows:\n \"\"\"Which of a cell's copies is the one in use: a per-player program has one a player, a task's body one a slot of its\n start (`Pool`), and a task's in a per-player program one a slot within each player's. Called, it is the index.\"\"\"\n\n def __init__(self, lowering, per_player, pool):\n self.lowering, self.per_player, self.pool = lowering, per_player, pool\n self.count = (12 if per_player else 1) * (pool.max if pool else 1)\n\n def __call__(self):\n if self.pool:\n return self.pool.row\n return self.lowering.player_of()\n\n\nclass RowCell:\n \"\"\"A cell of the lowering's own (a coroutine's place, a latch), with a copy for each row of `rows` when it has them.\"\"\"\n\n def __init__(self, rows, initial=0):\n self.rows = rows\n self.store = cell_array([initial] * rows.count) if rows else EUDVariable(initial) # initial: the lowering's own state\n\n def get(self):\n return self.store[self.rows()] if self.rows else self.store\n\n def set(self, value):\n if self.rows:\n cell_write(self.store, self.rows(), value, SetTo)\n else:\n self.store << value\n\n\nclass Storage:\n \"\"\"A variable's cell: plain, or one of rows (`Rows`) - a 12-slot row of a per-player program, a row a task's slot.\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = rows is not None\n self.player_of = rows\n self.store = cell_array([0] * rows.count) if self.rowed else EUDVariable(0) # initial: the variable's own cell\n if not self.rowed:\n ROUTE.names[id(self.store)] = decl.get(\"name\", \"?\")\n\n def get(self):\n return self.store[self.player_of()] if self.rowed else self.store\n\n def set(self, value):\n if self.bits:\n value = saturate(value, self.bits)\n if self.rowed:\n cell_write(self.store, self.player_of(), value, SetTo)\n else:\n self.store << value\n\n def add(self, value, op):\n \"\"\"The cell plus (`op` \"+\") or minus (\"-\") `value` where it is, wrapping as numbers do: eudplib's own add, with\n no copy through a temporary (`x = x + 1` was 9 records and 4 triggers through one, and is 1 action). False\n where it cannot: a saturated cell, or a row's cell less a variable (the game's Subtract stops at 0).\"\"\"\n if self.bits:\n return False\n if self.rowed:\n if op == \"-\" and not isinstance(value, int):\n return False\n cell_write(self.store, self.player_of(), value if op == \"+\" else (-value) & U32, Add)\n return True\n if op == \"+\":\n self.store += value\n else:\n self.store -= value\n return True\n\n def mask(self, op, c):\n \"\"\"The cell & c or | c where it is: one masked write (only a variable's own cell, not a row's).\"\"\"\n DoActions(_mask_action(self.store, op, c))\n\n\ndef _mask_action(cell, op, c):\n \"\"\"The masked write that leaves `cell & c` (clear the bits outside c) or `cell | c` (set the bits of c) in a variable.\"\"\"\n return cell.SetNumberX(0, (~c) & U32) if op == \"&\" else cell.SetNumberX(c, c)\n\n\ndef unpack_table(table, length):\n \"\"\"A table the IR carries packed (`compiler/eud.ts#packTable`): `bytes` a value, little-endian, in base64 - as a\n dword a value, the cells of the map.\"\"\"\n width = int(table[\"bytes\"])\n raw = base64.b64decode(table[\"data\"])\n if len(raw) != width * length:\n raise RuntimeError(\"a packed table is %d bytes, not %d values of %d\" % (len(raw), length, width))\n out = bytearray(4 * length)\n for b in range(width):\n out[b::4] = raw[b::width]\n return bytes(out)\n\n\nclass ArrayStorage:\n \"\"\"An array's cells: `length` of them, twelve rows of that in a per-player program, or - `values` - a list\n the script computed when it was built, which is in the map as it loads and which nothing writes. An index\n past either end (read from 0 up, so one below zero too) reads 0 and stores nothing.\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.bits = decl.get(\"bits\")\n values = decl.get(\"values\")\n table = decl.get(\"packed\")\n self.rowed = rows is not None and values is None and table is None\n self.player_of = rows\n if decl.get(\"texts\") is not None:\n # A list of texts the script has: a cell is the text's id in the built map's table.\n values = [text_id(t) for t in decl[\"texts\"]]\n # What a list known when the map is built holds, for what is worked out with it then (an atan2 of constants).\n self.values = [int(v) & U32 for v in values] if values is not None else None\n if table is not None:\n # A long table (a video's brushes): its bytes straight into the map, no list of a value at a time.\n # (An EUDArray is its cells' EPD, so that is what it is made from.)\n raw = unpack_table(table, self.length)\n self.values = [int.from_bytes(raw[i:i + 4], \"little\") for i in range(0, len(raw), 4)]\n self.store = EUDArray(_from=EPD(Db(raw)))\n else:\n self.store = cell_array([int(v) & U32 for v in values] if values is not None else [0] * (self.length * (rows.count if self.rowed else 1))) # initial: a table's values, or cells declareArray sets\n\n def at(self, index):\n \"\"\"The cell's place among the store's: the row first (the player's, the task's).\"\"\"\n if not self.rowed:\n return index\n p = self.player_of()\n row = p * self.length if isinstance(p, int) else f_mul(p, self.length)\n return row + index\n\n def get(self, index):\n if isinstance(index, int):\n return self.store[self.at(index)] if index < self.length else 0\n out = fresh(0)\n if EUDIf()(index <= self.length - 1):\n if is_varray(self.store):\n self.store.get(self.at(index), ret=[out]) # (the cell writes itself into out: no copy through another)\n else:\n out << self.store[self.at(index)]\n EUDEndIf()\n return out\n\n def placeable(self):\n return not self.rowed and is_varray(self.store)\n\n def get_placed(self, index, place, bound=None):\n out = fresh(0)\n if EUDIf()(index <= self.length - 1):\n varray_read(unProxy(self.store), place, out)\n EUDEndIf()\n return out\n\n def set_placed(self, index, place, value, modifier=SetTo, bound=None):\n if self.bits and modifier == SetTo:\n value = saturate(value, self.bits)\n if EUDIf()(index <= self.length - 1):\n varray_write(self.store._epd + VARRAY_VALUE // 4, place, value, modifier)\n EUDEndIf()\n\n def set(self, index, value):\n if self.bits:\n value = saturate(value, self.bits)\n if isinstance(index, int):\n if index < self.length:\n cell_write(self.store, self.at(index), value, SetTo)\n return\n if EUDIf()(index <= self.length - 1):\n cell_write(self.store, self.at(index), value, SetTo)\n EUDEndIf()\n\n def add(self, index, value, op):\n \"\"\"The cell plus (`op` \"+\") or minus (\"-\") the value, by the game's own Add: nothing read. Past either end,\n nothing, as a store.\"\"\"\n # A subtract is the add of the value's negative, which wraps as numbers do: the game's Subtract stops at 0\n # (`a[i] -= 5` of a 3 was 0 in the game and -2 in the simulator).\n if op == \"-\":\n value = (-value) & U32 if isinstance(value, int) else 0 - value\n act = lambda i, v: cell_write(self.store, i, v, Add)\n if isinstance(index, int):\n if index < self.length:\n act(self.at(index), value)\n return\n if EUDIf()(index <= self.length - 1):\n act(self.at(index), value)\n EUDEndIf()\n\n def fill(self, value):\n if self.length <= 16:\n for i in range(self.length):\n self.set(i, value)\n return\n if is_varray(self.store) and not self.rowed:\n varray_fill(self.store, self.length, value)\n return\n v, i = as_var(value), fresh(0)\n if EUDWhile()(i <= self.length - 1):\n self.set(i, v)\n i += 1\n EUDEndWhile()\n\n\nclass SliceStorage:\n \"\"\"A window on another array's cells - a row of a grid: cell i is cell `offset + i` of the array it is a window\n on, `offset` a variable of the program. Past its own end it reads 0 and stores nothing, so a row never reaches\n into the next; what it is a window on keeps its own ends (and its own width, and its row a player).\"\"\"\n\n def __init__(self, decl, lowering):\n self.decl = decl\n self.length = int(decl[\"length\"])\n self.lowering = lowering\n\n def of(self):\n return self.lowering.array(self.decl[\"slice\"][\"of\"], self.decl)\n\n def offset(self):\n return self.lowering.var(self.decl[\"slice\"][\"offset\"], self.decl).get()\n\n def get(self, index):\n if isinstance(index, int):\n return self.of().get(self.offset() + index) if index < self.length else 0\n out = fresh(0)\n if EUDIf()(index <= self.length - 1):\n out << self.of().get(self.offset() + index)\n EUDEndIf()\n return out\n\n def set(self, index, value):\n if isinstance(index, int):\n if index < self.length:\n self.of().set(self.offset() + index, value)\n return\n if EUDIf()(index <= self.length - 1):\n self.of().set(self.offset() + index, value)\n EUDEndIf()\n\n def fill(self, value):\n v, i = as_var(value), fresh(0)\n if EUDWhile()(i <= self.length - 1):\n self.set(i, v)\n i += 1\n EUDEndWhile()\n\n\n# The heap the programs' growing arrays share. A block is a power of two of cells, four at least; one given back\n# waits in its size's list (its first cell links the next) for whoever wants that size next; new ground is taken\n# from the bottom up. Cell 0 is never handed out: 0 is \"no block\". compiler/simulateIr.ts counts the same way, so\n# both run out at the same push. (Recursion's stack is an array of its own, below: how deep a function may go is\n# then the same whatever the arrays hold.)\nHEAP_CELLS = min(1 << 20, max(1024, int(IR.get(\"heap\", 16384)))) # the map's script settings, or the default\nHEAP_SMALLEST = 4\nCELL_HEAP_MOST = 1 << 16 # compiler/ir.ts#CELL_HEAP_MOST: up to this many, growing arrays have cells of their own\nHEAP_ROOMS = []\nwhile HEAP_SMALLEST << len(HEAP_ROOMS) <= HEAP_CELLS:\n HEAP_ROOMS.append(HEAP_SMALLEST << len(HEAP_ROOMS))\n_HEAP = {}\n\n\ndef heap():\n \"\"\"The heap's cells and its functions, made when the first growing array is met.\"\"\"\n if _HEAP:\n return _HEAP\n cells = EUDArray(HEAP_CELLS)\n rooms = EUDArray(HEAP_ROOMS + [0xFFFFFFFF]) # initial: the sizes, never written\n free = EUDArray(len(HEAP_ROOMS)) # initial: no block waits\n top = EUDVariable(1) # initial: the heap's own state, for the whole game\n said = EUDVariable(0) # initial: the heap's own state\n\n @EUDFunc\n def take(k):\n \"\"\"A block of size class k: its place among the cells, or 0 when there is none.\"\"\"\n at = EUDVariable()\n at << free[k]\n if EUDIf()(at >= 1):\n free[k] = cells[at]\n EUDReturn(at)\n EUDEndIf()\n end = top + rooms[k]\n if EUDIf()(end <= HEAP_CELLS):\n at << top\n top << end\n EUDReturn(at)\n EUDEndIf()\n EUDReturn(0)\n\n @EUDFunc\n def give(at, k):\n cells[at] = free[k]\n free[k] = at\n\n def growing(copy, write):\n \"\"\"grow and push for one kind of cell: `copy(block, ptr, length)` moves a block's cells, `write(at, value)`\n stores one.\"\"\"\n\n @EUDFunc\n def grow(ptr, length, room, k, need):\n \"\"\"Room for `need` cells: a larger block, the cells copied over, the old block given back. Returns the\n handle as it is now and whether there is room; when the heap has no block left the handle is unchanged.\"\"\"\n if EUDIf()(need <= room):\n EUDReturn(ptr, room, k, 1)\n EUDEndIf()\n nk = EUDVariable()\n nk << 0\n if EUDIf()(ptr >= 1):\n nk << k + 1\n EUDEndIf()\n if EUDWhile()(rooms[nk] < need):\n nk += 1\n EUDEndWhile()\n if EUDIf()(nk >= len(HEAP_ROOMS)):\n EUDReturn(ptr, room, k, 0)\n EUDEndIf()\n block = take(nk)\n if EUDIf()(block == 0):\n EUDReturn(ptr, room, k, 0)\n EUDEndIf()\n if EUDIf()(ptr >= 1):\n if EUDIf()(length >= 1):\n copy(block, ptr, length)\n EUDEndIf()\n give(ptr, k)\n EUDEndIf()\n EUDReturn(block, rooms[nk], nk, 1)\n\n @EUDFunc\n def push(ptr, length, room, k, value):\n \"\"\"One more cell at the end; the handle as it is afterwards. Nothing is pushed when the heap is full.\"\"\"\n ptr, room, k, ok = grow(ptr, length, room, k, length + 1)\n if EUDIf()(ok >= 1):\n write(ptr + length, value)\n length += 1\n if EUDElse()():\n if EUDIf()(said == 0):\n said << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: out of memory - an array could not grow (the programs' arrays share %d cells).\" % HEAP_CELLS)\n EUDEndIf()\n EUDEndIf()\n EUDReturn(ptr, length, room, k)\n\n return grow, push\n\n def copy_dwords(block, ptr, length):\n # An EUDArray's own value is where it is, as an EPD already.\n f_repmovsd_epd(cells + block, cells + ptr, length)\n\n def write_dword(at, value):\n cells[at] = value\n\n grow, push = growing(copy_dwords, write_dword)\n _HEAP.update(cells=cells, rooms=rooms, take=take, give=give, grow=grow, push=push, growing=growing)\n return _HEAP\n\n\ndef list_heap():\n \"\"\"The heap with the growing arrays' own cells beside its bytes: blocks are handed out as for texts (the same\n count, so a text and an array still run out at the same push in the game and in the simulator), but an array's\n cells are an EUDVArray of the heap's length, a variable a cell, which a read jumps to instead of reading a dword\n a bit at a time (32 triggers). Made when the first growing array is met; texts never touch these cells.\"\"\"\n h = heap()\n if \"vcells\" in h or HEAP_CELLS > CELL_HEAP_MOST:\n return h # (past CELL_HEAP_MOST, 72 bytes a cell is too much memory: the arrays keep the heap's dwords, as in 3.x)\n vcells = EUDVArray(HEAP_CELLS)()\n _VARRAYS[id(vcells)] = vcells\n\n def copy_cells(block, ptr, length):\n # Both blocks walked a cell at a time: the source by its address, the destination by its value's EPD.\n src, dst, n, cell = EUDVariable(), EUDVariable(), EUDVariable(), EUDVariable()\n src << unProxy(vcells) + ptr * VARRAY_STRIDE\n dst << vcells._epd + VARRAY_VALUE // 4 + block * (VARRAY_STRIDE // 4)\n n << length\n if EUDWhile()(n >= 1):\n varray_read_at(src, cell)\n varray_write_at(dst, cell, [src.AddNumber(VARRAY_STRIDE), dst.AddNumber(VARRAY_STRIDE // 4), n.SubtractNumber(1)])\n EUDEndWhile()\n\n def write_cell(at, value):\n cell_write(vcells, at, value, SetTo)\n\n vgrow, vpush = h[\"growing\"](copy_cells, write_cell)\n h.update(vcells=vcells, vgrow=vgrow, vpush=vpush)\n return h\n\n\n# The stack of the functions that call themselves. A function's variables are cells of the program, one of each, so\n# a call that may come back into the function it is in (the IR's `saves`) puts what that function holds here first\n# and takes it back after, with where the function returns to. One stack serves every program and every player:\n# such a function never sleeps, so the stack is empty whenever a frame ends. It is as many frames of the largest\n# frame as the map's script settings allow calls deep (the IR file's `stack`), and the depth is what is counted -\n# compiler/simulateIr.ts counts the same - so a program stops at the same call in both.\nSTACK_DEPTH = min(1 << 16, max(16, int(IR.get(\"stack\", 1024))))\nSTACK_CELLS_MAX = 1 << 20\nHANDLE = (\"ptr\", \"len\", \"room\", \"k\")\n_STACK = {}\n\n\ndef frame_cells(saves, kinds):\n return 1 + 4 * len(saves.get(\"arrays\", [])) + sum(3 if kinds.get(v) in (\"unit\", \"text\") else 1 for v in saves.get(\"vars\", []))\n\n\ndef largest_frame():\n most = [0]\n\n def walk(node, kinds):\n if isinstance(node, list):\n for x in node:\n walk(x, kinds)\n elif isinstance(node, dict):\n if isinstance(node.get(\"saves\"), dict):\n most[0] = max(most[0], frame_cells(node[\"saves\"], kinds))\n for v in node.values():\n if isinstance(v, (dict, list)):\n walk(v, kinds)\n\n def kinds_of(node, into):\n if isinstance(node, list):\n for x in node:\n kinds_of(x, into)\n elif isinstance(node, dict):\n if isinstance(node.get(\"id\"), str) and node.get(\"kind\") in (\"number\", \"boolean\", \"unit\", \"text\") and \"shared\" in node:\n # A text kept as its id is one cell, as a number is; one that was made is three.\n into[node[\"id\"]] = \"number\" if node[\"kind\"] == \"text\" and node.get(\"text\") == \"id\" else node[\"kind\"]\n for v in node.values():\n if isinstance(v, (dict, list)):\n kinds_of(v, into)\n return into\n\n for program in IR.get(\"programs\", []):\n walk(program.get(\"functions\", []), kinds_of(program, {}))\n return most[0]\n\n\ndef stack():\n \"\"\"The stack's cells, where the next frame goes (an EPD, so a cell is one write) and how deep it is.\"\"\"\n if _STACK:\n return _STACK\n frame = largest_frame()\n if frame * STACK_DEPTH > STACK_CELLS_MAX:\n raise Fail(\"trigscript: the stack would be %d cells - %d calls deep, %d cells a call - and %d is the most: lower the recursion depth in the script's Settings, or keep fewer variables in the function that calls itself\" % (frame * STACK_DEPTH, STACK_DEPTH, frame, STACK_CELLS_MAX))\n cells = EUDArray(max(1, frame) * STACK_DEPTH)\n base = cells # an EUDArray's own value is where it is, as an EPD already\n at = EUDVariable(base) # initial: the stack's own state; it is back here whenever a frame ends\n depth = EUDVariable(0) # initial: the stack's own state; nothing is on it between frames\n _STACK.update(cells=cells, base=base, at=at, depth=depth)\n return _STACK\n\n\nclass ListStorage:\n \"\"\"An array that grows: a handle - where its block is among the heap's cells (0: none yet), how many cells are\n in use, how many the block has room for, and the block's size class - a cell each, or a row of twelve each in\n a per-player program. Reads and stores are bounded by the cells in use; a store at exactly the length is a push.\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = rows is not None\n self.player_of = rows\n make = (lambda: cell_array([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no block\n self.handle = {name: make() for name in (\"ptr\", \"len\", \"room\", \"k\")}\n self.heap = list_heap()\n # Where its block starts, kept as the block moves, for reads and writes at a counter (`Place`): the first\n # cell's address, and its value's EPD. Only when the lowering asks (`keep_base`) before anything is lowered.\n self.base = None\n\n def cells(self):\n \"\"\"Whether the array's cells are the growing arrays' own (`list_heap`), not the heap's dwords.\"\"\"\n return \"vcells\" in self.heap\n\n def keep_base(self):\n if self.base is None and not self.rowed and self.cells():\n v = self.heap[\"vcells\"]\n first = v._epd + VARRAY_VALUE // 4\n self.base = (EUDVariable(unProxy(v)), EUDVariable(first)) # initial: block 0's\n # Where a push writes, base[1] + 18 × the length: a push with room is a write there and two adds.\n self.next = EUDVariable(first) # initial: block 0's first cell, the length 0\n\n def renext(self):\n \"\"\"`next` worked out again from the length (after a block moved, or the length was set).\"\"\"\n if self.base is not None:\n self.next << self.base[1] + self.handle[\"len\"] * (VARRAY_STRIDE // 4)\n\n def placeable(self):\n return self.base is not None\n\n def field(self, name):\n cell = self.handle[name]\n return cell[self.player_of()] if self.rowed else cell\n\n def put(self, name, value):\n if self.rowed:\n self.handle[name][self.player_of()] = value\n else:\n if name == \"ptr\" and self.base is not None:\n # The block moved (a push that grew it, a declaration): its first cell again, only when it did.\n if EUDIfNot()(self.handle[\"ptr\"] == value):\n v = self.heap[\"vcells\"]\n self.base[0] << unProxy(v) + value * VARRAY_STRIDE\n self.base[1] << v._epd + VARRAY_VALUE // 4 + value * (VARRAY_STRIDE // 4)\n EUDEndIf()\n self.handle[name] << value\n\n def get_placed(self, index, place, bound=None):\n \"\"\"`bound`: None, checked against the length; True, known inside (the loop's own array); or a variable,\n 1 when a loop found this array at least as long as its own on the way in, which is checked instead.\"\"\"\n out = fresh(0)\n if bound is True:\n varray_read(self.base[0], place, out)\n return out\n if bound is not None:\n if EUDIf()(bound >= 1):\n varray_read(self.base[0], place, out)\n if EUDElseIf()(as_var(index) < as_var(self.field(\"len\"))):\n varray_read(self.base[0], place, out)\n EUDEndIf()\n return out\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\n varray_read(self.base[0], place, out)\n EUDEndIf()\n return out\n\n def set_placed(self, index, place, value, modifier=SetTo, bound=None):\n if self.bits and modifier == SetTo:\n value = saturate(value, self.bits)\n v = fresh(value) if modifier == SetTo and not isinstance(value, int) else value\n if bound is True:\n varray_write(self.base[1], place, v, modifier)\n return\n i, n = as_var(index), as_var(self.field(\"len\"))\n if bound is not None:\n if EUDIf()(bound >= 1):\n varray_write(self.base[1], place, v, modifier)\n if EUDElseIf()(i < n):\n varray_write(self.base[1], place, v, modifier)\n if EUDElseIf()(i == n):\n self.push(v) # xs[xs.length] = v, as JavaScript has it (xs[n] += v pushes v: xs[n] reads 0)\n EUDEndIf()\n return\n if EUDIf()(i < n):\n varray_write(self.base[1], place, v, modifier)\n if EUDElseIf()(i == n):\n self.push(v) # xs[xs.length] = v, as JavaScript has it (xs[n] += v pushes v: xs[n] reads 0)\n EUDEndIf()\n\n def length(self):\n return self.field(\"len\")\n\n def write(self, at, value):\n \"\"\"Cell `at` of the cells (a place among them, the block's plus the index) set to `value`.\"\"\"\n if self.cells():\n cell_write(self.heap[\"vcells\"], at, value, SetTo)\n else:\n self.heap[\"cells\"][at] = value\n\n def get(self, index):\n out = fresh(0)\n if EUDIf()(as_var(index) < as_var(self.field(\"len\"))):\n if self.cells():\n self.heap[\"vcells\"].get(self.field(\"ptr\") + index, ret=[out])\n else:\n out << self.heap[\"cells\"][self.field(\"ptr\") + index]\n EUDEndIf()\n return out\n\n def push(self, value):\n if self.bits:\n value = saturate(value, self.bits)\n if self.base is None:\n self.push_growing(value)\n return\n if not isinstance(value, int):\n value = fresh(value)\n if EUDIf()(as_var(self.handle[\"len\"]) < as_var(self.handle[\"room\"])):\n varray_write_at(self.next, value, [self.handle[\"len\"].AddNumber(1), self.next.AddNumber(VARRAY_STRIDE // 4)])\n if EUDElse()():\n self.grown()(value)\n EUDEndIf()\n\n def grown(self):\n \"\"\"A push with no room left, as one function of the array's own (the heap's grow, the handle put back, where\n the next push writes worked out again): made once, where every push of the array calls it.\"\"\"\n if getattr(self, \"_grown\", None) is None:\n @EUDFunc\n def grown(value):\n self.push_growing(value)\n self.renext()\n self._grown = grown\n return self._grown\n\n def push_growing(self, value):\n ptr, length, room, k = self.heap[\"vpush\" if self.cells() else \"push\"](self.field(\"ptr\"), self.field(\"len\"), self.field(\"room\"), self.field(\"k\"), value)\n for name, v in ((\"ptr\", ptr), (\"len\", length), (\"room\", room), (\"k\", k)):\n self.put(name, v)\n\n def set(self, index, value):\n if self.bits:\n value = saturate(value, self.bits)\n i, v, n = as_var(index), fresh(value), as_var(self.field(\"len\"))\n if EUDIf()(i < n):\n self.write(self.field(\"ptr\") + i, v)\n if EUDElseIf()(i == n):\n self.push(v) # xs[xs.length] = v, as JavaScript has it\n EUDEndIf()\n\n def pop(self):\n out = fresh(0)\n n = fresh(self.field(\"len\"))\n if EUDIf()(n >= 1):\n n -= 1\n self.put(\"len\", n)\n if self.base is not None:\n self.next -= VARRAY_STRIDE // 4\n if self.cells():\n self.heap[\"vcells\"].get(self.field(\"ptr\") + n, ret=[out])\n else:\n out << self.heap[\"cells\"][self.field(\"ptr\") + n]\n EUDEndIf()\n return out\n\n def set_length(self, value):\n v = as_var(value)\n if EUDIf()(v < as_var(self.field(\"len\"))):\n self.put(\"len\", v)\n self.renext()\n EUDEndIf()\n\n def declare(self, values):\n \"\"\"Declared (again): the block it held goes back, and it starts over with these values.\"\"\"\n ptr = fresh(self.field(\"ptr\"))\n if EUDIf()(ptr >= 1):\n self.heap[\"give\"](ptr, self.field(\"k\"))\n EUDEndIf()\n for name in (\"ptr\", \"len\", \"room\", \"k\"):\n self.put(name, 0)\n self.renext()\n for v in values:\n self.push(v)\n\n def declare_filled(self, value, count):\n \"\"\"The same with one value `count` times over: a loop, where a push a cell would be a push a cell in the map.\"\"\"\n if count <= 8:\n self.declare([value] * count)\n return\n self.declare([])\n v, i = as_var(value), fresh(0)\n if EUDWhile()(i <= count - 1):\n self.push(v)\n i += 1\n EUDEndWhile()\n\n def fill(self, value):\n v, i = as_var(value), fresh(0)\n if EUDWhile()(i < as_var(self.field(\"len\"))):\n self.write(self.field(\"ptr\") + i, v)\n i += 1\n EUDEndWhile()\n\n\nclass InnerListStorage(ListStorage):\n \"\"\"An array that grows inside another: its handle is cell `index` of four arrays the outer one keeps (where the\n block is, how many cells are in use, its room, its size class), `index` a variable of the program. Everything\n else is a growing array's, which only ever asks for its handle's fields and puts them back.\"\"\"\n\n def __init__(self, decl, lowering):\n self.decl = decl\n self.bits = decl.get(\"bits\")\n self.rowed = False\n self.lowering = lowering\n self.heap = list_heap()\n self.base = None\n\n def at(self):\n return self.lowering.var(self.decl[\"through\"][\"index\"], self.decl).get()\n\n def field(self, name):\n return self.lowering.array(self.decl[\"through\"][name], self.decl).get(self.at())\n\n def put(self, name, value):\n self.lowering.array(self.decl[\"through\"][name], self.decl).set(self.at(), value)\n\n\n# ── texts ──\nTEXT_BYTES = 1023 # the most a made text holds; compiler/ir.ts has the same number\nTEXT_FIELD_BYTES = 255 # the room of a string kept for an action's field or a unit type's name\nBUILD_BYTES = 2047 # the most a buffer of buildText holds; compiler/ir.ts has the same number\nUNIT_MAP_STRING = 0x660260 # units.dat: the id of the name a map gives a unit type, a word each\n_TEXT = {}\n_SLOTS = {}\n\n\ndef texts():\n \"\"\"The texts' part of the run time, made when the first text that is made is met.\"\"\"\n if _TEXT:\n return _TEXT\n h = heap()\n cells, rooms, take, give = h[\"cells\"], h[\"rooms\"], h[\"take\"], h[\"give\"]\n # A text being made. It may run one part past its most before it is cut, and a part is at most that long again.\n scratch = Db(TEXT_BYTES + 1 + TEXT_BYTES + 1 + 8)\n empty = Db(4)\n said_cut = EUDVariable(0) # initial: said once a game\n said_full = EUDVariable(0) # initial: said once a game\n said_field = EUDVariable(0) # initial: said once a game\n\n @EUDFunc\n def count(addr):\n \"\"\"How many characters the text at addr is: every byte but those that continue a character (10xxxxxx).\"\"\"\n n = EUDVariable()\n n << 0\n br1.seekoffset(addr)\n if EUDInfLoop()():\n b = br1.readbyte()\n EUDBreakIf(b == 0)\n if EUDIfNot()(b.ExactlyX(0x80, 0xC0)):\n n += 1\n EUDEndIf()\n EUDEndInfLoop()\n EUDReturn(n)\n\n @EUDFunc\n def append(pos, src):\n \"\"\"The text at src written at pos, ended by a 0; where that 0 is. A text of the table may be any length, so no\n more of it than leaves the scratch whole: what is past a text's most is cut by make() anyway.\"\"\"\n br1.seekoffset(src)\n bw1.seekoffset(pos)\n if EUDWhile()(pos <= scratch + TEXT_BYTES + TEXT_BYTES):\n b = br1.readbyte()\n EUDBreakIf(b == 0)\n bw1.writebyte(b)\n pos += 1\n EUDEndWhile()\n bw1.writebyte(0)\n EUDReturn(pos)\n\n @EUDFunc\n def make(end):\n \"\"\"The text in the scratch, which ends at `end`, into a block of its own: where it is, the block, its\n length in characters. Past what a text holds it is cut, never inside a character.\"\"\"\n if EUDIf()(end >= scratch + TEXT_BYTES + 1):\n end << scratch + TEXT_BYTES\n if EUDWhile()(f_bread(end).ExactlyX(0x80, 0xC0)):\n end -= 1\n EUDEndWhile()\n f_bwrite(end, 0)\n if EUDIf()(said_cut == 0):\n said_cut << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: a text was cut off - one that is made holds %d bytes.\" % TEXT_BYTES)\n EUDEndIf()\n EUDEndIf()\n need = f_div(end - scratch, 4)[0] + 2\n k = EUDVariable()\n k << 0\n if EUDWhile()(rooms[k] < need):\n k += 1\n EUDEndWhile()\n block = take(k)\n if EUDIf()(block == 0):\n if EUDIf()(said_full == 0):\n said_full << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: out of memory - a text could not be made (the programs' arrays and texts share %d cells).\" % HEAP_CELLS)\n EUDEndIf()\n EUDReturn(empty, 0, 0)\n EUDEndIf()\n cells[block] = k\n f_repmovsd_epd(cells + block + 1, EPD(scratch), need - 1)\n addr = f_mul(cells + block + 1, 4) + 0x58A364\n EUDReturn(addr, block, count(addr))\n\n @EUDFunc\n def copy(addr, block, length):\n \"\"\"A text as something to keep: itself when it owns no block, else a block of the same size with the same cells.\"\"\"\n if EUDIf()(block == 0):\n EUDReturn(addr, 0, length)\n EUDEndIf()\n k = cells[block]\n mine = take(k)\n if EUDIf()(mine == 0):\n if EUDIf()(said_full == 0):\n said_full << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: out of memory - a text could not be copied (the programs' arrays and texts share %d cells).\" % HEAP_CELLS)\n EUDEndIf()\n EUDReturn(empty, 0, 0)\n EUDEndIf()\n f_repmovsd_epd(cells + mine, cells + block, rooms[k])\n EUDReturn(f_mul(cells + mine + 1, 4) + 0x58A364, mine, length)\n\n @EUDFunc\n def release(block):\n if EUDIf()(block >= 1):\n give(block, cells[block])\n EUDEndIf()\n\n @EUDFunc\n def skip(addr, n):\n \"\"\"Where character n of the text at addr starts; its end when it has fewer.\"\"\"\n br1.seekoffset(addr)\n if EUDInfLoop()():\n b = br1.readbyte()\n EUDBreakIf(b == 0)\n if EUDIfNot()(b.ExactlyX(0x80, 0xC0)):\n EUDBreakIf(n == 0)\n n -= 1\n EUDEndIf()\n addr += 1\n EUDEndInfLoop()\n EUDReturn(addr)\n\n @EUDFunc\n def size(addr):\n \"\"\"The bytes of the character that starts at addr; 0 at the text's end.\"\"\"\n b = f_bread(addr)\n n = EUDVariable()\n n << 1\n if EUDIf()(b == 0):\n n << 0\n if EUDElseIf()(b >= 0xF0):\n n << 4\n if EUDElseIf()(b >= 0xE0):\n n << 3\n if EUDElseIf()(b >= 0xC0):\n n << 2\n EUDEndIf()\n EUDReturn(n)\n\n @EUDFunc\n def slice_(addr, start, end):\n \"\"\"Characters start … end - 1 of the text at addr, made.\"\"\"\n a = skip(addr, start)\n z = EUDVariable()\n z << a\n if EUDIf()(end >= start + 1):\n z << skip(a, end - start)\n EUDEndIf()\n # A text of the table may be longer than one that is made: no more of it than the scratch holds, and make() cuts.\n n = z - a\n if EUDIf()(n >= TEXT_BYTES + 2):\n n << TEXT_BYTES + 1\n EUDEndIf()\n f_memcpy(scratch, a, n)\n f_bwrite(scratch + n, 0)\n EUDReturn(*make(scratch + n))\n\n @EUDFunc\n def pad(addr, length, width, fill, at_start):\n \"\"\"The text with `fill` over and over before or after it until it is `width` characters; as it is when it is that long, or `fill` is empty.\"\"\"\n pos = EUDVariable()\n pos << scratch\n need = EUDVariable()\n need << 0\n if EUDIf()([width >= length + 1, width <= 0x7FFFFFFF, f_bread(fill) >= 1]):\n need << width - length\n EUDEndIf()\n if EUDIf()(at_start == 0):\n pos << append(pos, addr)\n EUDEndIf()\n src = EUDVariable()\n src << fill\n if EUDWhile()([need >= 1, pos <= scratch + TEXT_BYTES]):\n n = size(src)\n if EUDIf()(n == 0):\n src << fill\n n << size(src)\n EUDEndIf()\n f_memcpy(pos, src, n)\n pos += n\n src += n\n need -= 1\n EUDEndWhile()\n f_bwrite(pos, 0)\n if EUDIf()([at_start >= 1, pos <= scratch + TEXT_BYTES]):\n pos << append(pos, addr)\n EUDEndIf()\n EUDReturn(*make(pos))\n\n @EUDFunc\n def repeat(addr, times):\n pos = EUDVariable()\n pos << scratch\n f_bwrite(pos, 0)\n if EUDIf()([times <= 0x7FFFFFFF, f_bread(addr) >= 1]):\n if EUDWhile()([times >= 1, pos <= scratch + TEXT_BYTES]):\n pos << append(pos, addr)\n times -= 1\n EUDEndWhile()\n EUDEndIf()\n EUDReturn(*make(pos))\n\n @EUDFunc\n def starts(addr, find):\n \"\"\"Whether the text at addr starts with the one at find.\"\"\"\n br1.seekoffset(addr)\n br2.seekoffset(find)\n if EUDInfLoop()():\n want = br2.readbyte()\n if EUDIf()(want == 0):\n EUDReturn(1)\n EUDEndIf()\n EUDBreakIfNot(br1.readbyte() == want)\n EUDEndInfLoop()\n EUDReturn(0)\n\n @EUDFunc\n def find_from(addr, find, start):\n \"\"\"The place, in characters, of the first match at or after character `start`; 0xFFFFFFFF when there is none.\"\"\"\n if EUDIf()(start >= count(addr) + 1):\n EUDReturn(0xFFFFFFFF)\n EUDEndIf()\n at = skip(addr, start)\n place = EUDVariable()\n place << start\n if EUDInfLoop()():\n if EUDIf()(starts(at, find) >= 1):\n EUDReturn(place)\n EUDEndIf()\n n = size(at)\n EUDBreakIf(n == 0)\n at += n\n place += 1\n EUDEndInfLoop()\n EUDReturn(0xFFFFFFFF)\n\n @EUDFunc\n def ends(addr, find):\n a, b = f_strlen(addr), f_strlen(find)\n if EUDIf()(b >= a + 1):\n EUDReturn(0)\n EUDEndIf()\n if EUDIf()(f_strcmp(addr + (a - b), find) == 0):\n EUDReturn(1)\n EUDEndIf()\n EUDReturn(0)\n\n @EUDFunc\n def code(addr, index):\n \"\"\"The number of character `index`; 0xFFFFFFFF past either end.\"\"\"\n if EUDIf()(index >= 0x80000000):\n EUDReturn(0xFFFFFFFF)\n EUDEndIf()\n at = skip(addr, index)\n n = size(at)\n if EUDIf()(n == 0):\n EUDReturn(0xFFFFFFFF)\n EUDEndIf()\n br1.seekoffset(at)\n v = EUDVariable()\n v << br1.readbyte()\n if EUDIf()(n == 2):\n v << (v & 0x1F)\n if EUDElseIf()(n == 3):\n v << (v & 0x0F)\n if EUDElseIf()(n == 4):\n v << (v & 0x07)\n EUDEndIf()\n if EUDWhile()(n >= 2):\n v << f_mul(v, 64) + (br1.readbyte() & 0x3F)\n n -= 1\n EUDEndWhile()\n EUDReturn(v)\n\n @EUDFunc\n def show(dst, src):\n \"\"\"At most TEXT_FIELD_BYTES bytes of the text at src over the string at dst, never half a character.\"\"\"\n used = EUDVariable()\n used << 0\n if EUDInfLoop()():\n n = size(src)\n EUDBreakIf(n == 0)\n if EUDIf()(used + n >= TEXT_FIELD_BYTES + 1):\n if EUDIf()(said_field == 0):\n said_field << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: a text was cut off - an action's text and a unit's name show %d bytes of one that is made.\" % TEXT_FIELD_BYTES)\n EUDEndIf()\n EUDBreak()\n EUDEndIf()\n f_memcpy(dst + used, src, n)\n used += n\n src += n\n EUDEndInfLoop()\n f_bwrite(dst + used, 0)\n\n @EUDFunc\n def address(id_):\n \"\"\"Where the text of the table with this id is; an empty one for id 0.\"\"\"\n if EUDIf()(id_ == 0):\n EUDReturn(empty)\n EUDEndIf()\n EUDReturn(GetMapStringAddr(id_))\n\n _TEXT.update(scratch=scratch, empty=empty, append=append, count=count, make=make, copy=copy, release=release, slice=slice_, pad=pad, repeat=repeat,\n starts=starts, ends=ends, find=find_from, code=code, show=show, size=size, address=address)\n return _TEXT\n\n\ndef slot(kind):\n \"\"\"The string of the built map's table kept for one kind of field, made the first time one is asked for: its\n room in bytes that nothing else shares, which is what ForceAddString is for.\"\"\"\n if kind not in _SLOTS:\n text = \"(TrigScript: %s)\" % kind\n _SLOTS[kind] = ForceAddString(text + \" \" * (TEXT_FIELD_BYTES - len(text)))\n return _SLOTS[kind]\n\n\n# The actions whose text the game shows from a string it reads again whenever it draws (played 2026-09-19), by the\n# kind of field: a player has one of each at a time, so one string a kind is enough.\nSLOT_OF_ACTION = {12: \"objectives\", 7: \"transmission\"}\nfor _t in (17, 18, 19, 20, 21, 33, 34, 35, 36, 37, 40):\n SLOT_OF_ACTION[_t] = \"leaderboard\"\n\n\ndef text_id(text):\n \"\"\"The id of a text written in the script; 0 for the empty one, which the table does not hold.\"\"\"\n return EncodeString(text) if text else 0\n\n\n_BUFFERS = {}\n\n\ndef buffer(name):\n \"\"\"A buffer of buildText: its bytes and where the writing has got to. One for the whole game, used up within the\n frame it is built in. It may run a part past its most before it is cut, and a part is at most a text's room.\"\"\"\n if name not in _BUFFERS:\n _BUFFERS[name] = (Db(BUILD_BYTES + 1 + TEXT_BYTES + 1 + 8), EUDVariable())\n return _BUFFERS[name]\n\n\n_BUILD = {}\n\n\ndef builds():\n \"\"\"What every buffer shares: copying a text onto the end of one, and cutting one that ran past its room.\"\"\"\n if _BUILD:\n return _BUILD\n said_cut = EUDVariable(0) # initial: said once a game\n\n @EUDFunc\n def append(pos, src, last):\n \"\"\"The text at src written at pos, ended by a 0; where that 0 is. Never past `last`.\"\"\"\n br1.seekoffset(src)\n bw1.seekoffset(pos)\n if EUDWhile()(pos <= last):\n b = br1.readbyte()\n EUDBreakIf(b == 0)\n bw1.writebyte(b)\n pos += 1\n EUDEndWhile()\n bw1.writebyte(0)\n EUDReturn(pos)\n\n @EUDFunc\n def cut(pos, end):\n \"\"\"A buffer whose writing got to `pos`, cut at `end` (never inside a character) when it went past it.\"\"\"\n if EUDIf()(pos > end):\n pos << end\n if EUDWhile()(f_bread(pos).ExactlyX(0x80, 0xC0)):\n pos -= 1\n EUDEndWhile()\n f_bwrite(pos, 0)\n if EUDIf()(said_cut == 0):\n said_cut << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: a text was cut off - one put together piece by piece holds %d bytes.\" % BUILD_BYTES)\n EUDEndIf()\n EUDEndIf()\n EUDReturn(pos)\n\n _BUILD.update(append=append, cut=cut)\n return _BUILD\n\n\nclass TextVal:\n \"\"\"A text as the lowering holds it: where it is, the block it owns (the int 0: none), and its length in\n characters - an int, a variable, or None when nobody has counted yet. `taken`: the block is the value's own, so\n what receives the value keeps the block or gives it back; a variable's is only looked at.\"\"\"\n\n def __init__(self, addr, block, length, taken):\n self.addr, self.block, self._length, self.taken = addr, block, length, taken\n\n def length(self):\n if self._length is None:\n self._length = texts()[\"count\"](self.addr)\n return self._length\n\n\nclass TextStorage:\n \"\"\"A text variable that is made: three cells, or three rows (of twelve in a per-player program, of a task's slots).\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.rowed = rows is not None\n self.player_of = rows\n make = (lambda: cell_array([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no text yet, and no block\n self.addr, self.block, self.len = make(), make(), make()\n\n def cell(self, cell):\n return cell[self.player_of()] if self.rowed else cell\n\n def get(self):\n # A variable that was never given a text is an empty one: its address is 0 until then.\n addr = fresh(self.cell(self.addr))\n if EUDIf()(addr == 0):\n addr << texts()[\"empty\"]\n EUDEndIf()\n return TextVal(addr, self.cell(self.block), self.cell(self.len), False)\n\n def write(self, addr, block, length):\n for cell, value in ((self.addr, addr), (self.block, block), (self.len, length)):\n if self.rowed:\n cell[self.player_of()] = value\n else:\n cell << value\n\n def take_out(self):\n \"\"\"The text, and its block with it: the variable holds no block from here on.\"\"\"\n v = self.get()\n out = TextVal(v.addr, fresh(v.block), fresh(v.length()), True)\n self.write(out.addr, 0, out.length())\n return out\n\n\nclass UnitRef:\n \"\"\"A unit of the game as the lowering holds it: the pointer, its EPD, and the slot's uniqueness byte\n (as it sits in its dword, masked 0xFF00). `uid` None is the unit of a loop's turn, there by\n construction; a pointer that is the int 0 is no unit at all.\"\"\"\n\n def __init__(self, ptr, epd, uid=None):\n self.ptr, self.epd, self.uid = ptr, epd, uid\n\n @property\n def none(self):\n return isinstance(self.ptr, int) and self.ptr == 0\n\n\nNO_UNIT = UnitRef(0, 0, 0)\n\n\nclass UnitStorage:\n \"\"\"A unit variable: three cells, or three rows (12-slot of a per-player program, of a task's slots).\"\"\"\n\n def __init__(self, decl, rows):\n self.decl = decl\n self.rowed = rows is not None\n self.player_of = rows\n make = (lambda: cell_array([0] * rows.count)) if self.rowed else (lambda: EUDVariable(0)) # initial: no unit\n self.ptr, self.epd, self.uid = make(), make(), make()\n\n def get(self):\n if self.rowed:\n p = self.player_of()\n return UnitRef(self.ptr[p], self.epd[p], self.uid[p])\n return UnitRef(self.ptr, self.epd, self.uid)\n\n def set(self, ref):\n uid = ref.uid\n if uid is None:\n # The unit of a loop's turn is there now: this is when its slot's uniqueness byte is taken.\n uid = f_maskread_epd(ref.epd + OFF_UID, 0xFF00)\n for cell, value in ((self.ptr, ref.ptr), (self.epd, ref.epd), (self.uid, uid)):\n if self.rowed:\n cell[self.player_of()] = value\n else:\n cell << value\n\n\nclass LoopUnit:\n \"\"\"The variable of a loop over units: the scan's own pointer, never kept past the loop.\"\"\"\n\n def __init__(self, ref):\n self.ref = ref\n\n def get(self):\n return self.ref\n\n def set(self, ref):\n raise Fail(\"trigscript: the unit of a loop's turn cannot be assigned\")\n\n\ndef saturate(value, bits):\n top = (1 << bits) - 1\n if isinstance(value, int):\n return min(value, top)\n v = EUDVariable()\n v << value\n if EUDIf()(v >= top + 1):\n v << top\n EUDEndIf()\n return v\n\n\n# ── fixed-point numbers (16.16, TrigScript 4's B6): a cell is the value times 65 536, read signed ──\nFIXED_ONE = 1 << 16\nFIXED_PI = 205887 # round(pi * 65536); compiler/fixedMath.ts has the same\nFIXED_HALF_PI = 102944\nDIGIT_TEXTS = Db(b\"0\\x001\\x002\\x003\\x004\\x005\\x006\\x007\\x008\\x009\\x00\") # a digit d's text at 2 * d\nDOT_TEXT = Db(b\".\\0\\0\\0\")\n_FIXED = {}\n\n\ndef fixed_text(v):\n \"\"\"A fixed number known when the map is built, as a program prints it: compiler/fixedMath.ts#fixedText.\"\"\"\n v = signed(v)\n m = abs(v)\n whole, places = m >> 16, ((m & 0xFFFF) * 10000 + 32768) >> 16\n if places == 10000:\n whole, places = whole + 1, 0\n sign = \"-\" if v < 0 and (whole or places) else \"\"\n return sign + str(whole) + (\".\" + (\"%04d\" % places).rstrip(\"0\") if places else \"\")\n\n\ndef magnitudes(a, b):\n \"\"\"a and b made their distances from 0 in place, and 1 when exactly one of them was below zero.\"\"\"\n neg = fresh(0)\n if EUDIf()(a >= SIGN):\n a << 0 - a\n neg << 1\n EUDEndIf()\n if EUDIf()(b >= SIGN):\n b << 0 - b\n neg << 1 - neg\n EUDEndIf()\n return neg\n\n\ndef chain_fdiv(a, b):\n \"\"\"a * 65536 / b, both read from 0 up and b below 2^24: eudplib's division chain (the divisor doubled into each step's\n condition and action, set up once), run for the whole part and then twice more for eight bits of the fraction each:\n what is left is less than b, so 256 times it still fits, and its quotient is below 256. The same digits as sixteen\n steps of a bit each, which cost a dozen triggers a step. `a` is the caller's own copy, used up.\"\"\"\n whole, frac, x = EUDVariable(), EUDVariable(), EUDVariable()\n SeqCompute([(whole, SetTo, 0), (frac, SetTo, 0), (x, SetTo, b)])\n main = [(Forward(), Forward()) for _ in range(32)]\n parts = [[(Forward(), Forward()) for _ in range(8)] for _ in range(2)]\n start = [Forward() for _ in range(32)]\n for i in range(32):\n cells = [main[i]] + ([parts[0][i], parts[1][i]] if i < 8 else [])\n SeqCompute([(EPD(c), SetTo, x) for pair in cells for c in pair])\n # Past 2^31 the next doubling would wrap: the chain starts here.\n p1, p2, p3 = Forward(), Forward(), Forward()\n p1 << RawTrigger(nextptr=p2, conditions=x.AtLeastX(1, 0x80000000), actions=SetNextPtr(p1, p3))\n p3 << RawTrigger(nextptr=start[i], actions=SetNextPtr(p1, p2))\n p2 << NextTrigger()\n SeqCompute([(x, Add, x)])\n\n def steps(chain, top, into, scale):\n for i in range(top, -1, -1):\n c, act = Forward(), Forward()\n t = RawTrigger(conditions=[c << a.AtLeast(0)], actions=[act << a.SubtractNumber(0), into.AddNumber((1 << i) * scale)])\n if chain is main:\n start[i] << t\n chain[i][0] << c + 8\n chain[i][1] << act + 20\n\n steps(main, 31, whole, 1)\n for part, scale in ((parts[0], 256), (parts[1], 1)):\n for _ in range(8):\n a += a\n steps(part, 7, frac, scale)\n return shift_const(whole, \"<<\", 16) + frac\n\n\ndef fixed_fns():\n \"\"\"The fixed numbers' functions, made when the first one is met: one body each in the map, called.\"\"\"\n if \"fdiv\" in _FIXED:\n return _FIXED\n\n @EUDFunc\n def fmul(a, b):\n \"\"\"a * b / 65536 towards zero, wrapping: the four products of the 16-bit halves of the magnitudes. A half is a\n masked copy (the low) or a bit a trigger (the high), where a division by 65 536 was a 32-step loop; each product\n has its smaller side first, which eudplib's goes over the bits of and stops past the highest.\"\"\"\n neg = magnitudes(a, b)\n al, bl = fresh(a), fresh(b)\n DoActions([al.SetNumberX(0, 0xFFFF0000), bl.SetNumberX(0, 0xFFFF0000)])\n ah, bh = shift_const(a, \">>>\", 16), shift_const(b, \">>>\", 16)\n r = shift_const(f_mul(ah, bh), \"<<\", 16)\n r += f_mul(ah, bl)\n r += f_mul(bh, al)\n r += shift_const(f_mul(al, bl), \">>>\", 16)\n if EUDIf()(neg == 1):\n r << 0 - r\n EUDEndIf()\n EUDReturn(r)\n\n @EUDFunc\n def fdiv(a, b):\n \"\"\"a * 65536 / b towards zero, wrapping; a divisor of 0 gives 0. The whole part by eudplib's division, then\n sixteen more bits of the quotient, one a step, from what is left over.\"\"\"\n if EUDIf()(b == 0):\n EUDReturn(0)\n EUDEndIf()\n neg = magnitudes(a, b)\n q = EUDVariable()\n if EUDIf()(b <= (1 << 24) - 1):\n q << chain_fdiv(a, b)\n if EUDElse()():\n # (A divisor of 256 or more: the quotient's sixteen bits past the point one a step, from what is left over.)\n w, r = [fresh(x) for x in f_div(a, b)]\n for _ in range(16):\n r += r\n w += w\n if EUDIf()(r >= b):\n r -= b\n w += 1\n EUDEndIf()\n q << w\n EUDEndIf()\n if EUDIf()(neg == 1):\n q << 0 - q\n EUDEndIf()\n EUDReturn(q)\n\n def root4(x, pairs, zeros):\n \"\"\"root() kept as four times the root found so far, t: the step's trial 4r + 1 is then rem > t, no copy of r\n made a step. Only while 4r fits in 32 bits: sqrt_fixed's root is below 2^24.\"\"\"\n t, rem = fresh(0), fresh(0)\n for i in range(pairs + zeros):\n rem += rem\n rem += rem\n if i < pairs:\n for weight, bit in ((2, 31 - 2 * i), (1, 30 - 2 * i)):\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=rem.AddNumber(weight))\n if EUDIf()(rem > t):\n rem -= t\n rem -= 1\n t += t\n t += 4\n if EUDElse()():\n t += t\n EUDEndIf()\n return shift_const(t, \">>>\", 2)\n\n def root(x, pairs, zeros):\n \"\"\"The largest r with r * r <= x * 4 ** zeros, x read as `pairs` pairs of bits from the top: two bits a step.\"\"\"\n r, rem = fresh(0), fresh(0)\n for i in range(pairs + zeros):\n rem += rem\n rem += rem\n if i < pairs:\n # The next two bits of x, read where they are (x was doubled twice a step to bring them to the top).\n for weight, bit in ((2, 31 - 2 * i), (1, 30 - 2 * i)):\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=rem.AddNumber(weight))\n trial = fresh(r)\n trial += trial\n trial += trial\n trial += 1\n r += r\n if EUDIf()(rem >= trial):\n rem -= trial\n r += 1\n EUDEndIf()\n return r\n\n @EUDFunc\n def sqrt_fixed(x):\n \"\"\"The square root of a fixed number, rounded down: isqrt(x * 65536); 0 below zero.\"\"\"\n if EUDIf()(x >= SIGN):\n EUDReturn(0)\n EUDEndIf()\n EUDReturn(root4(x, 16, 8))\n\n @EUDFunc\n def sqrt_whole(x):\n \"\"\"The square root of a whole number read from 0 up, as a fixed one: isqrt(x * 2 ** 32). From 2 ** 30 up it\n is past what a fixed number holds, and gives the largest.\"\"\"\n if EUDIf()(x >= 1 << 30):\n EUDReturn(0x7FFFFFFF)\n EUDEndIf()\n EUDReturn(root(x, 16, 16))\n\n _FIXED.update(fmul=fmul, fdiv=fdiv, sqrt_fixed=sqrt_fixed, sqrt_whole=sqrt_whole)\n return _FIXED\n\n\n# A trigger's first condition's number, and its first action's destination, number and modifier (a SetDeaths, its\n# modifier the top byte of the dword: 7 SetTo, 8 Add), from the trigger's address: 8 bytes of links, 16 conditions of\n# 20 bytes, then the actions. A cell of our own is a trigger of one action that adds its number to itself: running it\n# doubles the number, and nothing routes it while only the loop it sits in runs it (TrigScript 4, the backend, part 3).\nCOND_VALUE, ACT_DEST, ACT_VALUE, ACT_MODIFIER_DWORD = 16, 344, 348, 352\n\n\n_CONST_FDIV = {}\n\n\ndef const_fdiv(c, x, bound=None):\n \"\"\"fdiv(c, x) — c × 65 536 / x towards zero — for a constant c and a variable x, the very bits fixed_fns()'s fdiv\n gives (compiler/fixedMath.ts#fixedDiv). `bound`: the most |x| can be, when ranges.ts knows. One body for each c\n (and bound) in the map, called.\"\"\"\n c = signed(c & U32)\n if c == 0:\n return 0\n key = (abs(c), bound)\n if key not in _CONST_FDIV:\n _CONST_FDIV[key] = make_const_fdiv(abs(c), bound)\n q = fresh(_CONST_FDIV[key](as_var(x)))\n if c < 0:\n q << 0 - q\n return q\n\n\ndef make_const_fdiv(c, bound):\n \"\"\"The divisor, not the dividend, stays where it is: long division the way it is done on paper, the remainder\n doubled a step and the divisor taken from it when it fits, so the divisor is written into the one comparison and\n the one subtraction of a loop of four triggers, where eudplib's division writes it, doubled, into every step it\n has (each a copy, the cost of all the rest). The dividend c × 65 536 is known: where the divisor's highest bit is\n (a search of comparisons) says how far down the dividend the first step that can take the divisor away is, and\n what the remainder is there (a constant), so only the steps that make a bit of the quotient are run. The quotient\n is counted in a cell with a bit put above it, which reaches the top on the last step: the loop's test of its end.\n Quotients of 2^31 and more (a divisor of a few 65 536ths) are fixed_fns()'s fdiv's.\"\"\"\n d = c << 16\n top_d = d.bit_length() - 1\n t_most = 31 if bound is None else max(0, min(31, bound.bit_length() - 1))\n\n def plan(t):\n \"\"\"For a divisor whose highest bit is t: (steps, the remainder before them, the dividend's bits still to\n come at the top of a cell), or None when the loop cannot (no step: the quotient is 0; too many: wraps).\"\"\"\n n = top_d - t + 1\n if n <= 0:\n return \"zero\"\n if n > 31:\n return None\n return (n, d >> n, ((d & ((1 << n) - 1)) << (32 - n)) & U32)\n\n plans = {t: plan(t) for t in range(t_most + 1)}\n with_bits = any(isinstance(v, tuple) and v[2] for v in plans.values())\n\n @EUDFunc\n def fdiv_by(x):\n out = EUDVariable()\n if EUDIf()(x == 0):\n EUDReturn(0)\n EUDEndIf()\n neg = fresh(0)\n if EUDIf()(x >= SIGN):\n x << 0 - x\n neg << 1\n EUDEndIf()\n # The quotient, the remainder and the dividend's bits still to come: three cells of our own in one trigger, the\n # three numbers doubled by one run of it.\n cells, take_bit, test, end, leave, after = Forward(), Forward(), Forward(), Forward(), Forward(), Forward()\n q, r, bits = cells + ACT_VALUE, cells + ACT_VALUE + 32, cells + ACT_VALUE + 64\n # The divisor, where the loop compares and subtracts.\n SeqCompute([(EPD(test + COND_VALUE), SetTo, x), (EPD(test + ACT_VALUE), SetTo, x)])\n # The quotient's cell back to doubling itself (the last call sent it to `out`).\n DoActions([SetMemory(cells + ACT_DEST, SetTo, EPD(q)),\n SetMemoryX(cells + ACT_MODIFIER_DWORD, SetTo, 8 << 24, 0xFF000000),\n SetNextPtr(cells, take_bit if with_bits else test)])\n how = fresh(0) # 0: the loop, 1: the quotient is 0, 2: fdiv's own way\n\n def leaf(t):\n p = plans[t]\n if p is None:\n DoActions(how.SetNumber(2))\n elif p == \"zero\":\n DoActions(how.SetNumber(1))\n else:\n n, r0, rest = p\n # (The dividend's bits a place lower than where they are tested: the cells double them first.)\n acts = [SetMemory(r, SetTo, r0), SetMemory(q, SetTo, 1 << (31 - n))]\n if with_bits:\n acts.append(SetMemory(bits, SetTo, rest >> 1))\n DoActions(acts)\n\n def tree(lo, hi):\n if lo == hi:\n leaf(lo)\n return\n mid = (lo + hi + 1) // 2\n if EUDIf()(x >= (1 << mid)):\n tree(mid, hi)\n if EUDElse()():\n tree(lo, mid - 1)\n EUDEndIf()\n\n tree(0, t_most)\n if EUDIf()(how == 0):\n cells << RawTrigger(actions=[SetMemory(q, Add, 0), SetMemory(r, Add, 0)] + ([SetMemory(bits, Add, 0)] if with_bits else []))\n if with_bits:\n # The dividend's next bit into the remainder, from the top of its cell.\n take_bit << RawTrigger(conditions=Memory(bits, AtLeast, SIGN), actions=SetMemory(r, Add, 1))\n test << RawTrigger(conditions=Memory(r, AtLeast, 0), actions=[SetMemory(r, Subtract, 0), SetMemory(q, Add, 1)])\n end << RawTrigger(nextptr=cells, conditions=Memory(q, AtLeast, SIGN), actions=SetNextPtr(end, leave))\n # The quotient's cell run once more, its number written into `out`; the bit above it taken off after.\n leave << RawTrigger(nextptr=cells, actions=[\n SetNextPtr(end, cells),\n SetMemory(cells + ACT_DEST, SetTo, EPD(out.getValueAddr())),\n SetMemoryX(cells + ACT_MODIFIER_DWORD, SetTo, 7 << 24, 0xFF000000),\n SetNextPtr(cells, after),\n ])\n after << RawTrigger(actions=out.AddNumber(SIGN))\n if EUDElseIf()(how == 1):\n out << 0\n if EUDElse()():\n out << fixed_fns()[\"fdiv\"](c, x)\n EUDEndIf()\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n return fdiv_by\n\n\n_FIXED_BY_CONST = {}\n\n\ndef fixed_by_const(name, x, c, bound=None):\n \"\"\"fmul(x, c) or fdiv(x, c) of a variable x and a constant c: the very bits fixed_fns()'s give\n (compiler/fixedMath.ts), from comparisons with constants and constants added — nothing copied. `bound`: the most\n |x| can be, when ranges.ts knows. One body for each constant (and bound) in the map, called.\"\"\"\n c = signed(c & U32)\n if c == 0:\n return 0\n key = (name, abs(c), bound)\n if key not in _FIXED_BY_CONST:\n _FIXED_BY_CONST[key] = (make_fmul_by if name == \"fmul\" else make_fdiv_by_const)(abs(c), 31 if bound is None else max(0, min(31, bound.bit_length() - 1)))\n q = fresh(_FIXED_BY_CONST[key](as_var(x)))\n if c < 0:\n q << 0 - q\n return q\n\n\ndef magnitude_of(x):\n \"\"\"x made its distance from 0 in place; 1 when it was below 0.\"\"\"\n neg = fresh(0)\n if EUDIf()(x >= SIGN):\n x << 0 - x\n neg << 1\n EUDEndIf()\n return neg\n\n\ndef make_fmul_by(c, top):\n \"\"\"x × c / 65 536 towards zero: each bit of |x| adds c times its place, split at the point — the whole part added\n into the result and the part below it gathered apart, whose whole part is added at the end (so the result is the\n product's, not a sum of rounded parts). `top`: the highest bit |x| can have.\"\"\"\n parts = [((c << j) >> 16, (c << j) & 0xFFFF) for j in range(top + 1)]\n low_bits = sum(lo for _, lo in parts).bit_length()\n\n @EUDFunc\n def fmul_by(x):\n neg = magnitude_of(x)\n out, low = fresh(0), fresh(0)\n for j, (hi, lo) in enumerate(parts):\n acts = ([out.AddNumber(hi & U32)] if hi & U32 else []) + ([low.AddNumber(lo)] if lo else [])\n if acts:\n RawTrigger(conditions=x.AtLeastX(1, 1 << j), actions=acts)\n for k in range(16, low_bits):\n RawTrigger(conditions=low.AtLeastX(1, 1 << k), actions=out.AddNumber(1 << (k - 16)))\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n return fmul_by\n\n\ndef make_fdiv_by_const(c, top):\n \"\"\"x × 65 536 / c towards zero: long division with the divisor doubled into each step when the map is built, so a\n step is one comparison with a constant. The whole part's quotient bits go straight to their place 16 higher; the\n remainder, below c, is then moved up as far as stays under 2^32 (a bit tested, its new place added) and divided\n again, until the sixteen bits below the point are found. `top`: the highest bit |x| can have.\"\"\"\n c_top = c.bit_length() - 1\n chunk = max(1, 31 - c_top)\n\n @EUDFunc\n def fdiv_by_const(x):\n neg = magnitude_of(x)\n out = fresh(0)\n # The whole part: steps from the highest multiple of c that |x| can reach.\n for i in range(min(31 - c_top, top - c_top) if top >= c_top else -1, -1, -1):\n add = (1 << (i + 16)) & U32\n RawTrigger(conditions=x.AtLeast(c << i), actions=[x.SubtractNumber(c << i)] + ([out.AddNumber(add)] if add else []))\n # Below the point: x < c now.\n done = 0\n while done < 16:\n k = min(chunk, 16 - done)\n for bit in range(c_top, -1, -1): # x <<= k, from its top bit down (each lands where nothing is yet)\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=x.AddNumber(((1 << (bit + k)) - (1 << bit)) & U32))\n done += k\n for i in range(k - 1, -1, -1):\n RawTrigger(conditions=x.AtLeast(c << i), actions=[x.SubtractNumber(c << i), out.AddNumber(1 << (i + 16 - done))])\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n return fdiv_by_const\n\n\ndef fixed_vars():\n \"\"\"fmul and fdiv of two variables (TrigScript 4, the backend, part 3): loops over cells of our own, the other side\n written once into the step that adds or subtracts it, where eudplib's ways write it, shifted, into every step.\n One body each in the map, called.\"\"\"\n if \"fmul_vars\" in _FIXED:\n return _FIXED\n\n def take_out(cells, out, after, k=0):\n \"\"\"The actions that send the cell `k` of `cells` (its first, by default) into `out` when the trigger runs next,\n and go on to `after`; the cell is put back to doubling itself when its loop starts again.\"\"\"\n return [SetMemory(cells + ACT_DEST + 32 * k, SetTo, EPD(out.getValueAddr())),\n SetMemoryX(cells + ACT_MODIFIER_DWORD + 32 * k, SetTo, 7 << 24, 0xFF000000),\n SetNextPtr(cells, after)]\n\n def put_back(cells, nxt):\n \"\"\"The cell (the first action from `cells`, an action's offset from the trigger added) back to doubling itself;\n with `nxt`, the trigger going on to it.\"\"\"\n return [SetMemory(cells + ACT_DEST, SetTo, EPD(cells + ACT_VALUE)),\n SetMemoryX(cells + ACT_MODIFIER_DWORD, SetTo, 8 << 24, 0xFF000000)] + ([SetNextPtr(cells, nxt)] if nxt is not None else [])\n\n @EUDFunc\n def fmul_vars(a, b):\n \"\"\"a × b / 65 536 towards zero. With the 16-bit halves of the magnitudes, a × b / 65 536 is ah × b + al × bh\n plus al × bl / 65 536: one loop of sixteen steps reads a bit of ah and the same bit of al (a doubled a step,\n its bits 31 and 15 tested), and doubles two sums — w gathering ah × b + al × bh, z gathering al × bl, whose\n bits from 16 up are added to w at the end.\"\"\"\n neg = magnitudes(a, b)\n bh = shift_const(b, \">>>\", 16)\n bl = fresh(b)\n DoActions(bl.SetNumberX(0, 0xFFFF0000))\n out, count = EUDVariable(), EUDVariable()\n cells, high, low, end, leave, after = (Forward() for _ in range(6))\n w, z, x = cells + ACT_VALUE, cells + ACT_VALUE + 32, cells + ACT_VALUE + 64\n SeqCompute([(EPD(high + ACT_VALUE), SetTo, b), (EPD(low + ACT_VALUE), SetTo, bh), (EPD(low + ACT_VALUE + 32), SetTo, bl), (EPD(x), SetTo, a)])\n DoActions(put_back(cells, high) + [SetMemory(w, SetTo, 0), SetMemory(z, SetTo, 0), count.SetNumber(15)])\n # A step: the bits added, then (but on the last step) the sums and a doubled.\n high << RawTrigger(conditions=Memory(x, AtLeast, SIGN), actions=SetMemory(w, Add, 0))\n low << RawTrigger(conditions=MemoryX(x, AtLeast, 1, 0x8000), actions=[SetMemory(w, Add, 0), SetMemory(z, Add, 0)])\n end << RawTrigger(conditions=count.Exactly(0), actions=SetNextPtr(end, leave))\n cells << RawTrigger(nextptr=high, actions=[SetMemory(w, Add, 0), SetMemory(z, Add, 0), SetMemory(x, Add, 0), count.SubtractNumber(1)])\n leave << RawTrigger(actions=SetNextPtr(end, cells))\n for k in range(16, 32):\n RawTrigger(conditions=MemoryX(z, AtLeast, 1, 1 << k), actions=SetMemory(w, Add, 1 << (k - 16)))\n RawTrigger(nextptr=cells, actions=take_out(cells, out, after))\n after << NextTrigger()\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n @EUDFunc\n def fdiv_vars(a, b):\n \"\"\"a × 65 536 / b towards zero; a divisor of 0 gives 0. Long division the way it is done on paper: the dividend's\n bits shifted into the remainder from the top, the divisor taken away when it fits, the quotient doubled a step\n (wrapping, as fdiv's does). The divisor is written into the one comparison and the one subtraction of the loop.\n The dividend's leading zeros first: the loop's cells run on their own until its top bit is in place, each run\n a step with nothing in it (the quotient and remainder are 0, and stay so).\"\"\"\n if EUDIf()([b == 0]):\n EUDReturn(0)\n EUDEndIf()\n if EUDIf()([a == 0]):\n EUDReturn(0)\n EUDEndIf()\n neg = magnitudes(a, b)\n out, count = EUDVariable(), EUDVariable()\n if EUDIf()(a >= SIGN):\n # (2^31, the magnitude of -2^31 only: its top bit is past where the loop starts.)\n # (fdiv's own way reads it as -2^31, and b as -2^31 too when it is 2^31: the quotient of the magnitudes is\n # its quotient, negated when only a read below zero.)\n out << fixed_fns()[\"fdiv\"](a, b)\n if EUDIf()(b <= 0x7FFFFFFF):\n out << 0 - out\n EUDEndIf()\n if EUDElse()():\n cells, ready, take_bit, test, end, leave, after = (Forward() for _ in range(7))\n q, r, d = cells + ACT_VALUE, cells + ACT_VALUE + 32, cells + ACT_VALUE + 64\n SeqCompute([(EPD(test + COND_VALUE), SetTo, b), (EPD(test + ACT_VALUE), SetTo, b), (EPD(d), SetTo, a)])\n # 47 steps: the dividend's bits 30 … 0 and sixteen 0s.\n DoActions(put_back(cells, ready) + [SetMemory(q, SetTo, 0), SetMemory(r, SetTo, 0), count.SetNumber(47)])\n # Until the dividend's top bit is at bit 30: then a run of the cells brings it to 31, where it is taken.\n ready << RawTrigger(nextptr=cells, conditions=Memory(d, AtLeast, 1 << 30), actions=SetNextPtr(cells, take_bit))\n cells << RawTrigger(nextptr=ready, actions=[SetMemory(q, Add, 0), SetMemory(r, Add, 0), SetMemory(d, Add, 0), count.SubtractNumber(1)])\n take_bit << RawTrigger(conditions=Memory(d, AtLeast, SIGN), actions=SetMemory(r, Add, 1))\n test << RawTrigger(conditions=Memory(r, AtLeast, 0), actions=[SetMemory(r, Subtract, 0), SetMemory(q, Add, 1)])\n end << RawTrigger(nextptr=cells, conditions=count.Exactly(0), actions=SetNextPtr(end, leave))\n leave << RawTrigger(nextptr=cells, actions=[SetNextPtr(end, cells)] + take_out(cells, out, after))\n after << NextTrigger()\n EUDEndIf()\n if EUDIf()(neg == 1):\n out << 0 - out\n EUDEndIf()\n EUDReturn(out)\n\n @EUDFunc\n def udivmod_vars(a, b):\n \"\"\"a // b and a % b of two numbers read from 0 up, b not 0: fdiv_vars' loop with no bits past the point, the\n quotient and the remainder each sent out by its cell's own action. A dividend from 2^31 up (its top bit is past\n where the loop starts) is eudplib's division.\"\"\"\n q_out, r_out, count = EUDVariable(), EUDVariable(), EUDVariable()\n if EUDIf()(a >= SIGN):\n q_, r_ = f_div(a, b)\n SeqCompute([(q_out, SetTo, q_), (r_out, SetTo, r_)])\n if EUDElseIf()(a == 0):\n # (No top bit for the loop's start to wait for.)\n DoActions([q_out.SetNumber(0), r_out.SetNumber(0)])\n if EUDElse()():\n cells, ready, take_bit, test, end, leave, after = (Forward() for _ in range(7))\n q, r, d = cells + ACT_VALUE, cells + ACT_VALUE + 32, cells + ACT_VALUE + 64\n SeqCompute([(EPD(test + COND_VALUE), SetTo, b), (EPD(test + ACT_VALUE), SetTo, b), (EPD(d), SetTo, a)])\n # 31 steps: the dividend's bits 30 … 0, its leading zeros run off by the cells alone.\n DoActions(put_back(cells, ready) + put_back(cells + 32, None) + [SetMemory(q, SetTo, 0), SetMemory(r, SetTo, 0), count.SetNumber(31)])\n ready << RawTrigger(nextptr=cells, conditions=Memory(d, AtLeast, 1 << 30), actions=SetNextPtr(cells, take_bit))\n cells << RawTrigger(nextptr=ready, actions=[SetMemory(q, Add, 0), SetMemory(r, Add, 0), SetMemory(d, Add, 0), count.SubtractNumber(1)])\n take_bit << RawTrigger(conditions=Memory(d, AtLeast, SIGN), actions=SetMemory(r, Add, 1))\n test << RawTrigger(conditions=Memory(r, AtLeast, 0), actions=[SetMemory(r, Subtract, 0), SetMemory(q, Add, 1)])\n end << RawTrigger(nextptr=cells, conditions=count.Exactly(0), actions=SetNextPtr(end, leave))\n # The quotient's cell into q_out and the remainder's into r_out, by one run of the cells (another would\n # double the one not sent out yet).\n leave << RawTrigger(nextptr=cells, actions=[SetNextPtr(end, cells)] + take_out(cells, q_out, after) + take_out(cells, r_out, after, 1)[:2])\n after << NextTrigger()\n EUDEndIf()\n EUDReturn(q_out, r_out)\n\n _FIXED.update(fmul_vars=fmul_vars, fdiv_vars=fdiv_vars, udivmod_vars=udivmod_vars)\n return _FIXED\n\n\ndef divmod_vars(a, b, unsigned):\n \"\"\"(a / b, a % b) of two variables, b not 0: read from 0 up, or (`unsigned` false) towards zero, the remainder with\n the dividend's sign — udivmod_vars of the magnitudes, the signs after.\"\"\"\n f = fixed_vars()[\"udivmod_vars\"]\n if unsigned:\n return [fresh(x) for x in f(a, b)]\n x, y = fresh(a), fresh(b)\n neg_x, neg_q = fresh(0), fresh(0)\n if EUDIf()(x >= SIGN):\n x << 0 - x\n DoActions([neg_x.SetNumber(1), neg_q.SetNumber(1)])\n EUDEndIf()\n if EUDIf()(y >= SIGN):\n y << 0 - y\n neg_q << 1 - neg_q\n EUDEndIf()\n q, r = [fresh(v) for v in f(x, y)]\n if EUDIf()(neg_q == 1):\n q << 0 - q\n EUDEndIf()\n if EUDIf()(neg_x == 1):\n r << 0 - r\n EUDEndIf()\n return q, r\n\n\ndef as_var(value):\n if isinstance(value, int):\n return EUDVariable(value & U32) # initial: a constant, never written\n return value\n\n\ndef fresh(value=0):\n \"\"\"A temporary that starts from `value` every time the code runs. `EUDVariable(n)` is not\n that: n is the cell's value when the map loads, so a temporary built that way and then\n written keeps what the last run left in it — `ticks + 1` went 1, 2, 4, 8 (found in the\n first played probe, 2026-09-18). Anything the lowering writes to starts here.\"\"\"\n v = EUDVariable()\n v << value\n return v\n\n\nclass Coroutine:\n \"\"\"Where a coroutine is: the label it goes on from (0 its start, DONE once it ended) and the frames it still waits,\n a cell each, a row of twelve in a per-player program, a row a slot in a task's body. The program is one; each branch\n of a Promise.all / race is another, which runs inside the frame of what waits at that statement; a task is one too.\n `task`: the coroutine is a task's own, which a cancel() may have ended while it ran: parking leaves an end an end.\"\"\"\n\n def __init__(self, lowering, rows, initial=0, task=False):\n self.lowering = lowering\n self.state = RowCell(rows, initial)\n self.wait = RowCell(rows, 0)\n self.task = task\n self.resumes = [] # (index, Forward) for every place it can go on from\n self.frame_end = None\n\n def get_state(self):\n return self.state.get()\n\n def set_state(self, value):\n self.state.set(value)\n\n def get_wait(self):\n return self.wait.get()\n\n def set_wait(self, value):\n self.wait.set(value)\n\n def park(self, index, wait):\n \"\"\"Stop here for this frame, to go on from `index` once `wait` more frames have passed.\"\"\"\n if self.task:\n if EUDIfNot()(as_var(self.get_state()) == DONE):\n self.set_state(index)\n EUDEndIf()\n else:\n self.set_state(index)\n self.set_wait(wait)\n EUDJump(self.frame_end)\n\n def resume_point(self):\n \"\"\"A place to go on from: its number, which the state takes, and its label, to be placed where it is.\"\"\"\n index = len(self.resumes) + 1\n label = Forward()\n self.resumes.append((index, label))\n return index, label\n\n\nclass Pool:\n \"\"\"A `start` statement's tasks: `max` slots, each a row of the cells of the task's body - its coroutine's place\n (DONE: the slot is free), the generation of the task in it, the frame it last had its turn in. `slot` is the slot\n being run or started, `row` its row among the cells (the player's rows first in a per-player program, from `base`).\"\"\"\n\n def __init__(self, lowering, st):\n self.st = st\n self.task = int(st[\"task\"])\n self.max = int(st[\"max\"])\n self.slot = EUDVariable(0) # initial: the pool's own state\n self.row = EUDVariable(0) # initial: the pool's own state\n self.base = EUDVariable(0) # initial: the pool's own state\n self.said = EUDVariable(0) # initial: nothing said yet\n self.rows = Rows(lowering, lowering.per_player, self)\n self.co = Coroutine(lowering, self.rows, DONE, task=True)\n self.gen = RowCell(self.rows, 0)\n self.ran = RowCell(self.rows, 0)\n # How many of its slots are running (the player's, in a per-player program), and that count where the tasks'\n # turn can test it every time round: the slots after the last running one are not looked at.\n self.live = RowCell(Rows(lowering, True, None) if lowering.per_player else None, 0)\n self.count = self.live.store if not lowering.per_player else EUDVariable(0) # initial: the pool's own state\n # A listener's start (on.created, ...): set when one of its tasks is started and has not had its first turn, so\n # that without it and with nothing heard the tasks' turn passes the whole start by.\n self.fresh = RowCell(Rows(lowering, True, None) if lowering.per_player else None, 0) if st.get(\"listen\") is not None else None\n\n def add(self, n):\n \"\"\"One more task running (1) or one fewer (-1).\"\"\"\n if self.count is self.live.store:\n self.count += n\n return\n self.live.set(as_var(self.live.get()) + n)\n self.count += n\n\n def select(self, i):\n self.slot << i\n self.row << self.base + i\n\n def at(self, cell, slot):\n \"\"\"A cell of the slot `slot`, without selecting it: the one being run stays where it is.\"\"\"\n return cell.store[self.base + slot]\n\n\nclass SlotTable(EUDObject):\n \"\"\"A dword for each of the game's 1700 unit slots, 336 bytes apart as the slots are: a unit's own EPD plus one number is\n its cell, as eudplib's EUDLoopNewUnit keeps its marks. The space between the cells is left for anything else.\"\"\"\n\n def GetDataSize(self):\n return 4 + UNIT_SIZE * (UNIT_SLOTS - 1)\n\n def WritePayload(self, buf):\n buf.WriteDword(0)\n for _ in range(UNIT_SLOTS - 1):\n buf.WriteSpace(UNIT_SIZE - 4)\n buf.WriteDword(0)\n\n\nclass Events:\n \"\"\"The frame's events pass (on.created / on.died), before the programs' turns: the units that came onto the map since\n the pass before, and the ones that died. A slot's cell of `seen` holds the uniqueness byte of the unit seen in it (as\n it sits in its dword, masked 0xFF00) and, once that unit's death has been handed on, one more: so a unit is new when\n its slot's cell is not its byte, and a death is handed on once, and only for a unit that was seen on the map. Each\n list on the map is walked from its head to the record that was behind the head at the pass before, the units linked\n in since lying in between; when that record has left the list the walk goes to the end, which costs more and misses\n nothing. The dying list is walked whole, both ways from the record its address gives: in 1.16.1 that is the list's\n head, but Remastered links its dying list the other way round and hands an EUD map its tail there (its own code\n links a dying unit in behind the other end, which no map can read), so only a walk both ways sees every record in\n both. A record stays on it only until its sprite is gone. Nothing of the game is written.\"\"\"\n\n def __init__(self, created, died, ordered=False, morphed=False):\n self.created, self.died, self.ordered, self.morphed = created, died, ordered, morphed\n self.seen = SlotTable()\n # on.order / on.morphed: each slot's order (masked 0xFF00, as it sits in its dword) and type as the pass before found\n # them, and this frame's units that changed, each with what it was.\n self.orders = SlotTable() if ordered else None\n self.types = SlotTable() if morphed else None\n self.turned, self.turned_was = (EUDArray(EVENT_CAP), EUDArray(EVENT_CAP)) if ordered else (None, None)\n self.turned_n = EUDVariable(0) # initial: the pass's own state, set at its start\n self.changed, self.changed_was = (EUDArray(EVENT_CAP), EUDArray(EVENT_CAP)) if morphed else (None, None)\n self.changed_n = EUDVariable(0) # initial: the pass's own state, set at its start\n # Each a unit's pointer, which f_cunitepdread_epd reads back cheaply with its EPD: within the frame a slot is not\n # given to another unit (that takes a dying pass), so its uniqueness byte need not be kept.\n self.made = EUDArray(EVENT_CAP) if created else None\n self.made_n = EUDVariable(0) # initial: the pass's own state, set at its start\n self.gone = EUDArray(EVENT_CAP) if died else None\n self.gone_n = EUDVariable(0) # initial: the pass's own state, set at its start\n # For each list, as the pass before left it: the record behind the head (pointer, EPD, uniqueness byte) and the\n # head (pointer, EPD). Nothing is linked in but behind the head, so with both the same nothing came in since.\n self.bounds = {VISIBLE_HEAD: [EUDVariable(0) for _ in range(5)]} # initial: no pass before\n\n def cell(self, epd, table=None):\n return epd + (EPD(table or self.seen) - EPD(UNIT_TABLE))\n\n def walk(self, head, step):\n \"\"\"`step(ptr, epd, uid, cell, stop)` for each record from the head to the one behind the head at the pass before.\"\"\"\n bptr, bepd, buid, hptr, hepd = self.bounds[head]\n top, done, stop = Forward(), Forward(), Forward()\n # Nothing linked in since the pass before: the same head, the same record behind it (or none).\n walk = Forward()\n EUDJumpIfNot(Memory(head, Exactly, hptr), walk)\n EUDJumpIf(hptr == 0, stop)\n if EUDIf()(bptr == 0):\n EUDJumpIf(MemoryEPD(hepd + 1, Exactly, 0), stop)\n if EUDElse()():\n EUDJumpIf([MemoryEPD(hepd + 1, Exactly, bptr), MemoryXEPD(bepd + OFF_UID, Exactly, buid, 0xFF00)], stop)\n EUDEndIf()\n walk << NextTrigger()\n ptr, epd = f_cunitepdread_epd(EPD(head))\n first = fresh(1)\n top << NextTrigger()\n EUDJumpIf(ptr == 0, done)\n uid = f_maskread_epd(epd + OFF_UID, 0xFF00)\n EUDJumpIf([first == 0, ptr == bptr, uid == buid], done)\n first << 0\n step(ptr, epd, uid, self.cell(epd), stop)\n f_cunitepdread_epd(epd + 1, ret=[ptr, epd])\n EUDJump(top)\n done << NextTrigger()\n # What is behind the head now is where the next pass stops.\n f_cunitepdread_epd(EPD(head), ret=[hptr, hepd])\n bptr << 0\n if EUDIf()(hptr >= 1):\n f_cunitepdread_epd(hepd + 1, ret=[bptr, bepd])\n if EUDIf()(bptr >= 1):\n buid << f_maskread_epd(bepd + OFF_UID, 0xFF00)\n EUDEndIf()\n EUDEndIf()\n stop << NextTrigger()\n\n def walk_all(self, head, step):\n \"\"\"`step(ptr, epd, uid, cell, stop)` for every record of a list: the one at its address, those after it (+4), then\n those before it (+0).\"\"\"\n top, done, stop = Forward(), Forward(), Forward()\n EUDJumpIf(Memory(head, Exactly, 0), stop)\n ptr, epd = f_cunitepdread_epd(EPD(head))\n sepd = EUDVariable()\n sepd << epd\n link = fresh(1)\n top << NextTrigger()\n EUDJumpIf(ptr == 0, done)\n uid = f_maskread_epd(epd + OFF_UID, 0xFF00)\n step(ptr, epd, uid, self.cell(epd), stop)\n if EUDIf()(link == 1):\n f_cunitepdread_epd(epd + 1, ret=[ptr, epd])\n if EUDElse()():\n f_cunitepdread_epd(epd, ret=[ptr, epd])\n EUDEndIf()\n EUDJump(top)\n done << NextTrigger()\n # The way after it is done: now the way before it, from the record before the first.\n EUDJumpIf(link == 0, stop)\n link << 0\n f_cunitepdread_epd(sepd, ret=[ptr, epd])\n EUDJump(top)\n stop << NextTrigger()\n\n def watch(self):\n \"\"\"on.order / on.morphed: every unit on the map, its order and its type against what the pass before found. A unit\n not seen before (it comes on in this pass) only has them kept; the units that came on are taken after this. The\n type is read as its low byte (there are 228). Whether the unit was seen is asked only when something changed.\"\"\"\n top, done = Forward(), Forward()\n ptr, epd = f_cunitepdread_epd(EPD(VISIBLE_HEAD))\n top << NextTrigger()\n EUDJumpIf(ptr == 0, done)\n for table, buf, was, n, off, mask in ((self.orders, self.turned, self.turned_was, self.turned_n, OFF_OWNER_ORDER, 0xFF00),\n (self.types, self.changed, self.changed_was, self.changed_n, OFF_TYPE, 0xFF)):\n if table is None:\n continue\n now = f_maskread_epd(epd + off, mask)\n cell = self.cell(epd, table)\n if EUDIfNot()(MemoryEPD(cell, Exactly, now)):\n if EUDIf()([MemoryXEPD(epd + OFF_UID, Exactly, f_dwread_epd(self.cell(epd)), 0xFF00), n < EVENT_CAP]):\n buf[n] = ptr\n old = f_dwread_epd(cell)\n was[n] = f_bitrshift(old, 8) if mask == 0xFF00 else old\n n += 1\n EUDEndIf()\n f_dwwrite_epd(cell, now)\n EUDEndIf()\n f_cunitepdread_epd(epd + 1, ret=[ptr, epd])\n EUDJump(top)\n done << NextTrigger()\n\n def run(self):\n self.made_n << 0\n self.gone_n << 0\n self.turned_n << 0\n self.changed_n << 0\n if self.ordered or self.morphed:\n self.watch()\n\n def came(ptr, epd, uid, cell, stop):\n if EUDIfNot()(MemoryEPD(cell, Exactly, uid)):\n EUDJumpIf(self.made_n >= EVENT_CAP, stop)\n f_dwwrite_epd(cell, uid)\n if self.created:\n self.made[self.made_n] = ptr\n self.made_n += 1\n EUDEndIf()\n self.walk(VISIBLE_HEAD, came)\n if self.died:\n def went(ptr, epd, uid, cell, stop):\n if EUDIf()(MemoryEPD(cell, Exactly, uid)):\n EUDJumpIf(self.gone_n >= EVENT_CAP, stop)\n f_dwwrite_epd(cell, uid + 1)\n self.gone[self.gone_n] = ptr\n self.gone_n += 1\n EUDEndIf()\n self.walk_all(DYING_HEAD, went)\n\n\ndef uses_source(node, which):\n \"\"\"Whether a piece of the IR has a loop over the events pass's units of that kind.\"\"\"\n if isinstance(node, list):\n return any(uses_source(x, which) for x in node)\n if isinstance(node, dict):\n if node.get(\"kind\") == \"unitLoop\" and node.get(\"source\") == which:\n return True\n return any(uses_source(v, which) for v in node.values() if isinstance(v, (list, dict)))\n return False\n\n\ndef mentions(node, kind):\n \"\"\"Whether a node of that kind is anywhere in a piece of the IR.\"\"\"\n if isinstance(node, list):\n return any(mentions(x, kind) for x in node)\n if not isinstance(node, dict):\n return False\n if node.get(\"kind\") == kind:\n return True\n return any(mentions(v, kind) for v in node.values() if isinstance(v, (dict, list)))\n\n\ndef called_ids(node, out):\n \"\"\"The ids of the functions a piece of the IR calls.\"\"\"\n if isinstance(node, list):\n for x in node:\n called_ids(x, out)\n elif isinstance(node, dict):\n if node.get(\"kind\") in (\"call\", \"textCall\") and isinstance(node.get(\"call\"), dict) and node[\"call\"].get(\"fn\") is not None:\n out.add(node[\"call\"][\"fn\"])\n for v in node.values():\n if isinstance(v, (dict, list)):\n called_ids(v, out)\n return out\n\n\ndef throwing_functions(program):\n \"\"\"The called functions that may come back with an error in flight (B4): one that throws, or calls itself (its stack may\n run out), or calls one that may. Each call of one is followed by a look at the error in flight.\"\"\"\n if not program.get(\"errors\"):\n return set()\n fns = {f[\"id\"]: f for f in program.get(\"functions\", [])}\n out = {i for i, f in fns.items() if f.get(\"recursive\") or mentions(f[\"body\"], \"throw\")}\n callees = {i: called_ids(f[\"body\"], set()) for i, f in fns.items()}\n grew = True\n while grew:\n grew = False\n for i in fns:\n if i not in out and callees[i] & out:\n out.add(i)\n grew = True\n return out\n\n\ndef collect_decls(node, out):\n \"\"\"Every variable declaration in a piece of the IR, by id: what a `declare` and a loop declare, a call's\n parameters and result, a function's.\"\"\"\n if isinstance(node, list):\n for x in node:\n collect_decls(x, out)\n return\n if not isinstance(node, dict):\n return\n if isinstance(node.get(\"id\"), str) and node.get(\"kind\") in (\"number\", \"boolean\", \"unit\", \"text\") and \"shared\" in node:\n out.setdefault(node[\"id\"], node)\n for v in node.values():\n if isinstance(v, (list, dict)):\n collect_decls(v, out)\n\n\ndef mentions_array(e, array):\n \"\"\"Whether an expression reads the array anywhere in it.\"\"\"\n if isinstance(e, dict):\n if e.get(\"array\") == array:\n return True\n return any(mentions_array(x, array) for x in e.values())\n if isinstance(e, list):\n return any(mentions_array(x, array) for x in e)\n return False\n\n\ndef mentions_var(e, id_):\n \"\"\"Whether an expression reads the variable anywhere in it.\"\"\"\n if isinstance(e, dict):\n if e.get(\"kind\") == \"var\" and e.get(\"id\") == id_:\n return True\n return any(mentions_var(x, id_) for x in e.values())\n if isinstance(e, list):\n return any(mentions_var(x, id_) for x in e)\n return False\n\n\n# Expressions that read and do nothing else: worked out at any point of a statement, they give the same answer.\nPLAIN_KINDS = {\"const\", \"var\", \"cast\", \"unary\", \"binary\", \"element\", \"length\"}\n\n\ndef plain(e):\n if not isinstance(e, dict) or \"kind\" not in e:\n return True # (not an expression: a number, a position)\n if e.get(\"kind\") not in PLAIN_KINDS:\n return False\n return all(plain(v) for k, v in e.items() if isinstance(v, dict))\n\n\nclass Lowering:\n def __init__(self, program):\n self.p = program\n self.per_player = bool(program.get(\"perPlayer\"))\n self.owner = int(program.get(\"owner\", 0))\n self.slots = owner_slots(program)\n self.player = None\n self.vars = {}\n # Every variable of the program by id, and those made before their declaration was lowered: a function is\n # lowered where it is first called, which may come before a variable it uses is declared (a function kept\n # as a value, B3, is lowered with every other of its dispatcher's).\n self.decls = {}\n collect_decls(program, self.decls)\n self.early = set()\n # The `start` statements' tasks, by their number, in that order; the one whose body is being lowered.\n starts = []\n each_start([program.get(\"body\", []), program.get(\"functions\", [])], starts.append)\n self.pools = {}\n self.pool = None\n for st in sorted(starts, key=lambda st: int(st[\"task\"])):\n self.pools[int(st[\"task\"])] = Pool(self, st)\n self.tick = EUDVariable(0) # initial: frames the tasks have run\n self.again = EUDVariable(0) # initial: no task started during the tasks' turn\n self.arrays = {}\n for a in program.get(\"arrays\", []):\n if a.get(\"slice\"):\n self.arrays[a[\"id\"]] = SliceStorage(a, self)\n elif a.get(\"through\"):\n self.arrays[a[\"id\"]] = InnerListStorage(a, self)\n else:\n self.arrays[a[\"id\"]] = (ListStorage if a.get(\"dynamic\") else ArrayStorage)(a, self.rows_of(a))\n # Counters whose offset into the arrays they index is kept beside them (`Place`), by id; a growing array read\n # at one keeps where its block starts.\n self.places = {}\n # Inside a loop over a growing array at a counter, the arrays read there unchecked: index id → {array id: True,\n # or the flag the loop set on the way in}.\n self.inside = {}\n # The loops whose accesses at their counter go through registers (`Cursors`), by the counter's id.\n self.cursors = {}\n counters, saved = find_counters(program), saved_arrays(program)\n\n def find_places(node):\n if isinstance(node, list):\n for x in node:\n find_places(x)\n elif isinstance(node, dict):\n idx = node.get(\"index\")\n if node.get(\"kind\") in (\"element\", \"store\") and isinstance(idx, dict) and idx.get(\"kind\") == \"var\" and idx.get(\"id\") in counters:\n d, a = self.decls.get(idx[\"id\"]), self.arrays.get(node.get(\"array\"))\n if d is not None and self.rows_of(d) is None:\n if type(a) is ListStorage and node[\"array\"] not in saved:\n a.keep_base()\n if a is not None and getattr(a, \"placeable\", lambda: False)() and idx[\"id\"] not in self.places:\n self.places[idx[\"id\"]] = Place()\n for v in node.values():\n if isinstance(v, (list, dict)):\n find_places(v)\n\n find_places([program.get(\"body\", []), program.get(\"functions\", [])])\n # Every plain growing array keeps where its block starts, for a push with room to be a write and two adds.\n for id_, a in self.arrays.items():\n if type(a) is ListStorage and id_ not in saved:\n a.keep_base()\n # A task's parameters are set where it is started, which may be lowered after its body: theirs are made now.\n for pool in self.pools.values():\n for prm in pool.st[\"call\"][\"params\"]:\n self.declare(prm[\"decl\"])\n # The program's own coroutine, and the one being lowered (a branch of a Promise.all / race inside it, a task).\n self.top = Coroutine(self, self.rows())\n self.co = self.top\n self.latches = {}\n # The functions that are called, by id, and each one's EUDFunc once something has called it.\n self.functions = {f[\"id\"]: f for f in program.get(\"functions\", [])}\n self.made = {}\n # B4: the error in flight's variables (IR `errors`), the catches the lowering is inside, innermost last — or, in a\n # called function's body, its end, which it returns from with the error still in flight — and the functions whose\n # calls are followed by a look at it. The line an error was thrown at is a cell of the lowering's own.\n self.errors = program.get(\"errors\")\n self.handlers = []\n self.throwing = throwing_functions(program)\n if self.errors:\n self.declare({\"id\": \"(error line)\", \"name\": \"(error line)\", \"kind\": \"number\", \"shared\": True})\n self.in_function = 0\n # The function being lowered, when it is one that calls itself: whose return address a frame keeps.\n self.within = []\n\n # ── storage ──\n def player_of(self):\n if self.player is None:\n raise Fail(\"trigscript: a per-player variable outside the player loop\")\n return self.player\n\n def rows(self):\n \"\"\"The rows a cell of the lowering's own has where the lowering is: a player's, a task's slot's, or none.\"\"\"\n return Rows(self, self.per_player, self.pool) if self.per_player or self.pool else None\n\n def rows_of(self, decl):\n \"\"\"The rows of a variable's or an array's cells: one a player unless it is shared, one a slot of its task.\"\"\"\n pool = self.pools.get(decl[\"task\"]) if decl.get(\"task\") is not None else None\n per_player = self.per_player and not decl.get(\"shared\")\n return Rows(self, per_player, pool) if per_player or pool else None\n\n def declare(self, decl):\n made = self.vars.get(decl[\"id\"])\n if made is not None and decl.get(\"task\") is not None:\n return made # (a task's parameter, made with its pool)\n if made is not None and decl[\"id\"] in self.early:\n self.early.discard(decl[\"id\"])\n return made # (used by a function lowered before the declaration was)\n if decl.get(\"kind\") == \"unit\":\n s = UnitStorage(decl, Rows(self, self.per_player, self.pools.get(decl.get(\"task\"))) if self.per_player or decl.get(\"task\") is not None else None)\n elif decl.get(\"kind\") == \"text\" and decl.get(\"text\") != \"id\":\n s = TextStorage(decl, self.rows_of(decl))\n else:\n s = Storage(decl, self.rows_of(decl))\n self.vars[decl[\"id\"]] = s\n return s\n\n def array(self, id_, node=None):\n a = self.arrays.get(id_)\n if a is None:\n raise Fail(\"trigscript: unknown array %r%s\" % (id_, where(node)))\n return a\n\n def var(self, id_, node=None):\n s = self.vars.get(id_)\n if s is None and id_ in self.decls:\n s = self.declare(self.decls[id_])\n self.early.add(id_)\n if s is None:\n raise Fail(\"trigscript: unknown variable %r%s\" % (id_, where(node)))\n return s\n\n def place_of(self, index, a):\n \"\"\"The counter's `Place` when `index` is one and the array reads and writes at it; else None.\"\"\"\n if index.get(\"kind\") != \"var\" or index.get(\"id\") not in self.places:\n return None\n return self.places[index[\"id\"]] if getattr(a, \"placeable\", lambda: False)() else None\n\n def bound_of(self, index, array_id):\n return self.inside.get(index.get(\"id\"), {}).get(array_id) if index.get(\"kind\") == \"var\" else None\n\n def step_place(self, id_, value):\n place = self.places.get(id_)\n if place is not None:\n place.step(counter_step(value, id_))\n\n # ── numbers: an int (the 32 bits, 0 … 2^32 - 1) or an EUDVariable ──\n def cell_step(self, st, a):\n \"\"\"(op, step) when a store is the cell it stores into plus or minus a number - the same array, the same index,\n an index that reads nothing else (a constant or a variable) - into a plain array of whole numbers; else None.\"\"\"\n v, i = st[\"value\"], st[\"index\"]\n placed_list = type(a) is ListStorage and a.placeable() and self.place_of(i, a) is not None\n if not (isinstance(a, ArrayStorage) or placed_list) or a.bits or getattr(a, \"values\", None) is not None or a.decl[\"kind\"] != \"number\" or a.decl.get(\"fixed\"):\n return None\n if v.get(\"kind\") != \"binary\" or v.get(\"op\") not in (\"+\", \"-\") or v.get(\"unsigned\"):\n return None\n left = v[\"left\"]\n if left.get(\"kind\") != \"element\" or left.get(\"array\") != st[\"array\"] or i.get(\"kind\") not in (\"const\", \"var\") or left.get(\"index\") != i:\n return None\n if mentions_array(v[\"right\"], st[\"array\"]):\n return None\n return (v[\"op\"], v[\"right\"])\n\n def assign_in_place(self, target, st):\n \"\"\"A store worked out in the variable's own cell, step by step: `x = x + e` an add where it is (`x = e + x`,\n `x = x - e` too), `x = y + e` a copy and an add, `x = x & c` / `x | c` a masked write of the cell, and these\n nested (`k = (k + 1) & 15` an add and a mask). Each `e` only reads and does not read x, so the order of the\n reads cannot matter. False when the store is not of that shape (nothing is emitted then).\"\"\"\n if not isinstance(target, Storage) or target.bits:\n return False\n steps = self.into_steps(target, st[\"value\"], st[\"target\"])\n if not steps or steps == [(\"set\", st[\"value\"])]:\n return False\n for step in steps:\n if step[0] == \"set\":\n target.set(self.num(step[1]))\n elif step[0] == \"add\":\n target.add(self.num(step[2]), step[1])\n else:\n target.mask(step[1], step[2])\n return True\n\n def into_steps(self, target, v, id_):\n \"\"\"The steps that work `v` out in the target's own cell, or None when they cannot.\"\"\"\n is_me = lambda e: e.get(\"kind\") == \"var\" and e.get(\"id\") == id_\n if is_me(v):\n return []\n k, op = v.get(\"kind\"), v.get(\"op\")\n if k in (\"const\", \"var\"):\n return None if target.rowed else [(\"set\", v)]\n if k != \"binary\" or op not in (\"+\", \"-\", \"&\", \"|\"):\n return None\n left, right = v[\"left\"], v[\"right\"]\n if op in (\"+\", \"-\"):\n if op == \"+\" and is_me(right) and not is_me(left):\n left, right = right, left\n if not plain(right) or mentions_var(right, id_) or not plain(left):\n return None\n first = self.into_steps(target, left, id_)\n if first is None or (target.rowed and (first or (op == \"-\" and right.get(\"kind\") != \"const\"))):\n return None\n return first + [(\"add\", op, right)]\n # & | with a constant: a masked write of the cell (the other side worked out into it first).\n if target.rowed:\n return None\n c, other = (right, left) if right.get(\"kind\") == \"const\" else (left, right) if left.get(\"kind\") == \"const\" else (None, None)\n if c is None or not plain(other):\n return None\n first = self.into_steps(target, other, id_)\n return None if first is None else first + [(\"mask\", op, int(c[\"value\"]) & U32)]\n\n def num(self, e):\n k = e[\"kind\"]\n if k in (\"textLength\", \"textIndexOf\", \"textCode\"):\n return self.text_number(e)\n if k == \"input\":\n return INPUT.read(e[\"input\"], self, e)\n if k == \"const\":\n return int(e[\"value\"]) & U32\n if k == \"var\":\n return self.var(e[\"id\"], e).get()\n if k == \"element\":\n a = self.array(e[\"array\"], e)\n place = self.place_of(e[\"index\"], a)\n if place is not None:\n cur = self.cursor_at(e[\"index\"], a)\n if cur is not None:\n return self.cursor_read(cur, a, e)\n return a.get_placed(self.num(e[\"index\"]), place, self.bound_of(e[\"index\"], e[\"array\"]))\n return a.get(self.num(e[\"index\"]))\n if k == \"length\":\n a = self.array(e[\"array\"], e)\n return a.length() if isinstance(a, ListStorage) else a.length\n if k == \"pop\":\n return self.array(e[\"array\"], e).pop()\n if k == \"cast\":\n return self.num(e[\"expr\"])\n if k == \"unary\":\n x = self.num(e[\"expr\"])\n return (-x) & U32 if isinstance(x, int) else 0 - x\n if k == \"binary\":\n return self.binary(e)\n if k == \"read\":\n return self.read(e)\n if k == \"taskHandle\":\n # The task whose turn it is, as start() numbered it; 0 in the program's own turn.\n pool = self.pool\n if pool is None:\n return 0\n return pool.slot + pool.task * 256 + f_bitlshift(fresh(pool.gen.get()), 16)\n if k == \"randomInt\":\n # 0 … n - 1; an n of 0 gives 0, as a 0 divisor does.\n n = self.num(e[\"bound\"])\n if isinstance(n, int):\n if n <= 1:\n return 0\n return f_div(f_dwrand(), n)[1]\n out = fresh(0)\n if EUDIf()(n >= 1):\n out << f_div(f_dwrand(), n)[1]\n EUDEndIf()\n return out\n if k == \"unitField\":\n return self.unit_field(e)\n if k == \"unitPart\":\n return self.unit_part(e)\n if k == \"tableRead\":\n return self.table_read(e[\"cell\"], e)\n if k == \"ternary\":\n t = EUDVariable()\n if EUDIf()(self.cond(e[\"cond\"])):\n t << self.num(e[\"whenTrue\"])\n if EUDElse()():\n t << self.num(e[\"whenFalse\"])\n EUDEndIf()\n return t\n if k == \"intrinsic\" and e[\"name\"] == \"abs\":\n x = self.num(e[\"args\"][0])\n if isinstance(x, int):\n return abs(signed(x)) & U32\n if top_of(e[\"args\"][0]) is not None:\n return x\n t = fresh(x)\n if EUDIf()(t >= SIGN):\n t << 0 - t\n EUDEndIf()\n return t\n if k == \"intrinsic\" and e[\"name\"] in (\"fmul\", \"fdiv\", \"sqrt\", \"atan2\", \"sin\", \"cos\"):\n return self.fixed_op(e)\n if k == \"intrinsic\":\n a, b = self.nums(e[\"args\"])\n name = e[\"name\"]\n unsigned = bool(e.get(\"unsigned\"))\n if isinstance(a, int) and isinstance(b, int):\n key = (lambda v: v) if unsigned else signed\n return min(a, b, key=key) if name == \"min\" else max(a, b, key=key)\n # One side always the smaller (ranges.ts): no comparison. Read unsigned, only when both are never below zero.\n ra, rb = rng(e[\"args\"][0]), rng(e[\"args\"][1])\n if ra is not None and rb is not None and (not unsigned or (ra[0] >= 0 and rb[0] >= 0)):\n if ra[1] <= rb[0]:\n return a if name == \"min\" else b\n if rb[1] <= ra[0]:\n return b if name == \"min\" else a\n if isinstance(a, int) or isinstance(b, int):\n return clamp_const(b if isinstance(a, int) else a, a if isinstance(a, int) else b, name, unsigned)\n t = EUDVariable()\n if EUDIf()(self.ordered(a, \"<=\" if name == \"min\" else \">=\", b, unsigned)):\n t << a\n if EUDElse()():\n t << b\n EUDEndIf()\n return t\n if k == \"call\":\n return self.call(e[\"call\"])\n raise Fail(\"trigscript: unknown expression %r%s\" % (k, where(e)))\n\n def fixed_op(self, e):\n \"\"\"fmul / fdiv / sqrt / atan2 of fixed numbers (raw cells): worked out here when every argument is known, else a\n call of the function fixed_fns() makes. compiler/fixedMath.ts is the same arithmetic for the simulator.\"\"\"\n name = e[\"name\"]\n args = self.nums(e[\"args\"])\n if all(isinstance(x, int) for x in args):\n return fixed_const(name, [signed(x) for x in args], e.get(\"unsigned\"), [signed(v) for v in self.array(e[\"table\"], e).values] if name in (\"atan2\", \"sin\", \"cos\") else None) & U32\n fns = fixed_fns()\n bound = lambda i: (lambda r: None if r is None else max(abs(r[0]), abs(r[1])))(rng(e[\"args\"][i]))\n if name == \"fmul\":\n if isinstance(args[1], int):\n return fixed_by_const(\"fmul\", args[0], args[1], bound(0))\n if isinstance(args[0], int):\n return fixed_by_const(\"fmul\", args[1], args[0], bound(1))\n return fresh(fixed_vars()[\"fmul_vars\"](*[as_var(x) for x in args]))\n if name == \"fdiv\":\n if isinstance(args[0], int):\n return const_fdiv(args[0], args[1], bound(1))\n if isinstance(args[1], int):\n return fixed_by_const(\"fdiv\", args[0], args[1], bound(0))\n return fresh(fixed_vars()[\"fdiv_vars\"](*[as_var(x) for x in args]))\n if name == \"sqrt\":\n return fresh(fns[\"sqrt_whole\" if e.get(\"unsigned\") else \"sqrt_fixed\"](as_var(args[0])))\n if name in (\"sin\", \"cos\"):\n return self.sine(args[0], args[1], self.array(e[\"table\"], e), name == \"cos\")\n return self.atan2(args[0], args[1], self.array(e[\"table\"], e), e)\n\n @staticmethod\n def between(table, i, f, bits):\n \"\"\"table[i] + (table[i + 1] - table[i]) * f / 2 ** bits, the part of the step towards zero: a table read between\n its cells (compiler/fixedMath.ts#between). `i + 1` past the end reads 0, as any cell past the end does.\"\"\"\n a, b = fresh(table.get(i)), fresh(table.get(i + 1))\n d, out = fresh(b - a), fresh(a)\n if EUDIf()(d >= SIGN):\n d << 0 - d\n out -= f_div(f_mul(d, f), 1 << bits)[0]\n if EUDElse()():\n out += f_div(f_mul(d, f), 1 << bits)[0]\n EUDEndIf()\n return out\n\n def sine(self, x, scale, table, cos):\n \"\"\"sin(x) of a fixed number of radians (x * scale the place in the 4096 cells of the table), or with `cos` a\n quarter turn on: compiler/fixedMath.ts#fixedSin. The next cell is cell 0 again at the end of the turn.\"\"\"\n at = fresh(fixed_fns()[\"fmul\"](as_var(x), as_var(scale)))\n f = fresh(at & 0xFFFF)\n i = fresh(f_bitrshift(at, 16))\n if cos:\n i += 1024\n i << (i & 4095)\n # Cell 4096 is cell 0: the table read with its first cell after its last.\n a, b = fresh(table.get(i)), fresh(0)\n if EUDIf()(i == 4095):\n b << table.get(0)\n if EUDElse()():\n b << table.get(i + 1)\n EUDEndIf()\n d, out = fresh(b - a), fresh(a)\n if EUDIf()(d >= SIGN):\n d << 0 - d\n out -= f_div(f_mul(d, f), 1 << 16)[0]\n if EUDElse()():\n out += f_div(f_mul(d, f), 1 << 16)[0]\n EUDEndIf()\n return out\n\n def atan2(self, y, x, table, node):\n \"\"\"atan2(y, x) of fixed numbers: the smaller side over the larger to the nearest 1/1024, read from the table\n (atan(i / 1024)), then put in its octant: compiler/fixedMath.ts#fixedAtan2.\"\"\"\n fns = fixed_fns()\n ax, ay, t = fresh(x), fresh(y), fresh(0)\n if EUDIf()(ax >= SIGN):\n ax << 0 - ax\n EUDEndIf()\n if EUDIf()(ay >= SIGN):\n ay << 0 - ay\n EUDEndIf()\n if EUDIfNot()(EUDAnd(ax == 0, ay == 0)):\n if EUDIf()(ay <= ax):\n i, f = [fresh(v) for v in f_div(fns[\"fdiv\"](ay, ax), 64)]\n t << self.between(table, i, f, 6)\n if EUDElse()():\n i, f = [fresh(v) for v in f_div(fns[\"fdiv\"](ax, ay), 64)]\n t << FIXED_HALF_PI - self.between(table, i, f, 6)\n EUDEndIf()\n if EUDIf()(as_var(x) >= SIGN):\n t << FIXED_PI - t\n EUDEndIf()\n if EUDIf()(as_var(y) >= SIGN):\n t << 0 - t\n EUDEndIf()\n EUDEndIf()\n return t\n\n def fixed_pieces(self, v):\n \"\"\"A fixed number as printed pieces: its sign, its whole part, and the point and up to four places, without the\n zeros at the end. The most it comes to is 11 bytes.\"\"\"\n if isinstance(v, int):\n return [fixed_text(v)]\n sign, m = fresh(NO_SIGN), fresh(v)\n if EUDIf()(m >= SIGN):\n sign << MINUS_SIGN\n m << 0 - m\n EUDEndIf()\n whole, frac = [fresh(x) for x in f_div(m, FIXED_ONE)]\n places = fresh(f_div(f_mul(frac, 10000) + 32768, FIXED_ONE)[0])\n if EUDIf()(places == 10000):\n whole += 1\n places << 0\n EUDEndIf()\n if EUDIf()(EUDAnd(whole == 0, places == 0)):\n sign << NO_SIGN\n EUDEndIf()\n # The four places as digits, then each one's text, or none once only zeros are left after it.\n digits = []\n rest = fresh(places)\n for unit in (1000, 100, 10):\n d, rest2 = [fresh(x) for x in f_div(rest, unit)]\n digits.append(d)\n rest << rest2\n digits.append(rest)\n texts = [fresh(NO_SIGN) for _ in range(4)]\n dot = fresh(NO_SIGN)\n shown = fresh(0)\n for i in (3, 2, 1, 0):\n if EUDIf()(digits[i] >= 1):\n shown << 1\n EUDEndIf()\n if EUDIf()(shown == 1):\n texts[i] << DIGIT_TEXTS + digits[i] + digits[i]\n EUDEndIf()\n if EUDIf()(shown == 1):\n dot << DOT_TEXT\n EUDEndIf()\n return [ptr2s(sign), whole, ptr2s(dot)] + [ptr2s(t) for t in texts]\n\n def nums(self, exprs):\n \"\"\"Numbers worked out left to right, as JavaScript does: a value that is not a constant is copied when an\n expression after it holds a call, which may change what it reads (`a + bump()` read a after bump() did).\"\"\"\n out = []\n for i, x in enumerate(exprs):\n v = self.num(x)\n if not isinstance(v, int) and has_call(exprs[i + 1:]):\n v = fresh(v)\n out.append(v)\n return out\n\n def binary(self, e):\n a, b = self.nums([e[\"left\"], e[\"right\"]])\n op = e[\"op\"]\n if op in BITWISE:\n return self.bitwise(op, a, b, top_of(e[\"left\"]))\n both = isinstance(a, int) and isinstance(b, int)\n if op == \"+\":\n return (a + b) & U32 if both else a + b\n if op == \"-\":\n return (a - b) & U32 if both else a - b\n if op == \"*\":\n if both:\n return (a * b) & U32\n # By a power of two, a shift: x * 32 is x << 5, a fifth of the cost of eudplib's general product.\n k, v = (b, a) if isinstance(b, int) else (a, b) if isinstance(a, int) else (None, None)\n vtop = top_of(e[\"left\"] if k is b else e[\"right\"]) if k is not None else None\n if k is not None and k > 0 and k & (k - 1) == 0:\n return v if k == 1 else shift_const(v, \"<<\", k.bit_length() - 1, vtop)\n if k is not None and vtop is not None and mul_bits_pays(vtop, k):\n # The other side known to be 0 ... vtop (ranges.ts): a trigger a bit it can have.\n return mul_bits(as_var(v), vtop, k)\n if k is not None:\n # eudplib's product goes over its first operand's bits and stops past the highest that is set: whichever\n # of the two is smaller first, the constant by its distance from 0 (a number below zero has every top\n # bit set), negated after.\n m = signed(k)\n kv = EUDVariable(abs(m)) # initial: a constant, never written\n r = EUDVariable()\n if EUDIf()(v <= abs(m) - 1):\n f_mul(v, kv, ret=[r])\n if EUDElse()():\n f_mul(kv, v, ret=[r])\n EUDEndIf()\n return 0 - r if m < 0 else r\n # eudplib's product goes over its first operand's bits, up to the highest set: the side with fewer first.\n ta, tb = top_of(e[\"left\"]), top_of(e[\"right\"])\n if tb is not None and (ta is None or tb < ta):\n a, b = b, a\n return f_mul(as_var(a), as_var(b))\n # / and %: towards zero, the remainder with the dividend's sign, unless both sides are u32s. A divisor of 0 gives 0.\n unsigned = bool(e.get(\"unsigned\"))\n pick = (lambda q, r: q) if op == \"/\" else (lambda q, r: r)\n # Both sides known never below zero (ranges.ts): the division is the same read either way, and takes only the\n # bits the dividend can have. A dividend that is a constant is its own top.\n atop = a if isinstance(a, int) and a < SIGN else top_of(e[\"left\"])\n brange = (signed(b), signed(b)) if isinstance(b, int) else rng(e[\"right\"])\n if atop is not None and not both and brange is not None and brange[0] >= 1 and (atop >> 1).bit_length() <= 24:\n if isinstance(b, int):\n return pick(*div_bits(as_var(a), b, atop))\n return pick(*div_var_bits(as_var(a), b, atop))\n if isinstance(b, int):\n if b == 0:\n raise Fail(\"trigscript: division by zero%s\" % where(e))\n if both:\n if unsigned:\n return pick(a // b, a % b)\n x, y = signed(a), signed(b)\n q = abs(x) // abs(y) * (-1 if (x < 0) != (y < 0) else 1)\n return pick(q, x - q * y) & U32\n return pick(*(f_div(a, b) if unsigned else f_div_towards_zero(fresh(a), signed(b))))\n out = fresh(0)\n if EUDIf()(b >= 1):\n if atop is not None and brange is not None and brange[0] >= 0 and (atop >> 1).bit_length() <= 24:\n q, r = div_var_bits(as_var(a), b, atop)\n else:\n q, r = divmod_vars(as_var(a), b, unsigned)\n out << pick(q, r)\n EUDEndIf()\n return out\n\n def ordered(self, a, op, b, unsigned):\n \"\"\"`a op b` as one condition, the two read as u32s (`unsigned` true), as signed numbers (false), or one of\n each (\"left\" / \"right\" names the u32) - exactly: a number below zero is smaller than any u32. At least one\n side is a variable. A signed order is the unsigned one with the top bit of both sides flipped.\"\"\"\n if unsigned in (\"left\", \"right\"):\n # s is the signed side, u the u32, and the comparison is turned to read s op u.\n s_, u_ = (b, a) if unsigned == \"left\" else (a, b)\n if unsigned == \"left\":\n op = FLIPPED[op]\n if isinstance(s_, int):\n if s_ >= SIGN:\n return always(op in (\"<\", \"<=\", \"!=\"))\n return self.ordered(s_, op, u_, True)\n sv = as_var(s_)\n # The signed side at or above zero, and the order between the two read from 0 up: one trigger's conditions,\n # ANDed by the game (EUDAnd is a branch an item). < <= != are the other three turned round and negated.\n if op in (\"<\", \"<=\", \"!=\"):\n return not_all([sv <= SIGN - 1, relation(sv, {\"<\": \">=\", \"<=\": \">\", \"!=\": \"==\"}[op], u_)])\n return [sv <= SIGN - 1, relation(sv, op, u_)]\n if isinstance(a, int):\n a, b, op = b, a, FLIPPED[op]\n if unsigned or op in (\"==\", \"!=\"):\n return relation(as_var(a), op, b)\n if not isinstance(b, int):\n return signed_order(as_var(a), op, as_var(b))\n return relation(a + SIGN, op, (b + SIGN) & U32 if isinstance(b, int) else b + SIGN)\n\n @staticmethod\n def bitwise(op, a, b, atop=None):\n \"\"\"& | ^ << >> >>> over 32 bits. `>>` keeps the sign of what it shifts and `>>>` does not; a shift by 32 or\n more leaves nothing but that sign. `atop`: the left side is known to be 0 ... atop (ranges.ts).\"\"\"\n if op in (\"<<\", \">>\", \">>>\") and isinstance(b, int) and not isinstance(a, int) and 0 < b < 32:\n return shift_const(a, op, b, atop)\n if op in (\"<<\", \">>\", \">>>\"):\n if isinstance(a, int) and isinstance(b, int):\n if op == \"<<\":\n return 0 if b >= 32 else (a << b) & U32\n if op == \">>>\":\n return 0 if b >= 32 else a >> b\n return (signed(a) >> min(b, 31)) & U32\n if isinstance(b, int) and b == 0:\n return a\n if isinstance(b, int) and b >= 32 and op != \">>\":\n return 0\n if op == \"<<\":\n return f_bitlshift(fresh(a), b)\n if op == \">>>\":\n return f_bitrshift(fresh(a), b)\n # The sign kept: a number below zero is shifted as its complement, which has zeros where it has ones.\n x, below = fresh(a), fresh(0)\n if EUDIf()(x >= SIGN):\n below << 1\n x << ~x\n EUDEndIf()\n r = fresh(f_bitrshift(x, b)) if not (isinstance(b, int) and b >= 32) else fresh(0)\n if EUDIf()(below >= 1):\n r << ~r\n EUDEndIf()\n return r\n if isinstance(a, int) and isinstance(b, int):\n return {\"&\": a & b, \"|\": a | b, \"^\": a ^ b}[op]\n if op in (\"&\", \"|\") and (isinstance(a, int) or isinstance(b, int)):\n # Against a constant: a copy and one masked write (eudplib's & of two variables tests a bit a trigger).\n c, x = (a, b) if isinstance(a, int) else (b, a)\n t = fresh(x)\n DoActions(_mask_action(t, op, c))\n return t\n # Copies: eudplib computes in place into an operand nothing else refers to, and ours may be a variable's own cell.\n x, y = fresh(a), fresh(b)\n return x & y if op == \"&\" else x | y if op == \"|\" else x ^ y\n\n # ── reads: a value of the game ──\n def current(self):\n \"\"\"The player the program is running as: a slot, or the player loop's variable.\"\"\"\n return self.player if self.per_player else self.slots[0]\n\n def one_player(self, p, node):\n if p == CURRENT_PLAYER:\n return self.current()\n if 0 <= p < 12:\n return p\n raise Fail(\"trigscript: a read takes one player%s\" % where(node))\n\n def read(self, e):\n r = e[\"read\"]\n source = r.get(\"source\")\n if source == \"condition\":\n return self.read_condition(r[\"record\"], e)\n if source == \"player\" and r.get(\"fact\") == \"left\":\n return self.read_left(r, e)\n if source == \"player\":\n base = PLAYER_BYTES.get(r.get(\"fact\"))\n if base is None:\n raise Fail(\"trigscript: unknown player fact %r%s\" % (r.get(\"fact\"), where(e)))\n value = f_bread(base + self.one_player(r[\"player\"], e))\n if r.get(\"fact\") == \"slot\":\n # A computer of a Use Map Settings game keeps the map's own number, 5; a melee computer is 1\n # (the slice 2 probe read 5 for one). Both are \"a computer\": one number for the script.\n kind = fresh(value)\n if EUDIf()(kind == 5):\n kind << 1\n EUDEndIf()\n return kind\n return value\n if source == \"supply\":\n return self.read_supply(r, e)\n raise Fail(\"trigscript: unknown read %r%s\" % (source, where(e)))\n\n def read_left(self, r, e):\n \"\"\"1 once a player who was in the map's settings as a human or a computer is gone. Asked the way\n eudplib's f_playerexist asks (the player's trigger list), which the player loop already relies on.\n The byte table at 0x581D62 read 0 for players who were there, as it should, but nobody has seen it\n turn 1 in a game — a computer never leaves — so it is not what this rests on.\"\"\"\n p = self.one_player(r[\"player\"], e)\n if isinstance(p, int) and (p >= 8 or GetPlayerInfo(p).typestr not in (\"Human\", \"Computer\")):\n return 0\n gone = fresh(1)\n if EUDIf()(f_playerexist(p)):\n gone << 0\n EUDEndIf()\n return gone\n\n def read_supply(self, r, e):\n base = SUPPLY_TABLES.get(r.get(\"of\"))\n if base is None:\n raise Fail(\"trigscript: unknown supply %r%s\" % (r.get(\"of\"), where(e)))\n p = self.one_player(r[\"player\"], e)\n half = fresh(0)\n if r.get(\"race\") is None:\n # The race the player plays; a slot with none (neutral, empty) has no supply.\n race = f_bread(PLAYER_BYTES[\"race\"] + p)\n if EUDIf()(race <= 2):\n half << f_dwread_epd(EPD(base) + race * 36 + p)\n EUDEndIf()\n else:\n half << f_dwread_epd(EPD(base) + int(r[\"race\"]) * 36 + p)\n # As the top bar shows it: half a supply in use counts as one.\n if r[\"of\"] == \"used\":\n half += 1\n return half // 2\n\n def read_condition(self, rec, e):\n t, player, unit = rec[\"type\"], rec[\"player\"], rec[\"unitId\"]\n one = player == CURRENT_PLAYER or not 12 <= player <= 26 # not a group; beyond 26 is an EUD offset, read as it stands\n table = None\n if one and t in (COND_DEATHS, COND_KILL) and not 229 <= unit <= 232:\n table = (DEATHS_TABLE if t == COND_DEATHS else KILLS_TABLE, unit * 12)\n elif one and t == COND_ACCUMULATE and rec[\"resource\"] in (0, 1):\n table = (ORE_TABLE if rec[\"resource\"] == 0 else GAS_TABLE, 0)\n if table is not None:\n p = self.current() if player == CURRENT_PLAYER else player\n return f_dwread_epd(EPD(table[0]) + table[1] + p)\n # No table holds it (a group's sum, units counted at a location, a score, a clock): ask the\n # condition, a bit at a time from the top. A unit count stays under 2^12, a number of players under 2^4.\n bits = 12 if t in (COND_BRING, COND_COMMAND) else 4 if t == COND_OPPONENTS else 32\n found = fresh(0)\n for bit in reversed(range(bits)):\n probe = found + (1 << bit)\n if EUDIf()(Condition(rec[\"location\"], player, probe, unit, 0, t, rec[\"resource\"], rec[\"flags\"], eudx=rec.get(\"mask\", 0) or 0)):\n found << probe\n EUDEndIf()\n return found\n\n # ── booleans: an eudplib condition ──\n def cond(self, e):\n k = e[\"kind\"]\n if k == \"const\":\n return always(e[\"value\"])\n if k == \"cond\":\n return condition(e[\"record\"])\n if k == \"var\":\n return as_var(self.var(e[\"id\"], e).get()) >= 1\n if k == \"element\":\n return as_var(self.num(e)) >= 1\n if k == \"pop\":\n return as_var(self.array(e[\"array\"], e).pop()) >= 1\n if k == \"test\":\n return as_var(self.num(e[\"expr\"])) >= 1\n if k == \"compare\":\n a, b = self.nums([e[\"left\"], e[\"right\"]])\n op, unsigned = e[\"op\"], e.get(\"unsigned\", False)\n if isinstance(a, int) and isinstance(b, int):\n x = a if unsigned in (True, \"left\") else signed(a)\n y = b if unsigned in (True, \"right\") else signed(b)\n return always(compare(x, op, y))\n # One comparison, built once: a comparison between variables writes into its own\n # condition, and one that is built and dropped is an orphan eudplib refuses.\n return self.ordered(a, op, b, unsigned)\n if k in (\"and\", \"or\"):\n items = e[\"items\"]\n # JavaScript stops at the first item that settles it: an item after it that runs something — a call, a\n # pop, a random number — must not run. Each such item is worked out inside the test of the ones before.\n if any(has_effect(c) for c in items[1:]):\n t = fresh(0)\n self.short_circuit(items, k == \"and\", t)\n return t >= 1\n if k == \"and\":\n # Every item is worked out either way (none of them does anything): one trigger's conditions, ANDed by the game.\n return [self.cond(c) for c in items]\n return EUDOr(*[self.cond(c) for c in items])\n if k == \"not\":\n return EUDNot(self.cond(e[\"expr\"]))\n if k == \"random\":\n return (f_rand() & 1) >= 1\n if k in (\"unitAlive\", \"unitFlag\", \"unitSame\"):\n return as_var(self.truth(e)) >= 1\n if k == \"running\":\n return as_var(self.running(e)) >= 1\n if k == \"edge\":\n return self.edge(e)\n if k == \"ternary\":\n t = EUDVariable()\n if EUDIf()(self.cond(e[\"cond\"])):\n t << self.truth(e[\"whenTrue\"])\n if EUDElse()():\n t << self.truth(e[\"whenFalse\"])\n EUDEndIf()\n return t >= 1\n if k == \"call\":\n return as_var(self.call(e[\"call\"])) >= 1\n if k in (\"textCompare\", \"textTest\"):\n return as_var(self.text_truth(e)) >= 1\n raise Fail(\"trigscript: unknown condition %r%s\" % (k, where(e)))\n\n def short_circuit(self, items, conj, t):\n \"\"\"`a && b && …` (conj) or `a || b || …` into t, each item worked out only when those before it did not settle it.\"\"\"\n if not items:\n if conj:\n t << 1\n return\n if conj:\n if EUDIf()(self.cond(items[0])):\n self.short_circuit(items[1:], conj, t)\n EUDEndIf()\n else:\n if EUDIf()(self.cond(items[0])):\n t << 1\n if EUDElse()():\n self.short_circuit(items[1:], conj, t)\n EUDEndIf()\n\n def truth(self, e):\n \"\"\"A boolean expression as 0 / 1.\"\"\"\n if e[\"kind\"] == \"const\":\n return 1 if e[\"value\"] else 0\n if e[\"kind\"] == \"var\":\n return self.var(e[\"id\"], e).get()\n if e[\"kind\"] == \"element\":\n return self.array(e[\"array\"], e).get(self.num(e[\"index\"]))\n if e[\"kind\"] == \"pop\":\n return self.array(e[\"array\"], e).pop()\n if e[\"kind\"] == \"unitAlive\":\n ref = self.unit(e[\"unit\"])\n t = fresh(0)\n if not ref.none:\n self.when_alive(ref, lambda: t << 1)\n return t\n if e[\"kind\"] == \"unitFlag\":\n return self.unit_flag(e)\n if e[\"kind\"] == \"running\":\n return self.running(e)\n if e[\"kind\"] == \"unitSame\":\n a, b = self.unit(e[\"left\"]), self.unit(e[\"right\"])\n t = fresh(0)\n if not a.none and not b.none:\n ap, bp = as_var(a.ptr), as_var(b.ptr)\n if EUDIf()([ap >= 1, ap == bp]):\n t << 1\n EUDEndIf()\n return t\n t = fresh(0)\n if EUDIf()(self.cond(e)):\n t << 1\n EUDEndIf()\n return t\n\n def edge(self, e):\n \"\"\"rose(c): true on the frame c becomes true; once(c): true the first time it holds.\"\"\"\n key = id(e)\n if key not in self.latches:\n self.latches[key] = RowCell(self.rows(), 0) # initial: the latch\n latch = self.latches[key]\n get, put = latch.get, latch.set\n\n fired = fresh(0)\n held = self.truth(e[\"cond\"])\n if EUDIf()(as_var(held) >= 1):\n if EUDIf()(as_var(get()) == 0):\n fired << 1\n put(1)\n EUDEndIf()\n if EUDElse()():\n if e[\"edge\"] == \"rose\":\n put(0)\n EUDEndIf()\n return fired >= 1\n\n # ── units: a pointer into the game's unit table, checked before use ──\n def unit(self, e):\n k = e[\"kind\"]\n if k == \"unitNull\":\n return NO_UNIT\n if k == \"unitVar\":\n return self.var(e[\"id\"], e).get()\n if k == \"pick\":\n return self.pick(e)\n if k == \"unitAt\":\n # A unit kept as three numbers of the program (a cell each of an array of units): re-checked like any kept unit.\n ptr = self.num(e[\"ptr\"])\n if isinstance(ptr, int) and ptr == 0:\n return NO_UNIT\n return UnitRef(fresh(ptr), fresh(self.num(e[\"epd\"])), fresh(self.num(e[\"uid\"])))\n if k == \"call\":\n return self.call(e[\"call\"])\n if k == \"selected\":\n # A slot of this computer's selection (B5): the unit's address, as Remastered's emulator gives it, and the\n # slot's uniqueness byte taken now, so a use later re-checks it like any kept unit.\n ptr = f_dwread(SELECTION + 4 * int(e[\"index\"]))\n epd, uid = fresh(0), fresh(0)\n if EUDIf()(ptr >= 1):\n epd << (ptr - 0x58A364) // 4\n uid << (f_dwread_epd(epd + OFF_UID) & 0xFF00)\n EUDEndIf()\n return UnitRef(fresh(ptr), epd, uid)\n raise Fail(\"trigscript: unknown unit expression %r%s\" % (k, where(e)))\n\n def unit_part(self, e):\n \"\"\"One of the three numbers a unit is kept as. The unit of a loop's turn has no uniqueness byte taken yet: it is read here.\"\"\"\n ref = self.unit(e[\"unit\"])\n if ref.none:\n return 0\n part = e[\"part\"]\n if part == \"ptr\":\n return ref.ptr\n if part == \"epd\":\n return ref.epd\n return f_maskread_epd(ref.epd + OFF_UID, 0xFF00) if ref.uid is None else ref.uid\n\n def when_alive(self, ref, body):\n \"\"\"`body()` when the unit is still the one that was kept: the slot has a sprite, its order is not\n \"die\", and its uniqueness byte is the one taken with the pointer.\"\"\"\n if ref.none:\n return\n if ref.uid is None:\n body()\n return\n ptr, epd = as_var(ref.ptr), as_var(ref.epd)\n if EUDIf()([ptr >= 1, MemoryEPD(epd + OFF_SPRITE, AtLeast, 1), MemoryXEPD(epd + OFF_OWNER_ORDER, AtLeast, 0x100, 0xFF00), MemoryXEPD(epd + OFF_UID, Exactly, ref.uid, 0xFF00)]):\n body()\n EUDEndIf()\n\n @staticmethod\n def cunit(ref):\n return CUnit(ref.epd, ptr=ref.ptr)\n\n def unit_field(self, e):\n ref = self.unit(e[\"unit\"])\n out = fresh(0)\n self.when_alive(ref, lambda: out << self.field_of(self.cunit(ref), e[\"field\"], e))\n return out\n\n def field_of(self, cu, field, node):\n \"\"\"A unit's number, in the script's units: whole points for hit points (as the game shows them,\n a started point counting), shields and energy.\"\"\"\n if field == \"hp\":\n return f_div(cu.hp + 255, 256)[0]\n if field == \"maxHp\":\n return f_div(f_dwread_epd(EPD(UNITS_MAX_HP) + cu.unitType), 256)[0]\n if field == \"shields\":\n return f_div(cu.shield, 256)[0]\n if field == \"maxShields\":\n return f_wread(UNITS_MAX_SHIELDS + cu.unitType * 2)\n if field == \"energy\":\n return f_div(cu.energy, 256)[0]\n if field in UNIT_TIMERS:\n return getattr(cu, UNIT_TIMERS[field])\n name = {\"owner\": \"owner\", \"type\": \"unitType\", \"x\": \"posX\", \"y\": \"posY\", \"kills\": \"killCount\", \"orderId\": \"orderID\", \"cooldown\": \"groundWeaponCooldown\", \"resources\": \"resourceAmount\"}.get(field)\n if name is None:\n raise Fail(\"trigscript: unknown unit field %r%s\" % (field, where(node)))\n return getattr(cu, name)\n\n def unit_flag(self, e):\n ref = self.unit(e[\"unit\"])\n out = fresh(0)\n\n def body():\n cu = self.cunit(ref)\n flag = e[\"flag\"]\n if flag == \"underAttack\":\n held = cu.attackNotifyTimer >= 1\n else:\n mask = {\"hallucinated\": STATUS_HALLUCINATION, \"cloaked\": STATUS_CLOAKED, \"burrowed\": STATUS_BURROWED, \"invincible\": STATUS_INVINCIBLE}.get(flag)\n if mask is None:\n raise Fail(\"trigscript: unknown unit flag %r%s\" % (flag, where(e)))\n held = cu.check_status_flag(mask)\n if EUDIf()(held):\n out << 1\n EUDEndIf()\n\n self.when_alive(ref, body)\n return out\n\n def location_bounds(self, number):\n base = EPD(MRGN + (int(number) - 1) * 20)\n return [f_dwread_epd(base + i) for i in range(4)]\n\n def scan(self, flt, node, body):\n \"\"\"The game's unit table in the order the game hands its places out — slot 0, then 1699 downwards, the units\n oldest first — stopping once it has met as many units as the game says are in use (0x6283F0): the places in use\n lie at the top, so the walk costs what the map holds, not the 1 700 places (about 135 ns a place, 229 µs a loop).\n `body(ref, next_, exit_)` for every unit the filter matches. Every test is one condition whose address moves on\n with the slot, so a slot that does not match costs a trigger or two. (Counted are the places with a sprite: never\n more than the game's count, and at most one fewer in a 12 000-frame game of four, when the walk goes to the end.)\"\"\"\n ptr, epd = EUDVariable(), EUDVariable()\n empty = MemoryEPD(0, Exactly, 0)\n dying = MemoryXEPD(0, Exactly, 0, 0xFF00)\n moving = [(empty, OFF_SPRITE), (dying, OFF_OWNER_ORDER)]\n match, values = [], []\n owner = flt.get(\"owner\")\n if owner is not None:\n who = self.current() if owner == CURRENT_PLAYER else int(owner)\n c = MemoryXEPD(0, Exactly, who if isinstance(who, int) else 0, 0xFF)\n match.append(c)\n moving.append((c, OFF_OWNER_ORDER))\n if not isinstance(who, int):\n values.append((c, who))\n kind = flt.get(\"type\")\n if kind is not None and kind < 228:\n c = MemoryXEPD(0, Exactly, int(kind), 0xFFFF)\n match.append(c)\n moving.append((c, OFF_TYPE))\n elif kind is not None and kind not in GROUP_BITS:\n raise Fail(\"trigscript: unknown unit type %r%s\" % (kind, where(node)))\n if flt.get(\"at\"):\n left, top, right, bottom = self.location_bounds(flt[\"at\"])\n # A location reaching past the map's edge starts below zero, which a unit's position never is.\n for v in (left, top):\n if EUDIf()(v >= 0x80000000):\n v << 0\n EUDEndIf()\n for comparison, mask, value in ((AtLeast, 0xFFFF, left), (AtMost, 0xFFFF, right), (AtLeast, 0xFFFF0000, f_bitlshift(top, 16)), (AtMost, 0xFFFF0000, f_bitlshift(bottom, 16))):\n c = MemoryXEPD(0, comparison, 0, mask)\n match.append(c)\n moving.append((c, OFF_POS))\n values.append((c, value))\n # Slot 0 first; past it the walk jumps to slot 1699 and goes down.\n remaining = fresh(f_dwread(RECORDS_IN_USE))\n DoActions([ptr.SetNumber(UNIT_TABLE), epd.SetNumber(EPD(UNIT_TABLE))] + [SetMemory(c + 4, SetTo, EPD(UNIT_TABLE) + off) for c, off in moving])\n for c, value in values:\n f_dwwrite_epd(EPD(c + 8), value)\n top_slot = UNIT_TABLE + UNIT_SIZE * (UNIT_SLOTS - 1)\n head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(remaining == 0, exit_)\n EUDJumpIf(empty, next_)\n remaining -= 1\n EUDJumpIf(dying, next_)\n if match:\n matched = Forward()\n EUDJumpIf(match, matched)\n EUDJump(next_)\n matched << NextTrigger()\n if kind in GROUP_BITS:\n group = fresh(f_bread(UNITS_GROUP + f_maskread_epd(epd + OFF_TYPE, 0xFFFF)))\n EUDJumpIf((group & GROUP_BITS[kind]) == 0, next_)\n body(UnitRef(ptr, epd, None), next_, exit_)\n next_ << NextTrigger()\n down = Forward()\n EUDJumpIfNot(ptr == UNIT_TABLE, down)\n DoActions([SetMemory(c + 4, SetTo, EPD(top_slot) + off) for c, off in moving] + [ptr.SetNumber(top_slot), epd.SetNumber(EPD(top_slot))])\n EUDJump(head)\n down << NextTrigger()\n DoActions([SetMemory(c + 4, Add, (-(UNIT_SIZE // 4)) & U32) for c, _ in moving] + [ptr.SubtractNumber(UNIT_SIZE), epd.SubtractNumber(UNIT_SIZE // 4)])\n EUDJumpIfNot(ptr == UNIT_TABLE, head)\n exit_ << NextTrigger()\n\n def pick(self, e):\n \"\"\"One unit among the matching: the first, the nearest to a location's centre, or one at random.\"\"\"\n by, flt = e[\"by\"], e.get(\"filter\", {})\n found_ptr, found_epd = fresh(0), fresh(0)\n\n def take(ref):\n found_ptr << ref.ptr\n found_epd << ref.epd\n\n if by == \"first\":\n def body(ref, next_, exit_):\n take(ref)\n EUDJump(exit_)\n self.scan(flt, e, body)\n elif by == \"nearest\":\n if e.get(\"mouse\") is not None:\n # A player's mouse, and nothing farther from it than `within`.\n cx = as_var(INPUT.read({\"source\": \"mouse\", \"axis\": \"x\", \"player\": e[\"mouse\"]}, self, e))\n cy = as_var(INPUT.read({\"source\": \"mouse\", \"axis\": \"y\", \"player\": e[\"mouse\"]}, self, e))\n least = fresh(int(e.get(\"within\", 48)) + 1)\n else:\n left, top, right, bottom = self.location_bounds(e[\"near\"])\n cx, cy = f_div(left + right, 2)[0], f_div(top + bottom, 2)[0]\n least = fresh(U32)\n\n def body(ref, next_, exit_):\n cu = self.cunit(ref)\n # |dx| + |dy| is enough to say which is nearest, and never overflows.\n d = distance(cu.posX, cx) + distance(cu.posY, cy)\n if EUDIf()(d < least):\n least << d\n take(ref)\n EUDEndIf()\n self.scan(flt, e, body)\n elif by == \"random\":\n # Count the matching, draw one, take the drawn one on a second pass.\n count = fresh(0)\n self.scan(flt, e, lambda ref, next_, exit_: count.__iadd__(1))\n none = Forward()\n EUDJumpIf(count == 0, none)\n drawn = f_div(f_dwrand(), count)[1]\n i = fresh(0)\n\n def body(ref, next_, exit_):\n hit = Forward()\n EUDJumpIf(i == drawn, hit)\n i.__iadd__(1)\n EUDJump(next_)\n hit << NextTrigger()\n take(ref)\n EUDJump(exit_)\n self.scan(flt, e, body)\n none << NextTrigger()\n else:\n raise Fail(\"trigscript: unknown pick %r%s\" % (by, where(e)))\n uid = fresh(0)\n if EUDIf()(found_ptr >= 1):\n uid << f_maskread_epd(found_epd + OFF_UID, 0xFF00)\n EUDEndIf()\n return UnitRef(found_ptr, found_epd, uid)\n\n def unit_loop(self, st, ctx):\n def body(ref, next_, exit_):\n self.vars[st[\"decl\"][\"id\"]] = LoopUnit(ref)\n self.straight(st[\"body\"], dict(ctx, **{\"break\": exit_, \"continue\": next_}))\n if st.get(\"source\"):\n self.event_loop(st[\"source\"], st.get(\"filter\", {}), st, body)\n return\n self.scan(st.get(\"filter\", {}), st, body)\n\n def event_loop(self, source, flt, node, body):\n \"\"\"The units of this frame's events pass, in the order it found them: `body(ref, next_, exit_)` for each the\n filter matches. One that came on and has gone since is passed over; one that died is the dying record, read as\n it died (its slot is not given to another unit before the next frame's dying pass).\"\"\"\n if EVENTS is None:\n raise Fail(\"trigscript: a loop over the events pass's units, and no pass%s\" % where(node))\n cells, was, count = {\"created\": (EVENTS.made, None, EVENTS.made_n), \"died\": (EVENTS.gone, None, EVENTS.gone_n),\n \"killed\": (EVENTS.gone, None, EVENTS.gone_n), \"ordered\": (EVENTS.turned, EVENTS.turned_was, EVENTS.turned_n),\n \"morphed\": (EVENTS.changed, EVENTS.changed_was, EVENTS.changed_n)}[source]\n event = node.get(\"event\") or {}\n i = fresh(0)\n head, next_, exit_ = Forward(), Forward(), Forward()\n head << NextTrigger()\n EUDJumpIf(i >= count, exit_)\n ptr, epd = f_cunitepdread_epd(EPD(cells) + i)\n if source in (\"created\", \"ordered\", \"morphed\"):\n # Gone since the pass: killed or removed by a program before this turn (its slot is still its own).\n EUDJumpIfNot([MemoryEPD(epd + OFF_SPRITE, AtLeast, 1), MemoryXEPD(epd + OFF_OWNER_ORDER, AtLeast, 0x100, 0xFF00)], next_)\n match = []\n if source == \"killed\":\n # Died in a fight: no hit points, and one of the eight players the last whose attack hurt it.\n match += [MemoryEPD(epd + OFF_HP, Exactly, 0), MemoryXEPD(epd + OFF_LAST_ATTACKER, AtMost, 7, 0xFF)]\n by = event.get(\"by\")\n if by is not None:\n match.append(MemoryXEPD(epd + OFF_LAST_ATTACKER, Exactly, self.current() if by == CURRENT_PLAYER else int(by), 0xFF))\n if event.get(\"order\") is not None:\n match.append(MemoryXEPD(epd + OFF_OWNER_ORDER, Exactly, int(event[\"order\"]) << 8, 0xFF00))\n owner = flt.get(\"owner\")\n if owner is not None:\n who = self.current() if owner == CURRENT_PLAYER else int(owner)\n match.append(MemoryXEPD(epd + OFF_OWNER_ORDER, Exactly, who, 0xFF))\n kind = flt.get(\"type\")\n if kind is not None and kind < 228:\n match.append(MemoryXEPD(epd + OFF_TYPE, Exactly, int(kind), 0xFFFF))\n elif kind is not None and kind not in GROUP_BITS:\n raise Fail(\"trigscript: unknown unit type %r%s\" % (kind, where(node)))\n if flt.get(\"at\"):\n left, top, right, bottom = self.location_bounds(flt[\"at\"])\n for v in (left, top):\n if EUDIf()(v >= 0x80000000):\n v << 0\n EUDEndIf()\n match += [MemoryXEPD(epd + OFF_POS, AtLeast, left, 0xFFFF), MemoryXEPD(epd + OFF_POS, AtMost, right, 0xFFFF),\n MemoryXEPD(epd + OFF_POS, AtLeast, f_bitlshift(top, 16), 0xFFFF0000), MemoryXEPD(epd + OFF_POS, AtMost, f_bitlshift(bottom, 16), 0xFFFF0000)]\n if match:\n EUDJumpIfNot(match, next_)\n if kind in GROUP_BITS:\n group = fresh(f_bread(UNITS_GROUP + f_maskread_epd(epd + OFF_TYPE, 0xFFFF)))\n EUDJumpIf((group & GROUP_BITS[kind]) == 0, next_)\n if node.get(\"was\"):\n # What the unit was: the killer, or the order or the type the pass before found.\n self.declare(node[\"was\"]).set(f_maskread_epd(epd + OFF_LAST_ATTACKER, 0xFF) if source == \"killed\" else was[i])\n body(UnitRef(ptr, epd, None), next_, exit_)\n next_ << NextTrigger()\n i += 1\n EUDJump(head)\n exit_ << NextTrigger()\n\n def unit_write(self, st):\n ref = self.unit(st[\"unit\"])\n field = st[\"field\"]\n if field == \"invincible\":\n on = self.truth(st[\"value\"])\n\n def flag():\n cu = self.cunit(ref)\n if isinstance(on, int):\n cu.set_invincible() if on else cu.clear_invincible()\n return\n if EUDIf()(as_var(on) >= 1):\n cu.set_invincible()\n if EUDElse()():\n cu.clear_invincible()\n EUDEndIf()\n self.when_alive(ref, flag)\n return\n value = self.num(st[\"value\"])\n\n def write():\n cu = self.cunit(ref)\n if field == \"hp\":\n # Hit points at 0 are a dead unit, so that is what the write makes of it.\n if isinstance(value, int):\n if value == 0:\n cu.die()\n else:\n cu.hp = min(value, 0xFFFFFF) * 256\n return\n if EUDIf()(value == 0):\n cu.die()\n if EUDElse()():\n cu.hp = f_mul(saturate(value, 24), 256)\n EUDEndIf()\n elif field == \"shields\":\n cu.shield = points(value, 24)\n elif field == \"energy\":\n cu.energy = points(value, 8)\n elif field == \"kills\":\n cu.killCount = saturate(value, 8)\n elif field == \"resources\":\n cu.resourceAmount = saturate(value, 16)\n elif field == \"cooldown\":\n frames = saturate(value, 8)\n cu.groundWeaponCooldown = frames\n cu.airWeaponCooldown = frames\n cu.spellCooldown = frames\n elif field in UNIT_TIMERS:\n setattr(cu, UNIT_TIMERS[field], saturate(value, 8))\n else:\n raise Fail(\"trigscript: a unit's %s takes no write%s\" % (field, where(st)))\n self.when_alive(ref, write)\n\n def unit_do(self, st):\n ref = self.unit(st[\"unit\"])\n verb = st[\"verb\"]\n do = verb[\"do\"]\n amount = self.num(verb[\"amount\"]) if do in (\"damage\", \"heal\") else 0\n\n def act():\n cu = self.cunit(ref)\n if do == \"kill\":\n cu.die()\n elif do == \"remove\":\n cu.remove()\n elif do == \"give\":\n to = int(verb[\"to\"])\n cu.cgive(self.current() if to == CURRENT_PLAYER else to)\n forget_counts()\n elif do == \"order\":\n self.order(cu, verb, st)\n elif do == \"locate\":\n self.locate(cu, int(verb[\"location\"]))\n elif do in (\"damage\", \"heal\"):\n self.adjust(cu, do, amount, bool(verb.get(\"percent\")))\n else:\n raise Fail(\"trigscript: unknown unit verb %r%s\" % (do, where(st)))\n self.when_alive(ref, act)\n\n def order(self, cu, verb, node):\n \"\"\"The game's own Order, reaching this unit alone: a location is made a small box around the unit\n for the length of the action (the game did nothing with a box of no size), then put back.\"\"\"\n kind = {\"move\": Move, \"patrol\": Patrol, \"attack\": Attack}.get(verb[\"order\"])\n if kind is None:\n raise Fail(\"trigscript: unknown order %r%s\" % (verb[\"order\"], where(node)))\n base = EPD(MRGN + (SCRATCH_LOCATION - 1) * 20)\n kept = [f_dwread_epd(base + i) for i in range(5)]\n x, y = cu.posX, cu.posY\n for i, v in enumerate((x - 2, y - 2, x + 2, y + 2, 0)):\n f_dwwrite_epd(base + i, v)\n DoActions(Order(cu.unitType, cu.owner, SCRATCH_LOCATION, kind, int(verb[\"target\"])))\n for i, v in enumerate(kept):\n f_dwwrite_epd(base + i, v)\n\n def locate(self, cu, number):\n \"\"\"A location centred on the unit, its size kept.\"\"\"\n base = EPD(MRGN + (number - 1) * 20)\n left, top, right, bottom = [f_dwread_epd(base + i) for i in range(4)]\n width, height = right - left, bottom - top\n x, y = cu.posX - f_div(width, 2)[0], cu.posY - f_div(height, 2)[0]\n for i, v in enumerate((x, y, x + width, y + height)):\n f_dwwrite_epd(base + i, v)\n forget_counts()\n\n def select(self, st):\n \"\"\"select() (B5): on the player's own computer the selection becomes the units still there — the twelve slots\n (0x6284B8), the panel told it changed (0x59723C, the flag Remastered's panel refresh tests) — and a Select\n command of their tags (place + 1, uniqueness byte << 11) goes to every computer, as MSQC puts a selection back.\"\"\"\n p = self.one_player(st[\"player\"], st)\n refs = [self.unit(u) for u in st[\"units\"]]\n if EUDIf()(f_getuserplayerid() == p):\n n = fresh(0)\n for ref in refs:\n def keep(ref=ref):\n f_dwwrite(SELECTION + n * 4, ref.ptr)\n # The unit's tag: its place in the table from 1, its uniqueness byte above bit 11.\n uid = as_var(ref.uid) if ref.uid is not None else f_dwread_epd(as_var(ref.epd) + OFF_UID) & 0xFF00\n tag = f_div(as_var(ref.ptr) - 0x59CCA8, 336)[0] + 1 + uid * 8\n f_wwrite(SELECT_COMMAND + 2 + n * 2, tag)\n n.__iadd__(1)\n self.when_alive(ref, keep)\n if EUDIf()(n >= 1):\n i = fresh(n)\n if EUDWhile()(i <= 11):\n f_dwwrite(SELECTION + i * 4, 0)\n i.__iadd__(1)\n EUDEndWhile()\n DoActions(SetMemoryX(0x59723C, SetTo, 1, 0xFF))\n f_bwrite(SELECT_COMMAND + 1, n)\n QueueGameCommand(SELECT_COMMAND, n * 2 + 2)\n EUDEndIf()\n EUDEndIf()\n\n def center_location(self, st):\n \"\"\"A location centred on a point, its size kept - or the size given, when nothing of the old one is read.\"\"\"\n x, y = self.num(st[\"x\"]), self.num(st[\"y\"])\n base = EPD(MRGN + (int(st[\"location\"]) - 1) * 20)\n size = st.get(\"size\")\n if size:\n width, height = self.num(size[\"width\"]), self.num(size[\"height\"])\n if isinstance(width, int) and isinstance(height, int):\n # Each side is the centre plus a constant: all four in one go, the centre routed into the two sides that\n # take it, the constants added where they land (four temporaries made and copied was about 0.75 µs —\n # every brush of the fog library is one of these).\n sides = []\n for i, (c, d) in enumerate(((x, -(width // 2)), (y, -(height // 2)), (x, width - width // 2), (y, height - height // 2))):\n if isinstance(c, int):\n sides.append((base + i, SetTo, (c + d) & U32))\n else:\n sides.append((base + i, SetTo, c))\n if d & U32:\n sides.append((base + i, Add, d & U32))\n SeqCompute(sides)\n forget_counts()\n return\n else:\n nx, ny = x - f_div(width, 2)[0], y - f_div(height, 2)[0]\n for i, v in enumerate((nx, ny, nx + width, ny + height)):\n f_dwwrite_epd(base + i, v)\n forget_counts()\n return\n left, top, right, bottom = [f_dwread_epd(base + i) for i in range(4)]\n width, height = right - left, bottom - top\n nx, ny = x - f_div(width, 2)[0], y - f_div(height, 2)[0]\n for i, v in enumerate((nx, ny, nx + width, ny + height)):\n f_dwwrite_epd(base + i, v)\n forget_counts()\n\n def adjust(self, cu, do, amount, percent):\n \"\"\"Hit points down — at 0 the unit dies — or up to the type's maximum, in the game's own units (256 to a point).\"\"\"\n now = fresh(cu.hp)\n top = f_dwread_epd(EPD(UNITS_MAX_HP) + cu.unitType)\n if percent:\n step = f_div(f_mul(top, as_var(amount)), 100)[0]\n else:\n step = amount * 256 if isinstance(amount, int) else f_mul(saturate(amount, 24), 256)\n if do == \"damage\":\n if EUDIf()(now <= step):\n cu.die()\n if EUDElse()():\n cu.hp = now - step\n EUDEndIf()\n return\n new = fresh(now + step)\n if EUDIf()(new >= top):\n new << top\n EUDEndIf()\n cu.hp = new\n\n # ── the game's tables ──\n def cell_address(self, c):\n index = c[\"index\"]\n if c.get(\"player\") and index == CURRENT_PLAYER:\n index = self.current()\n base = int(c[\"base\"]) + int(c.get(\"key\") or 0)\n return base + index * int(c[\"stride\"])\n\n def table_read(self, c, node):\n if c.get(\"special\"):\n raise Fail(\"trigscript: %s cannot be read%s\" % (c.get(\"name\"), where(node)))\n addr = self.cell_address(c)\n width = c[\"width\"]\n if width == \"bit\":\n v = fresh(0)\n if EUDIf()(MemoryX(addr, AtLeast, 1, 1 << int(c[\"bit\"]))):\n v << 1\n EUDEndIf()\n return v\n v = f_dwread(addr) if width == 4 else f_wread(addr) if width == 2 else f_bread(addr)\n scale = int(c.get(\"scale\") or 1)\n return f_div(v, scale)[0] if scale > 1 else v\n\n def table_write(self, st):\n c = st[\"cell\"]\n addr = self.cell_address(c)\n special = c.get(\"special\")\n if special == \"name\":\n if self.has_id(st[\"value\"]):\n f_wwrite(addr, self.text_id_of(st[\"value\"]))\n return\n # A name that was made: over the string kept for this unit type, on every computer - a name is nobody's in particular.\n made = self.text(st[\"value\"])\n sid = slot(\"the name of unit type %d\" % int(c[\"index\"]))\n texts()[\"show\"](GetMapStringAddr(sid), made.addr)\n self.used(made)\n f_wwrite(addr, sid)\n return\n width = c[\"width\"]\n if st.get(\"boolean\"):\n value = self.truth(st[\"value\"])\n else:\n value = self.num(st[\"value\"])\n scale = int(c.get(\"scale\") or 1)\n if scale > 1 and not st.get(\"scaled\"):\n value = value * scale if isinstance(value, int) else f_mul(value, scale)\n top = 1 if width == \"bit\" else (1 << (8 * width)) - 1\n if isinstance(value, int):\n value = min(value, top)\n elif width != 4 and width != \"bit\":\n value = saturate(value, 8 * width)\n if special == \"speed\":\n self.write_speed(int(c[\"index\"]), value)\n elif special == \"color\":\n f_bwrite(addr, value)\n f_bwrite(addr + MINIMAP_COLOR_OFFSET, value)\n elif width == \"bit\":\n mask = 1 << int(c[\"bit\"])\n if isinstance(value, int):\n DoActions(SetMemoryX(addr, SetTo, mask if value else 0, mask))\n else:\n if EUDIf()(value >= 1):\n DoActions(SetMemoryX(addr, SetTo, mask, mask))\n if EUDElse()():\n DoActions(SetMemoryX(addr, SetTo, 0, mask))\n EUDEndIf()\n elif width == 4:\n f_dwwrite(addr, value)\n elif width == 2:\n f_wwrite(addr, value)\n else:\n f_bwrite(addr, value)\n\n def write_speed(self, unit, speed):\n \"\"\"A unit type's top speed is its flingy's: the flingy is switched to table control and given the\n speed, with acceleration about a seventeenth of it and the braking distance v² / 2a to match\n (the Vulture's own figures), as Magenta writes it. Units made afterwards move at the new speed.\"\"\"\n flingy = f_bread(UNITS_FLINGY + unit)\n if isinstance(speed, int):\n acceleration = max(1, round(speed / 17))\n halt = max(1, round(speed * speed / (2 * acceleration)))\n else:\n acceleration = fresh(f_div(speed, 17)[0])\n if EUDIf()(acceleration == 0):\n acceleration << 1\n EUDEndIf()\n halt = fresh(f_div(f_mul(speed, speed), acceleration * 2)[0])\n if EUDIf()(halt == 0):\n halt << 1\n EUDEndIf()\n acceleration = saturate(acceleration, 16)\n f_bwrite(FLINGY_CONTROL + flingy, 0)\n f_dwwrite(FLINGY_SPEED + flingy * 4, speed)\n f_wwrite(FLINGY_ACCELERATION + flingy * 2, acceleration)\n f_dwwrite(FLINGY_HALT + flingy * 4, halt)\n\n # ── statements ──\n def block(self, statements, ctx):\n for st in statements:\n self.statement(st, ctx)\n\n def statement(self, st, ctx):\n k = st[\"kind\"]\n if k == \"declare\":\n s = self.declare(st[\"decl\"])\n if not st.get(\"failed\"):\n self.put(s, st[\"init\"], st[\"decl\"][\"kind\"])\n self.step_place(st[\"decl\"][\"id\"], st[\"init\"])\n elif k == \"assignText\":\n self.put(self.var(st[\"target\"], st), st[\"value\"], \"text\")\n elif k == \"storeText\":\n self.store_text(st)\n elif k == \"releaseText\":\n block, i = self.array(st[\"block\"], st), fresh(self.num(st[\"index\"]))\n texts()[\"release\"](as_var(block.get(i)))\n block.set(i, 0)\n elif k == \"textLoop\":\n self.text_loop(st, ctx)\n elif k == \"assignUnit\":\n self.var(st[\"target\"], st).set(self.unit(st[\"value\"]))\n elif k == \"unitLoop\":\n self.unit_loop(st, ctx)\n elif k == \"unitWrite\":\n self.unit_write(st)\n elif k == \"unitDo\":\n self.unit_do(st)\n elif k == \"tableWrite\":\n self.table_write(st)\n elif k == \"assign\":\n target = self.var(st[\"target\"], st)\n if not self.assign_in_place(target, st):\n target.set(self.num(st[\"value\"]))\n self.step_place(st[\"target\"], st[\"value\"])\n elif k == \"assignBool\":\n self.var(st[\"target\"], st).set(self.truth(st[\"value\"]))\n elif k == \"declareArray\":\n a = self.array(st[\"array\"], st)\n cell = self.num if a.decl[\"kind\"] == \"number\" else self.truth\n if isinstance(a, ListStorage):\n if st.get(\"fill\") is not None:\n v = cell(st[\"fill\"])\n a.declare_filled(v if isinstance(v, int) else fresh(v), int(a.decl[\"length\"]))\n else:\n values = [cell(v) for v in st.get(\"init\", [])]\n a.declare([v if isinstance(v, int) else fresh(v) for v in values])\n elif st.get(\"fill\") is not None:\n a.fill(cell(st[\"fill\"]))\n else:\n # Every value first, then the stores: [b, a] of two cells of the array itself is a swap.\n values = [cell(v) for v in st.get(\"init\", [])]\n values = [v if isinstance(v, int) else fresh(v) for v in values]\n for i, v in enumerate(values):\n a.set(i, v)\n elif k == \"push\":\n a = self.array(st[\"array\"], st)\n a.push((self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"]))\n elif k == \"pop\":\n self.array(st[\"array\"], st).pop()\n elif k == \"setLength\":\n self.array(st[\"array\"], st).set_length(self.num(st[\"value\"]))\n elif k == \"store\":\n a = self.array(st[\"array\"], st)\n # `xs[i] = xs[i] + v` (`xs[i] += v`, a count): the game adds to the cell itself, nothing read.\n step = self.cell_step(st, a)\n place = self.place_of(st[\"index\"], a)\n if step is not None:\n if place is not None:\n v = self.num(step[1])\n if step[0] == \"-\":\n v = (-v) & U32 if isinstance(v, int) else 0 - v\n cur = self.cursor_at(st[\"index\"], a)\n if cur is not None:\n self.cursor_write(cur, a, st, v, Add)\n return\n a.set_placed(self.num(st[\"index\"]), place, v, Add, self.bound_of(st[\"index\"], st[\"array\"]))\n return\n a.add(self.num(st[\"index\"]), self.num(step[1]), step[0])\n return\n value = (self.num if a.decl[\"kind\"] == \"number\" else self.truth)(st[\"value\"])\n if place is not None:\n cur = self.cursor_at(st[\"index\"], a)\n if cur is not None:\n self.cursor_write(cur, a, st, value, SetTo)\n return\n a.set_placed(self.num(st[\"index\"]), place, value, SetTo, self.bound_of(st[\"index\"], st[\"array\"]))\n return\n a.set(self.num(st[\"index\"]), value)\n elif k == \"if\":\n self.if_(st, ctx)\n elif k == \"while\":\n self.while_(st, ctx)\n elif k == \"do\":\n self.do_(st, ctx)\n elif k == \"for\":\n self.for_(st, ctx)\n elif k == \"unrolled\":\n self.unrolled(st, ctx)\n elif k == \"switch\":\n self.switch(st, ctx)\n elif k == \"break\":\n if \"break\" not in ctx:\n raise Fail(\"trigscript: break outside a loop%s\" % where(st))\n EUDJump(ctx[\"break\"])\n raise Leave()\n elif k == \"continue\":\n if \"continue\" not in ctx:\n raise Fail(\"trigscript: continue outside a loop%s\" % where(st))\n EUDJump(ctx[\"continue\"])\n raise Leave()\n elif k == \"return\":\n fn = ctx.get(\"fn\")\n if fn is None:\n # At the top of a program: its end, for good.\n if ctx.get(\"end\") is None:\n raise Fail(\"trigscript: return outside a function%s\" % where(st))\n EUDJump(ctx[\"end\"])\n raise Leave()\n if st.get(\"value\") is not None and fn[\"result\"] is not None:\n self.put(fn[\"result\"], st[\"value\"], fn[\"kind\"])\n EUDJump(fn[\"end\"])\n raise Leave()\n elif k == \"sleep\":\n self.sleep(st)\n elif k == \"parallel\":\n self.parallel(st)\n elif k == \"start\":\n self.start(st)\n elif k == \"cancel\":\n self.cancel(st)\n elif k == \"action\":\n self.action(st)\n elif k == \"print\":\n self.print_(st)\n elif k == \"buildText\":\n self.build_text(st)\n elif k == \"centerLocation\":\n self.center_location(st)\n elif k == \"select\":\n self.select(st)\n elif k == \"syncSend\":\n INPUT.send_sync(int(st[\"slot\"]), self.one_player(st[\"player\"], st), self.num(st[\"value\"]))\n elif k == \"call\":\n self.call(st[\"call\"])\n elif k == \"block\":\n self.block(st[\"body\"], ctx)\n elif k == \"try\":\n self.try_(st, ctx)\n elif k == \"throw\":\n if not st.get(\"rethrow\"):\n self.var(\"(error line)\").set(int((st.get(\"at\") or {}).get(\"line\", 0)))\n EUDJump(self.handler(st))\n raise Leave()\n elif k == \"uncaught\":\n self.uncaught(st)\n elif k == \"remark\":\n pass\n elif k == \"graph\":\n self.graph(st, ctx)\n else:\n raise Fail(\"trigscript: unknown statement %r%s\" % (k, where(st)))\n\n # ── the emitter's graphs (compiler/backend/graph.ts) ──\n GRAPH_COMPARE = {\"atLeast\": AtLeast, \"atMost\": AtMost, \"exactly\": Exactly}\n GRAPH_MODIFIER = {\"set\": SetTo, \"add\": Add, \"subtract\": Subtract}\n\n def graph(self, st, ctx):\n \"\"\"A loop the emitter lowered (TrigScript 4, the backend: the emitter): its triggers made as they are, where the\n loop was. A dword of the graph is a game address, a variable's cell, a dword of the graph's own data (one in the\n map for each name), or a field of one of its triggers; the copies into it on the way in are routed reads of the\n variables, as any read is. A statement lowered twice (a `finally` copied to each way out) makes it twice.\"\"\"\n g = st[\"graph\"]\n if not hasattr(self, \"graph_data\"):\n self.graph_data = {}\n labels = {t[\"label\"]: Forward() for t in g[\"triggers\"]}\n end = Forward()\n labels[\"@end\"] = end\n\n def element0(id_):\n \"\"\"The EPD of the value of an array's first cell, and of the next cell's less it.\"\"\"\n store = self.array(id_, st).store\n if is_varray(store):\n return EPD(unProxy(store) + VARRAY_VALUE), VARRAY_STRIDE // 4\n return store._epd, 1\n\n # The lists the graph reads where they are: laid out as the emitter thought, or nothing is made.\n for a in g.get(\"arrays\", []):\n if element0(a[\"id\"])[1] != a[\"stride\"]:\n raise Fail(\"trigscript: the emitter read %s's cells %d EPD apart, which are %d%s\" % (a[\"id\"], a[\"stride\"], element0(a[\"id\"])[1], where(st)))\n\n def epd(d):\n if \"game\" in d:\n return EPD(d[\"game\"])\n if \"param\" in d:\n return EPD(self.graph_function(d[\"param\"])._fargs[d[\"index\"]].getValueAddr())\n if \"var\" in d:\n return EPD(self.var(d[\"var\"], st).get().getValueAddr())\n if \"data\" in d:\n if d[\"data\"] not in self.graph_data:\n self.graph_data[d[\"data\"]] = Db(4)\n return EPD(self.graph_data[d[\"data\"]])\n if \"element\" in d:\n return element0(d[\"element\"])[0]\n if \"getter\" in d:\n from eudplib.core.eudstruct.vararray import _getter, _getter_dst\n return _getter_dst if d[\"getter\"] == \"dest\" else EPD(_getter) + 1\n if \"temp\" in d:\n return EPD(graph_temp(d[\"temp\"]).getValueAddr())\n if \"counter\" in d:\n # The offset this side keeps beside a counter an array is indexed by, or (it keeps none) a dword nothing reads.\n place = self.places.get(d[\"counter\"])\n if place is None:\n if (\"junk\",) not in self.graph_data:\n self.graph_data[(\"junk\",)] = Db(4)\n return EPD(self.graph_data[(\"junk\",)])\n return EPD((place.bytes if d[\"part\"] == \"bytes\" else place.dwords).getValueAddr())\n base, f = labels[d[\"at\"]], d[\"field\"]\n if f == \"next\":\n return EPD(base + 4)\n if \"cond\" in f:\n return EPD(base + COND_VALUE + 20 * f[\"cond\"] - (4 if f.get(\"part\") == \"player\" else 0))\n return EPD(base + ACT_DEST - 16 + 32 * f[\"act\"] + {\"value\": 20, \"dest\": 16, \"modifier\": 24}[f[\"part\"]])\n\n def value(v):\n if isinstance(v, dict):\n if \"label\" in v:\n return labels[v[\"label\"]]\n if \"epd\" in v:\n return epd(v[\"epd\"])\n if \"addrOf\" in v:\n # (The first cell of an EUDVArray, a trigger: a jump into cell i goes 72 i bytes on.)\n return unProxy(self.array(v[\"addrOf\"], st).store) + v[\"plus\"]\n return element0(v[\"epdOf\"])[0] + v[\"plus\"]\n return v\n\n def graph_temp(name):\n \"\"\"A scratch cell of the graph's: a variable of this side's, one for each name (written before every read\n of it, so no initial value).\"\"\"\n key = (\"temp\", name)\n if key not in self.graph_data:\n self.graph_data[key] = EUDVariable()\n return self.graph_data[key]\n\n def copy(id_, into, top=None):\n \"\"\"The variable's cell into each dword (times its `scale`, plus its `add`): routed reads, a multiple worked\n out once for all the dwords that take it. `@name`: a scratch cell of the graph's. `top`: the most the value\n can be (value ranges), a multiple then worked out over only its bits.\"\"\"\n v = graph_temp(id_[1:]) if id_.startswith(\"@\") else self.var(id_, st).get()\n times = {1: v}\n for e in into:\n k = int(e.get(\"scale\", 1)) & U32\n if k not in times:\n # (A power of two is a shift, a bit a trigger; another multiple eudplib's product by a constant.)\n if top is not None and k != U32:\n times[k] = mul_bits(v, top, k)\n else:\n times[k] = fresh(0 - v) if k == U32 else shift_const(v, \"<<\", k.bit_length() - 1, top) if k & (k - 1) == 0 else fresh(v * k)\n for i in range(0, len(into), 16):\n group = []\n for e in into[i:i + 16]:\n d = epd(e[\"into\"] if \"into\" in e else e[\"dword\"])\n group.append((d, SetTo, times[int(e.get(\"scale\", 1)) & U32]))\n if e[\"add\"]:\n group.append((d, Add, value(e[\"add\"])))\n SeqCompute(group)\n\n entries = {}\n for e in g[\"entries\"]:\n entries.setdefault(e[\"from\"], []).append(e)\n for id_, es in entries.items():\n copy(id_, es)\n origins = globals().get(\"_ORIGINS\")\n for t in g[\"triggers\"]:\n # (With the map of triggers on, each trigger says the line of the statement it comes from.)\n if origins is not None and t.get(\"at\"):\n origins.append(_origin_of(t[\"at\"]))\n try:\n self.graph_trigger(t, labels, epd, value, copy, ctx)\n finally:\n if origins is not None and t.get(\"at\"):\n origins.pop()\n end << NextTrigger()\n\n def graph_trigger(self, t, labels, epd, value, copy, ctx):\n \"\"\"One trigger of a graph, or what stands in its place (a copy, an island, a call).\"\"\"\n if \"copy\" in t:\n # (In place of a trigger: the walk goes on to the one made after it.)\n labels[t[\"label\"]] << NextTrigger()\n if t[\"copy\"][\"into\"]:\n copy(t[\"copy\"][\"from\"], t[\"copy\"][\"into\"], t[\"copy\"].get(\"top\"))\n return\n if \"island\" in t:\n # A statement the emitter left to this side, lowered here as anywhere (nothing in it leaves it).\n labels[t[\"label\"]] << NextTrigger()\n self.statement(t[\"island\"], ctx)\n return\n if \"call\" in t:\n # A fixed-point function, its parameters written by the triggers before: it goes back to the trigger\n # after this one, its result written where the graph says (an EPD, as a call's return is).\n f, back = self.graph_function(t[\"call\"]), Forward()\n labels[t[\"label\"]] << RawTrigger(nextptr=f._fstart, actions=[SetMemory(f._nptr, SetTo, back), SetMemoryEPD(EPD(f._frets[0].getDestAddr()), SetTo, epd(t[\"call\"][\"into\"]))])\n back << NextTrigger()\n return\n conds = [MemoryXEPD(epd(c[\"dword\"]), self.GRAPH_COMPARE[c[\"compare\"]], c[\"value\"], c[\"mask\"]) if \"mask\" in c else MemoryEPD(epd(c[\"dword\"]), self.GRAPH_COMPARE[c[\"compare\"]], c[\"value\"]) for c in t[\"conds\"]]\n acts = [SetMemoryXEPD(epd(a[\"dword\"]), self.GRAPH_MODIFIER[a[\"modifier\"]], value(a[\"value\"]), a[\"mask\"]) if \"mask\" in a else SetMemoryEPD(epd(a[\"dword\"]), self.GRAPH_MODIFIER[a[\"modifier\"]], value(a[\"value\"])) for a in t[\"acts\"]]\n if \"next\" in t:\n trg = RawTrigger(nextptr=labels[t[\"next\"]], conditions=conds, actions=acts)\n else:\n trg = RawTrigger(conditions=conds, actions=acts)\n labels[t[\"label\"]] << trg\n # (With the map of triggers on, the line of each action whose statement is not the trigger's.)\n made = globals().get(\"_ACT_ORIGINS\")\n if made is not None and any(a.get(\"at\") for a in t[\"acts\"]):\n mine = _origin_of(t.get(\"at\"))\n made[trg] = [_origin_of(a[\"at\"]) if a.get(\"at\") else mine for a in t[\"acts\"]]\n\n def graph_function(self, spec):\n \"\"\"The fixed-point function a graph calls (compiler/backend/graph.ts, FixedFn), its body made.\"\"\"\n fn, c = spec[\"fn\"], int(spec.get(\"c\", 0))\n if fn in (\"fmul_vars\", \"fdiv_vars\"):\n f = fixed_vars()[fn]\n elif fn == \"fdiv_by\":\n if (c, None) not in _CONST_FDIV:\n _CONST_FDIV[(c, None)] = make_const_fdiv(c, None)\n f = _CONST_FDIV[(c, None)]\n else:\n name = \"fmul\" if fn == \"fmul_by\" else \"fdiv\"\n if (name, c, None) not in _FIXED_BY_CONST:\n _FIXED_BY_CONST[(name, c, None)] = (make_fmul_by if name == \"fmul\" else make_fdiv_by_const)(c, 31)\n f = _FIXED_BY_CONST[(name, c, None)]\n if f._fstart is None:\n f._create_func_body()\n return f\n\n def try_(self, st, ctx):\n \"\"\"The body with its catch as the place a throw goes to; the catch after it, reached only that way.\"\"\"\n caught, end = Forward(), Forward()\n self.handlers.append(caught)\n try:\n reached = self.straight(st[\"body\"], ctx)\n finally:\n self.handlers.pop()\n if reached:\n EUDJump(end)\n caught << NextTrigger()\n self.straight(st[\"catch\"], ctx)\n end << NextTrigger()\n\n def handler(self, node):\n \"\"\"Where an error goes from here: the nearest catch, or the end of the called function the lowering is in.\"\"\"\n if not self.handlers:\n raise Fail(\"trigscript: an error with nothing to go to%s\" % where(node))\n return self.handlers[-1]\n\n def uncaught(self, st):\n \"\"\"The error in flight, said in red where the player sees the game's own faults, and given up.\"\"\"\n e = self.errors\n at = st.get(\"at\")\n self.print_({\"kind\": \"print\", \"to\": CURRENT_PLAYER, \"position\": \"chat\", \"at\": at, \"label\": st.get(\"label\", \"\"), \"parts\": [\n {\"kind\": \"text\", \"text\": \"\\x06TrigScript: uncaught \"},\n {\"kind\": \"value\", \"text\": {\"kind\": \"textOf\", \"array\": e[\"names\"], \"index\": {\"kind\": \"var\", \"id\": e[\"thrown\"]}, \"at\": at}},\n {\"kind\": \"text\", \"text\": \": \"},\n {\"kind\": \"value\", \"text\": {\"kind\": \"textVar\", \"id\": e[\"message\"]}},\n {\"kind\": \"text\", \"text\": \", line \"},\n {\"kind\": \"number\", \"expr\": {\"kind\": \"var\", \"id\": \"(error line)\"}},\n {\"kind\": \"text\", \"text\": \".\"},\n ]})\n self.var(e[\"thrown\"], st).set(0)\n\n def straight(self, statements, ctx):\n \"\"\"A statement list whose end may not be reached (a break inside): Leave stops it. True when the end was reached.\"\"\"\n try:\n self.block(statements, ctx)\n return True\n except Leave:\n return False\n\n def if_(self, st, ctx):\n else_l, end = Forward(), Forward()\n EUDJumpIfNot(self.cond(st[\"cond\"]), else_l)\n if self.straight(st[\"then\"], ctx):\n EUDJump(end)\n else_l << NextTrigger()\n if st.get(\"else\"):\n self.straight(st[\"else\"], ctx)\n end << NextTrigger()\n\n def while_(self, st, ctx):\n head, exit_ = Forward(), Forward()\n head << NextTrigger()\n if st.get(\"cond\") is not None:\n EUDJumpIfNot(self.cond(st[\"cond\"]), exit_)\n if self.straight(st[\"body\"], dict(ctx, **{\"break\": exit_, \"continue\": head})):\n EUDJump(head)\n exit_ << NextTrigger()\n\n def do_(self, st, ctx):\n body, check, exit_ = Forward(), Forward(), Forward()\n body << NextTrigger()\n self.straight(st[\"body\"], dict(ctx, **{\"break\": exit_, \"continue\": check}))\n check << NextTrigger()\n EUDJumpIf(self.cond(st[\"cond\"]), body)\n exit_ << NextTrigger()\n\n def unchecked_loop(self, st):\n \"\"\"For a loop `for (…; i < xs.length; …)` over a growing array at a counter, whose body changes no array's length\n and stores nothing into i: the arrays its body reads and writes at i, unchecked — xs itself (the condition has\n just compared), each other one under a flag set on the way in when it is at least as long as xs. None when\n the loop is not of that shape.\"\"\"\n c = st.get(\"cond\")\n if not isinstance(c, dict) or c.get(\"kind\") != \"compare\" or c.get(\"op\") != \"<\":\n return None\n left, right = c.get(\"left\", {}), c.get(\"right\", {})\n if left.get(\"kind\") != \"var\" or left.get(\"id\") not in self.places or right.get(\"kind\") != \"length\":\n return None\n own = self.arrays.get(right.get(\"array\"))\n if type(own) is not ListStorage or not own.placeable():\n return None\n lists = {id_ for id_, a in self.arrays.items() if isinstance(a, ListStorage)}\n if not lengths_fixed_in(st[\"body\"], left[\"id\"], lists):\n return None\n out = {right[\"array\"]: True}\n for id_ in lists_at(st[\"body\"], left[\"id\"], lists):\n a = self.arrays[id_]\n if id_ in out or type(a) is not ListStorage or not a.placeable():\n continue\n flag = EUDVariable()\n flag << 0\n if EUDIf()(as_var(a.field(\"len\")) >= as_var(own.field(\"len\"))):\n flag << 1\n EUDEndIf()\n out[id_] = flag\n return left[\"id\"], out\n\n def for_(self, st, ctx):\n unchecked = self.unchecked_loop(st)\n cursor = self.cursors_for(st)\n flags = [] if unchecked is None else [f for f in unchecked[1].values() if f is not True]\n if unchecked is not None and cursor is not None and flags and len(flags) <= 16:\n # Unswitched: every other array the body reads at the counter found as long as the loop's own on the way in\n # (the usual case: a record's fields grow together) runs a copy of the loop that knows it, with no flag\n # tested at each access; otherwise the copy that tests them.\n self._copies = getattr(self, \"_copies\", 0) + 1\n try:\n if EUDIf()([f >= 1 for f in flags]):\n self.for_copy(st, ctx, (unchecked[0], {k: True for k in unchecked[1]}), True)\n if EUDElse()():\n self.for_copy(st, ctx, unchecked, True)\n EUDEndIf()\n finally:\n self._copies -= 1\n return\n self.for_copy(st, ctx, unchecked, cursor is not None)\n\n def for_copy(self, st, ctx, unchecked, cursors):\n \"\"\"One copy of a `for` loop: `unchecked` what `unchecked_loop` found (or the same with every flag known true),\n `cursors` whether its accesses at the counter go through registers (`Cursors`). An unswitched loop has two, each\n a region of its own for static routing, with a way in of its own.\"\"\"\n head, update, exit_ = Forward(), Forward(), Forward()\n region = None\n if ROUTE.enabled and ROUTE.on and ROUTE.stack and ROUTE.stack[-1].kind == \"loop\" and getattr(self, \"_copies\", 0):\n region = _Region(\"loop\")\n region.entry, region.after = Forward(), Forward()\n SetNextTrigger(region.entry)\n region.after << NextTrigger()\n ROUTE.loops.append(region)\n cursor = self.cursors_for(st) if cursors else None\n if cursor is not None:\n # The way in, which the registers' setting goes in front of once the body has said which there are.\n pre, pre_after = Forward(), Forward()\n SetNextTrigger(pre)\n pre_after << NextTrigger()\n self.cursors[cursor.counter] = cursor\n if region is not None:\n ROUTE.stack.append(region)\n try:\n if unchecked is not None:\n was = self.inside.get(unchecked[0])\n self.inside[unchecked[0]] = unchecked[1]\n try:\n self.for_body(st, ctx, head, update, exit_, cursor)\n finally:\n if was is None:\n self.inside.pop(unchecked[0], None)\n else:\n self.inside[unchecked[0]] = was\n else:\n self.for_body(st, ctx, head, update, exit_, cursor)\n finally:\n if region is not None:\n ROUTE.stack.pop()\n if cursor is not None:\n del self.cursors[cursor.counter]\n if cursor is not None:\n # Made out of the way of the code being made (what is pending is put aside and handed on after), and as\n # code in front of the loop, not in it.\n outer = ROUTE.stack.pop() if ROUTE.enabled and ROUTE.stack and ROUTE.stack[-1].kind == \"loop\" else None\n held = Forward()\n SetNextTrigger(held)\n if cursor.reads or cursor.writes:\n pre << NextTrigger()\n cursor.init()\n SetNextTrigger(pre_after)\n else:\n pre << pre_after\n held << NextTrigger()\n if outer is not None:\n ROUTE.stack.append(outer)\n\n def cursors_for(self, st):\n \"\"\"`Cursors` for a loop `for (…; …; i = i ± c)` at a counter with a place, when that store is its whole update and\n its body is as `lengths_fixed_in` wants it (no wait, no call, no length changed, nothing stored into i).\"\"\"\n upd = st.get(\"update\") or []\n if len(upd) != 1 or upd[0].get(\"kind\") != \"assign\":\n return None\n i = upd[0].get(\"target\")\n if i not in self.places:\n return None\n step = counter_step(upd[0].get(\"value\"), i)\n if step is None or step[0] != \"add\":\n return None\n lists = {a[\"id\"] for a in self.p.get(\"arrays\", []) if a.get(\"dynamic\") or a.get(\"through\")}\n if not lengths_fixed_in(st[\"body\"], i, lists):\n return None\n return Cursors(i, self.places[i], step[1])\n\n def cursor_at(self, index, a):\n \"\"\"The loop's `Cursors` an access at `index` of `a` can go through, or None: the index is a cursor loop's counter,\n and the array keeps its block where it is — a fixed one, or a growing one the loop found at least as long as its\n own (`inside`: known, or a flag set on the way in).\"\"\"\n if index.get(\"kind\") != \"var\":\n return None\n cur = self.cursors.get(index.get(\"id\"))\n if cur is None or self.places.get(cur.counter) is not cur.place:\n return None\n if type(a) is ArrayStorage and a.placeable():\n return cur\n if type(a) is ListStorage and a.placeable() and self.bound_of(index, a.decl[\"id\"]) is not None:\n return cur\n return None\n\n def cursor_read(self, cur, a, e):\n # (A read that always happens fills its temporary whole: it need not start from 0, as `fresh` does.)\n if type(a) is ArrayStorage:\n r = rng(e[\"index\"])\n if r is not None and r[0] >= 0 and r[1] <= a.length - 1:\n out = EUDVariable()\n cur.read(unProxy(a.store), out) # (inside, the ranges say)\n return out\n out = fresh(0)\n if EUDIf()(as_var(self.num(e[\"index\"])) <= a.length - 1):\n cur.read(unProxy(a.store), out)\n EUDEndIf()\n return out\n bound = self.bound_of(e[\"index\"], e[\"array\"])\n if bound is True:\n out = EUDVariable()\n cur.read(a.base[0], out)\n return out\n out = fresh(0)\n if EUDIf()(bound >= 1):\n cur.read(a.base[0], out)\n if EUDElseIf()(as_var(self.num(e[\"index\"])) < as_var(a.field(\"len\"))):\n varray_read(a.base[0], cur.place, out)\n EUDEndIf()\n return out\n\n def cursor_write(self, cur, a, st, value, modifier):\n if a.bits and modifier == SetTo:\n value = saturate(value, a.bits)\n v = fresh(value) if modifier == SetTo and not isinstance(value, int) else value\n if type(a) is ArrayStorage:\n r = rng(st[\"index\"])\n if r is not None and r[0] >= 0 and r[1] <= a.length - 1:\n cur.write(a.store._epd + VARRAY_VALUE // 4, v, modifier)\n else:\n if EUDIf()(as_var(self.num(st[\"index\"])) <= a.length - 1):\n cur.write(a.store._epd + VARRAY_VALUE // 4, v, modifier)\n EUDEndIf()\n return\n bound = self.bound_of(st[\"index\"], st[\"array\"])\n if bound is True:\n cur.write(a.base[1], v, modifier)\n return\n i, n = as_var(self.num(st[\"index\"])), as_var(a.field(\"len\"))\n if EUDIf()(bound >= 1):\n cur.write(a.base[1], v, modifier)\n if EUDElseIf()(i < n):\n varray_write(a.base[1], cur.place, v, modifier)\n if EUDElseIf()(i == n):\n a.push(v)\n EUDEndIf()\n\n def for_body(self, st, ctx, head, update, exit_, cursor=None):\n head << NextTrigger()\n if st.get(\"cond\") is not None:\n EUDJumpIfNot(self.cond(st[\"cond\"]), exit_)\n self.straight(st[\"body\"], dict(ctx, **{\"break\": exit_, \"continue\": update}))\n update << NextTrigger()\n if cursor is not None and self.cursor_update(st, cursor):\n EUDJump(head)\n elif self.straight(st[\"update\"], ctx):\n if cursor is not None:\n cursor.advance()\n EUDJump(head)\n exit_ << NextTrigger()\n\n def cursor_update(self, st, cursor):\n \"\"\"A cursor loop's update `i = i ± c` of a variable of the program's own: the counter, its place and every\n register moved on in one trigger, where they were three. False when the counter is kept otherwise (a row).\"\"\"\n target = self.vars.get(cursor.counter)\n if not isinstance(target, Storage) or target.rowed or target.bits:\n return False\n c = cursor.step\n acts = [target.store.AddNumber(c & U32), cursor.place.bytes.AddNumber((VARRAY_STRIDE * c) & U32), cursor.place.dwords.AddNumber((VARRAY_STRIDE // 4 * c) & U32)]\n acts += [SetMemory(t + 4, Add, (VARRAY_STRIDE * c) & U32) for t, _ in cursor.reads]\n acts += [SetMemory(act + 16, Add, (VARRAY_STRIDE // 4 * c) & U32) for act, _ in cursor.writes]\n DoActions(acts)\n return True\n\n def unrolled(self, st, ctx):\n exit_ = Forward()\n for statements in st[\"iterations\"]:\n nxt = Forward()\n self.straight(statements, dict(ctx, **{\"break\": exit_, \"continue\": nxt}))\n nxt << NextTrigger()\n exit_ << NextTrigger()\n\n def switch(self, st, ctx):\n v = as_var(self.num(st[\"value\"]))\n exit_ = Forward()\n labels = [Forward() for _ in st[\"cases\"]]\n default = None\n for c, label in zip(st[\"cases\"], labels):\n if c[\"value\"] is None:\n default = label\n continue\n n = c[\"value\"]\n if n != n or n < -SIGN or n > U32: # NaN, or a value the variable never holds\n continue\n EUDJumpIf(v == int(n) & U32, label)\n EUDJump(default if default is not None else exit_)\n for c, label in zip(st[\"cases\"], labels):\n label << NextTrigger()\n self.straight(c[\"body\"], dict(ctx, **{\"break\": exit_}))\n exit_ << NextTrigger()\n\n def sleep(self, st):\n if self.in_function:\n raise Fail(\"trigscript: sleep inside a function that is called%s\" % where(st))\n if st.get(\"cycles\") is not None:\n frames = int(st[\"cycles\"])\n else:\n frames = max(1, int(round(float(st.get(\"ms\", 0)) * FRAMES_PER_SECOND / 1000)))\n co = self.co\n index, resume = co.resume_point()\n # The frame after this one is one frame later: sleep(frames(1)) goes on in the next frame, as the\n # simulator has it. (Until 3.4 the wait was one frame longer, so a loop sleeping a frame ran every other.)\n co.park(index, frames - 1)\n resume << NextTrigger()\n\n def parallel(self, st):\n \"\"\"Promise.all / race: each branch a coroutine of its own, started here and run from here, in order, every frame\n the program waits at this statement; the program goes on when every branch has ended (all) or the first has\n (race, which leaves the others where they are). The program waits here through a place of its own to go on\n from, which this same statement is: the first frame falls into it, the later ones jump to it.\"\"\"\n if self.in_function:\n raise Fail(\"trigscript: await inside a function that is called%s\" % where(st))\n parent = self.co\n kids = [Coroutine(self, self.rows()) for _ in st[\"branches\"]]\n for k in kids:\n k.set_state(0)\n k.set_wait(0)\n index, again = parent.resume_point()\n again << NextTrigger()\n after = Forward()\n for k, body in zip(kids, st[\"branches\"]):\n skip = Forward()\n EUDJumpIf(as_var(k.get_state()) == DONE, skip)\n self.coroutine(k, body)\n if st[\"mode\"] == \"race\":\n EUDJumpIfNot(as_var(k.get_state()) == DONE, skip)\n for other in kids:\n if other is not k:\n other.set_state(DONE)\n EUDJump(after)\n skip << NextTrigger()\n if st[\"mode\"] == \"all\":\n EUDJumpIf([as_var(k.get_state()) == DONE for k in kids], after)\n parent.park(index, 0)\n after << NextTrigger()\n\n def action(self, st):\n r = st[\"record\"]\n fields = dict(locid1=r[\"location\"], strid=string_of(r[\"text\"]), wavid=string_of(r[\"wav\"]), time=r[\"time\"], player1=r[\"player\"], player2=r[\"target\"], unitid=r[\"unitId\"], acttype=r[\"type\"], amount=r[\"modifier\"], flags=r[\"flags\"])\n shown = None\n if st.get(\"text\") is not None:\n if self.has_id(st[\"text\"]):\n fields[\"strid\"] = self.text_id_of(st[\"text\"])\n else:\n kind = SLOT_OF_ACTION.get(int(r[\"type\"]))\n if kind is None:\n raise Fail(\"trigscript: this action's text cannot be one that is made%s\" % where(st))\n # Over the string kept for this kind of field - on the computer of the player the action is for, which is\n # the one the program is running as: the game reads the string again whenever it draws, and another\n # player's text must not be there when it does. Nothing of the script can read the string back.\n shown = self.text(st[\"text\"])\n if EUDIf()(IsUserCP()):\n texts()[\"show\"](GetMapStringAddr(slot(kind)), shown.addr)\n EUDEndIf()\n self.used(shown)\n fields[\"strid\"] = slot(kind)\n count = None\n for variable in st.get(\"variables\") or []:\n value = as_var(self.num(variable[\"expr\"]))\n field = variable[\"field\"]\n if field == \"modifier\":\n # A unit count: the byte field is not a variable's place, so the action is done once per\n # unit — as many as the variable says, 0 being none (in the record, 0 means \"all\").\n count = value\n continue\n name = {\"target\": \"player2\", \"time\": \"time\", \"player\": \"player1\", \"location\": \"locid1\", \"text\": \"strid\", \"wav\": \"wavid\", \"unitId\": \"unitid\"}.get(field)\n if name is None:\n raise Fail(\"trigscript: no variable can stand in the %s field%s\" % (field, where(st)))\n if name == \"unitid\":\n # A unit type the game has: past the table, an action reads what is not a unit.\n value = fresh(value)\n if EUDIf()(value >= 228):\n value << 228\n EUDEndIf()\n fields[name] = value\n moves = int(r[\"type\"]) in MOVES_UNITS\n if count is None:\n DoActions(Action(**fields))\n if moves:\n forget_counts()\n return\n fields[\"amount\"] = 1\n for _ in EUDLoopRange(0, count):\n DoActions(Action(**fields))\n if moves:\n forget_counts()\n\n def print_(self, st):\n \"\"\"Text with values in it: every value first, then the text, shown only on the screen of the player it is for.\"\"\"\n args = []\n temps = []\n for part in st[\"parts\"]:\n k = part[\"kind\"]\n if k == \"text\":\n args.append(part[\"text\"])\n elif k == \"value\":\n v = self.text(part[\"text\"])\n temps.append(v)\n args.append(ptr2s(v.addr))\n elif k == \"number\" and part.get(\"fixed\"):\n args.extend(self.fixed_pieces(self.num(part[\"expr\"])))\n elif k == \"number\":\n v = self.num(part[\"expr\"])\n if isinstance(v, int):\n args.append(str(v if part.get(\"unsigned\") else signed(v)))\n elif part.get(\"unsigned\"):\n args.append(v)\n else:\n # A number below zero: its minus sign, then how far below it is.\n sign, size = fresh(NO_SIGN), fresh(v)\n if EUDIf()(size >= SIGN):\n sign << MINUS_SIGN\n size << 0 - size\n EUDEndIf()\n args.append(ptr2s(sign))\n args.append(size)\n elif k == \"name\":\n args.append(PName(self.one_player(part[\"player\"], st)))\n elif k == \"color\":\n args.append(PColor(self.one_player(part[\"player\"], st)))\n elif k == \"char\":\n args.append(self.char(part[\"expr\"]))\n else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n if not args:\n return\n # eudplib prints for the current player, and only on that player's own computer.\n show = (lambda: f_eprintln(*args)) if st.get(\"position\") == \"center\" else (lambda: GetGlobalStringBuffer().print(*args))\n to = int(st.get(\"to\", CURRENT_PLAYER))\n if to == CURRENT_PLAYER:\n show()\n self.used(*temps)\n return\n for p in group_slots(to, st):\n f_setcurpl(p)\n show()\n f_setcurpl(self.current())\n self.used(*temps)\n\n # ── texts ──\n def has_id(self, e):\n \"\"\"Whether a text is one of the built map's table whatever happens in the game (compiler/ir.ts#textHasId).\"\"\"\n k = e[\"kind\"]\n if k in (\"text\", \"textOf\"):\n return True\n if k == \"textVar\":\n return not isinstance(self.var(e[\"id\"], e), TextStorage)\n if k == \"textTernary\":\n return self.has_id(e[\"whenTrue\"]) and self.has_id(e[\"whenFalse\"])\n return False\n\n def text_id_of(self, e):\n \"\"\"The id of a text that has one: an int, or a variable.\"\"\"\n k = e[\"kind\"]\n if k == \"text\":\n return text_id(e[\"text\"])\n if k == \"textVar\":\n return self.var(e[\"id\"], e).get()\n if k == \"textOf\":\n return self.array(e[\"array\"], e).get(self.num(e[\"index\"]))\n if k == \"textTernary\":\n t = EUDVariable()\n if EUDIf()(self.cond(e[\"cond\"])):\n t << self.text_id_of(e[\"whenTrue\"])\n if EUDElse()():\n t << self.text_id_of(e[\"whenFalse\"])\n EUDEndIf()\n return t\n raise Fail(\"trigscript: this text has no id%s\" % where(e))\n\n def used(self, *values):\n \"\"\"Values that have been used: a block that was the value's own goes back to the heap.\"\"\"\n for v in values:\n if v.taken and not isinstance(v.block, int):\n texts()[\"release\"](v.block)\n\n def texts_of(self, exprs):\n \"\"\"Several texts in the order they are written. A variable's text is only looked at - unless working a later\n one out runs a call, which may give that variable another text and its block back to the heap: then it is a copy.\"\"\"\n out = []\n for i, e in enumerate(exprs):\n v = self.text(e)\n if not v.taken and any(has_call(x) for x in exprs[i + 1:]):\n v = self.own(v)\n out.append(v)\n return out\n\n def own(self, v):\n \"\"\"A value as something to keep: its own block as it is, a copy of a variable's.\"\"\"\n if v.taken:\n return v\n addr, block, length = texts()[\"copy\"](v.addr, v.block, v.length())\n return TextVal(addr, block, length, True)\n\n def store_text(self, st):\n \"\"\"A text into cell i of three arrays (a row's): worked out first - it may be made from what the cells hold -\n then the block they held goes back, then they take the text, a copy of one that was only looked at.\"\"\"\n v = self.own(self.text(st[\"value\"]))\n addr, block, length = fresh(v.addr), fresh(v.block), fresh(v.length())\n i = fresh(self.num(st[\"index\"]))\n held = self.array(st[\"block\"], st)\n texts()[\"release\"](as_var(held.get(i)))\n self.array(st[\"addr\"], st).set(i, addr)\n held.set(i, block)\n self.array(st[\"chars\"], st).set(i, length)\n\n def put(self, storage, e, kind):\n \"\"\"A value into a variable. A text is worked out first - it may be made from what the variable holds - and only then does the block the variable held go back.\"\"\"\n if kind != \"text\":\n storage.set(self.value(e, kind))\n return\n if not isinstance(storage, TextStorage):\n storage.set(self.text_id_of(e))\n return\n v = self.own(self.text(e))\n addr, block, length = fresh(v.addr), fresh(v.block), fresh(v.length())\n texts()[\"release\"](storage.cell(storage.block))\n storage.write(addr, block, length)\n\n def char(self, e):\n \"\"\"One byte from a number, as something to print: a colour code chosen when the map is played; 0 is nothing.\"\"\"\n v = self.num(e)\n if isinstance(v, int):\n return chr(v & 0xFF) if v & 0xFF else \"\"\n one = Db(4)\n f_bwrite(one, v & 0xFF)\n return ptr2s(one)\n\n def written(self, parts, st):\n \"\"\"The parts of a text in the scratch, and where it ends. Every value is worked out first, since working one\n out may use the scratch itself; then the parts are written a group at a time, a group being what cannot\n come to more than a text holds, so that checking where the writing has got to between groups keeps it inside.\"\"\"\n T = texts()\n items, temps = self.items(parts, st)\n pos = fresh(T[\"scratch\"])\n f_bwrite(pos, 0)\n self.write_items(items, pos, T[\"scratch\"] + TEXT_BYTES, lambda at, value: T[\"append\"](at, value.addr), True)\n return pos, temps\n\n def build_text(self, st):\n \"\"\"Parts written onto the end of a buffer (emptied first on `reset`): a text put together a piece at a time.\"\"\"\n data, pos = buffer(st[\"buffer\"])\n B = builds()\n items, temps = self.items(st[\"parts\"], st)\n if st.get(\"reset\"):\n pos << data\n f_bwrite(data, 0)\n last = data + BUILD_BYTES + TEXT_BYTES\n self.write_items(items, pos, data + BUILD_BYTES, lambda at, value: B[\"append\"](at, value.addr, last), False)\n pos << B[\"cut\"](pos, data + BUILD_BYTES)\n self.used(*temps)\n\n def items(self, parts, st):\n \"\"\"The parts of a text as what f_dbstr_print takes, each with the most bytes it can come to; a value of the\n program as itself, to be copied in. Every value is worked out here, before anything is written.\"\"\"\n items, temps = [], []\n for part in parts:\n k = part[\"kind\"]\n if k == \"text\":\n data = part[\"text\"].encode(\"utf-8\")[:TEXT_BYTES].decode(\"utf-8\", \"ignore\")\n items.append(([data], len(data.encode(\"utf-8\")), None))\n elif k == \"number\" and part.get(\"fixed\"):\n pieces = self.fixed_pieces(self.num(part[\"expr\"]))\n items.append((pieces, 11 if len(pieces) > 1 else len(pieces[0]), None))\n elif k == \"number\":\n v = self.num(part[\"expr\"])\n if isinstance(v, int):\n data = str(v if part.get(\"unsigned\") else signed(v))\n items.append(([data], len(data), None))\n elif part.get(\"unsigned\"):\n items.append(([fresh(v)], 10, None))\n else:\n sign, size = fresh(NO_SIGN), fresh(v)\n if EUDIf()(size >= SIGN):\n sign << MINUS_SIGN\n size << 0 - size\n EUDEndIf()\n items.append(([ptr2s(sign), size], 11, None))\n elif k == \"name\":\n items.append(([PName(self.one_player(part[\"player\"], st))], 25, None))\n elif k == \"color\":\n items.append(([PColor(self.one_player(part[\"player\"], st))], 1, None))\n elif k == \"value\":\n v = self.text(part[\"text\"])\n if not v.taken and has_call(parts[parts.index(part) + 1:]):\n v = self.own(v)\n temps.append(v)\n items.append((None, TEXT_BYTES, v))\n elif k == \"char\":\n c = self.char(part[\"expr\"])\n if c != \"\":\n items.append(([c], 1, None))\n else:\n raise Fail(\"trigscript: unknown text part %r%s\" % (k, where(st)))\n return items, temps\n\n def write_items(self, items, pos, end, append, first_free):\n \"\"\"Items written at pos, which moves to where the text ends: a group at a time, a group being what cannot come\n to more than a text holds, so that checking pos against `end` between groups keeps the writing inside the room\n behind it. `first_free`: pos starts at the beginning, so the first group needs no check.\"\"\"\n group, room, first = [], 0, [first_free]\n\n def flush():\n if not group:\n return\n if first[0]:\n pos << f_dbstr_print(pos, *group)\n else:\n if EUDIf()(pos <= end):\n pos << f_dbstr_print(pos, *group)\n EUDEndIf()\n first[0] = False\n del group[:]\n\n for args, size, value in items:\n if value is not None:\n flush()\n room = 0\n if first[0]:\n pos << append(pos, value)\n else:\n if EUDIf()(pos <= end):\n pos << append(pos, value)\n EUDEndIf()\n first[0] = False\n continue\n if room + size > TEXT_BYTES + 1:\n flush()\n room = 0\n group.extend(args)\n room += size\n flush()\n\n def text(self, e):\n \"\"\"A text: where it is, its block, its length (TextVal).\"\"\"\n T = texts()\n k = e[\"kind\"]\n if k == \"text\":\n return TextVal(fresh(GetMapStringAddr(e[\"text\"]) if e[\"text\"] else T[\"empty\"]), 0, len(e[\"text\"]), True)\n if k == \"textVar\" and isinstance(self.var(e[\"id\"], e), TextStorage):\n return self.var(e[\"id\"], e).get()\n if k == \"textAt\":\n # Cells that were never given a text are 0: the empty one, as a variable's are.\n i = fresh(self.num(e[\"index\"]))\n addr = fresh(self.array(e[\"addr\"], e).get(i))\n if EUDIf()(addr == 0):\n addr << T[\"empty\"]\n EUDEndIf()\n return TextVal(addr, fresh(self.array(e[\"block\"], e).get(i)), fresh(self.array(e[\"chars\"], e).get(i)), False)\n if self.has_id(e):\n return TextVal(T[\"address\"](as_var(self.text_id_of(e))), 0, None, True)\n if k == \"built\":\n # Only looked at: what keeps it takes a copy, since the buffer is built again next time.\n return TextVal(fresh(buffer(e[\"buffer\"])[0]), 0, None, False)\n if k == \"template\":\n pos, temps = self.written(e[\"parts\"], e)\n addr, block, length = T[\"make\"](pos)\n self.used(*temps)\n return TextVal(addr, block, length, True)\n if k == \"textTernary\":\n addr, block, length = EUDVariable(), EUDVariable(), EUDVariable()\n if EUDIf()(self.cond(e[\"cond\"])):\n v = self.own(self.text(e[\"whenTrue\"]))\n addr << v.addr\n block << v.block\n length << v.length()\n if EUDElse()():\n v = self.own(self.text(e[\"whenFalse\"]))\n addr << v.addr\n block << v.block\n length << v.length()\n EUDEndIf()\n return TextVal(addr, block, length, True)\n if k == \"textSlice\":\n of = self.text(e[\"of\"])\n start = self.num(e[\"start\"]) if e.get(\"start\") is not None else 0\n end = self.num(e[\"end\"]) if e.get(\"end\") is not None else 0x7FFFFFFF\n addr, block, length = T[\"slice\"](of.addr, start, end)\n self.used(of)\n return TextVal(addr, block, length, True)\n if k == \"textPad\":\n of, fill = self.texts_of([e[\"of\"], e[\"with\"]])\n width = self.num(e[\"width\"])\n addr, block, length = T[\"pad\"](of.addr, of.length(), width, fill.addr, 1 if e[\"side\"] == \"start\" else 0)\n self.used(of, fill)\n return TextVal(addr, block, length, True)\n if k == \"textRepeat\":\n of = self.text(e[\"of\"])\n addr, block, length = T[\"repeat\"](of.addr, self.num(e[\"count\"]))\n self.used(of)\n return TextVal(addr, block, length, True)\n if k == \"textCall\":\n self.call(e[\"call\"])\n result = e[\"call\"].get(\"result\")\n if result is None:\n return TextVal(fresh(T[\"empty\"]), 0, 0, True)\n return self.var(result[\"decl\"][\"id\"], e).take_out()\n raise Fail(\"trigscript: unknown text %r%s\" % (k, where(e)))\n\n def text_number(self, e):\n T = texts()\n k = e[\"kind\"]\n if k == \"textIndexOf\":\n of, find = self.texts_of([e[\"of\"], e[\"find\"]])\n start = self.num(e[\"from\"]) if e.get(\"from\") is not None else 0\n out = T[\"find\"](of.addr, find.addr, start)\n self.used(of, find)\n return out\n of = self.text(e[\"of\"])\n if k == \"textLength\":\n n = of.length()\n out = n if isinstance(n, int) else fresh(n)\n self.used(of)\n return out\n out = T[\"code\"](of.addr, self.num(e[\"index\"]))\n self.used(of)\n return out\n\n def text_truth(self, e):\n \"\"\"A comparison of two texts, or a test of one, as 0 / 1.\"\"\"\n T = texts()\n t = fresh(0)\n if e[\"kind\"] == \"textTest\":\n of, find = self.texts_of([e[\"of\"], e[\"find\"]])\n if e[\"test\"] == \"startsWith\":\n t << T[\"starts\"](of.addr, find.addr)\n elif e[\"test\"] == \"endsWith\":\n t << T[\"ends\"](of.addr, find.addr)\n else:\n if EUDIfNot()(T[\"find\"](of.addr, find.addr, 0) == 0xFFFFFFFF):\n t << 1\n EUDEndIf()\n self.used(of, find)\n return t\n op = e[\"op\"]\n if op in (\"==\", \"!=\") and self.has_id(e[\"left\"]) and self.has_id(e[\"right\"]):\n # Two texts of the table: the same text is the same id.\n a, b = self.text_id_of(e[\"left\"]), self.text_id_of(e[\"right\"])\n if isinstance(a, int) and isinstance(b, int):\n return 1 if (a == b) == (op == \"==\") else 0\n if EUDIf()(as_var(a) == b):\n t << 1\n EUDEndIf()\n else:\n a, b = self.texts_of([e[\"left\"], e[\"right\"]])\n # The difference of the first bytes that differ, as a number of 32 bits: 0, below zero, or above.\n d = f_strcmp(a.addr, b.addr)\n self.used(a, b)\n if op in (\"==\", \"!=\"):\n if EUDIf()(d == 0):\n t << 1\n EUDEndIf()\n else:\n below = (lambda: d >= SIGN)\n if op in (\"<\", \">=\"):\n if EUDIf()(below()):\n t << 1\n EUDEndIf()\n else: # \"<=\" and \">\": whether it is not above\n if EUDIf()(EUDOr(d == 0, below())):\n t << 1\n EUDEndIf()\n return flipped(t) if op in (\">=\", \">\") else t\n return flipped(t) if op == \"!=\" else t\n\n def text_loop(self, st, ctx):\n \"\"\"`for (const ch of s)`: the text walked once, a character a turn, each a made text of its own.\"\"\"\n T = texts()\n turn = self.declare(st[\"decl\"])\n over = self.own(self.text(st[\"of\"]))\n held = fresh(over.block)\n at = fresh(over.addr)\n head, done = Forward(), Forward()\n inner = dict(ctx)\n inner[\"break\"], inner[\"continue\"] = done, head\n leave = None\n if ctx.get(\"fn\") is not None:\n # A return inside the loop leaves through here, so that the text the loop holds goes back first.\n leave = Forward()\n inner[\"fn\"] = dict(ctx[\"fn\"], end=leave)\n head << NextTrigger()\n n = T[\"size\"](at)\n EUDJumpIf(n == 0, done)\n f_memcpy(T[\"scratch\"], at, n)\n f_bwrite(T[\"scratch\"] + n, 0)\n addr, block, length = T[\"make\"](T[\"scratch\"] + n)\n T[\"release\"](turn.cell(turn.block))\n turn.write(addr, block, length)\n at += n\n # An error out of the body leaves through here too (B4), so that the text the loop holds goes back first.\n unwind = Forward() if self.handlers else None\n if unwind is not None:\n outer_handler = self.handlers[-1]\n self.handlers.append(unwind)\n try:\n if self.straight(st[\"body\"], inner):\n EUDJump(head)\n finally:\n if unwind is not None:\n self.handlers.pop()\n done << NextTrigger()\n T[\"release\"](held)\n if unwind is not None:\n past = Forward()\n EUDJump(past)\n unwind << NextTrigger()\n T[\"release\"](held)\n EUDJump(outer_handler)\n past << NextTrigger()\n if leave is not None:\n after = Forward()\n EUDJump(after)\n leave << NextTrigger()\n T[\"release\"](held)\n EUDJump(ctx[\"fn\"][\"end\"])\n after << NextTrigger()\n\n def value(self, e, kind):\n \"\"\"An expression as what a variable of `kind` holds.\"\"\"\n return self.num(e) if kind == \"number\" else self.unit(e) if kind == \"unit\" else self.truth(e)\n\n def function(self, id_, node):\n \"\"\"A called function: its parameters' cells, its result's, and its one body — an EUDFunc of no\n arguments, since the cells are the program's own (a row a player, as any variable is).\"\"\"\n made = self.made.get(id_)\n if made is not None:\n return made\n f = self.functions.get(id_)\n if f is None:\n raise Fail(\"trigscript: unknown function %r%s\" % (id_, where(node)))\n params = [self.declare(d) for d in f[\"params\"]]\n kind = f[\"result\"][\"kind\"] if f.get(\"result\") else \"void\"\n result = self.declare(f[\"result\"][\"decl\"]) if f.get(\"result\") else None\n lowering = self\n if f.get(\"recursive\"):\n return self.recursive_function(id_, f, params, kind, result)\n\n @EUDFunc\n def body():\n # A text result keeps what it held until `return` puts the next one in it, which is when that block goes back.\n if result is not None and kind != \"text\":\n result.set(NO_UNIT if kind == \"unit\" else 0)\n end = Forward()\n lowering.in_function += 1\n outer = lowering.handlers\n lowering.handlers = [end]\n try:\n lowering.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n lowering.in_function -= 1\n lowering.handlers = outer\n end << NextTrigger()\n\n made = self.made[id_] = (body, params, result)\n return made\n\n def recursive_function(self, id_, f, params, kind, result):\n \"\"\"A function that calls itself. eudplib's EUDFunc keeps one return address and is not there to be called\n until its body is whole, so this one is triggers of our own: the body in a scope apart, ended by a trigger\n whose next-trigger field is the return address - a cell a call writes, and a frame keeps. It is on record\n before its body is lowered, so a call of it met in that body finds it.\"\"\"\n start, tail = Forward(), Forward()\n # The return address a second time, in a variable: a frame keeps it from there, since reading a cell of\n # the game's memory back costs some thirty triggers and writing a variable out costs two.\n back = EUDVariable()\n\n def body():\n ROUTE.unknown_call()\n after = Forward()\n DoActions([SetNextPtr(tail, after), back.SetNumber(after)])\n EUDJump(start)\n after << NextTrigger()\n\n made = self.made[id_] = (body, params, result)\n entry = {\"f\": f, \"tail\": tail, \"back\": back}\n PushTriggerScope()\n start << NextTrigger()\n if result is not None and kind != \"text\":\n result.set(NO_UNIT if kind == \"unit\" else 0)\n end = Forward()\n self.in_function += 1\n self.within.append(entry)\n outer = self.handlers\n self.handlers = [end]\n try:\n self.straight(f[\"body\"], {\"fn\": {\"result\": result, \"kind\": kind, \"end\": end}})\n finally:\n self.within.pop()\n self.in_function -= 1\n self.handlers = outer\n end << NextTrigger()\n tail << RawTrigger()\n PopTriggerScope()\n return made\n\n def saved_cells(self, saves, node):\n \"\"\"What a frame keeps, as (read, write) pairs a cell: the variables, a unit three cells, an array's handle four.\"\"\"\n pairs = []\n for id_ in saves.get(\"vars\", []):\n s = self.vars.get(id_)\n if s is None:\n # Declared further down the body: it has no cell yet, and the declaration that makes it sets it.\n continue\n if isinstance(s, UnitStorage):\n for cell in (s.ptr, s.epd, s.uid):\n pairs.append(self.cell_pair(cell, s.player_of if s.rowed else None))\n elif isinstance(s, TextStorage):\n for cell in (s.addr, s.block, s.len):\n pairs.append(self.cell_pair(cell, s.player_of if s.rowed else None))\n elif isinstance(s, Storage):\n pairs.append(self.cell_pair(s.store, s.player_of if s.rowed else None))\n for id_ in saves.get(\"arrays\", []):\n a = self.array(id_, node)\n if isinstance(a, ListStorage):\n for name in HANDLE:\n pairs.append(self.cell_pair(a.handle[name], a.player_of if a.rowed else None))\n return pairs\n\n def saved_texts(self, saves):\n \"\"\"The made texts among what a frame keeps: each owns a block, which the frame keeps for it.\"\"\"\n return [s for s in (self.vars.get(id_) for id_ in saves.get(\"vars\", [])) if isinstance(s, TextStorage)]\n\n def cell_pair(self, cell, rows):\n if rows:\n return (lambda: cell[rows()]), (lambda v: cell.__setitem__(rows(), v))\n return (lambda: cell), (lambda v: cell << v)\n\n def keep(self, call):\n \"\"\"Before a call that may come back into the function it is in: that function's cells and its return\n address onto the stack, the handles of its arrays set to \"no block\". Past the depth the map allows the\n program says so and stops for good.\"\"\"\n saves = call[\"saves\"]\n st = stack()\n if not self.within:\n raise Fail(\"trigscript: a call keeps a frame outside a function that calls itself%s\" % where(call))\n mine = self.within[-1]\n if self.errors:\n # B4: a RangeError, as JavaScript's, where the call is and before anything of it is kept: the function this\n # call is in returns with it in flight.\n if EUDIf()(st[\"depth\"] >= STACK_DEPTH):\n self.put(self.var(self.errors[\"message\"], call), {\"kind\": \"text\", \"text\": STACK_OVERFLOW_MESSAGE}, \"text\")\n self.var(\"(error line)\").set(int((call.get(\"at\") or {}).get(\"line\", 0)))\n self.var(self.errors[\"thrown\"], call).set(RANGE_ERROR)\n EUDJump(self.handler(call))\n elif EUDIf()(st[\"depth\"] >= STACK_DEPTH):\n GetGlobalStringBuffer().print(\"\\x06TrigScript: stack overflow in %s, line %s - %d calls deep. The program has stopped.\" % (saves.get(\"within\", \"a function\"), (call.get(\"at\") or {}).get(\"line\", \"?\"), STACK_DEPTH))\n st[\"depth\"] << 0\n st[\"at\"] << st[\"base\"]\n # The program stops for good, or, in a task, the task does.\n stopped = self.pool.co if self.pool else self.top\n stopped.set_state(DONE)\n EUDJump(stopped.frame_end)\n EUDEndIf()\n pairs = self.saved_cells(saves, call)\n at = st[\"at\"]\n f_dwwrite_epd(at, mine[\"back\"])\n at += 1\n for read, _ in pairs:\n f_dwwrite_epd(at, read())\n at += 1\n st[\"depth\"] += 1\n for id_ in saves.get(\"arrays\", []):\n a = self.array(id_, call)\n if isinstance(a, ListStorage):\n for name in HANDLE:\n a.put(name, 0)\n # A text the function holds keeps its block on the stack; the variable holds none for the length of the call,\n # so that the inner run's first text does not give the outer run's block back.\n held = self.saved_texts(saves)\n for s in held:\n s.write(0, 0, 0)\n return pairs, mine, held\n\n def bring_back(self, call, kept):\n pairs, mine, held = kept\n st = stack()\n # The block the inner run left in a text goes back to the heap before the text is the outer run's again.\n for s in held:\n texts()[\"release\"](s.cell(s.block))\n # The block the inner run left in a handle goes back to the heap before the handle is the outer run's again.\n for id_ in call[\"saves\"].get(\"arrays\", []):\n a = self.array(id_, call)\n if isinstance(a, ListStorage):\n ptr = fresh(a.field(\"ptr\"))\n if EUDIf()(ptr >= 1):\n a.heap[\"give\"](ptr, a.field(\"k\"))\n EUDEndIf()\n at = st[\"at\"]\n for _, write in reversed(pairs):\n at -= 1\n write(f_dwread_epd(at))\n at -= 1\n mine[\"back\"] << f_dwread_epd(at)\n f_dwwrite_epd(EPD(mine[\"tail\"]) + 1, mine[\"back\"])\n st[\"depth\"] -= 1\n\n def call_function(self, call, result):\n body, params, returned = self.function(call[\"fn\"], call)\n if len(params) != len(call[\"params\"]):\n raise Fail(\"trigscript: %s takes %d values, not %d%s\" % (call[\"fn\"], len(params), len(call[\"params\"]), where(call)))\n # Every argument first, then the parameters: an argument may be a call of this same function. A variable's\n # own cell is copied when a later argument holds a call, which may be of a function that writes that variable.\n values = []\n for i, p in enumerate(call[\"params\"]):\n if p[\"decl\"][\"kind\"] == \"text\":\n # A text is a copy of its own by the time the parameter takes it: three numbers, read now.\n t = self.own(self.text(p[\"init\"]))\n values.append((fresh(t.addr), fresh(t.block), fresh(t.length())))\n continue\n v = self.value(p[\"init\"], p[\"decl\"][\"kind\"])\n later = any(has_call(q[\"init\"]) for q in call[\"params\"][i + 1:])\n if later and p[\"decl\"][\"kind\"] != \"unit\" and not isinstance(v, int):\n v = fresh(v)\n values.append(v)\n # A call that may come back here keeps this function's frame: after the arguments are worked out (they read\n # it), before the parameters are set (they may be this function's own).\n kept = None\n if call.get(\"saves\"):\n # Copies: an argument may be one of this function's own parameters, which the ones set before it write over.\n values = [v if isinstance(v, (int, tuple)) else UnitRef(v.ptr if isinstance(v.ptr, int) else fresh(v.ptr), v.epd if isinstance(v.epd, int) else fresh(v.epd), v.uid if v.uid is None or isinstance(v.uid, int) else fresh(v.uid)) if isinstance(v, UnitRef) else fresh(v) for v in values]\n kept = self.keep(call)\n for s, v in zip(params, values):\n if isinstance(v, tuple):\n # What the parameter held from the call before goes back; then it holds this call's text.\n texts()[\"release\"](s.cell(s.block))\n s.write(*v)\n else:\n s.set(v)\n body()\n if kept is not None:\n self.bring_back(call, kept)\n if call[\"fn\"] in self.throwing:\n # It came back with an error in flight: on to the nearest catch, nothing of the result used.\n EUDJumpIf(as_var(self.var(self.errors[\"thrown\"], call).get()) >= 1, self.handler(call))\n if result is None:\n return 0\n if isinstance(result, TextStorage):\n # The function's text moves to the call's own result, from where a textCall takes it.\n v = returned.take_out()\n texts()[\"release\"](result.cell(result.block))\n result.write(v.addr, v.block, v.length())\n return 0\n result.set(returned.get())\n return result.get()\n\n def call(self, call):\n result = self.declare(call[\"result\"][\"decl\"]) if call.get(\"result\") else None\n if call.get(\"fn\") is not None:\n return self.call_function(call, result)\n # A text result keeps what it held until `return` puts the next one in it, which is when that block goes back.\n if result is not None and call[\"result\"][\"kind\"] != \"text\":\n result.set(NO_UNIT if call[\"result\"][\"kind\"] == \"unit\" else 0)\n for p in call[\"params\"]:\n s = self.declare(p[\"decl\"])\n self.put(s, p[\"init\"], p[\"decl\"][\"kind\"])\n end = Forward()\n self.straight(call[\"body\"], {\"fn\": {\"result\": result, \"kind\": call[\"result\"][\"kind\"] if call.get(\"result\") else \"void\", \"end\": end}})\n end << NextTrigger()\n return result.get() if result is not None else 0\n\n # ── one frame ──\n def frame(self):\n if self.pools:\n self.tick += 1\n if self.per_player:\n for pool in self.pools.values():\n pool.base << f_mul(self.player, pool.max)\n end = Forward()\n self.coroutine(self.top, self.p[\"body\"], {\"end\": end}, end)\n self.run_tasks()\n\n def run_tasks(self):\n \"\"\"The tasks' turn, after the program's: each slot whose task is running and has not had its turn in this frame\n goes on until it sleeps, waits or ends - by the number of its start, then by slot. A task started meanwhile\n sets `again`, and the slots are gone through once more, so that it starts in this frame too.\"\"\"\n if not self.pools:\n return\n if self.per_player:\n for pool in self.pools.values():\n pool.count << pool.live.get()\n sweep = Forward()\n sweep << NextTrigger()\n self.again << 0\n for pool in self.pools.values():\n passed = Forward()\n if pool.fresh is not None:\n go = Forward()\n EUDJumpIf(as_var(pool.fresh.get()) >= 1, go)\n EUDJumpIfNot(self.heard(pool.st[\"listen\"]), passed)\n go << NextTrigger()\n pool.fresh.set(0)\n i, seen = EUDVariable(), EUDVariable()\n i << 0\n seen << 0\n # `seen`: the running slots so far, less those that ended on their turn - so while it is below the count,\n # a running slot is still ahead (or one started meanwhile, which the next time round finds).\n if EUDWhile()([i <= pool.max - 1, seen < pool.count]):\n pool.select(i)\n skip = Forward()\n EUDJumpIf(as_var(pool.co.get_state()) == DONE, skip)\n seen += 1\n EUDJumpIf(as_var(pool.ran.get()) == self.tick, skip)\n pool.ran.set(self.tick)\n listen = pool.st.get(\"listen\")\n if listen is not None:\n # A listener (on.created, on.chat, ...) that has begun waits at the top of its loop, where a turn\n # with nothing to hand on does nothing: it is not given one.\n go = Forward()\n EUDJumpIf(as_var(pool.co.get_state()) == 0, go)\n EUDJumpIfNot(self.heard(listen), skip)\n go << NextTrigger()\n outer = self.pool\n self.pool = pool\n end = Forward()\n call = pool.st[\"call\"]\n result = self.declare(call[\"result\"][\"decl\"]) if call.get(\"result\") else None\n self.coroutine(pool.co, call[\"body\"], {\"fn\": {\"result\": result, \"kind\": call[\"result\"][\"kind\"] if call.get(\"result\") else \"void\", \"end\": end}}, end)\n self.pool = outer\n if EUDIf()(as_var(pool.co.get_state()) == DONE):\n pool.add(-1)\n seen -= 1\n EUDEndIf()\n skip << NextTrigger()\n i += 1\n EUDEndWhile()\n passed << NextTrigger()\n EUDJumpIf(self.again >= 1, sweep)\n\n def heard(self, listen):\n \"\"\"Whether a listener has anything to hand on this frame: units the events pass found, or one of its inputs.\"\"\"\n source = listen.get(\"source\")\n if source is not None:\n return {\"created\": EVENTS.made_n, \"died\": EVENTS.gone_n, \"killed\": EVENTS.gone_n, \"ordered\": EVENTS.turned_n, \"morphed\": EVENTS.changed_n}[source] >= 1\n found = fresh(0)\n for inp in listen.get(\"inputs\", []):\n if EUDIf()(as_var(INPUT.read(inp, self, listen)) >= 1):\n found << 1\n EUDEndIf()\n return found >= 1\n\n def start(self, st):\n \"\"\"A task into a free slot of its start: the arguments worked out as things are now, then the slot taken - its\n generation one more, its parameters set, its place the body's start - and the number the task is known by\n into `handle`. With every slot running nothing starts, and the game says so once.\"\"\"\n pool = self.pools[int(st[\"task\"])]\n call = st[\"call\"]\n values = []\n for prm in call[\"params\"]:\n kind = prm[\"decl\"][\"kind\"]\n target = self.var(prm[\"decl\"][\"id\"], st)\n if kind == \"unit\":\n ref = self.unit(prm[\"init\"])\n uid = ref.uid if ref.uid is not None else f_maskread_epd(ref.epd + OFF_UID, 0xFF00)\n values.append(UnitRef(fresh(ref.ptr), fresh(ref.epd), fresh(uid)))\n elif kind == \"text\" and isinstance(target, TextStorage):\n v = self.own(self.text(prm[\"init\"]))\n values.append(TextVal(fresh(v.addr), fresh(v.block), fresh(v.length()), True))\n elif kind == \"text\":\n values.append(fresh(self.text_id_of(prm[\"init\"])))\n else:\n values.append(fresh(self.value(prm[\"init\"], kind)))\n found, after = Forward(), Forward()\n i = fresh(0)\n if EUDWhile()(i <= pool.max - 1):\n pool.select(i)\n EUDJumpIf(as_var(pool.co.get_state()) == DONE, found)\n i += 1\n EUDEndWhile()\n # Every slot is running.\n for v in values:\n if isinstance(v, TextVal):\n texts()[\"release\"](v.block)\n if EUDIf()(pool.said == 0):\n pool.said << 1\n GetGlobalStringBuffer().print(\"\\x06TrigScript: %s did not start, line %s - all %d of this start's tasks are running.\" % (call.get(\"name\") or \"a task\", (st.get(\"at\") or {}).get(\"line\", \"?\"), pool.max))\n EUDEndIf()\n if st.get(\"handle\"):\n self.var(st[\"handle\"], st).set(0)\n EUDJump(after)\n found << NextTrigger()\n gen = fresh(pool.gen.get())\n if EUDIf()(gen >= 0xFFFF):\n gen << 0\n EUDEndIf()\n gen += 1\n pool.gen.set(gen)\n for prm, v in zip(call[\"params\"], values):\n target = self.var(prm[\"decl\"][\"id\"], st)\n if isinstance(v, UnitRef):\n target.set(v)\n elif isinstance(v, TextVal):\n texts()[\"release\"](target.cell(target.block))\n target.write(v.addr, v.block, v.length())\n else:\n target.set(v)\n pool.co.set_state(0)\n pool.co.set_wait(0)\n pool.ran.set(self.tick - 1)\n pool.add(1)\n if pool.fresh is not None:\n pool.fresh.set(1)\n self.again << 1\n if st.get(\"handle\"):\n self.var(st[\"handle\"], st).set(pool.slot + pool.task * 256 + f_bitlshift(fresh(gen), 16))\n after << NextTrigger()\n\n def task_slot(self, e, act):\n \"\"\"The slot a task's number names, when the task is in it: `act(pool, slot)` runs for it, the slot being run left as it is.\"\"\"\n h = fresh(self.num(e))\n slot = self.bitwise(\"&\", h, 0xFF)\n task = self.bitwise(\"&\", self.bitwise(\">>>\", h, 8), 0xFF)\n gen = self.bitwise(\">>>\", h, 16)\n for pool in self.pools.values():\n if EUDIf()([as_var(task) == pool.task, as_var(slot) <= pool.max - 1]):\n if EUDIf()([EUDNot(as_var(pool.at(pool.co.state, slot)) == DONE), as_var(pool.at(pool.gen, slot)) == gen]):\n act(pool, slot)\n EUDEndIf()\n EUDEndIf()\n\n def running(self, e):\n out = fresh(0)\n self.task_slot(e[\"task\"], lambda pool, slot: out << 1)\n return out\n\n def cancel(self, st):\n def stop(pool, slot):\n pool.co.state.store[pool.base + slot] = DONE\n # The task whose turn this is ends on it: the tasks' turn counts it then.\n if self.pool is pool:\n if EUDIfNot()(as_var(slot) == pool.slot):\n pool.add(-1)\n EUDEndIf()\n else:\n pool.add(-1)\n self.task_slot(st[\"task\"], stop)\n\n def coroutine(self, co, body, ctx=None, end=None):\n \"\"\"One frame of a coroutine: count its wait down and leave, or go on from where it stopped until it sleeps or\n ends. Its body is lowered here, once, and the places to go on from are recorded as they are met; the jump\n table is filled in after.\"\"\"\n outer = self.co\n self.co = co\n co.frame_end = Forward()\n start = Forward()\n skip = Forward()\n # Still waiting: count down and leave.\n EUDJumpIfNot(as_var(co.get_wait()) >= 1, skip)\n co.set_wait(as_var(co.get_wait()) - 1)\n EUDJump(co.frame_end)\n skip << NextTrigger()\n table = Forward()\n EUDJump(table)\n start << NextTrigger()\n try:\n self.block(body, ctx or {})\n except Leave:\n pass\n if end is not None:\n end << NextTrigger() # (where a `return` in a task's body goes)\n # The body ended: it stops for good, as the classic backend's program does.\n co.set_state(DONE)\n EUDJump(co.frame_end)\n table << NextTrigger()\n EUDJumpIf(as_var(co.get_state()) == DONE, co.frame_end)\n for index, resume in co.resumes:\n EUDJumpIf(as_var(co.get_state()) == index, resume)\n EUDJump(start)\n co.frame_end << NextTrigger()\n self.co = outer\n\n def run(self):\n if self.per_player:\n for p in loop_players(self.slots):\n self.player = p\n f_setcurpl(p)\n self.frame()\n self.player = None\n else:\n # As a trigger owned by that player would: not at all once the player has left.\n if EUDIf()(f_playerexist(self.slots[0])):\n f_setcurpl(self.slots[0])\n self.frame()\n EUDEndIf()\n\n\ndef flipped(t):\n \"\"\"1 for 0 and 0 for anything else, in a variable of its own.\"\"\"\n out = fresh(0)\n if EUDIf()(t == 0):\n out << 1\n EUDEndIf()\n return out\n\n\ndef each_start(node, visit):\n \"\"\"Every `start` statement in a piece of the IR, outer ones before those in their bodies.\"\"\"\n if isinstance(node, list):\n for x in node:\n each_start(x, visit)\n return\n if not isinstance(node, dict):\n return\n if node.get(\"kind\") == \"start\" and isinstance(node.get(\"task\"), int) and isinstance(node.get(\"call\"), dict):\n visit(node)\n for v in node.values():\n if isinstance(v, (dict, list)):\n each_start(v, visit)\n\n\ndef has_effect(node):\n \"\"\"Whether working out a piece of the IR does something beyond reading: a call, a pop, a random number.\"\"\"\n if isinstance(node, list):\n return any(has_effect(x) for x in node)\n if not isinstance(node, dict):\n return False\n if node.get(\"kind\") in (\"pop\", \"random\", \"randomInt\") or (node.get(\"kind\") in (\"call\", \"textCall\") and isinstance(node.get(\"call\"), dict)):\n return True\n return any(has_effect(v) for v in node.values() if isinstance(v, (dict, list)))\n\n\ndef has_call(node):\n \"\"\"Whether a call is anywhere in a piece of the IR.\"\"\"\n if isinstance(node, list):\n return any(has_call(x) for x in node)\n if not isinstance(node, dict):\n return False\n if node.get(\"kind\") in (\"call\", \"textCall\") and isinstance(node.get(\"call\"), dict):\n return True\n return any(has_call(v) for v in node.values() if isinstance(v, (dict, list)))\n\n\ndef string_of(value):\n \"\"\"An action's text or sound: the string itself (eudplib adds it to the map), or an index the script named.\"\"\"\n if isinstance(value, str):\n return EncodeString(value) if value else 0\n return int(value or 0)\n\n\ndef group_slots(group, node):\n \"\"\"The player slots a player number stands for: a slot is itself, All Players and a force their human and computer players.\"\"\"\n playing = lambda p: GetPlayerInfo(p).typestr in (\"Human\", \"Computer\")\n if 0 <= group < 8:\n return [group]\n if group == 17:\n return [p for p in range(8) if playing(p)]\n if 18 <= group <= 21:\n return [p for p in range(8) if playing(p) and GetPlayerInfo(p).force == group - 18]\n raise Fail(\"trigscript: expected players 1 to 8, All Players or a force%s\" % where(node))\n\n\ndef owner_slots(program):\n \"\"\"The player slots a program runs for, from its owners and the map's player settings: a slot is\n itself, All Players and a force are their human and computer players — who a trigger runs for.\"\"\"\n slots = []\n for o in program.get(\"owners\", [program.get(\"owner\", 0)]):\n for p in group_slots(int(o), program):\n if p not in slots:\n slots.append(p)\n if not slots:\n raise Fail(\"trigscript: no human or computer player of this map is among the program's owners%s\" % where(program))\n return slots\n\n\ndef loop_players(slots):\n \"\"\"Each of `slots` who is in the game, as an EUDVariable: EUDLoopPlayer, for a list of our own.\"\"\"\n start, end = min(slots), max(slots)\n v = EUDVariable()\n v << start\n if EUDWhile()(v <= end):\n for i in range(start, end):\n if i not in slots:\n EUDContinueIf(v == i)\n EUDContinueIfNot(f_playerexist(v))\n yield v\n EUDSetContinuePoint()\n v += 1\n EUDEndWhile()\n\n\nBITWISE = (\"&\", \"|\", \"^\", \"<<\", \">>\", \">>>\")\nFLIPPED = {\"<\": \">\", \"<=\": \">=\", \">\": \"<\", \">=\": \"<=\", \"==\": \"==\", \"!=\": \"!=\"}\n\n\ndef points(value, bits):\n \"\"\"Whole points as the game stores them, 256 to a point; `bits` is how many bits of points the cell holds.\"\"\"\n if isinstance(value, int):\n return min(value, (1 << bits) - 1) * 256\n return f_mul(saturate(value, bits), 256)\n\n\ndef uses_random(node):\n if isinstance(node, dict):\n return node.get(\"kind\") in (\"random\", \"randomInt\") or (node.get(\"kind\") == \"pick\" and node.get(\"by\") == \"random\") or any(uses_random(v) for v in node.values())\n return isinstance(node, list) and any(uses_random(v) for v in node)\n\n\ndef distance(a, b):\n \"\"\"|a - b| of two places on the map, both from 0 up.\"\"\"\n d = fresh(a)\n d -= b\n if EUDIf()(d >= SIGN):\n d << 0 - d\n EUDEndIf()\n return d\n\n\ndef signed(v):\n \"\"\"The 32 bits as a signed number.\"\"\"\n v &= U32\n return v - (1 << 32) if v >= SIGN else v\n\n\ndef isqrt(n):\n r = int(n ** 0.5)\n while r * r > n:\n r -= 1\n while (r + 1) * (r + 1) <= n:\n r += 1\n return r\n\n\ndef between_wrapped(table, i, f):\n \"\"\"The sine table read between cell i and the next, cell 0 after the last.\"\"\"\n a, b = table[i], table[(i + 1) & 4095]\n part = (abs(b - a) * f) >> 16\n return a - part if b < a else a + part\n\n\ndef fixed_const(name, args, unsigned=False, table=None):\n \"\"\"A fixed operation of numbers known when the map is built, exactly as the functions of fixed_fns() compute it.\"\"\"\n def towards_zero(m, negative):\n return (-(m & U32)) & U32 if negative else m & U32\n if name == \"fmul\":\n a, b = args\n return towards_zero((abs(a) * abs(b)) >> 16, (a < 0) != (b < 0))\n if name == \"fdiv\":\n a, b = args\n return 0 if b == 0 else towards_zero((abs(a) << 16) // abs(b), (a < 0) != (b < 0))\n if name == \"sqrt\":\n x = args[0] & U32 if unsigned else args[0]\n if unsigned:\n return 0x7FFFFFFF if x >= 1 << 30 else isqrt(x << 32)\n return 0 if x <= 0 else isqrt(x << 16)\n def between(i, f, bits):\n a = table[i] if i < len(table) else 0\n b = table[i + 1] if i + 1 < len(table) else 0\n part = (abs(b - a) * f) >> bits\n return a - part if b < a else a + part\n if name in (\"sin\", \"cos\"):\n x, scale = args\n at = fixed_const(\"fmul\", [x, scale])\n i = ((at >> 16) + (1024 if name == \"cos\" else 0)) & 4095\n return between_wrapped(table, i, at & 0xFFFF)\n y, x = args\n ax, ay = abs(x), abs(y)\n if ax == 0 and ay == 0:\n return 0\n read = lambda small, large: between(((small << 16) // large) >> 6, ((small << 16) // large) & 63, 6)\n t = read(ay, ax) if ay <= ax else FIXED_HALF_PI - read(ax, ay)\n if x < 0:\n t = FIXED_PI - t\n if y < 0:\n t = -t\n return t\n\n\ndef rng(node):\n \"\"\"The range compiler/ranges.ts found for a number of the IR - its signed reading, lowest and highest - or None.\"\"\"\n if not isinstance(node, dict):\n return None\n if node.get(\"kind\") == \"const\":\n v = signed(int(node[\"value\"]) & U32)\n return (v, v)\n r = node.get(\"range\")\n return (int(r[0]), int(r[1])) if r else None\n\n\ndef top_of(node):\n \"\"\"The most a number of the IR can be, when ranges.ts found it never below zero; None otherwise.\"\"\"\n r = rng(node)\n return r[1] if r is not None and r[0] >= 0 else None\n\n\ndef mul_bits(v, top, k):\n \"\"\"v * k, wrapping, of a variable known to be 0 ... top and a constant k of either sign: a trigger a bit v can have,\n each adding k shifted to that bit (eudplib's product by a constant goes over all 32, through a call).\"\"\"\n r = EUDVariable()\n RawTrigger(actions=r.SetNumber(0))\n for bit in range(top.bit_length()):\n add = (k << bit) & U32\n if add:\n RawTrigger(conditions=v.AtLeastX(1, 1 << bit), actions=r.AddNumber(add))\n return r\n\n\ndef mul_bits_pays(top, k):\n \"\"\"Whether mul_bits beats eudplib's product for a side of 0 ... top and the constant k, by Remastered's weights: about\n 18 ns a bit of top, against a call (80 ns) and about 95 ns a bit of the smaller side for eudplib's.\"\"\"\n m = abs(signed(k))\n return 18 * top.bit_length() < 80 + 95 * min(m.bit_length(), top.bit_length())\n\n\ndef div_bits(a, d, top):\n \"\"\"(a // d, a % d) of a value known to be 0 ... top and a constant d >= 1: a trigger a bit the quotient can have,\n where eudplib's goes over every bit a 32-bit dividend could give, through a call. `a` is copied, not used up.\"\"\"\n if d == 1:\n return a, 0\n if d & (d - 1) == 0:\n k = d.bit_length() - 1\n rem = fresh(a)\n DoActions(rem.SetNumberX(0, (~(d - 1)) & U32))\n return shift_const(as_var(a), \">>>\", k, top), rem\n rem, q = fresh(a), EUDVariable()\n RawTrigger(actions=q.SetNumber(0))\n for i in range((top // d).bit_length() - 1, -1, -1):\n RawTrigger(conditions=rem.AtLeast(d << i), actions=[rem.SubtractNumber(d << i), q.AddNumber(1 << i)])\n return q, rem\n\n\ndef div_var_bits(a, b, top):\n \"\"\"(a // b, a % b) of a value known to be 0 ... top and a variable b >= 1, both read from 0 up: eudplib's division\n chain (the divisor doubled into each step's condition and action, then the steps from the highest down), with only\n as many steps as top has bits, and the chain started as soon as the next doubling would pass top - where eudplib's\n doubles the divisor until it would pass 2^31 and keeps 32 steps.\"\"\"\n n = max(1, top.bit_length())\n q, x, rem = EUDVariable(), EUDVariable(), fresh(a)\n SeqCompute([(q, SetTo, 0), (x, SetTo, b)])\n cells = [(Forward(), Forward()) for _ in range(n)]\n chain = [Forward() for _ in range(n)]\n half = (top >> 1) + 1\n for i in range(n):\n SeqCompute([(EPD(c), SetTo, x) for c in cells[i]])\n if i == n - 1:\n break\n # Past top / 2 the next doubling is past top, whose steps take nothing: the chain starts here.\n p1, p2, p3 = Forward(), Forward(), Forward()\n p1 << RawTrigger(nextptr=p2, conditions=x.AtLeast(half), actions=SetNextPtr(p1, p3))\n p3 << RawTrigger(nextptr=chain[i], actions=SetNextPtr(p1, p2))\n p2 << NextTrigger()\n SeqCompute([(x, Add, x)])\n for i in range(n - 1, -1, -1):\n c, act = Forward(), Forward()\n chain[i] << RawTrigger(conditions=[c << rem.AtLeast(0)], actions=[act << rem.SubtractNumber(0), q.AddNumber(1 << i)])\n cells[i][0] << c + 8\n cells[i][1] << act + 20\n return q, rem\n\n\ndef shift_const(x, op, k, top=None):\n \"\"\"x << k, x >> k (the sign kept) or x >>> k for a constant k from 1 to 31, one trigger a bit that moves: a bit of x\n tested, its place in the result added. A short shift left is doublings instead (a doubling is about two triggers).\n `top`: x is known to be 0 ... top (ranges.ts), so only its bits move - and `>>` is `>>>`, the sign bit being 0.\"\"\"\n if top is not None:\n if op == \">>\":\n op = \">>>\"\n if op == \">>>\" and top >> k == 0:\n return 0\n if op == \"<<\" and k <= 4:\n t = fresh(x)\n for _ in range(k):\n t += t\n return t\n r = EUDVariable()\n RawTrigger(actions=r.SetNumber(0))\n if op == \"<<\":\n for bit in range(32 - k if top is None else min(32 - k, top.bit_length())):\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=r.AddNumber(1 << (bit + k)))\n return r\n # From the top, eight bits a group: a group with none of its bits set is jumped over (a small number skips most).\n # Known to be at most `top`, the bits above it are never set, and a group of eight or fewer needs no jump.\n highest = 31 if top is None else top.bit_length() - 1\n if highest - k < 8:\n for bit in range(highest, k - 1, -1):\n add = 1 << (bit - k)\n if op == \">>\" and bit == 31:\n add = (add - (1 << (32 - k))) & U32\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=r.AddNumber(add))\n return r\n for top in range(highest, k - 1, -8):\n bits = range(top, max(k, top - 7) - 1, -1)\n group = sum(1 << bit for bit in bits)\n past = Forward()\n EUDJumpIf(x.ExactlyX(0, group), past)\n for bit in bits:\n add = 1 << (bit - k)\n if op == \">>\" and bit == 31:\n add = (add - (1 << (32 - k))) & U32 # the sign bit: its place, and every bit above the result's top\n RawTrigger(conditions=x.AtLeastX(1, 1 << bit), actions=r.AddNumber(add))\n past << NextTrigger()\n return r\n\n\ndef clamp_const(v, c, name, unsigned):\n \"\"\"min(v, c) or max(v, c) of a variable and a constant: a copy of v, set to c by a trigger whose conditions say it\n passes c — one or two comparisons with constants, where a comparison of two values and a branch was about 0.17 µs\n (the front end puts a max(…, 0) around every coordinate of a centerLocation, so the fog library's every brush).\"\"\"\n t = fresh(v)\n c &= U32\n if unsigned:\n if name == \"max\" and c > 0:\n RawTrigger(conditions=t.AtMost(c - 1), actions=t.SetNumber(c))\n elif name == \"min\" and c < U32:\n RawTrigger(conditions=t.AtLeast(c + 1), actions=t.SetNumber(c))\n return t\n # Signed: below zero is from 2^31 up, read from 0 up.\n if name == \"max\":\n if c < SIGN:\n RawTrigger(conditions=t.AtLeast(SIGN), actions=t.SetNumber(c))\n if c > 0:\n RawTrigger(conditions=t.AtMost(c - 1), actions=t.SetNumber(c))\n elif c > SIGN:\n RawTrigger(conditions=[t.AtLeast(SIGN), t.AtMost(c - 1)], actions=t.SetNumber(c))\n else:\n if c < SIGN:\n if c < SIGN - 1:\n RawTrigger(conditions=[t.AtMost(SIGN - 1), t.AtLeast(c + 1)], actions=t.SetNumber(c))\n else:\n RawTrigger(conditions=t.AtMost(SIGN - 1), actions=t.SetNumber(c))\n if c < U32:\n RawTrigger(conditions=t.AtLeast(c + 1), actions=t.SetNumber(c))\n return t\n\n\ndef signed_order(a, op, b):\n \"\"\"`a op b` of two signed variables, with no addition a side: a < b read as signed is a < b read from 0 up, turned\n over once for each side below zero (the top bits differ only when exactly one is). A count of those three is a\n condition on its low bit; > <= >= are < with the sides swapped, or the count's other answer.\"\"\"\n x, y, holds = (a, b, 1) if op == \"<\" else (b, a, 1) if op == \">\" else (b, a, 0) if op == \"<=\" else (a, b, 0)\n n = EUDVariable()\n lt = relation(x, \"<\", y)\n RawTrigger(actions=n.SetNumber(0))\n RawTrigger(conditions=lt, actions=n.AddNumber(1))\n RawTrigger(conditions=x.AtLeast(SIGN), actions=n.AddNumber(1))\n RawTrigger(conditions=y.AtLeast(SIGN), actions=n.AddNumber(1))\n return n.ExactlyX(holds, 1)\n\n\ndef not_all(conds):\n \"\"\"!(c1 && c2 && ...) as a light bool: set by one trigger, cleared by a second that has the conditions. (EUDNot of a\n list takes an if around the second, a third trigger.)\"\"\"\n v = EUDLightBool()\n RawTrigger(actions=v.Set())\n # (Trigger, not RawTrigger: a condition whose amount is a variable is patched before, as EUDIf does.)\n Trigger(conditions=conds, actions=v.Clear())\n return v\n\n\ndef forget_counts():\n \"\"\"Bring keeps the counts it found for a location number until the trigger pass ends or a query of another kind or\n location is asked: writing a location's sides, or making units in it, leaves the next Bring of it reading the old\n counts (Remastered and the lifted game alike, probes/location-stale.py). A Command in between makes it count again\n (about 30 ns, and the next Bring is a scan, as it would be for a location that moved). Only in a map whose programs\n ask Bring at all: the fog library moves a location and makes a unit at it for every brush, and asks nothing.\"\"\"\n if _asks_bring():\n RawTrigger(conditions=Command(P1, AtLeast, 0, 0))\n\n\n_BRING = []\n\n\ndef _asks_bring():\n \"\"\"Whether any program of the map reads a Bring condition: tests one (an `if`, an `until`) or counts with one.\"\"\"\n if not _BRING:\n def walk(node):\n if isinstance(node, list):\n return any(walk(x) for x in node)\n if not isinstance(node, dict):\n return False\n rec = node.get(\"record\")\n if isinstance(rec, dict) and (node.get(\"kind\") == \"cond\" or node.get(\"source\") == \"condition\") and rec.get(\"type\") == COND_BRING:\n return True\n return any(walk(v) for v in node.values() if isinstance(v, (list, dict)))\n _BRING.append(walk(IR.get(\"programs\", [])))\n return _BRING[0]\n\n\n# Actions that put units where a Bring already counted, or change whose they are: Create Unit with Properties, Move\n# Unit, Create Unit, Give Units to Player.\nMOVES_UNITS = {11, 39, 44, 48}\n\n\ndef always(truth):\n \"\"\"A condition that is known when the map is built.\"\"\"\n return EUDVariable(1 if truth else 0) >= 1 # initial: a constant, never written\n\n\ndef relation(av, op, b):\n \"\"\"`av op b` between a variable and a variable or an int, both read from 0 up.\"\"\"\n if op == \"==\":\n return av == b\n if op == \"!=\":\n return av != b\n if op == \"<\":\n return av < b\n if op == \">\":\n return av > b\n if isinstance(unProxy(b), EUDVariable):\n # eudplib orders two variables with < and > only: a >= b is b - a, which the game's Subtract stops at 0, being\n # 0 - worked out into the condition's own first dword, as eudplib's < does it.\n return floored_zero(b, av) if op == \">=\" else floored_zero(av, b)\n if op == \"<=\":\n return av <= b\n return av >= b\n\n\ndef floored_zero(x, y):\n \"\"\"x - y, stopped at 0, is 0: x <= y read from 0 up, one condition and a sum written into it.\"\"\"\n cell = Forward()\n cond = MemoryEPD(cell, Exactly, 0)\n cell << EPD(cond)\n SeqCompute(((cell, SetTo, x), (cell, Subtract, y)))\n return cond\n\n\ndef compare(a, op, b):\n return {\"==\": a == b, \"!=\": a != b, \"<\": a < b, \"<=\": a <= b, \">\": a > b, \">=\": a >= b}[op]\n\n\ndef condition(r):\n return Condition(r[\"location\"], r[\"player\"], r[\"amount\"], r[\"unitId\"], r[\"comparison\"], r[\"type\"], r[\"resource\"], r[\"flags\"], eudx=r.get(\"mask\", 0) or 0)\n\n\nclass Input:\n \"\"\"What the players do, as it reaches every computer: the cells chatEvent and MSQC write, by the\n names the editor put in their settings (compiler/input.ts, INPUT_NAMES), and the reads of them.\"\"\"\n\n MAX_NUMBER = 0xFFFFF\n\n def __init__(self, plan):\n self.plan = plan or {}\n self.keys = list(self.plan.get(\"keys\", []))\n self.held = list(self.plan.get(\"held\", []))\n self.buttons = list(self.plan.get(\"buttons\", []))\n self.chats = list(self.plan.get(\"chats\", []))\n self.mouse_base = self.plan.get(\"mouseBase\")\n # A person can sit in the map's human slots, which is where MSQC counts the mouse locations from.\n self.humans = [p for p in range(8) if GetPlayerInfo(p).typestr == \"Human\"]\n if plan and not self.humans:\n raise Fail(\"trigscript: a program reads keys, clicks, the mouse or chat, and the map has no human player to give any\")\n self.key_cells = [self.register(\"tsin_key%d\" % i, EUDArray(12)) for i in range(len(self.keys))]\n self.held_cells = [self.register(\"tsin_held%d\" % i, EUDArray(12)) for i in range(len(self.held))]\n # A held key is a message each frame it is down, but a multiplayer game carries commands in turns: two frames'\n # messages may arrive on one frame and none on the next. What arrives is banked and paid out a frame at a time,\n # so keyHeld() is true on as many frames as the key was down, however the turns grouped them.\n self.held_bank = [EUDArray(12) for _ in self.held]\n self.held_now = [EUDArray(12) for _ in self.held]\n # And a message can go missing for a frame (seen in the lifted game, the second frame of every hold): a key is\n # still held until HELD_GRACE frames have passed with no message, which delays a release by as much.\n self.held_quiet = [EUDArray(12) for _ in self.held]\n # With held keys every key is sent as held (compiler/input.ts#inputPlan): a press is held now and not before.\n self.held_pressed = [EUDArray(12) for _ in self.held]\n self.button_cells = [self.register(\"tsin_button%d\" % i, EUDArray(12)) for i in range(len(self.buttons))]\n # sync() (B5): each slot a value put on one computer (local) and what arrived of it, by player (everyone's).\n self.syncs = int(self.plan.get(\"syncs\", 0))\n self.sync_local = [self.register(\"tsin_sync%d\" % i, EUDVariable()) for i in range(self.syncs)]\n self.sync_in = [self.register(\"tsin_sync%d_in\" % i, EUDArray(12)) for i in range(self.syncs)]\n self.captures = max([len(c[\"captures\"]) for c in self.chats] or [0])\n if self.chats:\n # Local: what chatEvent found on this computer, and what the patterns made of it.\n self.heard = self.register(\"tsin_heard\", EUDVariable())\n self.pointer = self.register(\"tsin_pointer\", EUDVariable())\n self.length = self.register(\"tsin_length\", EUDVariable())\n self.register(\"tsin_pattern\", EUDVariable())\n self.chat_local = self.register(\"tsin_chat\", EUDVariable())\n self.capture_local = [self.register(\"tsin_capture%d\" % i, EUDVariable()) for i in range(self.captures)]\n # Everyone's: what MSQC delivered this frame, by player; 0xFFFFFFFF on a frame with nothing.\n self.chat_in = self.register(\"tsin_chat_in\", EUDArray(12))\n self.capture_in = [self.register(\"tsin_capture%d_in\" % i, EUDArray(12)) for i in range(self.captures)]\n self.unit_table = None\n\n SYNC_MOST = 1048574 # what MSQC's val carries, less the 1 that tells a value from nothing\n\n def sync_frame(self):\n \"\"\"Once a frame, before the programs: what was put to be sent has gone (MSQC sends in its beforeTriggerExec).\"\"\"\n if self.sync_local:\n DoActions([c.SetNumber(0) for c in self.sync_local])\n\n def send_sync(self, slot, p, value):\n \"\"\"sync()'s first half: on every computer the slot's arrival for the player cleared; on the player's own the\n value, kept to what MSQC carries, plus 1, put where MSQC sends it from.\"\"\"\n if slot >= self.syncs:\n raise Fail(\"trigscript: the IR's input plan has no sync slot %d\" % slot)\n self.sync_in[slot][p] = 0\n if EUDIf()(f_getuserplayerid() == p):\n v = fresh(value)\n if EUDIf()(v >= 0x80000000):\n v << 0\n EUDEndIf()\n if EUDIf()(v >= self.SYNC_MOST + 1):\n v << self.SYNC_MOST\n EUDEndIf()\n self.sync_local[slot] << v + 1\n EUDEndIf()\n\n HELD_BANK_MOST = 8 # a burst after a lag spike pays out at most this many frames\n HELD_GRACE = 2 # frames with no message before a held key counts as let go\n\n def frame(self):\n \"\"\"Once a frame, before the programs: this frame's held messages banked, one frame of each paid out, and a key\n with nothing left to pay still held for HELD_GRACE quiet frames.\"\"\"\n for cells, bank, now, quiet, pressed in zip(self.held_cells, self.held_bank, self.held_now, self.held_quiet, self.held_pressed):\n for p in self.humans:\n was = fresh(now[p])\n b = fresh(bank[p] + cells[p])\n if EUDIf()(b >= self.HELD_BANK_MOST + 1):\n b << self.HELD_BANK_MOST\n EUDEndIf()\n if EUDIf()(b >= 1):\n now[p] = 1\n bank[p] = b - 1\n quiet[p] = 0\n if EUDElse()():\n bank[p] = 0\n q = fresh(quiet[p] + 1)\n quiet[p] = q\n if EUDIf()([now[p] >= 1, q <= self.HELD_GRACE]):\n now[p] = 1\n if EUDElse()():\n now[p] = 0\n EUDEndIf()\n EUDEndIf()\n if EUDIf()([now[p] >= 1, was == 0]):\n pressed[p] = 1\n if EUDElse()():\n pressed[p] = 0\n EUDEndIf()\n\n @staticmethod\n def register(name, cell):\n EUDRegisterObjectToNamespace(name, cell)\n return cell\n\n # ── reads ──\n def read(self, i, low, node):\n source = i.get(\"source\")\n p = low.one_player(i[\"player\"], node)\n if source == \"key\" and i.get(\"held\"):\n return self.cell(self.held_now, self.held, i[\"key\"], node)[p]\n if source == \"key\" and self.held:\n return self.cell(self.held_pressed, self.held, i[\"key\"], node)[p]\n if source == \"key\":\n return self.cell(self.key_cells, self.keys, i[\"key\"], node)[p]\n if source == \"click\":\n return self.cell(self.button_cells, self.buttons, i[\"button\"], node)[p]\n if source == \"mouse\":\n return self.mouse(p, 0 if i[\"axis\"] == \"x\" else 1, node)\n if source == \"sync\":\n slot = int(i[\"slot\"])\n if slot >= self.syncs:\n raise Fail(\"trigscript: the IR's input plan has no sync slot %d%s\" % (slot, where(node)))\n return self.sync_in[slot][p]\n if source == \"chat\":\n number = self.chat_number(i[\"pattern\"], node)\n out = fresh(0)\n if EUDIf()(self.chat_in[p] == number):\n out << (1 if i.get(\"capture\") is None else self.capture_in[int(i[\"capture\"])][p])\n EUDEndIf()\n return out\n raise Fail(\"trigscript: unknown input %r%s\" % (source, where(node)))\n\n @staticmethod\n def cell(cells, names, name, node):\n if name not in names:\n raise Fail(\"trigscript: the IR's input plan has no %r%s\" % (name, where(node)))\n return cells[names.index(name)]\n\n def chat_number(self, pattern, node):\n for index, c in enumerate(self.chats):\n if c[\"pattern\"] == pattern:\n return index + 1\n raise Fail(\"trigscript: the IR's input plan has no pattern %r%s\" % (pattern, where(node)))\n\n def mouse(self, p, axis, node):\n \"\"\"MSQC keeps the mouse of the map's first human slot in location `mouseBase`, the next slot's in the next.\"\"\"\n if self.mouse_base is None:\n raise Fail(\"trigscript: the IR's input plan keeps no mouse%s\" % where(node))\n first = min(self.humans)\n cell = lambda h: EPD(MRGN + (int(self.mouse_base) - 1 + h - first) * 20) + axis\n if isinstance(p, int):\n return f_dwread_epd(cell(p)) if p in self.humans else 0\n out = fresh(0)\n for h in self.humans:\n if EUDIf()(p == h):\n out << f_dwread_epd(cell(h))\n EUDEndIf()\n return out\n\n # ── the typed line, on the computer it was typed on ──\n @staticmethod\n def hash_of(data):\n \"\"\"What `hashed` makes of the same bytes: capitals A to Z as small letters, h = h × 31 + byte.\"\"\"\n h = 0\n for b in data:\n if 65 <= b <= 90:\n b += 32\n h = (h * 31 + b) & U32\n return h\n\n def hashed(self, pos, stop_at_space):\n \"\"\"The hash of the line from `pos` to its end or, with `stop_at_space`, to the next space; `pos` moves past it.\"\"\"\n h = fresh(0)\n if EUDWhile()(pos < self.length):\n ch = fresh(f_bread(self.pointer + pos))\n if stop_at_space:\n EUDBreakIf(ch == 32)\n if EUDIf()([ch >= 65, ch <= 90]):\n ch += 32\n EUDEndIf()\n h << f_mul(h, 31) + ch\n pos += 1\n EUDEndWhile()\n return h\n\n def units(self):\n \"\"\"Unit names by hash, in hash order, for a search by halves: two arrays side by side.\"\"\"\n if self.unit_table is None:\n by_hash = {}\n for name, unit in self.plan.get(\"unitNames\", []):\n by_hash.setdefault(self.hash_of(name.encode(\"utf-8\")), int(unit))\n ordered = sorted(by_hash.items())\n self.unit_table = (EUDArray([h for h, _ in ordered] or [0]), EUDArray([u for _, u in ordered] or [0]), len(ordered))\n return self.unit_table\n\n def capture(self, c, pos, fail):\n \"\"\"One value out of the line at `pos`, or a jump to `fail`.\"\"\"\n value = fresh(0)\n if c[\"kind\"] == \"number\":\n digits = fresh(0)\n if EUDWhile()(pos < self.length):\n ch = f_bread(self.pointer + pos)\n EUDBreakIf(ch <= 47)\n EUDBreakIf(ch >= 58)\n value << f_mul(value, 10) + ch - 48\n if EUDIf()(value >= self.MAX_NUMBER + 1):\n value << self.MAX_NUMBER\n EUDEndIf()\n pos += 1\n digits += 1\n EUDEndWhile()\n EUDJumpIf(digits == 0, fail)\n elif c[\"kind\"] == \"word\":\n h = self.hashed(pos, True)\n found = fresh(0)\n for index, word in enumerate(c[\"words\"]):\n if EUDIf()(h == self.hash_of(word.encode(\"utf-8\"))):\n value << index\n found << 1\n EUDEndIf()\n EUDJumpIf(found == 0, fail)\n elif c[\"kind\"] == \"unit\":\n h = self.hashed(pos, False)\n hashes, units, count = self.units()\n lo, hi = fresh(0), fresh(count)\n if EUDWhile()(lo < hi):\n mid = f_div(lo + hi, 2)[0]\n if EUDIf()(hashes[mid] < h):\n lo << mid + 1\n if EUDElse()():\n hi << mid\n EUDEndIf()\n EUDEndWhile()\n EUDJumpIf(lo >= count, fail)\n EUDJumpIfNot(hashes[lo] == h, fail)\n value << units[lo]\n else:\n raise Fail(\"trigscript: unknown chat capture %r\" % (c[\"kind\"],))\n return value\n\n def match_line(self):\n \"\"\"The line the local player typed against every pattern, in order: the first that fits the whole\n line gives its number and values to the cells MSQC sends from. Nothing fits: nothing is sent.\"\"\"\n DoActions([self.chat_local.SetNumber(0)] + [c.SetNumber(0) for c in self.capture_local])\n if EUDIf()(self.heard >= 1):\n done = Forward()\n for index, chat in enumerate(self.chats):\n fail = Forward()\n pos = fresh(0)\n values = []\n for seg in chat[\"segments\"]:\n if isinstance(seg, str):\n data = seg.encode(\"utf-8\")\n EUDJumpIf(pos + len(data) >= self.length + 1, fail)\n EUDJumpIfNot(f_memcmp(self.pointer + pos, Db(data + b\"\\0\"), len(data)) == 0, fail)\n pos += len(data)\n else:\n values.append(self.capture(chat[\"captures\"][int(seg)], pos, fail))\n EUDJumpIfNot(pos == self.length, fail)\n DoActions(self.chat_local.SetNumber(index + 1))\n for cell, value in zip(self.capture_local, values):\n cell << value\n EUDJump(done)\n fail << NextTrigger()\n done << NextTrigger()\n EUDEndIf()\n\n\n# Where each trigger came from, for a run in the lifted game to name the line a trigger belongs to (TrigScript 4,\n# A1). Only when the build asks for it (the `triggerMap` setting): every RawTrigger made is tagged with the\n# statement being lowered, or with the eudplib function whose body it is, and once the payload is laid out one\n# header line and lines of rows say where each trigger sits in it:\n# TRIGSCRIPT-MAP {\"payload\": , \"count\": }\n# TRIGSCRIPT-ROWS [[offset, conditions, actions, origin], ...] (400 a line, as many lines as it takes)\n# An origin is \"line:column\" (or \"file:line:column\"), \"fn:\" for a function of eudplib's, \"\" for what no\n# statement made (a program's frame, its sleeps), or \"eudplib\" for what eudplib made before this file was read. The payload sits at the end of the strings section, so a run\n# finds it at the section's address plus its length less the payload's.\nif settings.get(\"triggerMap\"):\n import eudplib.core.allocator.payload as _payload\n from eudplib.core.eudfunc.eudfuncn import EUDFuncN as _EUDFuncN\n from eudplib.core.rawtrigger.rawtriggerdef import RawTrigger as _RawTrigger\n\n _ORIGINS = []\n _MADE = {}\n _ACT_ORIGINS = {}\n\n def _origin_of(at):\n if not at:\n return \"\"\n if at.get(\"file\"):\n return \"%s:%s:%s\" % (at.get(\"file\"), at.get(\"line\"), at.get(\"column\"))\n return \"%s:%s\" % (at.get(\"line\"), at.get(\"column\"))\n\n _trigger_init = _RawTrigger.__init__\n\n def _tagged_trigger(self, *args, **kwargs):\n _trigger_init(self, *args, **kwargs)\n _MADE[self] = _ORIGINS[-1] if _ORIGINS else \"\"\n\n _RawTrigger.__init__ = _tagged_trigger\n\n _func_body = _EUDFuncN._create_func_body\n\n def _tagged_body(self):\n _ORIGINS.append(\"fn:\" + getattr(self._bodyfunc, \"__name__\", \"?\"))\n try:\n return _func_body(self)\n finally:\n _ORIGINS.pop()\n\n _EUDFuncN._create_func_body = _tagged_body\n\n _statement = Lowering.statement\n\n def _tagged_statement(self, st, ctx):\n _ORIGINS.append(_origin_of(st.get(\"at\")) or (_ORIGINS[-1] if _ORIGINS else \"\"))\n try:\n return _statement(self, st, ctx)\n finally:\n _ORIGINS.pop()\n\n Lowering.statement = _tagged_statement\n\n _construct = _payload._construct_payload\n\n def _construct_and_say():\n built = _construct()\n found = _payload._found_objects_dict\n rows = []\n # (Every trigger in the payload, also those eudplib made before this file was read, at its own import:\n # those say \"eudplib\".)\n for trg, i in found.items():\n if not isinstance(trg, _RawTrigger):\n continue\n origin = _MADE.get(trg, \"eudplib\")\n row = [_payload._payload_builder.get_object_addr(i).offset, len(trg._conditions), len(trg._actions), origin]\n # (A trigger of the emitter's holding several statements' work: each action's line.)\n if trg in _ACT_ORIGINS:\n row.append(_ACT_ORIGINS[trg])\n rows.append(row)\n rows.sort()\n # (In parts, so that no one line of the log runs to megabytes.)\n print(\"TRIGSCRIPT-MAP \" + json.dumps({\"payload\": len(built.data), \"count\": len(rows)}, separators=(\",\", \":\")))\n for k in range(0, len(rows), 400):\n print(\"TRIGSCRIPT-ROWS \" + json.dumps(rows[k:k + 400], separators=(\",\", \":\")))\n return built\n\n _payload._construct_payload = _construct_and_say\n\n\n# Where each program's variables are, for a test in the lifted game to read them as `sim.program()` reads the\n# simulator's (TrigScript 4, A2). Only when the build asks for it (the `variableMap` setting): once the payload is\n# placed, a header line and a line a program give the address of every cell:\n# TRIGSCRIPT-VARS {\"programs\": , \"heap\":
, \"cells\":
, \"strings\":
}\n# TRIGSCRIPT-PROGRAM {\"index\", \"name\", \"perPlayer\", \"owners\", \"vars\": [...], \"arrays\": [...]}\n# A variable is its id (what the simulator keys it by), its kind, and its cells: `value` (a number, a boolean, a text\n# of the table by its id), `addr` + `len` (a text that is made), `ptr` + `uid` (a unit). An array is its cells' `at`,\n# or, growing, its handle's `ptr` + `len` among the growing arrays' cells. A cell of a per-player program's own (`rowed`) is a\n# row of twelve, one a player; an address is null for what nothing in the map uses, which eudplib leaves out.\nif settings.get(\"variableMap\"):\n import eudplib.core.allocator.payload as _vpayload\n import eudplib.maprw.injector.apply_injector as _injector\n from eudplib.core.allocator.constexpr import Evaluate as _evaluate\n from eudplib.core.mapdata.stringmap import get_string_section_name as _strings_name\n from eudplib.core.variable.vbuf import get_current_varbuffer as _varbuffer\n from eudplib.utils import unProxy as _unproxy\n\n _PLACES = {}\n\n def _offset(expr):\n \"\"\"A payload-relative address as a byte offset into the payload: eudplib's own (relocated whole) or an EPD\n (relocated a quarter at a time, as EPD(x) = (x - 0x58A364) / 4 is).\"\"\"\n r = _evaluate(expr)\n if r.rlocmode == 4:\n return r.offset\n if r.rlocmode == 1:\n return r.offset * 4 + 0x58A364\n return None\n\n def _place(obj):\n \"\"\"Where a variable's value or an array's first cell is in the payload; None for what it left out.\"\"\"\n if is_varray(obj):\n try:\n at = _offset(_unproxy(obj))\n except Exception:\n return None\n return None if at is None else at + VARRAY_VALUE\n if isinstance(obj, EUDVariable):\n vb = _varbuffer()\n if vb is None or obj.GetVTable() not in vb._vdict:\n return None\n return _offset(obj.getValueAddr())\n # An EUDArray is a proxy of its data's EPD (or of the data itself), the data an object of its own.\n data = _unproxy(obj)\n while not isinstance(data, EUDObject) and hasattr(data, \"_value\"):\n data = _unproxy(data)\n try:\n return _offset(data)\n except Exception:\n return None # (nothing in the map uses it, so eudplib did not place it)\n\n def _cells_of(s):\n if isinstance(s, UnitStorage):\n return {\"ptr\": s.ptr, \"uid\": s.uid}\n if isinstance(s, TextStorage):\n return {\"addr\": s.addr, \"len\": s.len}\n return {\"value\": s.store}\n\n def _program_places(k, low):\n p = low.p\n out = {\"index\": k, \"name\": p.get(\"name\"), \"perPlayer\": low.per_player, \"owners\": p.get(\"owners\", [low.owner]), \"vars\": [], \"arrays\": []}\n for id_, s in low.vars.items():\n # (A loop's unit is the scan's own pointer for one turn, kept nowhere: nothing to read.)\n d = getattr(s, \"decl\", None)\n if d is None or d.get(\"temp\") or d.get(\"task\") is not None:\n continue # (a task's variable is a row a slot: nothing a test reads by name)\n cells_ = _cells_of(s)\n out[\"vars\"].append({\"id\": id_, \"kind\": d.get(\"kind\"), \"text\": d.get(\"text\"), \"bits\": d.get(\"bits\"), \"unsigned\": bool(d.get(\"unsigned\")),\n \"rowed\": bool(s.rowed), \"cells\": {name: _place(cell) for name, cell in cells_.items()},\n \"stride\": VARRAY_STRIDE if any(is_varray(c) for c in cells_.values()) else 4})\n for id_, a in low.arrays.items():\n if isinstance(a, (InnerListStorage, SliceStorage)) or a.decl.get(\"task\") is not None:\n continue # (a row of another's cells, or a list inside one: read through what it is in, not here)\n d = a.decl\n row = {\"id\": id_, \"kind\": d.get(\"kind\"), \"bits\": d.get(\"bits\"), \"unsigned\": bool(d.get(\"unsigned\")), \"length\": int(d.get(\"length\", 0)), \"rowed\": bool(a.rowed)}\n if isinstance(a, ListStorage):\n row.update(dynamic=True, ptr=_place(a.handle[\"ptr\"]), len=_place(a.handle[\"len\"]), stride=VARRAY_STRIDE if is_varray(a.handle[\"ptr\"]) else 4)\n else:\n row.update(at=_place(a.store), stride=VARRAY_STRIDE if is_varray(a.store) else 4)\n out[\"arrays\"].append(row)\n return out\n\n _vconstruct = _vpayload._construct_payload\n\n def _construct_and_place():\n built = _vconstruct()\n # (eudplib works addresses out only while it writes the payload: for as long as this takes, it is again.)\n was = _vpayload.phase\n _vpayload.phase = _vpayload.PHASE_WRITING\n try:\n _PLACES[\"programs\"] = [_program_places(k, low) for k, low in enumerate(PROGRAMS)]\n _PLACES[\"heap\"] = _place(_HEAP[\"cells\"]) if _HEAP else None\n _PLACES[\"cells\"] = _place(_HEAP[\"vcells\"]) if _HEAP and \"vcells\" in _HEAP else None\n finally:\n _vpayload.phase = was\n _PLACES[\"bytes\"] = len(built.data)\n return built\n\n _vpayload._construct_payload = _construct_and_place\n\n _initialize = _injector.initialize_payload\n\n def _initialize_and_say(chkt, payload, *args, **kwargs):\n out = _initialize(chkt, payload, *args, **kwargs)\n # The payload is the end of the strings section, which Remastered keeps at 0x191943C8.\n base = 0x191943C8 + len(chkt.getsection(_strings_name())) - _PLACES[\"bytes\"]\n\n def at(o):\n return None if o is None else base + o\n\n print(\"TRIGSCRIPT-VARS \" + json.dumps({\"programs\": len(_PLACES[\"programs\"]), \"heap\": at(_PLACES[\"heap\"]), \"cells\": at(_PLACES.get(\"cells\")), \"strings\": 0x191943C8}, separators=(\",\", \":\")))\n for prog in _PLACES[\"programs\"]:\n for v in prog[\"vars\"]:\n v[\"cells\"] = {name: at(o) for name, o in v[\"cells\"].items()}\n for a in prog[\"arrays\"]:\n for key in (\"at\", \"ptr\", \"len\"):\n if key in a:\n a[key] = at(a[key])\n print(\"TRIGSCRIPT-PROGRAM \" + json.dumps(prog, separators=(\",\", \":\")))\n return out\n\n _injector.initialize_payload = _initialize_and_say\n\n\n# The loops of the IR and the code that runs apart from them, for static routing (above): a loop is a region with a way\n# in (`entry`, what came before jumps there; `after`, the loop's first trigger), a sleep or a parallel makes the loops\n# around it wait, a coroutine (a program's body, a task, a branch) runs apart, and a call of a function that calls itself\n# is one whose body this cannot follow.\nif ROUTE.enabled:\n _ROUTED_LOOPS = (\"while\", \"do\", \"for\", \"unitLoop\", \"textLoop\")\n _lower_statement = Lowering.statement\n\n def _routed_statement(self, st, ctx):\n k = st[\"kind\"]\n if k in (\"sleep\", \"parallel\"):\n ROUTE.waited()\n if k not in _ROUTED_LOOPS or not ROUTE.on:\n return _lower_statement(self, st, ctx)\n r = _Region(\"loop\")\n r.entry, r.after = Forward(), Forward()\n SetNextTrigger(r.entry)\n r.after << NextTrigger()\n ROUTE.loops.append(r)\n ROUTE.stack.append(r)\n try:\n return _lower_statement(self, st, ctx)\n finally:\n ROUTE.stack.pop()\n\n Lowering.statement = _routed_statement\n _lower_coroutine = Lowering.coroutine\n\n def _routed_coroutine(self, *args, **kwargs):\n ROUTE.stack.append(_Region(\"coroutine\"))\n try:\n return _lower_coroutine(self, *args, **kwargs)\n finally:\n ROUTE.stack.pop()\n\n Lowering.coroutine = _routed_coroutine\n\nINPUT = Input(IR.get(\"input\"))\n_SOURCES = {s: uses_source(IR.get(\"programs\", []), s) for s in (\"created\", \"died\", \"killed\", \"ordered\", \"morphed\")}\nEVENTS = Events(_SOURCES[\"created\"], _SOURCES[\"died\"] or _SOURCES[\"killed\"], _SOURCES[\"ordered\"], _SOURCES[\"morphed\"]) if any(_SOURCES.values()) else None\nPROGRAMS = [Lowering(p) for p in IR.get(\"programs\", [])]\n\n\ndef onPluginStart():\n # eudplib's generator starts from the same seed in every game; the game's own randomness (a switch randomized) seeds it.\n if uses_random(IR.get(\"programs\", [])):\n f_randomize()\n\n\ndef beforeTriggerExec():\n # After chatEvent has looked for a typed line, before MSQC sends what it was.\n if INPUT.chats:\n INPUT.match_line()\n\n\ndef afterTriggerExec():\n INPUT.sync_frame()\n if INPUT.held:\n INPUT.frame()\n if EVENTS is not None:\n EVENTS.run()\n for prog in PROGRAMS:\n prog.run()\n # Everything this file makes is made by now: which reads need no routing (above).\n ROUTE.decide()\n"; diff --git a/compiler/hoist.ts b/compiler/hoist.ts index e868554..7530757 100644 --- a/compiler/hoist.ts +++ b/compiler/hoist.ts @@ -94,6 +94,10 @@ export function libraryName(ts: typeof TS, checker: TS.TypeChecker, id: TS.Ident if (sym && sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); const decl = sym?.valueDeclaration ?? sym?.declarations?.[0]; if (!decl || decl.getSourceFile().fileName !== DECLARATIONS_FILE) return null; + // (A member of one of the library's interfaces — a unit's `order()` method, a field of a record it declares — is that + // type's, not the library's `order()`: `first(…)?.order("move", …)` is the unit's. A member of one of its objects — + // `on.died`, `local.selected`, `memory.game` — is the library's.) + if ((ts.isMethodSignature(decl) || ts.isPropertySignature(decl)) && ts.isInterfaceDeclaration(decl.parent)) return null; return sym!.name; } @@ -682,6 +686,8 @@ function planWithin(ts: typeof TS, checker: TS.TypeChecker, plan: ProgramPlan, a const scan = (n: TS.Node): void => { if (!ok) return; if (ts.isIdentifier(n)) { identifier(n, n.parent); return; } + // `this` inside an arrow written in a method is the instance the method runs on, which the program has. + if (n.kind === ts.SyntaxKind.ThisKeyword || n.kind === ts.SyntaxKind.SuperKeyword) { ok = false; return; } ts.forEachChild(n, scan); }; const identifier = (n: TS.Identifier, p: TS.Node) => { diff --git a/compiler/runtime.ts b/compiler/runtime.ts index 2fc6477..eae9bba 100644 --- a/compiler/runtime.ts +++ b/compiler/runtime.ts @@ -379,10 +379,20 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: if (n % 4 !== 0) throw new ScriptError(`${what}: expected a 4-byte-aligned memory address.`); return ((n - DEATHS_TABLE_ADDRESS) / 4) >>> 0; }; + /** + * A dword as a Deaths names it: its EPD in the player field, unit 0. A dword 12 to 26 dwords into the deaths table + * (Player 2's Ghosts to Player 3's Vultures) cannot be named so — the game reads the player group of that number + * instead (13 the current player, 17 all players, 18 to 21 the forces) — and is the deaths of player e mod 12 of + * unit e div 12, the same cell, so it is named as that in every build. + */ + const deathsAt = (address: unknown, what: string): { player: number; unitId: number } => { + const e = epd(address, what); + return e >= 12 && e <= 26 ? { player: e % 12, unitId: Math.floor(e / 12) } : { player: e, unitId: 0 }; + }; rt.memory = (address: unknown, comparison: unknown, value: unknown) => - condition({ ...emptyCondition(), type: ConditionType.Deaths, player: epd(address, "memory: address"), unitId: 0, comparison: argValue("comparison", comparison, "memory: comparison"), amount: integer(value, "memory: value") }); + condition({ ...emptyCondition(), type: ConditionType.Deaths, ...deathsAt(address, "memory: address"), comparison: argValue("comparison", comparison, "memory: comparison"), amount: integer(value, "memory: value") }); rt.setMemory = (address: unknown, modifier: unknown, value: unknown) => - action({ ...emptyAction(), type: ActionType.SetDeaths, player: epd(address, "setMemory: address"), unitId: 0, modifier: argValue("modifier", modifier, "setMemory: modifier"), target: integer(value, "setMemory: value") }); + action({ ...emptyAction(), type: ActionType.SetDeaths, ...deathsAt(address, "setMemory: address"), modifier: argValue("modifier", modifier, "setMemory: modifier"), target: integer(value, "setMemory: value") }); rt.disabled = (item: unknown) => { if (isCondition(item)) return condition({ ...item.record, flags: item.record.flags | ConditionFlag.Disabled }); if (isAction(item)) return action({ ...item.record, flags: item.record.flags | ActionFlag.Disabled }); @@ -519,6 +529,8 @@ export function createRuntime(names: ScriptNames, collector: Collector, options: const onlyInProgram = (ident: string, how: string): never => { throw new ScriptError(`${ident}() is about the units in the game, which exist while it runs: ${how}, inside program().`); }; const filterOf = (ident: string, v: unknown, given: UnitFilter): UnitFilter => { const out: UnitFilter = { ...given }; + // `first(unitsAt(locations.Base, { owner: P1 }))`: the set's own filter. + if (isUnitQuery(v)) v = v.filter; if (v !== undefined && v !== null) { if (typeof v !== "object" || Array.isArray(v) || isGameValue(v)) throw new ScriptError(`${ident}: the filter is an object such as { type: units.TerranMarine, owner: P1, at: locations.Base }, got ${describe(v)}.`); for (const [key, value] of Object.entries(v as Record)) { diff --git a/compiler/structured.ts b/compiler/structured.ts index 10b5bef..1abb166 100644 --- a/compiler/structured.ts +++ b/compiler/structured.ts @@ -57,14 +57,14 @@ * expression, with a note that it ran when the script was built. */ import type * as TS from "typescript"; -import { ActionType, PlayerGroup } from "../vendor/triggers"; +import { ActionFlag, ActionType, ConditionFlag, PlayerGroup } from "../vendor/triggers"; import type { ActionRecord } from "../vendor/triggers"; import type { HoistedThunks, ProgramPlan } from "./hoist"; import { clockCall, declarationOf, isOnCall, isScriptClass, isScriptFunction, keepsState, libraryCallName, ON_EVENTS, scriptArrow } from "./hoist"; import { ORDER_NAMES } from "./orders"; import { DEAF_KEYS, KEY_NAMES, keyName, parseChatPattern, type ChatPattern, type InputSource } from "./input"; import { scriptParams } from "./api"; -import { hasTextMark, isAction, isBuilder, isChat, isCondition, isDuration, isGameFunction, isGameValue, isInput, isMouse, isPrint, isRead, isReader, isTable, isTrigger, isUnitPick, isUnitQuery, playerColor, READ_ARITY, textParts, type GameFunctionValue, type InputValue, type ReadValue, type ScriptString, type TableValue, type UnitPickValue, isSelected, isSelectionCount, SELECTION_ADDRESS } from "./runtime"; +import { hasTextMark, isAction, isBuilder, isChat, isCondition, isDuration, isGameFunction, isGameValue, isInput, isMouse, isPrint, isRead, isReader, isTable, isTrigger, isUnitPick, isUnitQuery, playerColor, READ_ARITY, ScriptError, textParts, type ConditionValue, type GameFunctionValue, type InputValue, type ReadValue, type ScriptString, type TableValue, type UnitPickValue, isSelected, isSelectionCount, SELECTION_ADDRESS } from "./runtime"; import { cellMax } from "./tables"; import { markViewWrites, type View } from "./views"; import { DEFAULT_COLOR, EMPTY_CELL, EMPTY_COLOR, FILLED_CELL, JSX_ELEMENTS, TEXT_COLORS, colorCode } from "./jsx"; @@ -708,6 +708,14 @@ export class Structured { const e = this.unwrap(expr); const pin = this.pinned.get(e); if (pin) return pin; + // `new P(4).x`: the instance made where it is written, under no name, its fields then read as any instance's. + if (ts.isNewExpression(e)) { + const cls = this.classOf(e.expression); + if (!cls || this.isRefClass(cls)) return undefined; + const made = this.instantiate(e, "(made)"); + if (made) this.pinned.set(e, made); + return made ?? undefined; + } if (ts.isIdentifier(e)) { // A name taken out of a pattern (`for (const [k, v] of lost)`) is bound by its own element, not by the declaration it is part of. const raw = declarationOf(ts, this.c.checker, e); @@ -988,6 +996,13 @@ export class Structured { const message = err instanceof Error ? err.message : String(err); const i = (err as { __trigscriptConst?: number } | null)?.__trigscriptConst; const decl = i !== undefined ? this.body.plan.constList[i] : undefined; + // Inside a `try`, an error the script's own code threw as it was worked out — `try { total += check(9); }` with a + // `check` that throws for 9 — is the program's, thrown where the expression stands, as JavaScript would throw it: + // the catch takes it. (0 stands where the value would have, after the throw, which nothing reaches.) + if (!decl && this.tries > 0 && err instanceof Error && !(err instanceof LowerError) && !(err instanceof ScriptError) && Object.hasOwn(ERROR_CODES, err.name)) { + this.throwBuilt(err.name as BuiltinError, err.message, at); + return 0; + } if (decl?.initializer) throw new ValueError(decl.initializer, `${message} — this constant is computed when the script is built, not in the game.`); throw new ValueError(at, `${message} — this expression is computed when the script is built, not in the game.`); } @@ -1209,7 +1224,12 @@ export class Structured { } private returnStatement(s: TS.ReturnStatement, ctx: Ctx) { - if (!ctx.fn) { this.c.error(s, "return outside a function."); return; } + if (!ctx.fn) { + // `if (!raider) return;` at the top of a program: the program ends there, for good. + if (s.expression) { this.c.error(s.expression, "A program gives nothing back: return; alone ends it."); return; } + this.emit({ kind: "return", at: this.at(s), label: this.label(s) }, s); + return; + } const { fn } = ctx; if (fn.inFinally) { this.c.error(s, "A finally block runs on every way out of its try, an error's way too, so it does not return: return after the try."); return; } if (fn.finallies?.length) { this.returnThrough(s, ctx); return; } @@ -1524,6 +1544,21 @@ export class Structured { * `throw new Error("…")`, `throw new OutOfGold(need)`, `throw e`: the error is put in flight — its message, its class's * fields, its class's code last — and goes to the nearest `try` around it, out through the calls it is in. */ + /** How many `try` bodies the walk is inside: an error a build-time call throws there is the program's (`force`). */ + private tries = 0; + + /** A built-in error thrown where `node` stands, with its message: what `throw new Error("…")` emits. */ + private throwBuilt(name: BuiltinError, message: string, node: TS.Node) { + const at = this.at(node), label = this.label(node); + const f = this.flightOf(node); + this.emit({ kind: "assignText", target: f.message.id, value: { kind: "text", text: message }, at, label }, node); + const block: Stmt & { kind: "block" } = { kind: "block", body: [], at }; + this.stackSites.push({ block, name, at, label }); + this.emit(block, node); + this.emit({ kind: "assign", target: f.thrown.id, value: num(ERROR_CODES[name]), at, label }, node); + this.emit({ kind: "throw", at, label }, node); + } + private throwStatement(s: TS.ThrowStatement) { const { ts } = this; const at = this.at(s), label = this.label(s); @@ -1610,7 +1645,9 @@ export class Structured { const inner: Ctx = fin ? { ...ctx, ...(ctx.fn ? { fn: { ...ctx.fn, finallies: [...(ctx.fn.finallies ?? []), fin] } } : {}), exits: { break: [...(ctx.exits?.break ?? []), fin], continue: [...(ctx.exits?.continue ?? []), fin] } } : ctx; - const body = this.collect(() => this.block(s.tryBlock.statements, inner)); + this.tries++; + let body: Stmt[]; + try { body = this.collect(() => this.block(s.tryBlock.statements, inner)); } finally { this.tries--; } const guarded: Stmt = s.catchClause ? { kind: "try", body, catch: this.catchBody(s.catchClause, inner), at, label } : { kind: "block", body, at }; if (!fin) { this.emit(guarded, s); return; } const { save, restore } = this.keepFlight(at, label); @@ -2017,6 +2054,9 @@ export class Structured { if (race && race !== "all") { this.c.error(d, `${d.name.text} would be ${this.c.checker.typeToString(type)}: Promise.${race} gives the value of the branch that ends first, so every branch gives one. Give a sleep() or an until() a value with .then(() => …).`); continue; } // `let target: Enemy | null`, `let s: Shape = …` (B9): a variable that may be none, or one of several classes — they are kept by reference. if (!kind) { const wanted = this.instanceClasses(type); if (wanted && this.needRef(wanted)) { this.c.error(d, `${d.name.text} is ${this.c.checker.typeToString(type)}: its classes are kept by reference.`); continue; } } + // `const both = command(…) && bring(…)`: conditions of the game, kept to be asked where the name is tested, as + // `const there = bring(…)` is (the script hoists that one itself; `&&` of two is the program's). + if (!kind) { const conds = this.conditionsOf(init); if (conds) { this.scope.bind(d, { kind: "value", value: conds }); continue; } } if (!kind) { this.c.error(d, `Variables hold numbers, booleans, texts, units of the game or records of them ({ lives: 3 }); ${d.name.text} is ${this.c.checker.typeToString(type)}.`); continue; } // `let total = shared(0)`: one cell for every player of a per-player program, initialised with the argument. const shared = ts.isCallExpression(init) && this.isLibraryCall(init, "shared") ? init : null; @@ -3192,16 +3232,21 @@ export class Structured { return this.mark({ kind: "pop", array: a.id, at }, e); } case "fill": { - if (as !== "statement" || e.arguments.length !== 1) return wrong(`${a.name}.fill(value) stands on its own and takes one value.`); + if (as !== "statement" || e.arguments.length < 1 || e.arguments.length > 3) return wrong(`${a.name}.fill(value, start?, end?) stands on its own.`); if (a.values) return wrong(`${a.name} was computed when the script was built and is only read in a program.`); const value = one(e.arguments[0]); if (!value) return null; - // The value once, then every cell: a loop the game runs, as for…of is. + // The value once, then every cell: a loop the game runs, as for…of is. With `start` and `end` (JavaScript's, + // counted from the end when negative, cut to the array), from the one to before the other. const v = this.newVar(`(fill of ${a.name})`, a.kind, at, { temp: true, ...(a.bits ? { bits: a.bits } : {}), ...(a.unsigned ? { unsigned: true } : {}) }); const i = this.newVar(`(index of ${a.name})`, "number", at, { temp: true }); + const length: NumExpr = { kind: "length", array: a.id, at }; + const from = e.arguments[1] ? this.sliceEnd(e.arguments[1], length, e.arguments[1]) : num(0); + const to = e.arguments[2] ? this.sliceEnd(e.arguments[2], length, e.arguments[2]) : length; + if (!from || !to) return null; this.emit({ kind: "declare", decl: v, init: value, at, label }, e); - this.emit({ kind: "declare", decl: i, init: num(0), at, label }, e); - this.emit({ kind: "for", cond: { kind: "compare", op: "<", left: varRef(i), right: { kind: "length", array: a.id, at }, at, label }, update: [{ kind: "assign", target: i.id, value: { kind: "binary", op: "+", left: varRef(i), right: num(1), at, label }, at, label }], + this.emit({ kind: "declare", decl: i, init: from, at, label }, e); + this.emit({ kind: "for", cond: { kind: "compare", op: "<", left: varRef(i), right: to, at, label }, update: [{ kind: "assign", target: i.id, value: { kind: "binary", op: "+", left: varRef(i), right: num(1), at, label }, at, label }], body: [{ kind: "store", array: a.id, index: varRef(i), value: a.kind === "number" ? varRef(v) : boolRef(v), at, label }], at, label }, e); return true; } @@ -4039,6 +4084,43 @@ export class Structured { body: pairs.map(([to, of]): Stmt => ({ kind: "push", array: to.id, value: { kind: "element", array: of.id, index: varRef(i), at }, at, label })), at, label }, where); } + /** + * Conditions of the game known when the script is built, joined by `&&` — `command(…) && bring(…)` — each as the + * script made it. Null when the expression is anything else (a `!`, a comparison of a variable: the program's own). + */ + private conditionsOf(e: TS.Expression): ConditionValue[] | null { + const { ts } = this; + const x = this.unwrap(e); + if (ts.isBinaryExpression(x) && x.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { + const l = this.conditionsOf(x.left), r = this.conditionsOf(x.right); + return l && r ? [...l, ...r] : null; + } + let h: Hoisted | undefined; + try { h = this.evaluate(x); } catch { return null; } + if (!h) return null; + if (isCondition(h.value)) return [h.value]; + return Array.isArray(h.value) && h.value.length > 0 && h.value.every(isCondition) ? (h.value as ConditionValue[]) : null; + } + + /** + * `a ** n` and `Math.pow(a, n)`: the base multiplied into itself, `n` known when the script is built (the game has + * no power of a variable — a loop of products says it). `a ** 0` is 1. + */ + private power(baseE: TS.Expression, expE: TS.Expression, e: TS.Expression): NumExpr | null { + const h = this.evaluate(expE); + const n = h ? this.asInteger(h, expE) : null; + if (n === null) { if (!h) this.c.error(expE, "The power is known when the script is built: a ** 2, Math.pow(a, 3). For one the game works out, multiply in a loop."); return null; } + if (n < 0) { this.c.error(expE, "A negative power is a fraction below 1, which a whole number of the game has no room for."); return null; } + const base = this.num(baseE); + if (!base) return null; + if (n === 0) return num(1); + const at = this.at(e), label = this.label(e); + const x = n > 1 ? this.temp(base, baseE) : base; + let acc: NumExpr = x; + for (let i = 1; i < n; i++) acc = { kind: "binary", op: "*", left: acc, right: x, at, label }; + return this.mark(acc, e); + } + /** A place a `slice` is given, as JavaScript reads it: below zero it counts from the end, and it stays inside 0 … length. */ private sliceEnd(given: TS.Expression, length: NumExpr, where: TS.Node): NumExpr | null { const n = this.num(given); @@ -4756,8 +4838,11 @@ export class Structured { /** `hp[i] = v`, `hp[i] += v`: a store into a cell. An index that holds a call is evaluated once for the read and once for the store, as two reads of `i` would be. */ private storeElement(e: TS.BinaryExpression, el: { a: ArrayDecl; index: NumExpr }, op: TS.SyntaxKind) { const { ts } = this; - const { a, index } = el; + const { a } = el; if (a.values) { this.c.error(e.left, `${a.name} was computed when the script was built and is only read in a program; declare it with let inside the program to write to it.`); return; } + // (`xs[n++] = n * 10`: the index before the value, as JavaScript has it — an index with a call in it is worked out first.) + const calls = (x: unknown): boolean => !!x && typeof x === "object" && (Array.isArray(x) ? x.some(calls) : (x as { kind?: string }).kind === "call" || Object.values(x).some(calls)); + const index = calls(el.index) ? this.temp(el.index, e.left) : el.index; const emit = (value: NumExpr | BoolExpr) => this.emit({ kind: "store", array: a.id, index, value, at: this.at(e), label: this.label(e) }, e); if (a.kind === "boolean") { if (op !== ts.SyntaxKind.EqualsToken) { this.c.error(e, "Booleans take = only."); return; } @@ -5213,11 +5298,15 @@ export class Structured { } private emitAction(a: ActionRecord, at: TS.Node) { - if (a.type === ActionType.PreserveTrigger) return; // Every generated trigger is preserved already. + // Every generated trigger is preserved already: a word, and nothing emitted. + if (a.type === ActionType.PreserveTrigger) { this.emit({ kind: "remark", short: "does nothing in a program", text: "preserve() keeps a trigger running after it has fired. A program runs on its own until it ends, so this does nothing here and can go.", at: this.at(at) }, at); return; } + if (a.flags & ActionFlag.Disabled) { this.c.error(at, "disabled() switches an action of a trigger off, which the game then skips. In a program, leave the action out instead."); return; } const s = a.text > 0 ? this.c.strings[a.text - 1] : undefined; if (s && "text" in s && hasTextMark(s.text)) { // name() or color() in the text: only displayText has a form the game fills in. if (a.type !== ActionType.DisplayText) { this.c.error(at, "name() and color() are filled in while the game runs, which displayText() and print() can do; this action's text is fixed when the script is built."); return; } + // (A print is always displayed: the flag an action carries has no place in it.) + if (!(a.flags & ActionFlag.AlwaysDisplay)) this.emit({ kind: "remark", short: "always displayed", text: "A text with name() or color() in it is filled in while the game runs, as print() fills one, and such a text is always displayed: the false here changes nothing.", at: this.at(at) }, at); this.emitPrint(textParts(s.text), CURRENT_PLAYER, "chat", at); return; } @@ -7293,7 +7382,7 @@ export class Structured { */ private switchStatement(s: TS.SwitchStatement, ctx: Ctx) { const { ts } = this; - if (this.isTextTyped(s.expression) && !this.evaluate(s.expression)) { this.textSwitch(s, ctx); return; } + if (this.isTextTyped(s.expression)) { this.textSwitch(s, ctx); return; } const value = this.num(s.expression); if (!value) return; const clauses = s.caseBlock.clauses; @@ -7332,7 +7421,10 @@ export class Structured { const { ts } = this; const given = this.text(s.expression); if (!given) return; - const of = this.textTemp(given, s.expression); + // (A text known when the script is built — `switch (MODE)` of a const — is matched now: the case it falls into is + // the one taken, and no text is compared in the game.) + const known = given.kind === "text" ? given.text : undefined; + const of = known === undefined ? this.textTemp(given, s.expression) : given; const clauses = s.caseBlock.clauses; const at = this.at(s), label = this.label(s); let which: NumExpr = num(-1); @@ -7342,6 +7434,7 @@ export class Structured { if (ts.isDefaultClause(c)) continue; const h = this.evaluate(c.expression); if (!h || typeof h.value !== "string" || hasTextMark(h.value)) { this.notConstant(c.expression, "A case's text"); values[i] = Number.NaN; continue; } + if (known !== undefined) { if (h.value === known) which = num(i); continue; } which = { kind: "ternary", cond: { kind: "textCompare", op: "==", left: of, right: { kind: "text", text: h.value }, at, label }, whenTrue: num(i), whenFalse: which, at, label }; } const scope = new Scope(this.scope); @@ -10548,6 +10641,15 @@ export class Structured { asCalled({ init: unit, label }); return; } + if (!ts.isIdentifier(this.unwrap(arg)) && this.kindOf(this.c.checker.getTypeAtLocation(p.name)) === "boolean" && this.kindOf(this.c.checker.getTypeAtLocation(arg)) === "boolean") { + // `step(n, turns < 2)`: a comparison, or any condition of the program's, into a boolean of the parameter's own. + const test = this.bool(arg); + const copy = this.newVar(p.name.text, "boolean", this.sourceOfIn(target, p.name)); + out.params.push({ decl: copy, init: test, label }); + scope.bind(p, { kind: "var", v: copy }); + asCalled({ init: test, label }); + return; + } const value = ts.isIdentifier(this.unwrap(arg)) ? null : this.numQuietly(arg); if (value) { // An expression over variables: computed into a variable of the parameter's own. @@ -10993,6 +11095,14 @@ export class Structured { if (ts.isNewExpression(e)) { const cls = this.classOf(e.expression); if (cls && this.isRefClass(cls)) return this.newObject(e, cls); } // `this` of an instance kept by reference: its row. if (e.kind === ts.SyntaxKind.ThisKeyword) { const self = this.scope.lookup(THIS); if (self?.kind === "record" && self.ref) return self.ref.place; } + // `await sleep(frames(1)).then(() => 7)` as a value: the wait, then what the function gives, in a variable. + if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && e.expression.name.text === "then" && this.isPromise(e.expression.expression)) { + const at = this.at(e), label = this.label(e); + const v = this.newVar("(then)", "number", at, { temp: true, ...this.widthOf(this.c.checker.getTypeAtLocation(expr)) }); + this.emit({ kind: "declare", decl: v, init: num(0), at, label }, e); + this.task(e, { decl: v, kind: "number" }); + return varRef(v); + } const h = this.evaluate(expr); // No function: `let f: (() => void) | undefined = undefined`; no instance (B9): `let next: Node | null = null`. if (h && (h.value === undefined || h.value === null) && this.noFunction(expr)) return num(0); @@ -11117,14 +11227,27 @@ export class Structured { if (!inner) return null; if (e.operator === ts.SyntaxKind.PlusToken) return inner; if (e.operator === ts.SyntaxKind.MinusToken) return this.mark({ kind: "unary", op: "-", expr: inner, at: this.at(e) }, e); - this.c.error(e, "Only + and - apply to variables."); + // `~a`: every bit the other way, which is a ^ 0xFFFFFFFF (−a − 1 of a signed number, as in JavaScript). + if (e.operator === ts.SyntaxKind.TildeToken) return this.mark({ kind: "binary", op: "^", left: inner, right: num(-1), at: this.at(e), label: this.label(e) }, e); + this.c.error(e, "Only + - and ~ apply to variables."); return null; } // `queue.pop() ?? 0`: what TypeScript asks for, since a pop of an empty array is undefined there. The other value when it is empty. if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken) { const left = this.unwrap(e.left); + // `u?.hp ?? 0` of a unit or none: its field while it is there and on the map, the other value when not. + if (ts.isPropertyAccessExpression(left) && left.questionDotToken && this.isUnitTyped(left.expression)) { + const m = this.unitMember(left); + const other = this.num(e.right); + if (!m || !other) return null; + const value = this.unitField(left, m); + if (!value) return null; + const at = this.at(e), label = this.label(e); + return this.mark({ kind: "ternary", cond: { kind: "unitAlive", unit: m.unit, at, label }, whenTrue: value, whenFalse: other, at, label }, e); + } const taken = ts.isCallExpression(left) && ts.isPropertyAccessExpression(left.expression) ? left.expression.name.text : ""; - const list = taken === "pop" || taken === "shift" ? this.bindingOf((left as TS.CallExpression & { expression: TS.PropertyAccessExpression }).expression.expression) : undefined; + // (`rows[i].pop() ?? -1`: the row an array of arrays holds at i is the list, as `listOf` finds it.) + const list = taken === "pop" || taken === "shift" ? this.listOf((left as TS.CallExpression & { expression: TS.PropertyAccessExpression }).expression.expression) : undefined; // `xs.at(i) ?? 0`: the other value for a place past either end. const atOf = taken === "at" ? this.bindingOf((left as TS.CallExpression & { expression: TS.PropertyAccessExpression }).expression.expression) : undefined; if (atOf?.kind === "array" && atOf.a.kind === "number" && ts.isCallExpression(left) && left.arguments.length === 1) { @@ -11158,9 +11281,10 @@ export class Structured { return this.mark({ kind: "ternary", cond: { kind: "compare", op: ">", left: { kind: "length", array: list.a.id, at }, right: num(0), at, label: this.label(e) }, whenTrue: popped, whenFalse: other, at, label: this.label(e) }, e); } } + if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.AsteriskAsteriskToken) return this.power(e.left, e.right, e); if (ts.isBinaryExpression(e)) { const op = arithOp(ts, e.operatorToken.kind); - if (!op) { this.c.error(e, "Expected a number: variables take + - * / % and the bitwise & | ^ << >> >>>."); return null; } + if (!op) { this.c.error(e, "Expected a number: variables take + - * / % ** and the bitwise & | ^ << >> >>>."); return null; } const l = this.num(e.left); const r = this.num(e.right); if (!l || !r) return null; @@ -11356,6 +11480,33 @@ export class Structured { const a = args(1, "Math.abs()"); return a ? intrinsic("abs", [a[0]]) : null; } + if (callee === Math.pow) { + if (e.arguments.length !== 2) { this.c.error(e, "Math.pow() takes 2 arguments."); return null; } + return this.power(e.arguments[0], e.arguments[1], e); + } + if (callee === Math.sign) { + // 1, −1 or 0, the number read as signed, as JavaScript's is. + const a = args(1, "Math.sign()"); + if (!a) return null; + const at = this.at(e), label = this.label(e); + const x = this.temp(a[0], e); + const below: NumExpr = { kind: "ternary", cond: { kind: "compare", op: "<", left: x, right: num(0), at, label }, whenTrue: num(-1), whenFalse: num(0), at, label }; + return this.mark({ kind: "ternary", cond: { kind: "compare", op: ">", left: x, right: num(0), at, label }, whenTrue: num(1), whenFalse: below, at, label }, e); + } + if (callee === Math.log2) { + // The place of the highest bit set — floor(log2 x) of a whole number — found by halving: five comparisons, each + // with a power of two. Of 0 it is 0 (JavaScript's −Infinity is no number of the game). + const a = args(1, "Math.log2()"); + if (!a) return null; + const at = this.at(e), label = this.label(e); + const x = this.temp(a[0], e); + const between = (lo: number, hi: number): NumExpr => { + if (lo === hi) return num(lo); + const mid = (lo + hi + 1) >> 1; + return { kind: "ternary", cond: { kind: "compare", op: ">=", left: x, right: num(2 ** mid), at, label, unsigned: true }, whenTrue: between(mid, hi), whenFalse: between(lo, mid - 1), at, label }; + }; + return this.mark(between(0, 31), e); + } if (callee === Math.min || callee === Math.max) { if (e.arguments.length === 0) { this.c.error(e, "Math.min / Math.max take at least one argument."); return null; } let acc: NumExpr | null = null; @@ -11702,13 +11853,13 @@ export class Structured { if (typeof v === "boolean") return v ? TRUE : FALSE; if (typeof v === "number") return v !== 0 ? TRUE : FALSE; if (typeof v === "string") return v !== "" ? TRUE : FALSE; - if (isCondition(v)) return this.mark({ kind: "cond", record: { ...v.record } }, at); + if (isCondition(v)) return this.conditionOf(v, at); if (isRead(v)) return this.readBool(v, at); if (isTable(v)) { const read = this.tableRead(v, at); return read ? this.mark({ kind: "test", expr: read, at: this.at(at), label: this.label(at) }, at) : FALSE; } if (isInput(v)) return this.inputBool(v, at); if (isChat(v)) return this.inputBool(v.matched, at); if (isMouse(v)) { this.c.error(at, "mouse() is a place on the map, not a condition: compare its x or its y."); return FALSE; } - if (Array.isArray(v) && v.length > 0 && v.every(isCondition)) return { kind: "and", items: v.map((c) => this.mark({ kind: "cond", record: { ...c.record } }, at)) }; + if (Array.isArray(v) && v.length > 0 && v.every(isCondition)) return { kind: "and", items: v.map((c) => this.conditionOf(c, at)) }; if (isAction(v)) { this.c.error(at, "This is an action, not a condition."); return FALSE; } // Anything else known when the script is built is true or false as in JavaScript: a note or a rest (null) of a // pattern, a clip, a picture — \`if (note) playWav(note)\` in a loop unrolled over them. @@ -11718,6 +11869,12 @@ export class Structured { return FALSE; } + /** A condition of the game as the program tests it. `disabled(…)` is a trigger's: there, the game skips the condition; a program would hold it, so it is refused instead. */ + private conditionOf(v: ConditionValue, at: TS.Node): BoolExpr { + if (v.record.flags & ConditionFlag.Disabled) { this.c.error(at, "disabled() switches a condition of a trigger off, which the game then skips. A program tests its conditions: leave this one out, or take disabled() off it."); return FALSE; } + return this.mark({ kind: "cond", record: { ...v.record } }, at); + } + /** A condition as a `BoolExpr` tree. */ private bool(expr: TS.Expression): BoolExpr { return this.boolInner(expr, 0); diff --git a/docs/v4-plan.md b/docs/v4-plan.md index 0118990..f292edf 100644 --- a/docs/v4-plan.md +++ b/docs/v4-plan.md @@ -3984,8 +3984,9 @@ the ledger is the list it can be written again from at 4.0. *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.* + programs in its build. Decided 2026-10-23: Magenta's build rows go on our backend too — a slice of its own after + step 17 (*Magenta on our backend*, under *Open decisions*). *Protection.* Decided 2026-10-23: none. The own build + scrambles nothing (eudplib's MRGN, shuffle and fake ISOM), and will not. *The fixes' lines.* "227 rows of UNIx at the defaults with settings left in: zeroed" is true of nearly every map StarEdit wrote (it fills the defaults in); the line could be quieter. *A refusal found at build only.* The apply's compile answers for the map as it was; a map whose players change after the apply (a slot made a computer, say) is built with the @@ -3993,23 +3994,16 @@ the ledger is the list it can be written again from at 4.0. *The step's purpose.* `document.export` from a plugin builds too (the host runs the steps); nothing says which plugin asked. *Sounds.* A clip the apply wrote as a member is in the archive; a build after `withSounds` refused one is not reached (the apply throws first). -- `new P(4).x` written in a program's expression is "A value must be known when the script is built"; the same through - a function (`function made(x) { return new P(x).x; }`) is taken. -- `displayText(text, always)` made a print loses its flag — to check against the flags a print gives Display Text. -- `disabled(…)` inside a program is neither refused nor said: a disabled condition in an `if` holds, as in a trigger. - `preserve()` and `preserveTrigger()` there are dropped (every program is preserved), also without a word. -- False errors: `n++` used as a value in a loop's body ("This loop's condition never changes" — `checkSleeps` does not - see writes inside expressions or calls); a comparison passed as a boolean argument (`step(n, turns < 2)`); a `switch` - on a text known when built; `?:` between two closures; an arrow using `this` inside a method (a JavaScript exception - as the message); `first(…)?.order(…)` on a pick; `u?.hp ?? 0`; `await sleep(…).then(() => 7)`. -- `return` at the top of a program is "return outside a function", and `trigscript/cutscene`'s own example uses it (and - `first(unitsAt(…))`, which `first` does not take). -- Not in a program, against JavaScript: `~a`, `**`, `Math.pow`, `Math.sign`, `Math.log2`, `fill(v, start, end)`, - `reduce` with no first value. A `const HALF = 7 / 2` worked out when built is 3.5, a fixed number; the same `7 / 2` in - the game is 3. -- A condition kept in a variable: `const both = command(…) && bring(…)` is "Variables hold numbers, booleans, … before - is Condition", where `switchIs(…) && !switchIs(…)` is taken as a boolean, and either in an `if` is taken - (found writing `probes/own-game.ts`, 2026-10-20). +- Taken by the bug pass of 2026-10-23 (*The Open list's bug pass*, below): `new P(4).x`; `disabled()` and `preserve()` + in a program; the false errors (`n++` as an index, a comparison as a boolean argument, a `switch` on a known text, an + arrow's `this` in a method, `first(…)?.order(…)`, `u?.hp ?? 0`, `sleep(…).then(() => 7)`); `return` at the top of a + program and the cutscene example; `~`, `**`, `Math.pow`, `Math.sign`, `Math.log2`, `fill(v, start, end)`; a + condition kept in a `const` with `&&`; a build-time throw inside a `try`; `rows[i].pop() ?? -1`; the emitter's + fold of a masked scratch cell into a constant; `memory()` of the deaths table's cells 12 to 26. +- `displayText(text, false)` with `name()` or `color()` in the text is a print, and a print is always displayed: said + as a remark since 2026-10-23 (a flag on the print IR would be the way to keep the `false`, in both backends). +- A `const HALF = 7 / 2` worked out when built is 3.5, a fixed number; the same `7 / 2` in the game is 3. (JavaScript's + arithmetic when the script is built, the game's in the game: as documented, not a bug.) - `command()` straight after `killUnit()` or `removeUnit()` still counts the units (the game counts them until the next frame) where the simulator does not; and two Transmissions, the second while the first one's time runs, hold the second until the program reaches it again (both builds; found writing `probes/own-fields.ts`, 2026-10-20). @@ -4064,16 +4058,16 @@ the ledger is the list it can be written again from at 4.0. colon keeps it right either way) — the probe settles it. Two lines typed in one frame keep the later one. The carriers stand at (64, 64): a `Bring` of the map's own triggers over a location at the corner counts them for the human. The passes' cost a frame is not weighed. A sync() value is at most 2^20 − 2 on any map of 64 by 64 or more, as before; on a smaller map the point carries fewer bits (18 on 32 by 32), and the - value is cut to that. The emitter folds a sum of bit-slices of one read with a negative constant wrongly (above, - "Found on the way"): to find in the emitter's sliced copies and fix, with a random-program test that mixes `&`, - `>>>` and a subtraction. + value is cut to that. The emitter's fold of a sum of bit-slices of one read with a negative constant (above, "Found + on the way") was the lone-place fold of a masked scratch cell, fixed 2026-10-23 (*The Open list's bug pass*). - **The map's own triggers on a map of our own (step 13), what is left:** the order is eudplib's — every player's triggers, then the programs — and no other can be said yet (a program before the triggers would run from the first player's list, before their entry's chain: a word in `program()`'s options). A trigger for a player group the game gives no list (Players 9 to 12, Foes, Allies, Neutral, Non-Allied Victory Players) is in no list, as in the game, - without a word. The map's own triggers' Comments are kept as the game keeps them (a Comment action in the payload: 32 - bytes each). A map maker's own hyper triggers in the map's TRIG section are left out as the script's are, found by - their shape (preserved, 32 or more `Wait(0)`s) — said in the build's line, nowhere else yet. The chain's nodes are + said as a build warning since 2026-10-23 (a notice in the editor and a line of the log). The map's own triggers' + Comments are kept as the game keeps them (a Comment action in the payload: 32 bytes each). A map maker's own hyper + triggers in the map's TRIG section are left out as the script's are, found by their shape (preserved, 32 or more + `Wait(0)`s) — said in the build's line, and as a build warning since 2026-10-23. The chain's nodes are stacked as the graph's triggers are, 2,408 bytes a node only when full. Open in eudplib's build, found by the probe: it runs a trigger whose Disabled flag is set (the game does not), and with `hyperTriggers()` in the map it goes through the triggers every second frame (the game's own doing under eudTurbo, measured: a list with hyper triggers in @@ -4081,14 +4075,12 @@ the ledger is the list it can be written again from at 4.0. - A `deaths(p, u)` read whose cell is 12 to 26 dwords into the deaths table (Player 2's Ghosts, Player 1's Vultures) cannot be asked by its EPD — the game reads the player group of that number instead (13 the current player, 17 all players, 18 to 21 the forces) — so a map of our own asks the game's own Deaths condition of that player and unit, the - mask in its record (fixed in step 13, found by its probe: the read gave the current player's Marines). The same holds - for `memory(0x58A394 … 0x58A3CC)` in both builds, which still read the group: in the Open list for the Python side. + mask in its record (fixed in step 13, found by its probe: the read gave the current player's Marines). `memory()` and + `setMemory()` of those fifteen dwords name the cell as the deaths of player e mod 12 of unit e div 12 since + 2026-10-23 — the front end's doing, so both builds and the simulator have it. - 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). -- A call worked out when the script is built that throws is a compile error even inside a `try` (both builds): - `try { total += check(9); } catch { … }` says "too big: 9 — this expression is computed when the script is built"; - JavaScript would catch it. Found writing `tests/emitter.test.ts` ("a map of our own, errors"). - A map of our own: JSX inside a program was "made while the game runs" even when it held nothing of the program's — taken since step 8's part 3; (`Math.tan` and `Math.hypot`, refused as what they become, are taken since step 4's part 5; `a >> 0`, refused where @@ -5518,6 +5510,46 @@ every probe is built our way by default. - **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. +### The Open list's bug pass (2026-10-23) + +Between step 15 and step 16, the Open list's bugs (above, *The ledger* → *Open*), taken on branch `open-bugs`. Each +has a row in `tests/open-list.test.ts` (the bodies run in JavaScript too, and compared), and a ledger script where it +is a thing a script can say. + +- **The emitter's fold.** `(v & 0xffff) − 1025` came out as v's low half under −1025's high half (0xFFFF0003 for + v = 3): the plan's *lone place* fold puts a scratch cell straight into the one dword that takes it, and when that + dword starts from a constant it turns every `set 0` act of the scratch cell into a set of the constant — the `&`'s + own `set 0 under ~mask` among them, which then wrote the constant's bits under the mask. The fold is not made when a + masked write works the scratch cell (`&`, `|`, `^` by a constant); the copy with the constant added is right. The + test (`tests/emitter.test.ts`, "a sum of bit-slices of one read") has eight shapes of it. +- **`return` at the top of a program** ends it: the front end emits a `return` with no function around it, the emitter + goes to the run's END (`Emitter.ret` with no frame), the Python side jumps to a label after the body (its top + coroutine's `end`), and the simulator already ended the run. A value is refused: a program gives nothing back. +- **The deaths table's cells 12 to 26.** `memory()` and `setMemory()` of a dword 12 to 26 dwords into the table name it + as the deaths of player e mod 12 of unit e div 12 — the same cell — in the front end (`runtime.ts`, `deathsAt`), so + both builds and the simulator have it; the game would have read the player group of that number. +- **Said now.** `disabled(…)` in a program is refused (a condition or an action); `preserve()` is a remark; + `displayText(…, false)` with `name()` or `color()` in it is a remark (a print is always displayed). The build warns, + as a notice in the editor and a line of the log, of hyper triggers of the map's own left out and of triggers for a + player group the game runs no list for (`OwnBuild.warnings`, the service's `warning` event). +- **False errors.** `n++` as an index in a loop (`checkSleeps`'s `assigned` now follows calls inside expressions; the + index is worked out before the value, as JavaScript has it); a comparison as a boolean argument; a `switch` on a text + known when built (matched then); an arrow's `this` inside a method (the hoister leaves a function value with `this` + in it to the program); `first(…)?.order(…)` (`libraryName` no longer takes a member of one of the library's + interfaces — the unit's `order()` — for the library's `order()` action); `u?.hp ?? 0`; `sleep(…).then(() => 7)` as + a value; `new P(4).x`; `const both = command(…) && bring(…)` (kept as the conditions, asked where tested); + `rows[i].pop() ?? -1`; a build-time throw inside a `try` (the program's error, which the catch takes). +- **Against JavaScript.** `~a` (a ^ −1), `a ** n` and `Math.pow(a, n)` by a power known when built (products), + `Math.sign` (two signed comparisons), `Math.log2` of a whole number (rounded down: five comparisons with powers of + two, by halving; 0 for 0), `fill(v, start, end)` (`sliceEnd`'s bounds). +- **The library.** `first()` and `randomUnit()` take a set of units — `first(unitsAt(locations.Base, { owner: P1 }))`. + The cutscene module's header example compiles as written (a test reads it): the walk the camera follows is + `start(walk(…))`, a task, since a promise kept in a const and awaited later is not the language's. + +Still open from the list: the step 10 to 15 leftovers (sizes and speeds), `j = 1 - j` copied a bit at a time, the +texts' 1,023-byte count, `shared(0)` set again by each run, `let total = shared(0)`, and P9 to P12 owners confirmed in +the game. + ## Part C — the game, pushed Each is a library over Part B, offered only after its probe. "Known" means a played probe @@ -6187,6 +6219,12 @@ build, contract 2) ships first, then plugin-scm-sim, then TrigScript. routines reached by pointer, only in a run that asks for EUD. - **The version number.** 4.0.0 for A1 + A2 because *Run in the game* is a new mode, or 3.13 until the language changes in B1. Proposed: 4.0.0 at A1 + A2. +- ~~Map protection on the own build.~~ Decided 2026-10-23 (the user): none. eudplib scrambles MRGN, shuffles its + payload and adds a fake ISOM; the own build does nothing of the kind, and will not. +- **Magenta on our backend.** Decided 2026-10-23 (the user): a map with Magenta's triggers in it goes on our backend + too, not through eudplib. Magenta's build rows (text, maths, unit passes, checks, chat, keys, clicks) are expanded + today by its euddraft plugins in Python; the slice lays them out as TrigScript programs on the own layout, after + step 17. Until then a map with Magenta's member is built through eudplib (`whyNotOwn`). ## Facts this plan leans on diff --git a/editor.ts b/editor.ts index 318137e..1a6ce8d 100644 --- a/editor.ts +++ b/editor.ts @@ -54,12 +54,12 @@ import { countTests, foldPlayers, idsUnder, marksOf, mergeReport, NO_TESTS, note import { testName } from "./service"; import { applyMoves, buildTree, filesUnder, folderOf, movesOf, refuseMoves, tabLabels, validFolder, type Moves, type TreeNode } from "./tree"; import { ProgramSimulation, type ProgramEvent, type ProgramSimulationOptions, type SimBounds, type SimUnitInit } from "./compiler/simulateIr"; -import { positionIn, type BuildRefusal, type MapNames, type ScriptArtifact, type ScriptService } from "./service"; +import { positionIn, type BuildRefusal, type MapNames, type OwnBuildEvent, type ScriptArtifact, type ScriptService } from "./service"; import { COMPACT_LAYOUT, DEFAULT_LAYOUT, SHELL_STYLE, createShell, type IconName, type ShellLayout } from "./shell"; import { DEBUG_STYLE, Debugger, type DebugGameView } from "./debugger"; import type { DebugRecording } from "./compiler/gameDebug"; import type { ScmSimDebugView } from "./vendor/scmSim"; -import type { EudplibBuildEvent, EudplibService } from "./vendor/eudplib"; +import type { EudplibService } from "./vendor/eudplib"; import type { Translate } from "./i18n"; import { describeMedia, isImageFile, isMediaFile, isNotesFile, isSoundFile } from "./compiler/media"; import { importMedia, showImage, showVideo } from "./videoImport"; @@ -1534,8 +1534,14 @@ function createWorkspace(svc: ScriptService, options: OpenOptions, mode: Workspa }; /** A build of the programs, whoever started it (Save, Test Map, Test): its state in the status bar, its log in Output, a failure on its line. */ - const onLibraryBuild = (e: EudplibBuildEvent) => { - if (cancelled || (e.kind !== "log" && !e.contributors.includes("trigscript"))) return; + const onLibraryBuild = (e: OwnBuildEvent) => { + if (cancelled || (e.kind !== "log" && e.kind !== "warning" && !e.contributors.includes("trigscript"))) return; + if (e.kind === "warning") { + // TrigScript's own build has a word for the map maker: a trigger of the map's own left out, or one that never runs. + log(t("Build warning: {message}", { message: e.message }), false); + shell.notify({ key: `build-warning:${e.message}`, kind: "warn", text: e.message, actions: [{ label: t("Show the log"), run: () => shell.showPanel("output") }] }); + return; + } if (e.kind === "start") { streamed = 0; shell.dismiss("build"); diff --git a/ko.ts b/ko.ts index e9f512f..dd4a0f1 100644 --- a/ko.ts +++ b/ko.ts @@ -23,6 +23,7 @@ export const KO: Record = { "Beside the map: open the script as a panel, so the map stays in reach": "맵 옆에: 스크립트를 패널로 열어 맵을 계속 다룰 수 있게 합니다", "Build failed": "빌드 실패", "Build failed: {message}": "빌드 실패: {message}", + "Build warning: {message}": "빌드 경고: {message}", "Building for Save…": "저장용으로 빌드 중…", "Building for Test Map…": "맵 테스트용으로 빌드 중…", "Building for an export…": "내보내기용으로 빌드 중…", diff --git a/ledger/arrays/fill-range.ts b/ledger/arrays/fill-range.ts new file mode 100644 index 0000000..09475e0 --- /dev/null +++ b/ledger/arrays/fill-range.ts @@ -0,0 +1,10 @@ +// ledger: step 3, takes +// `fill(value, start, end)`: a part of an array set, the places counted from the end when below zero and cut to the array. +program(() => { + const xs = [1, 2, 3, 4, 5]; + let v = 9; + let s = 1; + xs.fill(v, s, 3); + xs.fill(0, -1); + print(`${xs[0]} ${xs[1]} ${xs[2]} ${xs[3]} ${xs[4]}`); +}); diff --git a/ledger/arrays/pop-of-a-row.ts b/ledger/arrays/pop-of-a-row.ts new file mode 100644 index 0000000..f80af28 --- /dev/null +++ b/ledger/arrays/pop-of-a-row.ts @@ -0,0 +1,11 @@ +// ledger: step 9, takes +// `rows[i].pop() ?? -1`: a pop of a row of an array of arrays, the other value when the row is empty. +program(() => { + const rows = [[1, 2], [3]]; + let i = 1; + let a = 0; + let b = 0; + a = rows[i].pop() ?? -1; + b = rows[i].pop() ?? -1; + print(`${a} ${b}`); +}); diff --git a/ledger/language/comparison-as-an-argument.ts b/ledger/language/comparison-as-an-argument.ts new file mode 100644 index 0000000..c927caf --- /dev/null +++ b/ledger/language/comparison-as-an-argument.ts @@ -0,0 +1,12 @@ +// ledger: step 1, takes +// A comparison of the program's variables handed to a boolean parameter. +function step(n: number, early: boolean) { + return early ? n + 1 : n; +} +program(() => { + let turns = 1; + let n = 5; + n = step(n, turns < 2); + n = step(n, turns > 2); + print(`${n}`); +}); diff --git a/ledger/language/condition-in-a-const.ts b/ledger/language/condition-in-a-const.ts new file mode 100644 index 0000000..920c136 --- /dev/null +++ b/ledger/language/condition-in-a-const.ts @@ -0,0 +1,6 @@ +// ledger: step 5, takes +// Conditions of the game joined by && and kept in a const, asked where the const is tested. +program(() => { + const both = command(P1, units.TerranMarine, ">=", 1) && bring(P1, units.TerranMarine, locations.Anywhere, ">=", 1); + if (both) print("both"); +}); diff --git a/ledger/language/return-at-the-top.ts b/ledger/language/return-at-the-top.ts new file mode 100644 index 0000000..bad18cb --- /dev/null +++ b/ledger/language/return-at-the-top.ts @@ -0,0 +1,8 @@ +// ledger: step 0, takes +// `return` at the top of a program: the program ends there, for good. +program(() => { + let n = 1; + print("a"); + if (n > 0) return; + print("b"); +}); diff --git a/ledger/language/switch-on-a-known-text.ts b/ledger/language/switch-on-a-known-text.ts new file mode 100644 index 0000000..ae0b1df --- /dev/null +++ b/ledger/language/switch-on-a-known-text.ts @@ -0,0 +1,12 @@ +// ledger: step 0, takes +// A `switch` on a text known when the script is built takes its case then: no text is compared in the game. +const MODE: string = "fast"; +program(() => { + let n = 0; + switch (MODE) { + case "slow": n = 2; break; + case "fast": n = 1; break; + default: n = 9; + } + print(`${n}`); +}); diff --git a/ledger/language/this-in-an-arrow.ts b/ledger/language/this-in-an-arrow.ts new file mode 100644 index 0000000..94857ec --- /dev/null +++ b/ledger/language/this-in-an-arrow.ts @@ -0,0 +1,18 @@ +// ledger: step 1, takes +// An arrow written inside a method reads the instance through `this`, and a field read straight off `new`. +class M { + x = 1; + f() { + const g = () => this.x + 1; + return g() + this.x; + } +} +class P { + constructor(public x: number) {} +} +program(() => { + const m = new M(); + m.x = 5; + let k = 4; + print(`${m.f()} ${new P(k).x}`); +}); diff --git a/ledger/language/throw-when-built-in-a-try.ts b/ledger/language/throw-when-built-in-a-try.ts new file mode 100644 index 0000000..f1006e6 --- /dev/null +++ b/ledger/language/throw-when-built-in-a-try.ts @@ -0,0 +1,15 @@ +// ledger: step 11, takes +// A call worked out when the script is built that throws, inside a try: the program's error, which the catch takes. +function check(n: number) { + if (n > 5) throw new RangeError("too big: " + n); + return n; +} +program(() => { + let total = 0; + try { + total += check(9); + } catch (e) { + total = e instanceof RangeError ? -1 : -2; + } + print(`${total}`); +}); diff --git a/ledger/memory/deaths-cells-12-to-26.ts b/ledger/memory/deaths-cells-12-to-26.ts new file mode 100644 index 0000000..c8d2927 --- /dev/null +++ b/ledger/memory/deaths-cells-12-to-26.ts @@ -0,0 +1,6 @@ +// ledger: step 6, takes +// memory() and setMemory() of the deaths table's dwords 12 to 26, which the game would read as player groups: named as the deaths of player e mod 12 of unit e div 12. +program(() => { + setMemory(0x58a364 + 4 * 13, "set", 7); + if (memory(0x58a364 + 4 * 13, ">=", 7)) print("the cell"); +}); diff --git a/ledger/numbers/bitwise-not.ts b/ledger/numbers/bitwise-not.ts new file mode 100644 index 0000000..6517f07 --- /dev/null +++ b/ledger/numbers/bitwise-not.ts @@ -0,0 +1,8 @@ +// ledger: step 4, takes +// `~a`: every bit the other way, a ^ −1 — JavaScript's −a − 1 of a signed number. +program(() => { + let a = 5; + let b = 0; + b = ~a; + print(`${b} ${~a + a}`); +}); diff --git a/ledger/numbers/power.ts b/ledger/numbers/power.ts new file mode 100644 index 0000000..d2ffe10 --- /dev/null +++ b/ledger/numbers/power.ts @@ -0,0 +1,8 @@ +// ledger: step 4, takes +// `a ** n` and `Math.pow(a, n)` by a power known when the script is built; `Math.sign` and `Math.log2` (of a whole number: rounded down). +program(() => { + let a = 3; + let b = -7; + let c = 1000; + print(`${a ** 2} ${Math.pow(a, 3)} ${(a + 1) ** 0} ${Math.sign(a)} ${Math.sign(b)} ${Math.sign(a - a)} ${Math.log2(c)}`); +}); diff --git a/ledger/tasks/then-as-a-value.ts b/ledger/tasks/then-as-a-value.ts new file mode 100644 index 0000000..b871df7 --- /dev/null +++ b/ledger/tasks/then-as-a-value.ts @@ -0,0 +1,7 @@ +// ledger: step 11, takes +// `await sleep(…).then(() => 7)` as a value: the wait, then what the function gives. +program(async () => { + let n = 0; + n = await sleep(frames(1)).then(() => 7); + print(`${n}`); +}); diff --git a/ledger/units/first-of-a-set.ts b/ledger/units/first-of-a-set.ts new file mode 100644 index 0000000..98885ad --- /dev/null +++ b/ledger/units/first-of-a-set.ts @@ -0,0 +1,9 @@ +// ledger: step 10, takes +// `first(unitsAt(…))`: a set of units as a pick's filter; a pick's method and field through `?.`. +program(() => { + const u = first(unitsAt(locations.Anywhere, { owner: P1, type: units.TerranMarine })); + let hp = 0; + hp = u?.hp ?? 0; + first({ owner: P2 })?.order("move", locations.Anywhere); + print(`${hp}`); +}); diff --git a/python/trigscript.py b/python/trigscript.py index 4ab41a8..97fb442 100644 --- a/python/trigscript.py +++ b/python/trigscript.py @@ -4600,7 +4600,11 @@ def statement(self, st, ctx): elif k == "return": fn = ctx.get("fn") if fn is None: - raise Fail("trigscript: return outside a function%s" % where(st)) + # At the top of a program: its end, for good. + if ctx.get("end") is None: + raise Fail("trigscript: return outside a function%s" % where(st)) + EUDJump(ctx["end"]) + raise Leave() if st.get("value") is not None and fn["result"] is not None: self.put(fn["result"], st["value"], fn["kind"]) EUDJump(fn["end"]) @@ -5906,7 +5910,8 @@ def frame(self): if self.per_player: for pool in self.pools.values(): pool.base << f_mul(self.player, pool.max) - self.coroutine(self.top, self.p["body"]) + end = Forward() + self.coroutine(self.top, self.p["body"], {"end": end}, end) self.run_tasks() def run_tasks(self): diff --git a/service.ts b/service.ts index b8956b4..1da91f7 100644 --- a/service.ts +++ b/service.ts @@ -122,8 +122,11 @@ export type OwnBuilder = (input: OwnBuildInput, dist: string) => Promise { @@ -311,6 +314,7 @@ export class ScriptService { 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); + for (const message of out.warnings) say({ kind: "warning", message }); say({ kind: "done", purpose: input.purpose, contributors, log: lines.join("\n"), chkBytes: out.chk.length, ms: Date.now() - started }); return out.chk; } catch (err) { @@ -417,7 +421,7 @@ export class ScriptService { } /** Hear about the builds of the programs — TrigScript's own, and the library's (both run on Save, Test Map and export): the log, and where a failure points. */ - onBuild(listener: (event: EudplibBuildEvent) => void): { dispose(): void } { + onBuild(listener: (event: OwnBuildEvent) => void): { dispose(): void } { this.ownListeners.add(listener); let mine: { dispose(): void } | null = null; const watch = this.watchLibrary((library) => { mine?.dispose(); mine = library?.onBuild?.(listener) ?? null; }); diff --git a/stdlib/cutscene.ts b/stdlib/cutscene.ts index e537e80..12f9e0b 100644 --- a/stdlib/cutscene.ts +++ b/stdlib/cutscene.ts @@ -6,7 +6,7 @@ * async function intro(raider: Unit) { * const cam = new Camera(raider.x, raider.y); * await line(units.JimRaynorMarine, "Something's moving out there.", 3 * SECOND); - * const there = walk(raider, 1200, 640); + * const there = start(walk(raider, 1200, 640)); // the walk goes on while the camera follows * await cam.follow(raider, 2 * SECOND); * await there; * await cam.pan(1600, 400, 2 * SECOND); diff --git a/tests/emitter.test.ts b/tests/emitter.test.ts index 20e2e89..784a0d0 100644 --- a/tests/emitter.test.ts +++ b/tests/emitter.test.ts @@ -1270,6 +1270,35 @@ describe("a map of our own, memory at a value's address", () => { }); }); +describe("a map of our own, a sum of bit-slices of one read", () => { + it("folds a sum whose terms are slices of one dword read and a negative constant as the program does (step 12's Open list)", () => { + // `(v & 0xffff) − 1 + ((v >>> 16) − 1) × 1024` of a dword 0x00050003: 2 + 4 × 1024. + const decl = (id: string): VarDecl => ({ id, name: id, kind: "number", shared: false, at }); + const v = (id: string): NumExpr => ({ kind: "var", id }); + const c = (value: number): NumExpr => ({ kind: "const", value }); + const bin = (op: "+" | "-" | "*" | "&" | ">>>", left: NumExpr, right: NumExpr): NumExpr => ({ kind: "binary", op, left, right, at, label: "" }); + const deaths: ConditionRecord = { location: 0, player: 1000, amount: 0, unitId: 0, comparison: 0, type: 15, resource: 0, flags: 0, mask: 0 }; + const read: NumExpr = { kind: "read", read: { source: "condition", record: deaths }, at, label: "" } as NumExpr; + const low = bin("&", v("v"), c(0xffff)), high = bin(">>>", v("v"), c(16)); + const cases: [string, NumExpr, number][] = [ + ["d", bin("+", bin("-", low, c(1)), bin("*", bin("-", high, c(1)), c(1024))), 2 + 4 * 1024], + ["e", bin("+", bin("-", bin("&", v("v"), c(0xff)), c(3)), bin("-", bin("&", bin(">>>", v("v"), c(8)), c(0xff)), c(5))), u(3 - 3 + (0 - 5))], + ["f", bin("-", low, c(1025)), u(3 - 1025)], + ["g", bin("-", bin("*", high, c(1024)), c(1025)), u(5 * 1024 - 1025)], + ["h", bin("+", low, bin("*", high, c(1024))), 3 + 5 * 1024], + ["i", bin("-", bin("+", low, high), c(1025)), u(8 - 1025)], + ["j", bin("+", bin("*", high, c(1024)), c(7)), 5 * 1024 + 7], + ["k", bin("-", high, c(1)), 4], + ]; + const body = [{ kind: "declare", decl: decl("v"), init: read, at }, ...cases.map(([id, init]) => ({ kind: "declare", decl: decl(id), init, at }))] as Stmt[]; + const decls = new Map(["v", ...cases.map(([id]) => id)].map((id) => [id, decl(id)])); + const g = emitBody(body, decls, "bs", undefined, undefined, undefined, 0); + if (typeof g === "string") throw new Error(g); + const got = runOwn(g, new Map([[0x58a364 + 4 * 1000, 0x00050003]])); + expect(cases.map(([id]) => [id, got.get(id)])).toEqual(cases.map(([id, , want]) => [id, want])); + }); +}); + describe("a map of our own, texts made while the game runs", () => { /** What a one-program script prints, its payload walked from its bytes (one pass: it sleeps nowhere). */ const printed = (body: string, file = "main.ts", memory?: Map): string[] => { diff --git a/tests/open-list.test.ts b/tests/open-list.test.ts new file mode 100644 index 0000000..ebca9d2 --- /dev/null +++ b/tests/open-list.test.ts @@ -0,0 +1,139 @@ +/** + * The Open list's bugs of 2026-10-23 (docs/v4-plan.md, "The ledger" → *Open*), taken: false compile errors, gaps against + * JavaScript, silent drops, and two cells of the deaths table the game reads as player groups. The bodies that can run + * are run in JavaScript too, and compared. + */ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import ts from "typescript"; +import { describe, expect, it } from "vitest"; +import { compileScript, type CompileResult } from "../compiler/compiler"; +import { defaultScriptNames, scriptNames } from "../compiler/names"; +import { simulatePrograms } from "../compiler/simulateIr"; +import type { Stmt } from "../compiler/ir"; +import { defaultLib } from "../bundle/lib.mjs"; + +const LIB = defaultLib(); +const NAMES = defaultScriptNames(); +const raw = (body: string, head = "", async = false): CompileResult => compileScript(ts, { "main.ts": `${head}\nprogram(${async ? "async " : ""}() => {${body}});` }, NAMES, { lib: LIB }); +const compile = (body: string, head = "", async = false): CompileResult => { const r = raw(body, head, async); expect(r.diagnostics.map((d) => d.message)).toEqual([]); return r; }; +const run = (body: string, head = "", async = false, cycles = 3) => { const r = compile(body, head, async); return simulatePrograms(r.ir, cycles, { strings: r.strings }); }; +const messages = (body: string, head = "", async = false) => raw(body, head, async).diagnostics.map((d) => d.message); +const shown = (sim: ReturnType) => sim.events.map((e) => e.text); +/** What JavaScript itself says of the same lines: the body is run as it is, `displayText` collecting. */ +const js = (body: string, head = ""): string[] => { const out: string[] = []; new Function("displayText", ts.transpile(`${head}\n${body}`, { target: ts.ScriptTarget.ES2023 }))((t: string) => out.push(t)); return out; }; +const same = (body: string, head = "") => { const sim = run(body, head); expect(sim.faults).toEqual([]); expect(shown(sim)).toEqual(js(body, head)); }; +/** Every statement of a program, the nested ones too. */ +function* statements(body: Stmt[]): Generator { + for (const s of body) { + yield s; + for (const v of Object.values(s as unknown as Record)) { + if (Array.isArray(v) && v.length && typeof v[0] === "object" && v[0] && "kind" in v[0]) yield* statements(v as Stmt[]); + } + } +} + +describe("false compile errors, no longer", () => { + it("n++ as an array's index in a loop moves the loop on", () => { + same("const xs = [0, 0, 0, 0]; let n = 0; while (n < 4) { xs[n++] = n * 10; } displayText(`${xs[0]} ${xs[3]} ${n}`);"); + }); + it("a comparison as a boolean argument", () => { + same("function step(n: number, early: boolean) { return early ? n + 1 : n; } let turns = 1; let n = 5; n = step(n, turns < 2); n = step(n, turns > 2); displayText(`${n}`);"); + }); + it("a switch on a text known when the script is built takes its case now", () => { + same("const MODE: string = 'fast'; let n = 0; switch (MODE) { case 'slow': n = 2; break; case 'fast': n = 1; break; default: n = 9; } displayText(`${n}`);"); + same("const MODE: string = 'odd'; let n = 0; switch (MODE) { case 'slow': n = 2; break; default: n = 9; } displayText(`${n}`);"); + // (No text is compared in the game.) + const r = compile("const MODE: string = 'fast'; let n = 0; switch (MODE) { case 'fast': n = 1; break; } displayText(`${n}`);"); + expect(JSON.stringify(r.ir[0].body)).not.toContain("textCompare"); + }); + it("an arrow inside a method reads the instance's fields through this", () => { + same("class M { x = 1; f() { const g = () => this.x + 1; return g() + this.x; } } const m = new M(); m.x = 5; displayText(`${m.f()}`);"); + }); + it("a field read straight off new", () => { + same("class P { constructor(public x: number, public y: number) {} get sum() { return this.x + this.y; } } let k = 4; let n = 0; n = new P(k, 2).x + new P(1, 1).sum; displayText(`${n}`);"); + }); + it("sleep(…).then(() => 7) as a value", () => { + const sim = run("let n = 0; n = await sleep(frames(1)).then(() => 7); displayText(`${n}`);", "", true); + expect(shown(sim)).toEqual(["7"]); + }); + it("a unit's field through ?. with ?? for when there is none, and a pick's method through ?.", () => { + expect(messages("const u = first({ owner: P1 }); let hp = 0; hp = u?.hp ?? 0; displayText(`${hp}`);")).toEqual([]); + expect(messages("first({ owner: P1 })?.order('move', locations.Anywhere); first(unitsAt(locations.Anywhere, { owner: P2 }))?.kill();")).toEqual([]); + }); + it("rows[i].pop() ?? -1 of an array of arrays", () => { + same("const rows = [[1, 2], [3]]; let i = 1; let a = 0; let b = 0; a = rows[i].pop() ?? -1; b = rows[i].pop() ?? -1; displayText(`${a} ${b}`);"); + }); + it("a condition kept in a const with && is asked where the const is tested", () => { + const r = compile("const both = command(P1, units.TerranMarine, '>=', 1) && bring(P1, units.TerranMarine, locations.Anywhere, '>=', 1); if (both) displayText('x');"); + const test = [...statements(r.ir[0].body)].find((s) => s.kind === "if") as Extract; + expect(test.cond.kind).toBe("and"); + expect((test.cond as Extract).items.map((c) => (c as { kind: string; record?: { type: number } }).record?.type)).toEqual([2, 3]); + }); +}); + +describe("the program ends at a return of its own", () => { + it("nothing after it runs, in a program and in an async one", () => { + expect(shown(run("let n = 1; displayText('a'); if (n > 0) return; displayText('b');"))).toEqual(["a"]); + expect(shown(run("displayText('a'); await sleep(frames(1)); return; displayText('b');", "", true))).toEqual(["a"]); + expect(messages("let n = 1; return n;").join("\n")).toMatch(/gives nothing back/); + }); +}); + +describe("against JavaScript, in a program", () => { + it("~a, a ** n, Math.pow, Math.sign, Math.log2", () => { + same("let a = 5; let b = 0; b = ~a; displayText(`${b} ${~0} ${~-1}`);"); + same("let a = 3; let b = 0; b = a ** 2; let c = a ** 0; displayText(`${b} ${c} ${Math.pow(a, 3)} ${(a + 1) ** 2}`);"); + same("let a = -3; let b = 7; let c = 0; displayText(`${Math.sign(a)} ${Math.sign(b)} ${Math.sign(c)}`);"); + same("let a = 1024; let b = 1000; let c = 1; let d: u32 = 4294967295; let e = 2147483647; displayText(`${Math.floor(Math.log2(a))} ${Math.floor(Math.log2(b))} ${Math.floor(Math.log2(c))} ${Math.floor(Math.log2(d))} ${Math.floor(Math.log2(e))}`);"); + expect(messages("let a = 3; let n = 2; let b = 0; b = a ** n;").join("\n")).toMatch(/known when the script is built/); + }); + it("fill(value, start, end), counted from the end when negative and cut to the array", () => { + same("const xs = [1, 2, 3, 4, 5]; let v = 9; let s = 1; let e = 3; xs.fill(v, s, e); displayText(`${xs[0]} ${xs[1]} ${xs[2]} ${xs[3]} ${xs[4]}`);"); + same("const xs = [1, 2, 3, 4, 5]; let v = 9; xs.fill(v, -2); displayText(`${xs[2]} ${xs[3]} ${xs[4]}`); xs.fill(0, 1, 99); displayText(`${xs[0]} ${xs[1]} ${xs[4]}`); xs.fill(7, 4, 2); displayText(`${xs[2]} ${xs[3]}`);"); + }); + it("an error a build-time call throws inside a try is the program's, which the catch takes", () => { + // (`check` outside the program: worked out when the script is built, where it throws.) + const CHECK = "function check(n: number) { if (n > 5) throw new RangeError('too big: ' + n); return n; }"; + same("let total = 0; let m = 0; try { total += check(9); } catch (e) { total = -1; m = e instanceof RangeError ? 1 : 2; } displayText(`${total} ${m}`);", CHECK); + // (Outside a try it is still the script's own mistake, said as such.) + expect(messages("let total = 0; total += check(9);", CHECK).join("\n")).toMatch(/too big: 9/); + }); +}); + +describe("a trigger's words inside a program", () => { + it("disabled() is refused, on a condition and on an action", () => { + expect(messages("if (disabled(bring(P1, units.TerranMarine, locations.Anywhere, '>=', 1))) displayText('x');").join("\n")).toMatch(/disabled\(\) switches a condition/); + expect(messages("disabled(displayText('x'));").join("\n")).toMatch(/disabled\(\) switches an action/); + }); + it("preserve() is a remark, and nothing emitted", () => { + const r = compile("preserve(); preserveTrigger(); displayText('x');"); + const remarks = [...statements(r.ir[0].body)].filter((s) => s.kind === "remark"); + expect(remarks.length).toBe(2); + expect(r.ir[0].body.filter((s) => s.kind === "action").length).toBe(1); + }); +}); + +describe("the deaths table's cells 12 to 26", () => { + it("memory() and setMemory() of one name it as the deaths of player e mod 12 of unit e div 12, which the game reads as the cell", () => { + const r = compile("if (memory(0x58A364 + 4 * 17, '>=', 1)) displayText('a'); setMemory(0x58A364 + 4 * 13, 'set', 1); if (memory(0x58A364 + 4 * 26, '>=', 1)) displayText('b'); setMemory(0x58A364 + 4 * 27, 'set', 1); if (memory(0x58A364 + 4 * 11, '>=', 1)) displayText('c');"); + const body = [...statements(r.ir[0].body)]; + const records = JSON.stringify(body).match(/"record":\{[^}]*\}/g)!.map((m) => JSON.parse(`{${m}}`).record as { player: number; unitId: number; type: number }).filter((x) => x.type === 15 || x.type === 45); + expect(records.map((x) => [x.player, x.unitId])).toEqual([[5, 1], [1, 1], [2, 2], [27, 0], [11, 0]]); + }); +}); + +describe("the standard library's examples", () => { + it("the cutscene module's header example compiles as written", () => { + const text = readFileSync(resolve(__dirname, "../stdlib/cutscene.ts"), "utf8"); + const header = text.slice(0, text.indexOf("*/")).split("\n").map((l) => l.replace(/^\s*\*\s?/, "").trim()); + const from = header.findIndex((l) => l.startsWith("import ")); + const to = header.findIndex((l, i) => i > from && l === "});"); + expect(from).toBeGreaterThan(0); + expect(to).toBeGreaterThan(from); + const example = header.slice(from, to + 1).join("\n"); + // (On a map with a location named Base, as the example has it.) + const r = compileScript(ts, { "main.ts": example }, scriptNames({ locations: [{ index: 0, name: "Base" }] }), { lib: LIB }); + expect(r.diagnostics.map((d) => d.message)).toEqual([]); + }); +}); diff --git a/tests/script-structured.test.ts b/tests/script-structured.test.ts index e299e3b..86f77dc 100644 --- a/tests/script-structured.test.ts +++ b/tests/script-structured.test.ts @@ -524,7 +524,7 @@ describe("structured: diagnostics", () => { });`); // Dividing by a variable is fine now. expect(msgs.some((m) => m.startsWith("4:"))).toBe(false); - expect(msgs).toContain("5:Expected a number: variables take + - * / % and the bitwise & | ^ << >> >>>."); + expect(msgs).toContain("5:The power is known when the script is built: a ** 2, Math.pow(a, 3). For one the game works out, multiply in a loop."); expect(msgs).toContain("6:wait's milliseconds must be known when the script is built. An amount with a modifier (setResources, setDeaths, setScore, setCountdownTimer), a unit count (createUnit, killUnitAt, removeUnitAt, giveUnits) and a unit type can be a variable of the program."); expect(msgs.some((m) => m.startsWith("7:Variables hold numbers, booleans, texts, units of the game or records of them ({ lives: 3 }); s is symbol"))).toBe(true); expect(msgs.some((m) => m.startsWith("8:f calls itself on every path through it"))).toBe(true); diff --git a/tests/service.test.ts b/tests/service.test.ts index 6940262..4c7684b 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -16,6 +16,7 @@ import { Sections, readFacts, writeChunks } from "../compiler/backend/chk"; import { firstFault, MAGENTA_MEMBER, positionIn, ScriptService, type Compiler } from "../service"; import { readFiles, readManifest, scriptState } from "../script"; import type { EudplibBuildEvent, EudplibContribution, EudplibService } from "../vendor/eudplib"; +import type { OwnBuildEvent } from "../service"; import { defaultLib } from "../bundle/lib.mjs"; const LIB = defaultLib(); @@ -252,7 +253,7 @@ describe("service: saving applies the script and builds its programs", () => { expect(manifest?.programs).toBe(1); expect(manifest?.ownRefusal).toBeUndefined(); // The step: the scenario in, the built one out — no second compile, the apply's is fresh. - const heard: EudplibBuildEvent[] = []; + const heard: OwnBuildEvent[] = []; svc.onBuild((e) => heard.push(e)); const before = compiles; const out = await step.run({ map: chk(), fileName: "x.scx", purpose: "save", signal: new AbortController().signal }); @@ -293,7 +294,7 @@ describe("service: saving applies the script and builds its programs", () => { expect(h.steps[0].applies()).toBe(false); expect(lib.contributions[0].applies()).toBe(true); // Asked to build all the same (an older manifest, say): the refusal is the error, said to the listeners too. - const heard: EudplibBuildEvent[] = []; + const heard: OwnBuildEvent[] = []; svc.onBuild((e) => heard.push(e)); await expect(h.steps[0].run({ map: chk(), fileName: "x.scx", purpose: "test", signal: new AbortController().signal })).rejects.toThrow(/^TrigScript's own build does not take the script: the action Wait .*\. Apply the script again to build it through eudplib\.$/); expect(heard.at(-1)).toMatchObject({ kind: "failed", purpose: "test", from: "trigscript" });